Android OpenCV 〜サンプルアプリを動かす〜

イントロダクション

アンドロイドでのOpenCVアプリを作成しようと考えています。
そんなわけで、下のサイトを参照してプログラムを動かしてみました。

以下の記述は参考サイトを実行して見た内容です。

AndroidでOpenCV

参考サイト: Android Pub

Step 1: Download OpenCV Android Library

ライブラリのダウンロード

  1. OpenCV + Androidのチュートリアルページを開きます。

  2. そして、OpenCV-android-sdkをダウンロードします。ZIPファイルをAndroid開発用のフォルダに展開します。

Step 2: Setup project

プロジェクトの作成

  1. Android Studioを開き、Emptyプロジェクトを作成する
    activity1

  2. プロジェクトの名前などを設定する

Step 3: Import OpenCV Module

OpenCVの取り込み

  1. 作成したプロジェクトを開いた状態で、File -> New -> Import Moduleを選択する

  2. プロジェクトの作成でダウンロード、展開した、ZIPファイルから「sdk/java」を指定する

  1. モジュールをインポートしたらビルドが始まるがエラーになる

Step 4: Fixing Gradle Sync Errors

build.gradleファイルを修正する


使用する実機がバージョン4.XだったのでminSdkVersion、targetSdkVersionを4に修正します。

Step 5: Add the OpenCV Dependency

OpenCVの依存性追加

  1. OpenCVライブラリの追加、ProjectStructure->Dependencies

Step 6: Add Native Libraries

ネイティブライブラリをコピーする

  1. OpenCVライブラリからAndroidプロジェクトのmainフォルダにペースト
  2. ペーストしたフォルダの名前を「jniLibs」に修正

Step 7: Add Required Permissions

AndroidManifest.xmlの修正

作成するプロジェクトのappフォルダにあるAndroidManifest.xmlに以下のコードを追記する

<uses-permission android:name="android.permission.CAMERA"/>

<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<uses-feature android:name="android.hardware.camera.front" android:required="false"/>
<uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>

最終的に下のようなファイルになる

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.zenryokuservice.androidopencv">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <uses-permission android:name="android.permission.CAMERA"/>

    <uses-feature android:name="android.hardware.camera" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>
</manifest>

adbコマンド準備

  1. AndroidSDKのインストール場所を確認する
    • File -> androidOtherSettings -> DefaultProjectStructure...
      DefaultProjectStructure!
  2. ターミナルを立ち上げて、パスを通す
    • viコマンドで「.bash_profile」を開く
    • sdk/platform-toolsを追加する

サンプルソースを読む

起動している、コードは参考サイトにあるコード(MainActivity.java)です。
アンドロイドアプリの実装では、準備処理と起動時の処理でメソッドが分かれているので、主要な部分を見ます。

    public boolean onTouch(View v, MotionEvent event) {
        int cols = mRgba.cols();
        int rows = mRgba.rows();

        int xOffset = (mOpenCvCameraView.getWidth() - cols) / 2;
        int yOffset = (mOpenCvCameraView.getHeight() - rows) / 2;

        int x = (int)event.getX() - xOffset;
        int y = (int)event.getY() - yOffset;

        Log.i(TAG, "Touch image coordinates: (" + x + ", " + y + ")");

        if ((x < 0) || (y < 0) || (x > cols) || (y > rows)) return false;

        Rect touchedRect = new Rect();

        touchedRect.x = (x>4) ? x-4 : 0;
        touchedRect.y = (y>4) ? y-4 : 0;

        touchedRect.width = (x+4 < cols) ? x + 4 - touchedRect.x : cols - touchedRect.x;
        touchedRect.height = (y+4 < rows) ? y + 4 - touchedRect.y : rows - touchedRect.y;

        Mat touchedRegionRgba = mRgba.submat(touchedRect);

        Mat touchedRegionHsv = new Mat();
        Imgproc.cvtColor(touchedRegionRgba, touchedRegionHsv, Imgproc.COLOR_RGB2HSV_FULL);

        // Calculate average color of touched region
        mBlobColorHsv = Core.sumElems(touchedRegionHsv);
        int pointCount = touchedRect.width*touchedRect.height;
        for (int i = 0; i < mBlobColorHsv.val.length; i++)
            mBlobColorHsv.val[i] /= pointCount;

        mBlobColorRgba = converScalarHsv2Rgba(mBlobColorHsv);

        Log.i(TAG, "Touched rgba color: (" + mBlobColorRgba.val[0] + ", " + mBlobColorRgba.val[1] +
                ", " + mBlobColorRgba.val[2] + ", " + mBlobColorRgba.val[3] + ")");

        mDetector.setHsvColor(mBlobColorHsv);

        Imgproc.resize(mDetector.getSpectrum(), mSpectrum, SPECTRUM_SIZE, 0, 0, Imgproc.INTER_LINEAR);

        mIsColorSelected = true;

        touchedRegionRgba.release();
        touchedRegionHsv.release();

        return false; // don't need subsequent touch events
    }

このコードで注目したいのは、「赤い線をつけている部分」です。

