遅い!!!とさすがに思った
字幕から文字抽出してみた(OpenCV:tesseract-ocr編)で動画で画像処理をしてみた結果810秒(約13.5分)とかかってしまいました。ここでは、リファクタリングをして動画作成する速度を改善してみました。
方法1 PIL <-> OpenCV
同一の画像データをPILからOpenCVに移して処理しようとした時にファイルに一時的に保存してからリードをしていたが、よくよく調べてみるとnumpyの型を変換するだけでできるみたいです。これで
810秒 (約13.5分) -> 450秒 (約7.5分)
まで削減することができました。
修正前
def createTextImage(src, sentence, px, py, color=(8,8,8), fsize=28):
#画像を保存
tmp_path = "src_temp.png"
cv2.imwrite(tmp_path, src)
#PILオブジェクトに格納
img = Image.open(tmp_path)
draw = ImageDraw.Draw(img)
#PILで文字を画像に書き込み
font = ImageFont.truetype("./IPAfont00303/ipag.ttf", fsize)
draw.text((px, py), sentence, fill=color, font=font)
img.save(tmp_path)
#openCVに格納
return cv2.imread(tmp_path)
修正後
opencv(BGR) -> PIL(RGB)
cvimg = cv2.imread("sample.png")
rgbImg = cv2.cvtColor(cvimg, cv2.COLOR_BGR2RGB)
pilImg = Image.fromarray(rgbImg)
------------------
PIL(RGB) -> opencv(BGR)
cvImg = np.array(pilImg, dtype=np.uint8)
dst = cv2.cvtColor(cvImg, cv2.COLOR_RGB2BGR)
方法2 並列処理
一つのフレームごとに動画の読み込み、処理、書き込みを逐次処理していたわけですが、処理のみを並列化するために下記のフローでスクリプトを書いてみました。
- 配列にフレームを一時的に格納
- 各フレームの字幕抽出を並列に処理して集約する
- id別にソートをかける
- 動画に保存
並列処理は joblib を使っています。 callback元では内包処理を駆使してほぼ1行で書けます。 n_jobs=16はプロセス数です。
from joblib import Parallel, delayed
def main_image_process(src, tool):
#ここでいくつかの画像処理をしてみる
#前処理
gray_frame = pre_process(src.content)
#文字抽出
#字幕作成
...
Parallel(n_jobs=16)( [delayed(main_image_process)(f, tool) for f in frames] )
...
開発
import sys
import cv2
import io
import os
import numpy as np
import pyocr
import pyocr.builders
from PIL import Image, ImageDraw, ImageFont
from collections import namedtuple
from joblib import Parallel, delayed
import time
MovieFrame = namedtuple("MovieFrame", ["id", "content", "timestamp"])
telop_height = 50
cap_width = 1
cap_height = 1
def pre_process(src):
kernel = np.ones((3,3),np.uint8)
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
#二値化
o_ret, o_dst = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU)
#オープニング 縮小->拡大
dst = cv2.morphologyEx(o_dst, cv2.MORPH_OPEN, kernel)
#反転
dst = cv2.bitwise_not(dst)
# channel 1 -> 3 に変換
dst = cv2.cvtColor(dst, cv2.COLOR_GRAY2BGR)
return dst
# 文字抽出
def extractTelopText(src, tool):
rgbImg = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
dst = tool.image_to_string(
Image.fromarray(rgbImg),
lang='jpn',
builder=pyocr.builders.WordBoxBuilder(tesseract_layout=6)
)
sentence = []
for item in dst:
sentence.append(item.content)
return "".join(sentence)
# 空の字幕作成
def createFooterTelop(src):
telop = np.zeros((telop_height, cap_width, 3), np.uint8)
telop[:] = tuple((128,128,128))
images = [src, telop]
dst = np.concatenate(images, axis=0)
return dst
# 並列処理
def main_image_process(src, tool):
#文字認識しやすいように加工
gray_frame = pre_process(src.content)
#テロップが出そうなところだけトリミング
roi = gray_frame[435:600, :]
#テキストを抽出
text = extractTelopText(roi, tool)
#字幕作成
dst = createFooterTelop(src.content)
#画像に文字を追加
dst = addJapaneseTelop(dst, text, 20, cap_height + telop_height - 30)
dst = addASCIITelop(dst, str(src.timestamp) + "[sec]", cap_width - 250, cap_height + telop_height - 10, color=(0,255,0))
#nametubleに格納
return MovieFrame(src.id, dst, src.timestamp)
# 文字追加(英数字のみ)
def addASCIITelop(src, sentence, px, py, color=(8,8,8), fsize=28):
cv2.putText(src, sentence,
(px, py),
cv2.FONT_HERSHEY_SIMPLEX,
1,
color,
2,
cv2.LINE_AA)
return src
# 文字追加(日本語)
def addJapaneseTelop(src, sentence, px, py, color=(8,8,8), fsize=28):
rgbImg = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
#openCV -> PIL
canvas = Image.fromarray(rgbImg).copy()
draw = ImageDraw.Draw(canvas)
font = ImageFont.truetype("./IPAfont00303/ipag.ttf", fsize)
#文字追加
draw.text((px, py), sentence, fill=color, font=font)
#PIL -> openCV
dst = cv2.cvtColor(np.array(canvas, dtype=np.uint8), cv2.COLOR_RGB2BGR)
return dst
if __name__ == '__main__':
tools = pyocr.get_available_tools()
if len(tools) == 0:
print("No OCR tool found")
sys.exit(1)
tool = tools[0]
cap = cv2.VideoCapture('one_minutes.mp4')
cap_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
cap_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
telop_height = 50
fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
writer = cv2.VideoWriter('extract_telop_async.mp4',fourcc, fps, (cap_width, cap_height + telop_height))
frames = []
start = time.time()
idx = 0
#動画読み込み
try :
while True:
if not cap.isOpened():
break
if cv2.waitKey(1) & 0xFF == ord('q'):
break
ret, frame = cap.read()
if frame is None:
break
frames.append(MovieFrame(idx,frame, round(idx/fps, 4)) )
idx += 1
except cv2.error as e:
print(e)
cap.release()
print("read movie file")
#並列処理(呼び出し元)
r = Parallel(n_jobs=16)( [delayed(main_image_process)(f, tool) for f in frames] )
#sort
sorted_out = sorted(r, key=lambda x: x.id)
#動画書き込み
try :
for item in sorted_out:
writer.write(item.content)
except cv2.error as e:
print(e)
writer.release()
print("write movie file")
print("Done!!! {}[sec]".format(round(time.time() - start,4)))
その他のポイント
nametuples
- フレームの順番
- 画像データ
- タイムスタンプ
をひとまとめにして処理したかったので簡易的なオブジェクトを作成しようと思ってnamedtuplesを使ってみました。
MovieFrame = namedtuple("MovieFrame", ["id", "content", "timestamp"])
これでプロパティ(getter)と同様に src.id、src.contentと設定ができるのでコード短縮ができていいです。
ソート
並列処理後に集約されたフレームですが、少しだけ順番がずれている可能性があります。
そのため、表示する順番(id)にソートをかけています
sorted_out = sorted(r, key=lambda x: x.id)
結果
処理時間は。。。
267.7924秒(約4.5分)
に短縮しました。![]()
最初の810秒に比べて大きな違いです。
| 処理時間 [sec] | 処理時間 [min] | |
|---|---|---|
| 修正前 | 810 | 13.5 |
| 方法1(PIL <-> OpenCV) | 450 | 7.5 |
| 方法1(PIL <-> OpenCV) + 方法2(並列処理) | 268 | 4.46 |
おわりに
エラーメッセージが表示されにくいので、一度プロセス数をひとつにしてバグをつぶしてからでないと最初から並列処理をかけるのは難しいです。
日本語で文字追加する処理では canvas = Image.fromarray(rgbImg).copy() と画像データを複製しないと逐次処理は出来ても並列処理ではエラーになりました。
少し環境を準備しないと並列処理出来ないので、処理は速いのですがどうしても面倒に思ってしまうとです。![]()
参考にしたリンク
- pil python multiple Image independent draw
- joblib.Parallel (公式 doc)
- [Fluent Python 第17章 futuresを使った並行処理 (Python 3.7版)] (https://qiita.com/daizutabi/items/abdd0a3ded0dfc3b232a)
- Python documentation : namedtuple
- namedtupleで美しいpythonを書く!(翻訳)
- Pythonでリストの内容をソートする方法|sort・sorted