2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

動画作成を並列処理して高速化してみた

2
Last updated at Posted at 2019-12-12

遅い!!!とさすがに思った

字幕から文字抽出してみた(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.idsrc.contentと設定ができるのでコード短縮ができていいです。

ソート

並列処理後に集約されたフレームですが、少しだけ順番がずれている可能性があります。
そのため、表示する順番(id)にソートをかけています

sorted_out = sorted(r, key=lambda x: x.id)

結果

処理時間は。。。
267.7924秒(約4.5分)
に短縮しました。:tada:

最初の810秒に比べて大きな違いです。

処理時間 [sec] 処理時間 [min]
修正前 810 13.5
方法1(PIL <-> OpenCV) 450 7.5
方法1(PIL <-> OpenCV) + 方法2(並列処理) 268 4.46

おわりに

エラーメッセージが表示されにくいので、一度プロセス数をひとつにしてバグをつぶしてからでないと最初から並列処理をかけるのは難しいです。
日本語で文字追加する処理では canvas = Image.fromarray(rgbImg).copy() と画像データを複製しないと逐次処理は出来ても並列処理ではエラーになりました。
少し環境を準備しないと並列処理出来ないので、処理は速いのですがどうしても面倒に思ってしまうとです。:tired_face:

参考にしたリンク

2
4
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
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?