結論、下の部分で画面に表示している情報を更新しているように思います。
<MainActivity.java>

// Calculate average color of touched region
mBlobColorHsv = Core.sumElems(touchedRegionHsv);
int pointCount = touchedRect.width * touchedRect.height;
for (int i = 0; i < mBlobColorHsv.val.length; i++)
    mBlobColorHsv.val[i] /= pointCount;

mBlobColorRgba = converScalarHsv2Rgba(mBlobColorHsv);

Log.i(TAG, "Touched rgba color: (" + mBlobColorRgba.val[0] + ", " + mBlobColorRgba.val[1] +
        ", " + mBlobColorRgba.val[2] + ", " + mBlobColorRgba.val[3] + ")");

mDetector.setHsvColor(mBlobColorHsv);

Imgproc.resize(mDetector.getSpectrum(), mSpectrum, SPECTRUM_SIZE, 0, 0, Imgproc.INTER_LINEAR);

動かして見たところ、上記のメソッドで画面をタッチしたときの色を取得しているようです。

そして、その色の輪郭部分を描画している。。。ように見えます。
その部分が以下のコードです。
<MainActivity.java>

if (mIsColorSelected) {
    mDetector.process(mRgba);
    List<MatOfPoint> contours = mDetector.getContours();
    Log.e(TAG, "Contours count: " + contours.size());
    Imgproc.drawContours(mRgba, contours, -1, CONTOUR_COLOR);

    Mat colorLabel = mRgba.submat(4, 68, 4, 68);
    colorLabel.setTo(mBlobColorRgba);

    Mat spectrumLabel = mRgba.submat(4, 4 + mSpectrum.rows(), 70, 70 + mSpectrum.cols());
    mSpectrum.copyTo(spectrumLabel);
}

mDetectorはサンプルコードにあるクラスで輪郭部分の描画を行なっているように見えます。

ちょっと自信がないので、「〜のように見えます」と記載しています。

輪郭部分を塗りつぶす

サンプルの実行では下の通りです。

あとは、コードを弄り確認することにします。

でわでわ。。。

Java OpenCV 機会学習 〜背景を学習する1〜

JavaでOpenCVを使用して背景を除去しようとやってきたのですが、色々と欠点が見えてきて、軽くKOされていました。出来なかったという事です。。。

しかし、OpenCVの本を読んでみると「背景除去の弱点」という形で記載されていました。

。。。では、どうしたら良いのか?

シーンのモデル化

背景と前景を定義するところから記載されていました。
まとめると、高度な「シーンのモデル」が必要ということです。モデルというのは、Javaではデータクラスの事で、C言語では、ストラクチャーなどと言います。

本の中では、動画を撮影している時の話をしているのですが、静止画ではなく動画なので、『物を動かしたら「前景」と「背景」が変わってしまう。』ということを話していました。

内容がまとまりきらなかったので、文章を書きぬきます。

一般にシーンモデルは、「新しい前景」オブジェクトレベルに入れられ、ポジティブな物体、または穴として印がつけられます。前景オブジェクトのない領域は、背景モデルを更新し続けることができます。前景オブジェクトが所定の時間動かなかったら、それは「より古い前景」に降格されます。
そこで、ピクセルの統計値が一時的に学習され、最後に、その学習されたモデルが学習済みの背景モデルに組み入れられます。
部屋の赤ライをつけるようなグローバルな変化の検出については、グローバルなフレーム差分を使います、たとえ、多くのピクセルが一度に変化したら、それは局所的な変化ではなくグローバルな変化として分類でき、あたらいい状況用のモデルを使うように切り替えることができます。

書いてみると、ちょっとまとまりました。

シーンモデルを作成して、そのモデルが「前景」と「背景」にわけられ、それぞれに、学習し続けることでそれぞれのモデルに対して、「前景」なのか「背景」なのか?のレベルを持たせることで「前景」と「背景」を区別する

というところに落ち着きました。
そのために。。。

ピクセルの断面(要約です)

ピクセルの変化のモデル化に入る前に、ちょっと考えましょう。考える(イメージする)シーンは下のようなイメージです。

風に吹かれる気のシーンを窓から見張っている(撮影している)カメラを考える

そして、所定の線の上にあるピクセルが60フレームに渡りどう見えるか?つまり、この種類の変動をモデル化したいということです。

平均背景法

平均背景法は、背景モデルとして画素ごとに閾値
を設定し、画素ごとに前景を判定する。

各ピクセルの平均値と標準偏差を背景のモデルとして学習する。
この平均背景法は、OpenCVの以下の4つのルーチンを使用します。
C言語での名前です。(Java版doc)

  1. cvAbsDiff():フレーム艦の画像差分を累積する
  2. cvInRage(): 前景領域と背景領域に分割
  3. cvOr(): 異なるからチャンネルの分割結果えお単一のマスク画像にまとめる

色々な処理を行い、結局のところは。。。

平均、分散、共分散を累積する

というところに至ります。このページから失敬しました文言だとわかりやすいです。

