LoginSignup
22
27

More than 1 year has passed since last update.

【Python】動画から特徴量マッチングでPDF資料を自動生成するWebアプリを作ってみた

Posted at

授業動画だけ配布されてPDF資料が配布されなかった経験、Zoomのウェビナー等で「資料が欲しい」と言いにくかった経験はありませんか?

この記事では、

  1. 動画から一定間隔でフレームを切り出す
  2. AKAZE(特徴量マッチング手法の1つ)で、類似スライドを削除する
  3. 選ばれたスライド画像を結合してPDFにする

という3ステップで問題を解決したいと思います。

0. デモ

Streamlitで作成したWebアプリを公開しています。

▼ WebアプリのURL
https://share.streamlit.io/kitsuya0828/lecapy/main/app.py

main.jpg

YouTubeでも開発→デモの流れを紹介しているのでご覧ください。

1. 動画から一定間隔でフレームを切り出す

ユーザーがn秒ごとにキャプチャしたい場合、フレーム抽出間隔step_frame

\begin{align}
{step\_frame \ \rm{[frames]}} = \frac{frame\_rate\ \rm{[fps]}}{n\ \rm{[s]}} 
\end{align}

と事前に求めることができます。
OpenCVを使って、一定間隔で抽出したフレームを画像として保存します。

def capture_frame(video_path: str, step_frame: int, UUID: str):
    cap = cv2.VideoCapture(video_path)
    video_frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

    if not cap.isOpened():
        return
    
    digit = len(str(video_frame_count)) # 総フレーム数の桁数
    frame_index = 0
    caps =[]

    for n in range(0, video_frame_count, step_frame):
        cap.set(cv2.CAP_PROP_POS_FRAMES, n)
        ret, frame = cap.read()
        frame_index += 1

        if ret:
            image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            im = Image.fromarray(image)
            im.save(Path(f'static/{UUID}_{str(n).zfill(digit)}.jpg').absolute())
            caps.append(n)
    
    return frame_index, digit, caps

UUID (Universally Unique Identifier) は、アプリケーションを通して使われるユーザーの(一意な)識別子です。
保存した画像をユーザーごとに区別するために使いましたが、個人で開発する分には気にしなくて大丈夫です。

2. AKAZEで類似スライドを削除する

AKAZE特徴量マッチングの1つです。
Deep Learningではなく、またテンプレートマッチングよりも回転や拡大・縮小に強いです。
compare.jpg

今回は、(PowerPointなどの)スライドショーを用いて行われる講義動画を想定しているので、アニメーションの動きにも影響されにくいと考えました。
AKAZE(Accelerated KAZE)特徴量検出は、OpenCVを使って簡単に実装できます。
OpenCV公式ドキュメント : OpenCV: AKAZE local features matching

以下の記事も参考にさせていただきました。
OpenCVのAKAZEで顔写真の類似度判定をやってみた | パソコン工房 NEXMAG

def deduplicate_frame(caps: list, digit: int, UUID: str, threshold: int, frame_size: tuple, scale: int):
    IMG_SIZE = tuple(map(lambda x: x//scale, frame_size))    # 任意(フレームをリサイズすると処理が高速化する)

    dump_list = []    # 削除する類似画像のパスを格納するリスト
    files = [f'static/{UUID}_{str(n).zfill(digit)}.jpg' for n in caps]
    que = deque(files[::-1])    # 抽出した画像ファイルのパスを格納したキュー

    while que:
        # AKAZE特徴量検出
        target_file = que.popleft()
        target_img = cv2.imread(target_file, cv2.IMREAD_GRAYSCALE)    # 比較元の画像
        if scale != 1:
            target_img = cv2.resize(target_img, IMG_SIZE)

        bf = cv2.BFMatcher(cv2.NORM_HAMMING)
        detector = cv2.AKAZE_create()
        (target_kp, target_des) = detector.detectAndCompute(target_img, None)

        same_slide = True    # 類似スライドかどうかの判定フラグ

        # 類似スライドである限り比較元は変更しない
        while same_slide:
            if not que:
                break

            comparing_file = que.popleft()

            try:
                comparing_img = cv2.imread(comparing_file, cv2.IMREAD_GRAYSCALE)    # 比較する画像
                if scale != 1:
                    comparing_img = cv2.resize(comparing_img, IMG_SIZE)

                (comparing_kp, comparing_des) = detector.detectAndCompute(comparing_img, None)
                matches = bf.match(target_des, comparing_des)

                dist = [m.distance for m in matches]
                ret = sum(dist) / len(dist)    # 特徴量の距離平均
            except cv2.error:
                ret = 10**6

            if ret < threshold:    # ユーザー定義の閾値よりも小さい場合
                dump_list.append(comparing_file)
            else:
                que.appendleft(comparing_file)    # 次の比較元の画像に使う
                same_slide = False
    return dump_list[::-1]

類似画像をネガポジ反転させた結果を表示してみると、いい感じのPDFが完成しそうです。
bb241f6ec6f1d71b199d7524536f9231455c7c234134c5fb0c013197 (2).png
※ Webアプリの結果プレビュー画面です(著作権保護のためモザイクをかけています)

3. 選ばれたスライド画像を結合してPDFにする

img2pdfモジュールを使って、画像を簡単にPDFファイルに変換できます。
参考記事 : img2pdfを使用しPythonで画像をPDFファイルに変換する | Men of Letters(メン・オブ・レターズ) – 論理的思考/業務改善/プログラミング

抽出したすべてのフレームのリストall_img_listから、類似フレームのリストdump_listの画像を削除してPDF化します。

def generate_pdf(UUID: str, dump_list: list, all_img_list: list):
    with open(f'{UUID}.pdf', "wb") as f:
        pdf_file_path_list = []
        for file_path in all_img_list:
            if file_path not in dump_list:
                pdf_file_path_list.append(file_path)
        f.write(img2pdf.convert(pdf_file_path_list))

4. 全体ソースコード

StreamlitでWebアプリ化した全体ソースコードは、以下のGitHubで公開しています。
Kitsuya0828/Lecapy
よろしければご覧ください。

5. おわりに

この記事や作成したWebアプリが少しでも皆さんのお役に立てれば幸いです。

22
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
27