概要
前回に引き続きこんなものあったら良いか?シリーズです
今回は静止画ではなく動画をフレームごとに処理する方法について調べてみたいと思います
前回の静止画ごとに画像の文を読み込んでいたものを動画にすることはできないかということで、少しだけ関連性があるのではと思っております
本題
今回の要件、やりたいことに適していそうなのは「OpenCV」というライブラリになります
正式名称はOpen Source Computer Vision Libraryという名称です
画像処理やコンピュータビジョン分野で最も使われているライブラリであるそうです
使用するにはライブラリをインストールする必要があります
pip install opencv-python
できること
実際にこのライブラリは何ができるのか少しだけ羅列します
-
画像の読み込み
- 読み込み時にグレースケール化を行うこともできるそうです(第二引数に0を指定する)
original = cv2.imread('sample.jpg',0)
- 読み込み時にグレースケール化を行うこともできるそうです(第二引数に0を指定する)
-
画像サイズ変更
- 画像サイズの変更ができます
original = cv2.imread('sample.jpg',0) imgge = cv2.resize(original,(250,180))
- 画像サイズの変更ができます
などなど例に挙げた2種類以外にもやれることはたくさんありますので。ぜひ調べてみてください
試してみる
前回の記事でやったこととしては、静止画像を読み込ませてそこに書かれている文をOCRというものを利用して、文字起こしを行うということをしていました
読み込ませたい静止画像が少なければ手作業でやっても良いかもしれませんが、動画を対象にしようとするとやることが膨大になると思います
- 動画の中から抜き出す部分を探す(動画全体であれば省略可能)
- 抜き出したい場所の画像を作成する(スクリーンショットを取得する)
- プログラムに読み込ませる
上記を手作業で繰り返すことになると思います
さすがにこれを繰り返すことはやりたくないので、これをopenCVで動画を読み込んだあとに処理することで、半自動化を目指します
やってみる
やろうとしていることに必要なライブラリのインストールコマンドは以下です
(動画データの読み込みの後に画像化する必要があるため、Pillowライブラリというものが必要になります)
pip install opencv-python pytesseract Pillow
まずは動画を読み込ませてフレームごとに処理するコードが以下になります
import cv2
import pytesseract
from PIL import Image
# ※読み込ませるファイルによって修正が必要です
VIDEO_PATH = 'your_video_file.mp4'
# 字幕領域 (ROI: Region of Interest) の座標設定 (ピクセル単位)
# ※読み込ませる動画によって修正が必要です
SUBTITLE_REGION = (0, 300, 1056, 754)
# 出力テキストファイル
OUTPUT_FILE = 'output.txt'
# 連続する同じ字幕の誤検出を防ぐための閾値
TEXT_SIMILARITY_THRESHOLD = 5
last_text = ""
cap = cv2.VideoCapture(VIDEO_PATH)
if not cap.isOpened():
print("動画ファイルを開けませんでした")
while True:
# retは読み取り成功した(フレームが存在しているかのbool)
# frameはNumpy形式の画像データ
ret, frame = cap.read()
if not ret:
break
x_start, y_start, x_end, y_end = SUBTITLE_REGION
subtitle_area = frame[y_start:y_end, x_start:x_end]
# グレースケール化
gray = cv2.cvtColor(subtitle_area, cv2.COLOR_BGR2GRAY)
# 画像にするために二値化
_, binary = cv2.threshold(
gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
img_for_ocr = binary
# PIL Imageオブジェクトに変換
pil_img = Image.fromarray(cv2.cvtColor(img_for_ocr, cv2.COLOR_BGR2RGB))
# pil_img.show()
current_text = pytesseract.image_to_string(pil_img, lang='eng').strip()
current_text_clean = ' '.join(current_text.split())
if current_text_clean and abs(len(current_text_clean) - len(last_text)) > TEXT_SIMILARITY_THRESHOLD:
if last_text in current_text_clean:
last_text = current_text_clean
continue
with open(OUTPUT_FILE, 'a', encoding='utf-8') as f:
# タイムスタンプを追加することも可能(cap.get(cv2.CAP_PROP_POS_MSEC)など)
f.write(current_text_clean + "\n")
# 検出されたテキストを「前回のテキスト」として保存
last_text = current_text_clean
cap.release()
print("動画の処理が完了しました")
動画の座標領域の情報などの取得は以下を使うことで、簡単に調べることができます
import cv2
VIDEO_PATH = 'your_video_file.mp4'
cap = cv2.VideoCapture(VIDEO_PATH)
ret, frame = cap.read()
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
fps = cap.get(cv2.CAP_PROP_FPS)
print(f"width:{width}, height:{height}, count:{count}, fps:{fps}")
ローカルで適当な動画ファイルを選択して実行した結果になります
widthとheightの値はSUBTITLE_REGIONの第3,4引数に指定できる最大値となります
countがフレーム数でfpsがフレーム数です
サンプルで使用した動画ファイルは440 / 60 ≒ 7s なのですが、フレームごとの処理とすると440回も繰り返すことになりますね
人間が観る動画の字幕ではなかなかフレーム単位で更新はされないと思いますので、できれば秒単位で処理を行うことができればより短い時間で効率的に処理が進められると思うので改善を試みます
width:1056.0, height:754.0, count:439.0, fps:58.66369710467706
フレーム単位の処理から秒単位の処理に変更してみる
総フレーム数とfpsが事前に把握できるのであれば、whileで読み込んでいた部分をforで回すことができると考えました
特定のフレームに移動するには「cap.set(cv2.CAP_PROP_POS_FRAMES, 特定のフレーム番号)」というメソッドを使用することで実現ができました
第一引数で設定したいプロパティの指定、今回はフレームの番号をいじるので第二引数にフレーム番号を入れるということになります
import cv2
import pytesseract
from PIL import Image
VIDEO_PATH = 'your_video_file.mp4'
# 字幕領域 (ROI: Region of Interest) の座標設定 (ピクセル単位)
SUBTITLE_REGION = (0, 300, 1056, 754)
# 出力テキストファイル
OUTPUT_FILE = 'output.txt'
# 連続する同じ字幕の誤検出を防ぐための閾値
TEXT_SIMILARITY_THRESHOLD = 5
def process_video_for_subtitles(video_path, region, output_file):
last_text = ""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print("動画ファイルを開けませんでした")
return
# 改善した点----
count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
fps = cap.get(cv2.CAP_PROP_FPS)
for num in range(1, int(count), int(fps)):
cap.set(cv2.CAP_PROP_POS_FRAMES, num)
# ----ここまで
ret, frame = cap.read()
if not ret:
break
# 1. 字幕領域を切り出し (ROI)
# BGR形式のフレームから NumPy のスライス機能で領域の切り出し
x_start, y_start, x_end, y_end = region
subtitle_area = frame[y_start:y_end, x_start:x_end]
# グレースケール化
gray = cv2.cvtColor(subtitle_area, cv2.COLOR_BGR2GRAY)
# 二値化 (THRESH_OTSUで最適な閾値を自動決定)
_, binary = cv2.threshold(
gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
img_for_ocr = binary
# 2. PIL Imageオブジェクトに変換してOCR実行
pil_img = Image.fromarray(cv2.cvtColor(img_for_ocr, cv2.COLOR_BGR2RGB))
# 3. OCR処理 (言語: 英語 'eng')
current_text = pytesseract.image_to_string(pil_img, lang='eng').strip()
# 空白や改行が多いテキストを無視
current_text_clean = ' '.join(current_text.split())
# 4. 比較と検出
if current_text_clean and abs(len(current_text_clean) - len(last_text)) > TEXT_SIMILARITY_THRESHOLD:
if last_text in current_text_clean:
last_text = current_text_clean
continue
# 5. 処理の実行 (テキストファイルに書き出し)
with open(output_file, 'a', encoding='utf-8') as f:
# タイムスタンプを追加することも可能です (cap.get(cv2.CAP_PROP_POS_MSEC)など)
f.write(current_text_clean + "\n")
last_text = current_text_clean
cap.release()
print("動画の処理が完了しました")
if __name__ == "__main__":
process_video_for_subtitles(VIDEO_PATH, SUBTITLE_REGION, OUTPUT_FILE)
実行結果を比較してみると以下のようになりました
単純に処理する回数が減っているので改善した方は一瞬で完了していますね
載せていませんが出力されたファイルについても結果は変わらなかったので、改善は成功できたと思って良さそうです
time python main.py
動画の処理が完了しました
python3 main.py 43.95s user 5.14s system 85% cpu 57.560 total
time python main_kaizen.py
動画の処理が完了しました
python3 main_kaizen.py 1.16s user 0.21s system 90% cpu 1.506 total
おわりに
ということで、動画から字幕部分を自動で文字起こしするという紹介でした
OpenCVがなかなかに強力だなという、所感でした
字幕の文字起こし以外にもいろいろ使えそうな気がするので、また何かあれば記事にしたいと思います