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 < 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. 統計ベースの背景モデルを作成する

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

でわでわ。。。


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



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

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

基本1

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

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

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

とりあえずはその通りに、実装してみます。
作成したクラスは下のクラスです。準備で実装したCommandIFを実装(implements)したクラスを作成します。

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

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

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

@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)

描画処理の部分

イメージをロードしてMatクラスを取得した後にはjavafxの部品を使用してCavasよりGraphicContextを取得して
Mat(画像データ)を描画しています。

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

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



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:コマンドで起動する実装〜

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

準備の概要

  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


結局のところは

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

でわでわ。。。



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);

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

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

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

でわでわ。。。



Java OpenCV 〜背景除去、輪郭を学習する準備4:コマンドで起動する実装〜

画面に入力した文字列(コマンド)で起動する処理をプログラムを変更しないでも追加できるように実装を変更します。

現状の実装

  1. Executeボタンを押下すると処理が動く
  2. 画面の上部中央にあるテキストふフィールドの値を取得
  3. 画面の下にあるClearボタンで表示したイメージを消す

シンプルに、ボタンを押下→Canvas部分にイメージを描画

という処理を行っています。Clearボタンを押下するとCanvasに描画したイメージがクリアされて何もなくなります。

次の実装

実装した時の動画を作成しました。

行っていることは、下の通りです。

  1. CommandIF(インターフェースクラス)を作成
  2. LearnOpenCvクラスにCommandIFを実装(impllements)
  3. 起動確認

Githubにて、実装の差分を見ることができます。

とりあえず、ここまでの実装では、動き的にはじめのものと変わりません。そのように作りました。
ポイントとしては、CommandIFを追加したので、CommandIFを実装したクラスなら、なんでも実行することができるようになったというところです。

クラスを動的に変更して見る

ここで追加するのが、Propertyファイルです。
処理内容としては下のような手順です。

  1. Propetiesクラスを使用してプロパティファイルを読み込む
  2. キー(コマンド)がプロパティファイル内にないときはエラーメッセージをコンソールに出力
  3. キー(コマンド)で起動するクラスの完全名を取得する
  4. 完全クラス名でクラスのインスタン種を取得する
  5. 取得したクラスはCommandIFを実装しているのでexecute()を実行する
  6. 結果をGraphicsContextクラスに描画する→JavaFXのCanvasに描画される

こんな感じです。

実行した結果は動画にしました。余計な音が入っています。

前半

後半

次回

OpenCVでの学習を開始します。ようやく準備ができました。

でわでわ。。。