Java
OpenCV
JavaFX

JavaとOpenCVで似ている動画を検出するrev.1

はじめに

ちょっと「似ている画像を検出する」ということについて調べる機会があり、その過程で動画について考えていました。

  1. 動画から画像を抽出する
  2. 画像を比較する

\(^o^)/

というシンプルな考えが浮かび、ちょっと作ってみることにしました。
** 開発中です**

画面イメージ
image

環境

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で処理していますが、処理時間面でのチューニングの余地がありそう。
    • 処理状況の可視化

      • プログレスバーの実装
      • 処理状況ログの表示
  • 画像の比較の精度向上

    • 特徴量による比較。
    • その他…?

休日プログラミングたのしかったです。おわり。

参考

OpenCV: OpenCV modules

クライアント・テクノロジ: Java Platform, Standard Edition (Java SE) 8リリース8

JavaFX Scene Builder 1.x Archive

デザイナを使用してEclipseでJavaFX8開発をする - Qiita

bitWalk's: JavaFX: CheckBox 付き ListView