平均値でデータ全体の平均的な大きさを把握出来て、
標準偏差で、そのデータ列のバラつき度合を見る

平均

これを求めるには、cvAcc()を使用します。
画像の各値、画素値の平均は「移動平均」を使うと良いと本に記載してあるのですが、意味がわからなかったので調べました。

移動平均:株価や気温など時間で細かく変化するデータを眺めるとき、変動が細かすぎて全体の傾向を掴みにくい場合、変化をより滑らかにしてデータを見やすくできます。

具体的には、こちらのページを参考にしましたが、細かいデータだと1日の変化しかわからないが、移動平均にすると全体の変化が観れるということ

なので、動画や、複数の画像のcvAccで計算してやると移動平均が取れるというわけです。

分散

これを求めるには、cvSquareAcc()を使用します。
分散は、「ばらつき」を示し
ここのページから失敬した文言ですが、

低コントラスト画像は、バラつきが小さい、
高コントラスト画像は、バラつきが大きい、

なので、コントラストを求めることができるであろうというわけです。

共分散

これを求めるには、cvMultiplyAcc()を使用します。
この関数を使って、様々な統計ベースの背景モデルを作成することができます。

※ここら辺のjava版ドキュメントは見つけることができませんでした。。。

これらは、統計で使用する計算と同じです。
以前学習した「数理モデル」でも平均、分散、共分散(ちょっと怪しい)について記載しています。

<コードで見る>
これはC言語ですが、メイン処理を含めた、1フレーム分の背景の統計値を学習するコード

// グローバル変数(Javaの場合はフィールド変数)
CvCapture* capture = cvCreateFileCapture(argv[1]);
int max_buffer;
IplImage* rawImage;
// 配列の要素数を指定して宣言する
int r[1000], g[1000], b[1000];
CvLineIterator iterator;

FILE * fptrb = fopen("blueLines.csv", "w");
FILE * fptrg = fopen("GreenLines.csv", "w");
FILE * fptrr = fopen("RedLines.csv", "w");

CvSize sz;
IplImage *Iscratch;
IplImage *Iscratch2;
// 上の書き方をまとめると下のようになる
IplImage *IIavgF, *IdiffF, *IprevF;
float Icount;

// メイン処理ループ
for(;;) {
    if (!cvGrabFrame(capture)) break;

    rawImage = cvRetrieve(capture);
    max_buffer = cvInitLineIterator(rawImage, pt1, pt2, &iterator, 8.0);
    for(int j = 0; j &lt max_buffer; j++) {
        fprintf(fptrb, "%d,", iterator.ptr[0]); // 青の値を書き込む
        fprintf(fptrb, "%d,", iterator.ptr[1]); // 緑の値を書き込む
        fprintf(fptrb, "%d,", iterator.ptr[2]); // 赤の値を書き込む
        iterator.ptr[2] = 255; // このサンプルに赤で印をつける
        CV_NEXT_LINE_POINT(iterator);// 次のピクセル
    }
    // データを行ごと出力する
    fprintf(fptrb, "\n");
    fprintf(fptrg, "\n");
    fprintf(fptrr, "\n");
}
// メモリの解放
fclose(fptrb);
fclose(fptrg);
fclose(fptrr);
cvReleaseCapture(&capture);
// こちらが先に動く
void AllocateImage(IplImage *I) {
    CvSize sz = CVGetSize(I);
    *Iscratch = cvCreateImage(sz, IPL_IMAGE_DEPTH_32_F, 3);
    *Iscratch2 = cvCreateImage(sz, IPL_IMAGE_DEPTH_32_F, 3);
    Icount = 0.00001;
}
// 1フレーム文の背景の統計値を追加学習する
// 引数は3チャンネル富豪なし8ビット背景色サンプル、
void accmulateBackground( IplImage *I) {
    static int first = 1;
    cvCvtScale( I, Iscratch, 1, 0);
    if (!first) {// 1 = true : 0 = false
        cvAcc(Iscratch, IavgF);
        cvAbsDiff(Iscratch, IprevF, Iscratch2);
        cvAcc(Iscratch2, IdiffF);
        Icount += 1.0;
    }
    cvCopy(Iscratch, IprevF);
}

このような手法で背景を割り出すのも1つの方法です。

まとめ

背景を取得する(判別する)ためには、findContuorのようなメソッドで〜という方法もあるが、これではいろんな背景に対応ができない。
解決方法としては、上記の「平均背景法」という手法で解決ができそうだということがわかった。

ここで使用するのは「統計値」、これらの値により以下のことができる

  1. 画像のコントラストの算出
  2. 変化量を算出
  3. 統計ベースの背景モデルを作成する

次に学習する「コードブック法」で「モデル」という部分をより詳細に学習していく予定です。
※各画素に背景モデルを複数作成して、
常に変動する画素値に対応する手法

この続きは、AndroidでOpenCvを動かす方向で行なっていきます。

サンプルコードを動かしたので、そこからカスタムしていこうと思います

Android OpenCV 〜サンプルアプリを動かす〜

でわでわ。。。


<!— BODY広告 —>



