OpenCV tutorial 解析 〜ヒストグラム〜

イントロダクション

前回は、チュートリアルを読んでもよくわかならなかったので。。。

必殺の解析パターンをやろうという事になりました。(自分の中で)

  1. 写経する
  2. 動かす
  3. 中身を理解する

蒸気が解析パターンです。よくわからないものを触ったりして調べるのに似ていますね(笑)。そして、前回は上記工程の1と2を行いましたので、まぁ動いたかな?という感じです。ちょっと中途半端な表現ですがこれにはわけがあります。

上の動画が作成したものを動かしたときのものですが「Show logo」のチェックボックスをONにするとエラリます。

この部分の解決も含めてチュートリアルの記載とコードを解析して理解をしようというわけです。

参照するチュートリアルはこちらです。

解析開始

まずは、Mainメソッドからいきますので、「Tutorial2.java」からみていきます。

/**
 * Copyright (c) 2012-present Lightweight Java Game Library All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * Neither the name Lightweight Java Game Library nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 */
package zenryokuservice.opencv.fx.tutorial;

import org.opencv.core.Core;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;

/**
 * OpenCVのチュートリアルを写経します。
 * @author takunoji
 * 2019/02/02
 */
public class Tutorial2 extends Application {
	/** ネイティブライブラリを読み込む */
	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 {
		FXMLLoader loader = new FXMLLoader(getClass().getResource("Video.fxml"));
		BorderPane root = (BorderPane) loader.load();
		Scene scene = new Scene(root, 800, 600);
		scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());

		primaryStage.setTitle("Video Processing");
		primaryStage.setScene(scene);
		primaryStage.show();
		// 作成するクラス
		VideoController controller = loader.getController();
		primaryStage.setOnCloseRequest((new EventHandler() {
			public void handle(WindowEvent we) {
				controller.setClosed();
			}
		}));
	}
}

今までにJavaFXを作成したりしていたので、細かいところは省きます。

「start()」メソッドが走りFXMLLoaderにより、FXMLが読み込まれます。

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.text.*?>
<BorderPane xmlns:fx="http://javafx.com/fxml" fx:controller="zenryokuservice.opencv.fx.tutorial.VideoController">
<center>
   <ImageView fx:id="currentFrame" />
   </center>
   <right>
      <VBox alignment="CENTER_LEFT" spacing="10">
         <padding>
            <Insets left="10" right="20"/>
         </padding>
         <ImageView fx:id="histogram" />
         <Text text="Controls" />
         <CheckBox fx:id="grayscale" text="Show in gray scale" />
         <CheckBox fx:id="logoCheckBox" text="Show logo" onAction="#loadLogo" />
      </VBox
   </right>
   <bottom>
      <HBox alignment="CENTER" >
         <padding>
            <Insets top="25" right="25" bottom="25" left="25"/>
         </padding>
         <Button fx:id="button" alignment="center" text="Start camera" onAction="#startCamera" />
      </HBox>
   </bottom>
</BorderPane>

「fx:controller」に指定されている「VideoController」クラスにあるフィールド変数がこのタグの中に記載されている「fx:id」に対応しています。

名前を間違うとエラーが出ます。

ついでに、SceneBuilderで「FXML」を開いてみます。

表示されたのはチュートリアルにあるものと同じです、でも実際にアプリを起動したときには下のような感じです。

どうやら問題ないようですが、ちょっと納得がいきません。なのでコードを見ますと。。。

// フレームサイズ指定
this.currentFrame.setFitWidth(600);

これでFXMLにある「fx:id="currentFrame"」の部分(赤くなっています)に幅を600に設定しているようです。つまりSceneBuilderに表示されたときはサイズが0になっていたので下のような表示になっていた、という予測ができます。まぁ検証はしないのでここで終わりにします。時間があればFXMLにある「currentFrma」に横幅を与えて表示させてみると結果が見れると思います。

