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



引き続き手を加えています。

[JavaとOpenCVで似ている動画を検出するrev.1 - Qiita](https://qiita.com/estaro/items/c9df99f7ef8413537019)

[JavaとOpenCVで似ている動画を検出するrev.2 - Qiita](https://qiita.com/estaro/items/2312fdc45787efeab7da)


# 結果のキャッシュ

一度処理をしたファイルについては結果を保存することによって、2回目にOpenCVにより画像処理が行われないようにしました。

## Matを保存する

画像のMatはimwriteで画像として保存すればよいのですが、ヒストグラムの計算結果などはimwriteで保存してimreadで読み込むことができませんでした。

Matを汎用的にファイルに保存するメソッドはC++にはあるのですが、Javaでラップされたものがないため、自分で作成する必要があるようです。

MatというのはMatrix、つまり行列のデータなので、縦、横、それにデータの配列で表現できることになります。Matの場合はそれに加えて、データの型をあらわす種別コードを持つことになります。

今回の目的ではデータ量はたいしたことがないので、jsonデータとして保存することにします。
jsonの扱いにはgoogleのgsonというライブラリを使用しました。

保存部分です。

```java
	/**
	 * Matをファイルに保存する
	 * @param filename
	 * @param mat
	 * @throws IOException
	 */
	public static void storeMat(String filename, Mat mat) throws IOException {
		String jsondata = matToJson(mat);
		PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(new File(filename))));
		writer.write(jsondata);
		writer.close();
	}

	/**
	 * MatをJson形式に変換する
	 * @param mat
	 * @return
	 */
	public static String matToJson(Mat mat) {
		JsonObject obj = new JsonObject();

		if (mat.isContinuous()) {
			int cols = mat.cols();
			int rows = mat.rows();
			int elemSize = (int) mat.elemSize();
			int type = mat.type();
			String typeStr = CvType.typeToString(type);
			int dataSize = cols * rows * elemSize;
			if (rows == 0) {
				dataSize = cols * elemSize;
			}

			String dataString = "";
			if (typeStr.contains("32F")) {
				float[] data = new float[dataSize];
				String[] strData = new String[dataSize];
				mat.get(0, 0, data);
				for (int i = 0; i < data.length; i++) {
					strData[i] = String.valueOf(data[i]);
				}
				dataString = String.join(",", strData);
			} else if (typeStr.contains("8U")) {
				byte[] data = new byte[dataSize];
				String[] strData = new String[dataSize];
				mat.get(0, 0, data);
				for (int i = 0; i < data.length; i++) {
					strData[i] = String.valueOf(data[i]);
				}
				dataString = String.join(",", strData);
			}

			obj.addProperty("rows", mat.rows());
			obj.addProperty("cols", mat.cols());
			obj.addProperty("type", mat.type());
			obj.addProperty("data", dataString);

			Gson gson = new Gson();
			String json = gson.toJson(obj);

			return json;
		} else {
			System.err.println("");
		}
		return "{}";
	}
```

読み込み部分です。

```java
	/**
	 * Matをファイルから読み込む
	 * @param filename
	 * @return
	 * @throws IOException
	 */
	public static Mat loadMat(String filename) throws IOException {
		InputStream input = new FileInputStream(filename);
		int size = input.available();
		byte[] buffer = new byte[size];
		input.read(buffer);
		input.close();
		return matFromJson(new String(buffer));
	}

	/**
	 * MatをJsonから作成する
	 * @param json
	 * @return
	 */
	public static Mat matFromJson(String json) {
		JsonParser parser = new JsonParser();
		JsonObject JsonObject = parser.parse(json).getAsJsonObject();

		int rows = JsonObject.get("rows").getAsInt();
		int cols = JsonObject.get("cols").getAsInt();
		int type = JsonObject.get("type").getAsInt();
		String typeStr = CvType.typeToString(type);

		String dataString = JsonObject.get("data").getAsString();
		Mat mat = new Mat(rows, cols, type);
		String[] splitedStr = dataString.split(",");
		if (typeStr.contains("32F")) {
			float[] data = new float[splitedStr.length];
			for (int i = 0; i < data.length; i++) {
				data[i] = Float.parseFloat(splitedStr[i]);
			}
			mat.put(0, 0, data);
		} else if (typeStr.contains("8U")) {
			byte[] data = new byte[splitedStr.length];
			for (int i = 0; i < data.length; i++) {
				data[i] = Byte.parseByte(splitedStr[i]);
			}
			mat.put(0, 0, data);
		} else {
			System.err.println("");
		}

		return mat;
	}
```

Matのタイプが8U, 32Fだけを対象としていて、汎用としてはちょっと手を抜いています。
64F→doubleなど必要ならタイプ毎に追加する必要があります。

このあたりの行列やjsonの扱いの容易さはPython(pandas)が優れていますね。

## その他動画情報を保存する

java.util.Propertiesを使用して、プロパティファイルとして保存しました。

```java
Properties videoProperty = new Properties();
videoProperty.setProperty("playtime", String.valueOf(time));
videoProperty.setProperty("width", String.valueOf(width));
videoProperty.setProperty("height", String.valueOf(height));
videoProperty.setProperty("fps", String.valueOf(fps));
videoProperty.setProperty("size", String.valueOf(fileSize));
videoProperty.store(new FileOutputStream(metadata.getMetaFilename()), "");
```

## 比較結果を保存する

最初はCSVで保存しようとしていましたが、最終的にSQLiteに保存することにしました。
CSVはライブラリがいろいろあって、適当に選んだものがうまく動かせなかったので、あきらめました。

作成したテーブル。

```sql
CREATE TABLE IF NOT EXISTS compare_video ( 
	key text PRIMARY KEY, 
	file1 text, 
	file2 text, 
	timediff integer, 
	hist real, 
	feature real, 
	skip integer 
)
```

ここで「skip」というフィールドを用意しました。
これはこの比較結果は結果から除くことを示すフラグとし、人の目で確認したときに異なると判断したときや、再生時間が10%以上異なるときに立てておきます。


# UIの改善

使い勝手を考えて以下を追加しました。

- メタデータをラベルに表示
- 結果の選択を上下に動かすボタン
- 結果を次から表示しないためのボタン
- 再生ボタン
- ごみ箱ボタン

再生については関連付けされたアプリケーションで起動したいのですが、うまくいかず･･･。
ひとまず、固定のアプリで起動するようにしています。

変更後の画面。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/e3186550-e976-cab3-5568-331172368553.png)

デザインセンスは微妙ですね。。

# その他

## こまめなrelease

大量の画像を扱う際にはこまめにMat#releaseを呼び出すことを忘れないようにします。
特にJavaの場合、JNIで確保していると思われるMatオブジェクトのリソースは見えないため、特に注意です。

VideoCaptureも同様です。

## ウィルス対策ソフトの設定

大量のファイルを作成しているときに、CPU使用率が高くなっていたのですが、その原因がウィルス対策ソフトでした。
対象フォルダのリアルタイム検索をオフにするなど設定して置いたほうがよさそうです。

※これはWindowsサーバでもあるあるですね。


# 参考

[OpenCV Mat object serialization in java - Stack Overflow](https://stackoverflow.com/questions/27065062/opencv-mat-object-serialization-in-java)