Java OpenCV 〜輪郭:輪郭検出処理の順序〜

OpenCVでの輪郭抽出の処理手順を確認しようと思います。

今までに、OpenCV(JavaCV)での、学習準備と基本的な処理方法の学習を学習しました。

輪郭抽出のことを調べるとPythonでの処理とかアルゴリズムの理論とか、肝心要の部分が見つからず。。。

ようやく。。。

輪郭抽出の処理順序

  1. 画像を読み込む
  2. グレースケール(白黒画像)に変更する
  3. グレースケール画像から輪郭を抽出

大まかに、上のような手順で処理を行うと表示できる。
ただし、グレースケースの画像から輪郭を取得するのでわりかしリアルな白黒だと、外側の枠しか輪郭が取得できない。

ちょっとわかりづらいけど、緑色の線が輪郭です。

そして、これをわかりやすくするには
Imgproc.threshold()を使用する必要がある。

動かしてみると下のようなイメージです。

ここまできたら、あとはトライアンドエラーで星輪郭が得られるように、下の値を調整してやります。

ちょと画像は違いますが、下のような感じで試しました。

しかし、これは、写真により値を変更しないと欲しい輪郭が取れない。。。

よく見かけるサンプルでは、輪郭がくっきりしているので、問題ないだろうが、ちょっと考え直す必要があると思いました。

→輪郭の取得方法に関して、考え直すと言う意味です。



Java OpenCV 〜画像の一部分を書き換える〜

画像の一部分を変更して、他のファイルから読み取った画像を書き込む方法

結論


下の画像のように、道の写真に「Tea!」と書いた絵を重ねたようなものに書き換える処理です。

手こずったところ

Roiの使い方、つまりはMat#submat()とMat#copyTo()の使い方に手こずりました。

以下のような画像を使用しました。
道路のイメージ=img1
Teaのイメージ=img2

Mat roi = img.submat(new Rect(20, 20, tea.width(), tea.height()));
Mat dst = new Mat();
tea.copyTo(roi);

上の処理で、土台になるいmg1にimg2の内容を書き込みます。
あとは、表示するだけです。

ソースはGithubにあります。

ポイント

画像の取得部分

URL url = this.getClass().getResource("/road.png");
Mat img = Imgcodecs.imread(url.getPath(), Imgcodecs.IMREAD_UNCHANGED);

画像の抜き出し

Mat roi = img.submat(new Rect(20, 20, tea.width(), tea.height()));

変数teaはimg2の画像です。
つまりは、道路の画像からx=20, y=20からteaのサイズ分を抜き出していると言うことです。

書き込み

tea.copyTo(roi);

これだけです。

ここにたどり着くのに、3日くらいかかりました。(笑)

しかし!

これでは、いまいち納得がいきませんでした。
なぜなら、「重ねる」のが目的であるからです。
これじゃ「上書き」です。

そんなわけで

やっと見つけました。こうすれば良いと言うものです。
理論的には、以下のような手順です。

  1. 書き込みたい画像を取得する
  2. 取得した画像と同じサイズ文のROI(画像の一部分)を取得する
  3. 抜き出したROIをCore.addWeighted()で書き込みたい画像とROIを重ねます。
  4. copyToで重ねたデータをROIにコピーする
URL url = this.getClass().getResource("/road.png");
Mat img = Imgcodecs.imread(url.getPath(), Imgcodecs.IMREAD_UNCHANGED);
printAttribute(img);

 URL url2 = this.getClass().getResource("/Tea.png");
 Mat tea = Imgcodecs.imread(url2.getPath(), Imgcodecs.IMREAD_UNCHANGED);
// 文字列の配置
Imgproc.putText(img, "sample!", new Point(10, 10), Core.FONT_HERSHEY_SIMPLEX, 0.5, Scalar.all(0));
// 
Mat roi = img.submat(new Rect(20, 20, tea.width(), tea.height()));
Mat dst = new Mat();
Core.addWeighted(tea, 0.2, roi, 0.8, 0.5, dst);
dst.copyTo(roi);

すると以下のような画像になります。

元にした画像が、透過PNG担っていないのが気になりますが、今回はここら辺で

でわでわ。。。



OpenCV エラー 〜Sizes of input arguments do not match〜

下のようなエラー(Exception)が出ました。

(-209:Sizes of input arguments do not match) The operation is neither 'array op array' (where arrays have the same size and the same number of channels), nor 'array op scalar', nor 'scalar op array' in function 'arithm_op'
]

結論から言うと、

「入力と出力する画像のサイズが違います」

と言うことでした。あと、チャンネルも一緒である必要がある

翻訳すると。。。

操作は、「array op array」(配列のサイズとチャネル数が同じ)、「array op scalar」、「arithm_op」の「scalar op array」のいずれでもありません



OpenCvを動かすときにはOpenCvの仕様にのっとらないといけない事が多いのので、使用する時は、リファレンスをよく読む事をお勧めします。

OpenCvリファレンス

Java OpenCV 〜基礎: 画像上の基本的な処理〜

