はじめに
ちょっと「似ている画像を検出する」ということについて調べる機会があり、その過程で動画について考えていました。
- 動画から画像を抽出する
- 画像を比較する
\(^o^)/
というシンプルな考えが浮かび、ちょっと作ってみることにしました。
** 開発中です**
環境
Windows上でJava(Java9)、IDEはEclipseを使用します。
GUIの作成はScene Builder、つまりJavaFXです。
- Windows7 64bit
- OpenCV 3.3.0
- Eclipse4.7.1(pleiades ulitimateを利用)
- e(fx)clipse
- Scene Builder 9.0
OpenCVをインストールするとjarとdllが作成されます。
それぞれ読み込める場所に配置しておきます。
-
opencv/build/java/x64/opencv_java330.dll
-
opnecv/build/bin/opencv_ffmpeg330_64.dll
→ Windows\System32\ 下などにコピー
- opencv/build/java/opencv-330.jar
→ クラスパス下にコピー、Eclipseから追加
さらにJavaでは以下のようにOpenCVを使うクラスでロード処理を記述する必要がありました。
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
動画から画像を抽出する
動画からいくつかのフレームを画像として抜き出します。
ここでは9枚の画像を抜きだすことにしました。根拠は特にありませんが、3×3でGUIで並べたときにキリがよさそうだという判断です。
最初と最後のフレームは暗転していることが多そうなので、10分割した区切りの9フレームを保存します。
-->FPS違いの検出…?
また、抽出した画像はすべて横幅が200pxとなるように拡大縮小してから保存します。
-->サイズ違いの検出…?
↓こんな感じになりました。
static public VideoMetadata captureFrame(Config config, String videoFile) {
// ------------------------------------------------------------------
// 動画を開く
// ------------------------------------------------------------------
System.out.println(videoFile);
VideoCapture capture = new VideoCapture(videoFile);
if (!capture.isOpened()) {
System.out.println("file not opened.");
return null;
}
String outputDir = config.getTempDir() + "/" + String.valueOf(videoFile.hashCode()).replace("-", "_");
File dir = new File(outputDir);
if (!dir.exists()) {
dir.mkdir();
}
// ------------------------------------------------------------------
// 動画情報の取得
// ------------------------------------------------------------------
double fps = capture.get(Videoio.CV_CAP_PROP_FPS);
int resizeHeight = (int) (config.getImageWidth() / capture.get(Videoio.CV_CAP_PROP_FRAME_WIDTH)
* capture.get(Videoio.CV_CAP_PROP_FRAME_HEIGHT));
double allFrameCnt = capture.get(Videoio.CV_CAP_PROP_FRAME_COUNT);
int captureInterval = (int) (allFrameCnt / (config.getCaptureCount() + 1));
long time = (long) (allFrameCnt / fps);
// ------------------------------------------------------------------
// 設定で指定した数のフレームを取得する
// ------------------------------------------------------------------
for (int i = 1; i < config.getCaptureCount() + 1; i++) {
int frameIndex = captureInterval * i;
Mat orgFrame = new Mat();
Mat resizedFrame = new Mat();
capture.set(Videoio.CV_CAP_PROP_POS_FRAMES, frameIndex - 1);
if (!capture.read(orgFrame)) {
continue;
}
Imgproc.resize(orgFrame, resizedFrame, new Size(config.getImageWidth(), resizeHeight));
Imgcodecs.imwrite(outputDir + "/" + i + ".jpg", resizedFrame);
}
VideoMetadata metadata = new VideoMetadata();
metadata.setFilename(videoFile);
metadata.setFrameDirname(outputDir);
metadata.setPlayTime(time);
return metadata;
}
画像を比較する
前項で1動画あたり、9枚の画像をしました。
今回はこれを比較します。
画像の比較ですが、ヒストグラムを比較することにしました。
これについては検討の余地が多いと思います。
9枚それぞれの比較結果の平均を動画全体の比較結果としました。
コードは以下の通りです。
static public VideoComparison compareImages(Config config, VideoMetadata video1, VideoMetadata video2) {
List<Double> histList = new ArrayList<Double>();
// ------------------------------------------------------------------
// 画像毎に比較
// ------------------------------------------------------------------
for (int i = 1; i < config.getCaptureCount() + 1; i++) {
String filename1 = video1.getFrameDirname() + "/" + i + ".jpg";
String filename2 = video2.getFrameDirname() + "/" + i + ".jpg";
File file1 = new File(filename1);
File file2 = new File(filename2);
if (!(file1.exists() && file2.exists())) {
continue;
}
// ------------------------------------------------------------------
// ヒストグラム
// ------------------------------------------------------------------
Mat img1 = Imgcodecs.imread(filename1);
List<Mat> src1 = new ArrayList<Mat>();
src1.add(img1);
Mat hist1 = new Mat();
Imgproc.calcHist(src1, new MatOfInt(0), new Mat(), hist1, new MatOfInt(256), new MatOfFloat(0, 256));
Mat img2 = Imgcodecs.imread(filename2);
List<Mat> src2 = new ArrayList<Mat>();
src2.add(img2);
Mat hist2 = new Mat();
Imgproc.calcHist(src2, new MatOfInt(0), new Mat(), hist2, new MatOfInt(256), new MatOfFloat(0, 256));
histList.add(Imgproc.compareHist(hist1, hist2, 0));
}
VideoComparison videoComparison = new VideoComparison();
videoComparison.setVideo1(video1);
videoComparison.setVideo2(video2);
videoComparison.setHist(calcAvg(histList));
return videoComparison;
}
GUIにしてみる
画像を扱うのでGUIにしたいところです。
悩みましたが、Java9リリース記念でJavaFXで作ることにしました。(JavaFXは8ですが…)
JavaFXではデザインレイアウトをXML(fxml)で記述でき、これをグラフィカルに編集できるのがScene Builderです。
XMLとコードの紐付けは、XML内のfx:id属性と.javaの@FMXLアノテーションで行います。
例えば実行ボタンのコントロールを以下のように配置した場合は、
<Button fx:id="executeBtn" layoutX="214.0" layoutY="31.0" mnemonicParsing="false" onAction="#doExecuteAction" prefHeight="38.0" prefWidth="115.0" text="execute" />
Javaコード側では以下のようにします。
@FXML
void doExecuteAction(ActionEvent event) {
// ...
}
これでボタンを押した場合に実行される処理がアタッチできます。
今回使用するコントロールとアクションは以下です。
-
Button
-
実行
-
追加
-
削除
-
-
ListView
-
TableView
- 選択
-
ImgView
最後に
久しぶりにJavaでコードを書きましたが、パーソナルユーズではめんどくさい面が多いですね。
実は最初はPythonで書いていたのですが、GUIにしたかった、スレッドとかそのあたりでどうしようかな…→JavaでもOpenCVできるじゃん。ってことでJavaにしました。
JavaでGUIするのはAWT以来な気がしますがJavaFXは思ったより良さげでした。
残件は以下あたりですね。
-
ファイルが多いと時間がかかる
-
parallelStreamで処理していますが、処理時間面でのチューニングの余地がありそう。
-
処理状況の可視化
-
プログレスバーの実装
-
処理状況ログの表示
-
-
-
画像の比較の精度向上
-
特徴量による比較。
-
その他…?
-
休日プログラミングたのしかったです。おわり。
参考
クライアント・テクノロジ: Java Platform, Standard Edition (Java SE) 8リリース8
JavaFX Scene Builder 1.x Archive