授業動画だけ配布されてPDF資料が配布されなかった経験、Zoomのウェビナー等で「資料が欲しい」と言いにくかった経験はありませんか?
この記事では、
- 動画から一定間隔でフレームを切り出す
- AKAZE(特徴量マッチング手法の1つ)で、類似スライドを削除する
- 選ばれたスライド画像を結合してPDFにする
という3ステップで問題を解決したいと思います。
0. デモ
Streamlitで作成したWebアプリを公開しています。
▼ WebアプリのURL
https://share.streamlit.io/kitsuya0828/lecapy/main/app.py
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ではなく、またテンプレートマッチングよりも回転や拡大・縮小に強いです。
今回は、(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が完成しそうです。
※ 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アプリが少しでも皆さんのお役に立てれば幸いです。