OpenCvの処理を実装して内容を改めて確認します。その為に実装したメソッドを追加して個別に実行出来る仕組みを作りました。以下のリンクを参照下さい。

Java OpenCV 〜学習準備のまとめ〜

そして今回の参考にするサイトはこちらです。
実はPython版のチュートリアルなのですが、これをJavaに置き換えて学んでいきます。。。(Java版だと動画からしか見つからなかった〜)

基本1

参考サイトでは、以下の内容を学びます。

  1. 画素値のアクセス及び変更方法
  2. 画像の属性情報の取得
  3. 画像中の注目領域(ROI)の設定
  4. 画像の分割と統合

まず初めにカラー画像を読み込みましょう:

とりあえずはその通りに、実装してみます。
作成したクラスは下のクラスです。準備で実装したCommandIFを実装(implements)したクラスを作成しました。
<CommandIFについて>
CommandIFは自作のインターフェースです。。。表示画面上部のテキストボックスに入力した文字列で起動するCommandIFを実装したクラスの「execute()」メソッドを実行します。

実行結果は下のような感じです。

使用しているCanvasのサイズが200x200なのではみ出しています。。。

実行するコードはこんな感じです。
<CommandIF>

/**
 * コマンド実行するためのインターフェース・クラス。
 * 
 * @author takunoji
 *
 * 2020/05/17
 */
public interface CommandIF {
    /** Canvasクラスから取得したGraphics2Dに描画する */
    public abstract void execute(Pane pane) throws Exception;
    /** 実装クラスを取得する */
    public  abstract CommandIF getCommand();
    /** 描画したCanvasを取得する */
    public abstract Canvas getBefore() throws Exception;
    public abstract Canvas getAfter() throws Exception;
    public abstract GraphicsContext getBeforeGraphics();
    public abstract GraphicsContext getAfterGraphics(); 
    public abstract BufferedImage getBeforeImage();
    public abstract BufferedImage getAfterImage();
}

<CommandIF実装(implementsクラス>

@Override
public void execute(Pane pane) throws Exception {
    ObservableList<Node> obsList = pane.getChildren();
    Canvas before = null;
    for (Node node : obsList) {
        before = (Canvas) node.lookup("#testCanvasBefore");
    }
    // イメージファイルパスを取得する
    URL url = getClass().getResource("/himawari.png");
    // イメージファイルをロードして行列(Mat)に格納
    Mat img = Imgcodecs.imread(url.getPath(), Imgcodecs.IMREAD_COLOR);
    MatOfByte bikeByte = new MatOfByte();
    // 画像データをMatOfByteに書き込む
    Imgcodecs.imencode(".jpeg", img, bikeByte);
    // BuffereImageを取得する
    BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(bikeByte.toArray()));
    // Canvasへの描画準備
    GraphicsContext g = before.getGraphicsContext2D();
    // 描画ポイントを指定して描画する
    g.drawImage(SwingFXUtils.toFXImage(outImage, null), 0, 0);

}


これで、起動できるのは、CommandIFの実装によるところです。
詳細はこちらの記事に記載しております。

大まかな作りは、下のような感じです。

OpenCVの処理

ようやく、本題に入ります。
OpenCVでの基本ということで、まずは「画像を描画する」というところに焦点を置き実装しました。
Javaでの実装の場合はテクノロジー的には以下のものを使用します。

  1. java.awt(描画関連API)
  2. java.io(入出力関連API)
  3. opencvライブラリ

具体的には、以下の通りです。番号で上のリストと対応しています。

  1. BufferedImage(java.awt.image)
  2. ByteArrayInputStream(java.io)
  3. Imagcodecs(org.opencv.core.imagecodecs)

描画処理の部分

  1. イメージをロード(読み込み)してMatクラスを取得
  2. javafxの部品を使用してCavasよりGraphicContextを取得
  3. Mat(画像データ)を描画しています。

早い話が、イメージファイルを読み取ったら、それをCanvasに書き込んでいる処理です。

上のキャプチャはキャンバスサイズが読み込んだ画像よりも小さかったので、表示した時に中途半端なことになりました。

画像サイズは中途半端ですが、この処理で描画したMatクラスが画像データになります。このクラスの変数名は「img」としました。

読み取った画像を編集する為には、imgを変換してやる必要があります。それは、OpenCvのライブラリに定義してあるメソッドを使用してやれば可能です。

つまりは、メソッドを呼び出した後の返り値が変換後の画像データになります。

あまり詰め込んでも仕方ないので今回はここまでにします。
あー頭イタイ。。。

Java OpenCV 〜学習準備のまとめ〜

こちらのサイトを参考にしてOpenCVの基本から学習していこうと思いました。何度かOpenCvのプロジェクト(Eclipseで)作成していますが、その手順をまとめておこうと思いました。

改めてOpenCvを学習しようと思ってます、輪郭取得もよくわからなかったからです。。。

インストールに関しては、こちらの記事に記載しています。
ライブラリのダウンロードはこちらです。

まずは、必要なライブラリをダウンロードします。

javacv1.5.3の場合

