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担っていないのが気になりますが、今回はここら辺で

でわでわ。。。