はじめに
こんにちは。ACS事業部の過部です。
今回は、OpenCVを用いてMP4ファイルからサムネイル画像を生成する手順についてご紹介します。
アウトプットイメージは以下の画像です。某動画サイトの様な再生マーク付きのサムネイルを生成するPythonスクリプトを作っていきます。
概要
大まかな処理の流れを以下に載せます。
- 動画を読み込む
- 動画から1フレーム切り出す
- フレームと再生マークをリサイズする
- フレームに再生マークを重ねる
- 画像として保存する
とりあえずコード全体が見たい方は、最後の方に貼ってますので、そちらをご覧ください。
また、この記事では合成する再生マークが透過画像であることを前提としていますが、非透過画像を合成するパターンも補足として紹介しています。
1. 動画ファイルを読み込む
import cv2 import sys # Pythonスクリプトのコマンドライン引数を取得する video_path = sys.argv[1] # 動画ファイルのパス overlay_image_path = sys.argv[2] # 再生マーク画像ファイルのパス output_path = sys.argv[3] # サムネイル画像の出力先パス # ファイルパスを指定して動画ファイルを読み込む cap = cv2.VideoCapture(video_path)
コマンドライン引数から各種ファイルパスを受け取った後、ファイルパスを指定してVideoCaptureクラスをインスタンス化し動画を読み込んでいます。
2. 動画から1フレーム切り出す
# 動画のFPS(1秒当たり何フレーム存在するか)を取得する fps = cap.get(cv2.CAP_PROP_FPS) # 動画開始時点から1秒後のフレームをサムネイル画像として利用するように指定する target_second = 1 # 動画開始時点から1秒後のフレーム枚数目を読み込む様に設定する cap.set(cv2.CAP_PROP_POS_FRAMES, fps * target_second) # 対象フレームを取得する ret, frame = cap.read() # フレームの取得に失敗した場合はエラーを出力して終了する if not ret: print("Error: failed to read video") sys.exit() # これ以降の処理でVideoCaptureオブジェクトは不要なのでリリースする cap.release()
今回は例として動画開始位置から1秒後経過したフレームを画像として切り出します。 切り出し対象フレームは、動画開始位置からの経過秒数ではなく、開始から何枚目のフレームであるかを指定する必要があります。 そのため、FPS(frame per second)を取得し開始位置から1秒後のフレームが何枚目に相当するかを計算しています。
3. フレームと再生マークをリサイズする
# フレームの高さと幅からアスペクト比を計算する frame_aspect_ratio = frame.shape[1] / frame.shape[0] # フレームの高さを200pxに固定して幅を計算する frame_h = 200 frame_w = int(frame_aspect_ratio * frame_h) # フレームをリサイズする resized_frame = cv2.resize(frame, (frame_w, frame_h), interpolation=cv2.INTER_AREA) # 再生マーク画像を読み込む overlay_image = cv2.imread(overlay_image_path, cv2.IMREAD_UNCHANGED) # 再生マークの高さと幅からアスペクト比を計算する overlay_aspect_ratio = overlay_image.shape[1] / overlay_image.shape[0] # 再生マークの高さをフレームの高さの半分に固定して幅を計算する overlay_h = frame_h // 2 overlay_w = int(overlay_aspect_ratio * overlay_h) # 再生マークをリサイズする resized_overlay = cv2.resize(overlay_image, (overlay_w, overlay_h), interpolation=cv2.INTER_AREA)
再生マークとフレームの比率を調整していきます。 具体的には、フレームは高さが200pxに、再生マークは高さがフレームの半分になる様にアスペクト比を保ちつつリサイズします。
cv2.imread関数の第2引数では画像の読み込み方を指定しています。cv2.IMREAD_UNCHANGEDフラグを指定することで透過度情報(アルファチャンネル)を含む色情報を読み込んでいます。
cv2.resize関数のinterpolationオプションではサイズ変更時の画像の補間方法を指定しています。 詳しくはこちらをご覧ください。
4. フレームに再生マークを重ねる
置換領域の座標計算
# フレーム中央に再生マーク配置時の、再生マーク左上の頂点の座標を計算する position_x, position_y = ( int((frame_w - resized_overlay.shape[1]) / 2), int((frame_h - resized_overlay.shape[0]) / 2), )
画像の合成手法は色々ありますが、今回はベース画像の特定領域の色情報をオーバーレイ画像の色情報で置き換える方法を採用しています。 置き換えに際して対象領域の頂点4つの座標を指定する必要がありますが、左上の頂点座標を基にその他頂点座標は容易に計算できるので、左上の頂点座標のみ計算します。
※例えば、左上頂点の座標が(x, y)の場合、右上頂点の座標は(x+再生マークの画像幅, y)、左下頂点の座標は(x, y+再生マークの画像高さ)、右下頂点の座標は(x+再生マークの画像幅, y+再生マークの画像高さ)となる。
今回はフレーム中央に再生マークが配置されるように、置き換え領域左上の頂点座標を計算しています。
配列化された画像情報
フレーム画像及び再生マーク画像はどちらも三次元配列として読み込まれています。
[ [[色情報], [色情報], [色情報], ・・・,[色情報]], //※y=0の行におけるx=0,1,2...nの色情報 [[色情報], [色情報], [色情報], ・・・,[色情報]], //※y=1の行におけるx=0,1,2...nの色情報 ︙ [[色情報], [色情報], [色情報], ・・・,[色情報]] //※y=nの行におけるx=0,1,2...nの色情報 ]
フレーム画像の場合は透過情報を含まないので、[色情報]には[R(RED)チャンネル値, G(GREEN)チャンネル値, B(BLUE)チャンネル値]の配列が入ります。 ex. [240, 250, 249]
再生マーク画像の場合は透過情報を含むので、[色情報]には[Rチャンネル値, Gチャンネル値, Bチャンネル値, A(ALPHA)チャンネル値]の配列が入ります。 ex. [0, 0, 0, 0]
各チャンネルの取りうる値は、0~255の間であり、各色の強度及び透過度を256段階で表しています。Aチャンネルの値が0の場合、完全に透明であることを示しています。
合成処理
# フレームに再生マークを合成する resized_frame[ position_y : position_y + resized_overlay.shape[0], position_x : position_x + resized_overlay.shape[1] ] = resized_frame[ position_y : position_y + resized_overlay.shape[0], position_x : position_x + resized_overlay.shape[1] ] * ( 1 - resized_overlay[:, :, 3:] / 255 ) + resized_overlay[ :, :, :3 ] * ( resized_overlay[:, :, 3:] / 255 )
上では話を簡単にするために"ベース画像の特定領域の色情報をオーバーレイ画像の色情報で置き換える"と書きましたが、実際は再生マークが透過画像のため実際の合成処理は少し異なります。実際には再生マーク画像の各ピクセルが持つ透過度に応じて、フレーム画像と再生マーク画像間の色情報の配分に差をつけて合成後の色情報を算出します。(透過度が高い場合はフレーム画像の色情報を多めに採用し、透過度が低い場合は再生マーク画像の色情報を多めに採用する様なイメージ)。再生マーク画像の方だけ透過度情報を持っているので、R,G,Bチャンネルごとに以下の様な計算式で合成後の色情報を求める必要があります。
合成後の色強度 = フレーム画像の色強度 × (1 - 再生マーク画像の透明度合) + 再生マーク画像の色強度 × 再生マーク画像の透過度合
透過度合は、Aチャンネル値(0~255)を255で除算した値であり、0~1の範囲を取ります。0が完全に透明、1が完全に不透明な状態です。
上記を踏まえると、フレーム画像と再生マーク画像が重なる各座標の色情報の算出方法は以下の様になります。
[合成後のRチャンネル値, 合成後のGチャンネル値, 合成後のBチャンネル値] = [フレーム画像のRチャンネル値 × ((1 - 再生マーク画像のAチャンネル値) / 255) + 再生マーク画像のRチャンネル値 × (再生マーク画像のAチャンネル値 / 255), フレーム画像のGチャンネル値 × ((1 - 再生マーク画像のAチャンネル値) / 255) + 再生マーク画像のGチャンネル値 × (再生マーク画像のAチャンネル値 / 255), フレーム画像のBチャンネル値 × ((1 - 再生マーク画像のAチャンネル値) / 255) + 再生マーク画像のBチャンネル値 × (再生マーク画像のAチャンネル値 / 255)]
上記のPythonスクリプトにおける合成処理では、フレーム画像と再生マーク画像が重なる全座標毎に合成後の色情報を計算し、それをフレーム画像の合成領域内の各座標の色情報と置換する処理を行っています。
(補足)非透過画像を合成する場合
# 非透過画像を重ねる場合の処理 resized_frame[ position_y : position_y + resized_overlay.shape[0], position_x : position_x + resized_overlay.shape[1] ] = resized_overlay
非透過画像を合成する場合は透明度情報を考慮する必要がありません。 そのため、合成領域の座標毎に、フレーム画像の色情報を再生マーク画像の色情報で置換するだけで済みます。
5. 画像として保存する
# 画像として指定パスに保存する
cv2.imwrite(output_path, resized_frame)
フォルダパスを指定し画像として保存します。
実行例
カレントディレクトリにPythonスクリプト、MP4動画、再生マーク画像が存在する状態で、サムネイル画像を同ディレクトリに出力する例です。
python3 generate_thubnail.py sample.mp4 play_button.png thumbnail.png
コード全体
import cv2 import sys # Pythonスクリプトのコマンドライン引数を取得する video_path = sys.argv[1] # 動画ファイルのパス overlay_image_path = sys.argv[2] # 再生マーク画像ファイルのパス output_path = sys.argv[3] # サムネイル画像の出力先パス # ファイルパスを指定して動画ファイルを読み込む cap = cv2.VideoCapture(video_path) # 動画のFPS(1秒当たり何フレーム存在するか)を取得する fps = cap.get(cv2.CAP_PROP_FPS) # 動画開始時点から1秒後のフレームをサムネイル画像として利用するように指定する target_second = 1 # 動画開始時点から1秒後のフレーム枚数目を読み込む様に設定する cap.set(cv2.CAP_PROP_POS_FRAMES, fps * target_second) # 対象フレームを取得する ret, frame = cap.read() # フレームの取得に失敗した場合はエラーを出力して終了する if not ret: print("Error: failed to read video") sys.exit() # これ以降の処理でVideoCaptureオブジェクトは不要なのでリリースする cap.release() # フレームの高さと幅からアスペクト比を計算する frame_aspect_ratio = frame.shape[1] / frame.shape[0] # フレームの高さを200pxに固定して幅を計算する frame_h = 200 frame_w = int(frame_aspect_ratio * frame_h) # フレームをリサイズする resized_frame = cv2.resize(frame, (frame_w, frame_h), interpolation=cv2.INTER_AREA) # 再生マーク画像を読み込む overlay_image = cv2.imread(overlay_image_path, cv2.IMREAD_UNCHANGED) # 再生マークの高さと幅からアスペクト比を計算する overlay_aspect_ratio = overlay_image.shape[1] / overlay_image.shape[0] # 再生マークの高さをフレームの高さの半分に固定して幅を計算する overlay_h = frame_h // 2 overlay_w = int(overlay_aspect_ratio * overlay_h) # 再生マークをリサイズする resized_overlay = cv2.resize(overlay_image, (overlay_w, overlay_h), interpolation=cv2.INTER_AREA) # フレーム中央に再生マーク配置時の、再生マーク左上の頂点の座標を計算する position_x, position_y = ( int((frame_w - resized_overlay.shape[1]) / 2), int((frame_h - resized_overlay.shape[0]) / 2), ) # フレームに再生マークを合成する(透過画像を重ねる場合) resized_frame[ position_y : position_y + resized_overlay.shape[0], position_x : position_x + resized_overlay.shape[1] ] = resized_frame[ position_y : position_y + resized_overlay.shape[0], position_x : position_x + resized_overlay.shape[1] ] * ( 1 - resized_overlay[:, :, 3:] / 255 ) + resized_overlay[ :, :, :3 ] * ( resized_overlay[:, :, 3:] / 255 ) # フレームに再生マークを合成する(非透過画像を重ねる場合) # resized_frame[ # position_y : position_y + resized_overlay.shape[0], position_x : position_x + resized_overlay.shape[1] # ] = resized_overlay # 画像として指定パスに保存する cv2.imwrite(output_path, resized_frame)
本記事内で利用しているサンプル動画及び再生マーク画像は、それぞれYoutuberのための素材屋さん様、Simple color icon Jrs様からお借りしています。
終わりに
私達ACS事業部はAzure・AKSを活用した内製化のご支援をしております。ご相談等ありましたらぜひご連絡ください。
また、一緒に働いていただける仲間も募集中です!
切磋琢磨しながらスキルを向上できる、エンジニアには良い環境だと思います。ご興味を持っていただけたら嬉しく思います。