下のようなライブラリを読み込むコードがいらなくなったようです。

   /** ネイティブライブラリを読み込む */
    static {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    }


これをつけたままだと下のようなエラーが出ます。

Exception in thread "JavaFX Application Thread" Exception in thread "main" java.lang.UnsatisfiedLinkError: no opencv_java430 in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
    at java.lang.Runtime.loadLibrary0(Runtime.java:871)
    at java.lang.System.loadLibrary(System.java:1124)
    at zenryokuservice.opencv.fx.Main.<clinit>(Main.java:46)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplicationWithArgs$2(LauncherImpl.java:352)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$7(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl.lambda$null$5(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$6(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$3(WinApplication.java:177)
    at java.lang.Thread.run(Thread.java:748)
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:767)
Caused by: java.lang.NullPointerException
    at com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:383)
    at com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:328)
    ... 5 more


しかし、ライブラリ参照は必要なことで、設定方法が間違っていました。参考サイトはこちらです。

そして、実行する事は以下になります

  1. Window -> Preference(設定)
  2. opencvXXX.jarとネイティブライブラリのフォルダを設定します。

これで、OpenCvを動かす準備完了です。そんなに手間ではありません。

そして、チュートリアル(Java版)を見ると、JavaFXを使用しています。C言語やPythonでは、HighGUIを使用していましたが、Javaでは、既に画面描画API(JavaFX)を使用します。

学習開始

今までの記事でOpenCVを学習をしていました。しかし、コードを写経するだけで、理解には及びませんでした。これはウェブサイトにある内容だけで理解出来なかったので、書籍を読み進めました。

つまるところ、サンプルコードを実行、描画、処理結果の意味を理解。。。の様に3行程を踏む必要があると思って、JavaFXの画面にテキストボックスを追加、入力テキストの文字によって、起動するOpenCvのサンプルコードを切り替えて実行出来る仕組みを作りました。下のリンクがそうです。

  1. Java OpenCV 〜背景除去、輪郭を学習する準備1: SceneBuilder〜
  2. Java OpenCV 〜背景除去、輪郭を学習する準備2コントロラー追加〜
  3. Java OpenCV 〜背景除去、輪郭を学習する準備3:画像を表示する〜
  4. Java OpenCV 〜背景除去、輪郭を学習する準備4:コマンドで起動する実装〜

結論、書籍の中を流し読みではなくキチンと読む事で解決に向かうことができました。実際に学習中です。

以上で、学習準備ができたので、チュートリアルで実行するコードとその意味を理解しながら学習を進められると思います。お互い頑張りましょう!

余談ですが、こちらのやってきたことをまとめると以下のようになります。

準備の概要

  1. SceneBuilderを用いて、簡単に画面の土台を作成する
  2. 作成した土台を元に、コントローラーを追加
  3. 起動確認を兼ねて画像を表示する
  4. コントローラーから、コマンド的に新規に作成するプログラムを起動できる仕組みを作る(Java Basic 〜インターフェースの扱い方2〜)

最後のインターフェースの扱いに関しては、別に記載しました。話がややこしくなるからです。

プログラムの解説

githubに作成したプログラムをアップロードしてあります。

この階層にある「Main.java」クラスがこのアプリを起動するクラスになります。

Main.java

/** ネイティブライブラリを読み込む */
static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
/**
 * メインメソッド。
 * @param args
 */
public static void main(String[] args) {
    // 親クラス(Superクラス)のメソッド起動
    launch();
}

/* (non-Javadoc)
 * @see javafx.application.Application#start(javafx.stage.Stage)
 */
@Override
public void start(Stage primaryStage) throws Exception {
    primaryStage.initStyle(StageStyle.TRANSPARENT);
    FXMLLoader loader = new FXMLLoader(ClassLoader.getSystemResource("TestingCv.fxml"));
    BorderPane root = (BorderPane) loader.load();
    Scene scene = new Scene(root, 800, 600);
    scene.setFill(null);
    scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());

    // 作成するクラス
    final TestingCvController controller = loader.getController();
    controller.setPane(root);
    primaryStage.setOnCloseRequest((new EventHandler<WindowEvent>() {
        public void handle(WindowEvent we) {
            controller.setClosed();
        }
    }));
    xPos = 200;
    yPos = 200;
    // キーアクションを追加する
    scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {
            if (event.getEventType() == KeyEvent.KEY_PRESSED) {
                try {
                keyHandle(event.getCode(), root, primaryStage, controller);
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
        }
    });
    primaryStage.setTitle("Video Processing");
    primaryStage.setScene(scene);
    primaryStage.show();
}


Mainプログラムの主な部分を抜粋しました。
シンプルに、mainメソッドからアプリケーションを起動します。※バージョンにより不要になる

/** ネイティブライブラリを読み込む */
static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

このstaticで囲んだ部分がJavaを起動した時に(はじめに)読み込まれます。これで、OpenCVのライブラリを読み込みます。

そして、以下のコードでFXMLをロード(読み込み)して作成した画面の土台を取得。

