PythonとOpenCVで似ている動画を検出する



Javaで書いているアプリの画像処理の部分は、Pythonでも途中まで書いていたので、まとめておきます。

[Java版はこっち](https://qiita.com/estaro/items/c9df99f7ef8413537019)


## 準備

WindowsのAnacondaをインストールします。
OpenCV3を使いたいため、3.6版です。

https://www.anaconda.com/download/

3.6版ではOpenCV3をcondaからインストールできなかったので、3.5へダウングレードしてからOpenCV3のインストールを行います。

```shell-session
conda install python=3,5
conda install -c https://conda.anaconda.org/menpo opencv3
```

## 動画から画像を抜き出す

```py
# -*- coding: utf-8 -*-
"""
    capture_frame.py
    Usage: python capture_frame.py {video file} {output dir}
    動画ファイルからいくつかのフレームをキャプチャして指定のディレクトリに画像ファイルを保存する。
"""

import configparser
from logging import getLogger, DEBUG
import sys
import cv2

# --------------------------------------------------------------------------
# Preprocessing
# --------------------------------------------------------------------------
logger = getLogger(__name__)
logger.setLevel(DEBUG)

# --------------------------------------------------------------------------
# constants
# --------------------------------------------------------------------------
# OpenCV
CV_CAP_PROP_POS_FRAMES = 1
CV_CAP_PROP_FRAME_WIDTH = 3
CV_CAP_PROP_FRAME_HEIGHT = 4
CV_CAP_PROP_FPS = 5
CV_CAP_PROP_FRAME_COUNT = 7

# 動画からキャプチャするフレーム数
CAPUTURE_COUNT = 9

# 保存する画像のwidth
CAPTURE_WIDTH = 200

# メタ情報の保存ファイル
METAFILE = 'meta.dat'

# --------------------------------------------------------------------------
# functions
# --------------------------------------------------------------------------
def capture_frame(video_file, output_dir):
    """動画ファイルから画像をキャプチャする

    :param video_file 動画ファイル
    :param output_dir キャプチャしたフレームを保存するディレクトリ

    :return 結果コード
        -1 error
    """
    logger.debug("start capture_frame ... ")

    capture = cv2.VideoCapture(video_file)
    if not capture.isOpened():
        logger.error("ファイルが開けませんでした")
        return -1

    # 動画の情報を取得する
    fps = capture.get(CV_CAP_PROP_FPS)
    resize_height = int(CAPTURE_WIDTH / capture.get(CV_CAP_PROP_FRAME_WIDTH) * capture.get(CV_CAP_PROP_FRAME_HEIGHT))
    all_frame_cnt = capture.get(CV_CAP_PROP_FRAME_COUNT)
    interval = all_frame_cnt / (CAPUTURE_COUNT + 1)
    all_time = all_frame_cnt / fps

    # キャプチャ
    capture_frame_list = list(map(lambda pos: interval * pos, range(1, CAPUTURE_COUNT + 1)))
    for i, capture_frame in enumerate(capture_frame_list):
        capture.set(CV_CAP_PROP_POS_FRAMES, capture_frame - 1)
        ret, frame = capture.read()
        if not ret:
            continue

        resized_frame = cv2.resize(frame, (CAPTURE_WIDTH, resize_height))
        cv2.imwrite(output_dir + "/" + str(i) + ".jpg", resized_frame)

    # メタ情報を保存する
    config = configparser.ConfigParser()
    config['metadata'] = {'filename=':video_file, 'time':str(all_time)}
    with open(output_dir + '/' + METAFILE, 'w') as configfile:
        config.write(configfile)

    logger.debug("finish capture_frame ... ")
    return 0

# --------------------------------------------------------------------------
# main
# --------------------------------------------------------------------------
if __name__ == "__main__":
    capture_frame(sys.argv[1], sys.argv[2])
```

## 画像を比較する

抜き出したフレームの画像比較をします。

```py
# -*- coding: utf-8 -*-
"""
    compare_image.py
    Usage: python compare_image.py {dir1} {dir2}
"""

import configparser
from logging import getLogger, DEBUG, basicConfig
import os
import sys
import cv2

# --------------------------------------------------------------------------
# Preprocessing
# --------------------------------------------------------------------------
basicConfig(level=DEBUG)
logger = getLogger(__name__)
logger.setLevel(DEBUG)

# --------------------------------------------------------------------------
# constants
# --------------------------------------------------------------------------
# 動画からキャプチャするフレーム数
CAPUTURE_COUNT = 9

# メタ情報の保存ファイル
METAFILE = 'meta.dat'

# 許容する再生時間のずれ（%）
PERMIT_TIME_DIFF_RATE = 15

# --------------------------------------------------------------------------
# functions
# --------------------------------------------------------------------------
def compare_images(dir1, dir2, output_dir):
    """2つのディレクトリについて、同名画像ファイルを比較する

    :param
    :param

    :return 結果コード
        -1 error
    """
    logger.debug("start capture_frame ... ")

    config1 = configparser.ConfigParser()
    config1.read(dir1 + '/' + METAFILE)
    config2 = configparser.ConfigParser()
    config2.read(dir2 + '/' + METAFILE)

    time1 = float(config1['metadata']['time'])
    time2 = float(config2['metadata']['time'])
    res_time_diff = abs(time1 - time2)

    # 明らかに再生時間が異なる
    if res_time_diff >= time1 * PERMIT_TIME_DIFF_RATE / 100:
        logger.warn("time1:" + str(time1) + ", time2:" + str(time2) + " , diff rate:" + str(time1 * PERMIT_TIME_DIFF_RATE / 100))
        return

    # 画像の比較
    hist_sum = []
    feature_sum = []
    bf = cv2.BFMatcher(cv2.NORM_HAMMING)
    detector = cv2.AKAZE_create()
    for i in range(CAPUTURE_COUNT):
        dir1_file = dir1 + "/" + str(i) + ".jpg"
        dir2_file = dir2 + "/" + str(i) + ".jpg"

        # 対象ファイルの存在チェック
        if not (os.path.exists(dir1_file) and os.path.exists(dir2_file)):
            continue;

        # 画像の読み込み
        dir1_img = cv2.imread(dir1_file)
        dir2_img = cv2.imread(dir2_file)

        # ヒストグラムを計算する
        dir1_hist = cv2.calcHist([dir1_img], [0], None, [256], [0, 256])
        dir2_hist = cv2.calcHist([dir2_img], [0], None, [256], [0, 256])
        hist_diff = cv2.compareHist(dir1_hist, dir2_hist, 0)
        hist_sum.append(hist_diff)
        logger.debug(hist_diff)

        # グレースケール化して特長量を抽出
        dir1_img_g = cv2.cvtColor(dir1_img, cv2.COLOR_RGB2GRAY)
        dir2_img_g = cv2.cvtColor(dir2_img, cv2.COLOR_RGB2GRAY)
        (dir1_kp, dir1_des) = detector.detectAndCompute(dir1_img_g, None)
        (dir2_kp, dir2_des) = detector.detectAndCompute(dir2_img_g, None)

        # 比較
        matches = bf.match(dir1_des, dir2_des)
        dist = [m.distance for m in matches]
        feature_sum.append(sum(dist) / len(dist))

    logger.debug(compare_result)
    logger.debug("finish compare_images ... ")
    return 0

# --------------------------------------------------------------------------
# main
# --------------------------------------------------------------------------
if __name__ == "__main__":
    compare_images(sys.argv[1], sys.argv[2], sys.argv[3])
```

## おわりだよ～

## 参考

[Managing Python — Conda documentation](https://conda.io/docs/user-guide/tasks/manage-python.html)

[Python + OpenCVで画像の類似度を求める - Qiita](https://qiita.com/best_not_best/items/c9497ffb5240622ede01)