OpenCV記事一覧

イントロダクション

OpenCvというライブラリを使用してJava言語での画像処理を学んだ時の記事を一覧化しました。

OpenCvとは

画像処理を行う為のプログラムがセットになっているプログラムの集まりです。この様なプログラムの集まりの事を「ライブラリ」と呼びます。

どんなもの?

写真や動画を加工して文字や四角を画像の中に書き込んだり出来ます。

イメージとしては、下の様に写真の中に四角を表示して、写真内の写ってる人やモノを示すようなプログラムの塊をライブラリとして持っていります。

つまりは、OpenCVを使うと画像処理が(1から作るより)簡単にできるというわけです。

こんな感じのコードです。Javaプログラムでの実装の場合は、JavaFXを使用して画面表示を行うのでJavaFXの理解も必要になりますが、サンプルコードがあるので、それを書き写して実行すればOKです。

// optimize the dimension of the loaded image
Mat padded = this.optimizeImageDim(this.image);
padded.convertTo(padded, CvType.CV_32F);
// prepare the image planes to obtain the complex image
this.planes.add(padded);
this.planes.add(Mat.zeros(padded.size(), CvType.CV_32F));
// prepare a complex image for performing the dft
Core.merge(this.planes, this.complexImage);

// dft
Core.dft(this.complexImage, this.complexImage);

// optimize the image resulting from the dft operation
Mat magnitude = this.createOptimizedMagnitude(this.complexImage);

Java OpenCv Lv1 〜入門: 写真の表示〜
Java OpenCV Lv2 〜JavaFXでの画像表示〜
Java OpenCV Lv2 〜画像を表示する〜
Java OpenCV Lv3 〜画像の平滑化(smooth())〜
Java OpenCV Lv3 〜画像にガウシアンフィルタ(GaussianBlur())〜
Java OpenCV Lv3 〜画像に中央値フィルタ(medianBlur())〜
Java OpenCV Lv4 〜画像の中身をみてみる〜
Java OpenCV Lv5 〜Matクラスで描画処理〜
Java OpenCV Lv7 〜MatクラスでEllipseしてみる〜
Java OpenCV Lv8 〜ROIについて〜
Java OpenCV Lv9 〜画像編集「足し算」(cvAdd)〜
Java OpenCV Lv9 〜画像編集「引き算」(cvSubtract)〜
Java OpenCV Lv9 〜画像の掛け算〜
Java OpenCV Lv10 〜行列演算Mat#submat()〜
Java OpenCv Lv10〜画像の平均輝度をだす〜
Java OpenCv ビデオキャプチャ〜カメラからの入力を表示〜
OpenCV tutorial〜ヒストグラム〜
OpenCV tutorial 解析 〜ヒストグラム〜
Android OpenCV 〜サンプルアプリを動かす〜

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の本を読んでみると「背景除去の弱点」という形で記載されていました。

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

シーンのモデル化

背景と前景を定義するところから記載されていました。
まとめると、高度な「シーンのモデル」が必要ということです。

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

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

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

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

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

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

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

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

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

そして、所定の線の上にあるピクセルが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. 統計ベースの背景モデルを作成する

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

でわでわ。。。

数理モデル関連

第二弾

第三弾

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」のいずれでもありません



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

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

基本1

参考サイトによると以下の内容を学びます。

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

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

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

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

CommandIF(インタフェース)を使用したポリモーフィズムの実行動画があります。

使用している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に書き込んでいる処理です。

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

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

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

こちらのサイトを参考にして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を学習する準備をしていました。

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

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

そして、ポリモーフィズムの実装については以下のような形になります。

CommandIF(インタフェース)を使用したポリモーフィズムの実行動画があります。

準備の概要

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

プログラムの解説

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


結局のところは

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

でわでわ。。。