FXMLLoader loader = new FXMLLoader(ClassLoader.getSystemResource("TestingCv.fxml"));
BorderPane root = (BorderPane) loader.load();
Scene scene = new Scene(root, 800, 600);
scene.setFill(null);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());

// 作成するクラス
final TestingCvController controller = loader.getController();
controller.setPane(root);
primaryStage.setOnCloseRequest((new EventHandler<WindowEvent>() {
    public void handle(WindowEvent we) {
        controller.setClosed();
    }
}));

今回使用したFXMLファイルはTestingCv.fxmlです。

読み込んだFXMLで定義しているコントローラーTestingCvControllerクラスを取り出しています。このコントローラで

final TestingCvController controller = loader.getController();

SceneBuilderで作成した土台にあるコンポーネントのオブジェクトをコントローラークラスで管理します。
具体的には。。。

コントローラー

SceneBuilderで作成した画面が下の画像です。

この土台には、下のようなコンポーネントを追加しています。

そして、洗濯している部分は。。。

上のようにフォーカスされます。

こんな感じで、追加したコンポーンネントがコントローラーで操作することができます。

TestingCvController.java

public class TestingCvController {

    @FXML
    private Canvas testCanvasBefore;
    @FXML
    private Canvas testCanvasAfter;

    @FXML
    private TextField input;

    private Properties prop;

    private CommandIF cmd;

    private Pane pane;

    /** コンストラクタ */
    public TestingCvController() {
        this.testCanvasBefore = new Canvas();
        this.testCanvasAfter = new Canvas();
//      this.testing = new LearnOpenCv();
        this.prop = new Properties();
        String propPath = "/command.properties";
        try {
            this.prop.load(this.getClass().getResourceAsStream(propPath));
        } catch (IOException e) {
            System.out.println(">>> Error! プロパティファイルの読み込みに失敗しました。" + propPath);
            e.printStackTrace();
        }
        // 確認
        System.out.println("プロパティ: " + prop.get("hello"));
    }

    /**
     * 画面のExecuteボタンを押下した時に起動する処理
     */
    @FXML
    protected void clickExecute() throws Exception {
        // 初期化する
        cmd = null;
        // 入力確認用
//      System.out.println(this.input.getText());
        String inputStr = this.input.getText();
        cmd = this.getCommand(inputStr);
        if (cmd == null) {
            // プロパティファイルに、コマンドがない
            System.out.println("コマンドがあません。" + this.input.getText());
        } else {
            // コマンド実行
            cmd.execute(this.pane);
        }
    }

    @FXML
    public void setClosed() {
        // 現状は空実装
    }

    /**
     * Clearボタンを押下した時の処理
     */
    @FXML
    public void clear() {
        System.out.println("Clear");
        // 描画したものをクリアする
        this.testCanvasBefore.getGraphicsContext2D().clearRect(0, 0, this.testCanvasBefore.getWidth(), this.testCanvasBefore.getHeight());
        // 描画したものをクリアする
        this.testCanvasAfter.getGraphicsContext2D().clearRect(0, 0, this.testCanvasAfter.getWidth(), this.testCanvasAfter.getHeight());
    }

    @FXML
    private void terminated() {
        System.exit(0);
    }

    public void setPane(Pane pane) {
        this.pane = pane;
    }
}

上のコードでcode>@FXMLアノテーションのついているフィールドは以下のものです。

@FXML
private Canvas testCanvasBefore;
@FXML
private Canvas testCanvasAfter;

@FXML
private TextField input;


TestingCV.fxml

<TextField fx:id="input" promptText="App No" />
<Canvas fx:id="testCanvasBefore" height="200.0" width="200.0" />
<Canvas fx:id="testCanvasAfter" height="200.0" width="200.0">


上記の部分が上のクラスにあるcode>@FXMLをつけたフィールド変数と対応しています。

同様に。。。

<Button mnemonicParsing="false" onAction="#clickExecute" text="Execute" />
<Button mnemonicParsing="false" onAction="#clear" text="Clear" />
<Button mnemonicParsing="false" onAction="#terminated" text="Exit" />


の部分はcode>@FXMLのついたメソッドに対応しています。

動かして見ると、下のような感じです。(コーディング〜なので最後の方に表示した画面があります)



Java OpenCV 〜Error: Empty JPEG image〜

下のようなエラーメッセージが出ました、

Empty JPEG image (DNL not supported) in function 'throwOnEror'

javaFXでのCanvasに「バイク.jpg」を描画しようとした時です。

ここのサイトによると、スマホで撮影した画像は変換が必要なようで。。。

結局自分は、スマホで撮ったファイルは使用しない方向に切り替えました(笑)

以前、ファイルをレンタルサーバーにアップロードするときもスマホの写真がエンコードできなくてつまづきました。

変換ツールがいるんだなぁ。。。

でわでわ。。。



Java OpenCV 〜輪郭:輪郭検出処理の調査〜

今回は、とりあえずは、輪郭を取得するfindContours()メソッドを使用して輪郭の情報を取得します。

前回は、画像データの中身を見て画面の白い部分を透明にする処理を実装しました。

なので、作成したアプリの改造をして背景をJavaFXに出力します。