ちなみに「Show logo」のチェックボックスをクリックするとエラーが出る原因はファイルの指定が間違っていたからでした。チュートリアルは「resources」フォルダの直下にイメージファイルを置いていましたが、自分のところは違う場所に置いていたので。。。

チュートリアルを読み進める

実行してみると記載内容が驚くほど簡単にわかります。(実行した後だから当然といえば当然か(笑))

まとめると、JavaFXで画面を表示するのにFXMLとControllerクラスの関連付けを行なった後に。。。

ロゴを表示する

チェックされると、カメラストリーム上に画像の表示をトリガーします

この文言は翻訳したものです、「チェックされると」とあるのでその部分のコードを見ます。

fx:id="logoCheckBox" text="Show logo" onAction="#loadLogo"

とFXMLに記載があるので「VideoController#loadLogo」メソッドを見ます。※VideoControllerクラスのloadLogoメソッド

シンプルにロゴのイメージファイルを読んでフィールドに設定する処理でした。

this.logo = Imgcodecs.imread("resources/images/Poli.png");

これだけでなんで下にロゴが表示されるのでしょうか?

「startCamera」ボタンが謳歌された後に、キャプチャクラスの設定をしてRunnableインターフェースを実装しています(匿名クラス)というやつです。)

// フレームを30ミリ秒ごとに掴む (30 frames/sec)
Runnable frameGrabber = new Runnable() {
	@Override
	public void run() {
		Mat frame = grabFrame();
		Image imageToShow = Utils.mat2Image(frame);
		updateImageView(currentFrame, imageToShow);
	}
};

こいつをTimerクラスでスケジュールしてやるとそのように動いてくれるようです。※Timerクラスの中身はわからないけどそのように動いているので。。。

この実装はいろんなところで使えそうです。自分で無限ループの処理を書かなくてもこのクラスに登録(2行目の処理)をしてやれば、そのように動いてくれるようです。

this.timer = Executors.newSingleThreadScheduledExecutor();
this.timer.scheduleAtFixedRate(frameGrabber, 0, 33, TimeUnit.MILLISECONDS);

OpenCVの操作に入る

ストリームの特定の領域にロゴを表示するために、コードにいくつかのバリアントを追加します。つまり、フレームキャプチャごとに、画像を1つまたは3つのチャンネルに変換する前に、ロゴを配置するROI(関心領域)を設定する必要があります。通常、画像のROIはその一部です。ROIをRectオブジェクトとして定義できます。Rectは、次のパラメータで記述された2D矩形のテンプレートクラスです。

loadLogoメソッドの処理での説明文です。画面上では確かに右下部分にロゴが表示されています。

そして、this.logo(フィールド変数)はVideoController#grabFrame()メソッドで使用されていて、なにやら処理を行っていました。ちなみに「grabFrame()」は上記の「Runnable」インターフェースを実装した匿名クラスで呼ばれていて、カメラが起動したら無限ループがスタートします(33(30)ミリ秒ごと)。

下の処理部分でROI(イメージファイルの表示領域)を指定しているのですが、

Rect roi = new Rect(frame.cols() - logo.cols()
     , frame.rows() - logo.rows(), logo.cols(),logo.rows());

「frame」 のcolsとrowsはキャプチャを読み込んでそのサイズが入っているので以下のようになります。

デバックして見た結果、キャプチャ全体のサイズからロゴのサイズを引き算した領域(Rectクラス)をキャプチャの領域に載せたように見せるための計算処理を行います。

今の所、ロゴのサイズがやたら小さいのがおかしいので「なんで?」と疑問に思っているのですが、このドキュメントを見て「addWeighted」の処理内容を調べましたが、よくわからなかった。。。

とりあえずは、画像の上に指定領域(Rect)部分を乗せることができるようだ。

そして、今回はここまでにします。(頭から煙が出てきたようで。。。)

でわでわ。。。