今回の表示する画像は自分の顔です(笑)
具体的には下のような感じです。

この表示した画像の背景部分が顔の輪郭から外の部分。。。

しかし、今回は背景も、顔の内側も全て「白」なので前回のように色指定をして「この色の時はアルファ値を透明にする」というようなことができません。。。

とりあえず輪郭を表示

調べて見ると[findContours(http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html)]()を使用して輪郭を取得することができるようです。

とりあえずは実行して見る!

なんかチビクロサンボみたいだな。。。

定数:RETR_TREE

とりあえずは輪郭の表示ができているようです。
この輪郭の表示(データの書き込み)に関しては下のコードで行なっています。
Imgproc.findContours(gray, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);

定数:RETR_EXTERNAL

上の赤文字の部分をImgproc.RETR_EXTERNALに変更すると真っ黒になります(笑)

定数:RETR_LIST

同様にRETR_LISTに変更

定数:RETR_CCOMP

調べて見た定数を使用して表示した白い部分が輪郭として検出されている部分になります。
つまり、輪郭部分の座標が取得できているということです。

第三引数を変えて見る

// CHAIN_APPROX_NONE
// CHAIN_APPROX_SIMPLE
// CHAIN_APPROX_TC89_L1
// CHAIN_APPROX_TC89_KCOS


結局のところは

よくわかりませんでした。というか輪郭部分を取得(白色)して表示した状態です。しかし肝心の輪郭に関してはいまいちな状態です。ちょいと調査します。。。

でわでわ。。。



OpenCVエラー 〜contours.cpp:199: error〜

下のようなエラーが出ました。

opencv/modules/imgproc/src/contours.cpp:199: error: (-210:Unsupported format or combination of formats) [Start]FindContours supports only CV_8UC1 images when mode != CV_RETR_FLOODFILL otherwise supports CV_32SC1 images only in function 'cvStartFindContours_Impl'

ここで改めてログを見て見ると

[Start]FindContours supports only CV_8UC1 images

TRY1

とあるので、ソースを下のようなものから(テスト中で。。。)

Mat charactor = Imgcodecs.imread(url.getPath(), Imgcodecs.IMREAD_UNCHANGED);
// 背景除去
Mat hierarchy = Mat.zeros(new Size(200, 200), CvType.CV_8UC1);
List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Imgproc.findContours(charactor, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_TC89_L1);
Mat dstContuor = Mat.zeros(new Size(charactor.width(),charactor.height()),CvType.CV_8UC3);
Scalar color=new Scalar(255,255,255);  
Imgproc.drawContours(dstContuor, contours, -1, color,1); 

描画モード(引数に渡している定数を「CvType.CV_8UC1」統一しました。

しかし、また。。。

opencv/modules/imgcodecs/src/loadsave.cpp:925: error: (-215:Assertion failed) code in function 'imencode'

Try2

調べて見ると、画像はグレースケースでないとけ内容だ。。。

読み取りを下のように修正

Mat charactor = Imgcodecs.imread(url.getPath(), Imgcodecs.IMREAD_GRAYSCALE);
Mat gray = new Mat();
Imgproc.cvtColor(charactor, gray, Imgproc.COLOR_BGR2GRAY);

駄菓子菓子またもエラー

Invalid number of channels in input image:
'VScn::contains(scn)'
where
'scn' is 1
]

こんな感じのエラー。。。

TRY3

色々と試して見るが、どれもエラーになる。。。
ソースを少し見直して見た!

// 表示イメージを読み取る
Mat charactor = Imgcodecs.imread(url.getPath(), Imgcodecs.IMREAD_COLOR);
Mat gray = new Mat();
Imgproc.cvtColor(charactor, gray, Imgproc.COLOR_BGR2GRAY);
// 背景除去
Mat hierarchy = Mat.zeros(new Size(200, 200), CvType.CV_8UC1);
List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Imgproc.findContours(gray, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);
Mat dstContuor = Mat.zeros(new Size(charactor.width(),charactor.height()),CvType.CV_8UC1);
Scalar color=new Scalar(255,255,255);  
Imgproc.drawContours(dstContuor, contours, -1, color,1); 

//      Size resize = new Size(before.getWidth() / 3, before.getHeight() / 3);
//      Mat reSizeChar = new Mat();
//      Imgproc.resize(charactor, reSizeChar, resize);
MatOfByte charaByteBefore = new MatOfByte();
// 表示イメージをcharaByteに書き込む
Imgcodecs.imencode(".png", dstContuor, charaByteBefore);

Mat dst = new Mat();
//      Imgproc.threshold(reSizeChar, dst, 100, 255, Imgproc.THRESH_BINARY);
MatOfByte charaByte = new MatOfByte();

// この行で落ちていた。。。。
☆Imgcodecs.imencode(".png", dst, charaByte);

上の☆マーク部分で落ちていたのですが、これは不要な処理でした。。。(色々と試した時の残骸です(笑))

そんなわけで実行することができました。

変更前は下のような感じです。ハナっからグレースケール(笑)

でわでわ。。。