2023年1月5日木曜日

MP4動画から撮影日時入りのJPEG画像を作成する方法

GoProのようなアクションカムで動画撮影した場合,そのひとコマを写真として保存して残しておきたいことが良くある. 大抵両手が塞がってるのでカメラのシャッターを押せないことが多いから. でも,殆どのアプリでは動画から静止画を保存していくと撮影日時を埋め込んでくれないので,ファイル名順になり順番はめちゃくちゃになる.

動画の撮影日時にそのフレームまでの再生時間を足して静止画の撮影日時とするプログラムがあれば,順番がめちゃくちゃにならずに済む. MP4動画には撮影日時が埋め込まれており,JPEG画像にもExifという撮影に関する情報を埋め込むことができる. そこでPythonで動画を再生しながら適当な箇所で一時停止して撮影日時付きの画像を保存できるツールを作ったので紹介する.

ソースコード解説

このプログラムのポイントは,MP4の撮影日時情報の取得にffmpegのprobeという機能を使い, JPEG保存後にExif情報だけをpiexifというライブラリで上書きするところ. それ以外は基本的なライブラリしか使わない.ここでは動画ファイルをtest.mp4としておく.

import sys
import cv2
import piexif
import datetime as dt
import ffmpeg

fname = "test.mp4"

次は動画キャプチャの骨格部分.main関数になってるので最下部のmain関数の実行部分がこのソースの本体. 基本的にOpenCVで動画再生して必要な箇所をキーボード入力で一時停止,キャプチャなどするための機能だけ. もう少しシンプルにしたかったけど一応動作してるのであきらめた. 重要な箇所は「# MP4の撮影日時取得」,「# 動画再生時間の取得」,「# JPEG保存とExif情報の上書き」の部分だけ.ここだけ簡単に解説.

def main(argv):
    # MP4の撮影日時取得
    video_info = ffmpeg.probe(fname)
    date_time = dt.datetime.strptime(video_info['format']['tags']['creation_time'],"%Y-%m-%dT%H:%M:%S.%fZ")
    print("creation_time: " + date_time.strftime("%Y:%m:%d %H:%M:%S"))

    cap = cv2.VideoCapture(fname)
    n_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT)
    ret, frame = cap.read()
    print("number of frames: "+ str(n_frames))

    is_playing = True
    cv2.namedWindow("win")

    while True:
        # 動画再生時間の取得
        play_time = int(cap.get(cv2.CAP_PROP_POS_MSEC))
        frame_id = cap.get(cv2.CAP_PROP_POS_FRAMES)
        play_datetime = date_time + dt.timedelta(microseconds=play_time*1000)

        # key input
        key = cv2.waitKey(1)
        # ESC
        if key == 27:
            break
        elif key == 13: # Enter
            is_playing = not is_playing
        elif key == 32: # space
            frame_id += 1
            is_playing = False
        elif key == 8: # back space
            frame_id -= 1
            is_playing = False
        elif key == ord('c'): # capture !!
            # JPEG保存とExif情報の上書き
            cv2.imwrite("%06d.jpg" % frame_id, frame)
            insertTime("%06d.jpg" % frame_id, play_datetime)

        if is_playing:
            if 0 <= frame_id and frame_id < n_frames:
                ret, frame = cap.read()
            else:       
                cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
                ret = True
        else:
            if 0 <= frame_id-1 and frame_id < n_frames:
                cap.set(cv2.CAP_PROP_POS_FRAMES, frame_id-1)
                ret, frame = cap.read()
            ret = True

        if ret:
            showFrame("win", frame, is_playing, frame_id, play_time, play_datetime)
        else:
            break
            
    cap.release()
    cv2.destroyWindow("win")

まず「# MP4の撮影日時取得」の部分. ffmpeg.probe関数でファイル名を直接していすると必要な情報が取得できる. ただ,撮影日時以外の様々な情報が階層構造をもつ辞書形式になっているのでvideo_info['format']['tags']['creation_time']で取得すると中身が文字列として得られる.それをdatetime.datetime.strptime関数で指定した書式として解釈して日時情報に変換している.これで動画の撮影日時が得られた.

# MP4の撮影日時取得
video_info = ffmpeg.probe(fname)
date_time = dt.datetime.strptime(video_info['format']['tags']['creation_time'],"%Y-%m-%dT%H:%M:%S.%fZ")

次に「# 動画再生時間の取得」の部分. ここではOpenCVのcv2.VideoCapture.get関数で現在のフレームの再生時間を取得している. この時間をさっき取得した動画撮影日時date_timeに加えることで動画の各フレームの撮影日時を計算している. ミリ秒とマイクロ秒の変換を間違えないように.

# 動画再生時間の取得
play_time = int(cap.get(cv2.CAP_PROP_POS_MSEC))
frame_id = cap.get(cv2.CAP_PROP_POS_FRAMES)
play_datetime = date_time + dt.timedelta(microseconds=play_time*1000)

最後は「# JPEG保存とExif情報の上書き」の部分. 写真の保存は単純に現在のフレームをcv2.imwriteで保存するだけ. その直後にその画像ファイルに対して自作のinsertTime関数を使ってExif情報を加えている.

# JPEG保存とExif情報の上書き
cv2.imwrite("%06d.jpg" % frame_id, frame)
insertTime("%06d.jpg" % frame_id, play_datetime)

insertTime関数は次のとおり. piexif.load関数で直接JPEGファイル名を指定して一旦Exif情報を取り出して,その必要箇所だけ変更して再度保存する方法をとっている.piexif.ExifIFD.DateTimeOriginalには,Exif情報の中のDateTimeOriginalという撮影日時を表す項目のIDで36867が入ってる. piexif.ExifIFD.SubSecTimeOriginalには,SubSecTimeOriginalという撮影日時の秒単位よりも小さい時間を表す項目のIDで37521が入っている. この2つの情報をdate_time.strftimeで指定された書式にして追加している.

def insertTime(fname, date_time):
    exif_dict = piexif.load(fname)
    exif_ifd = {piexif.ExifIFD.DateTimeOriginal: date_time.strftime("%Y:%m:%d %H:%M:%S"),
                piexif.ExifIFD.SubSecTimeOriginal: date_time.strftime("%f"),
                }
    exif_dict['Exif']=exif_ifd
    exif_bytes = piexif.dump(exif_dict)
    piexif.insert(exif_bytes, fname)

showFrame関数は現在のフレームを表示する関数. cv2.putTextで各種情報を同時に表示している.

def showFrame(wname, frame, is_playing, frame_id, play_time, date_time):
    dsp = cv2.resize(frame, (frame.shape[1]//2, frame.shape[0]//2))
    cv2.putText(dsp,
        text=str("%s: %06d-th frame, %06d msec, %s"  
            % ("play" if is_playing else "pause", frame_id, play_time, date_time.strftime("%Y:%m:%d %H:%M:%S.%f"))),
        org=(20, 20),
        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
        fontScale=0.5,
        color=(255, 255, 255),
        thickness=1,
        lineType=cv2.LINE_4)

    cv2.imshow(wname, dsp)

プログラム本体をmain関数化しておくための行. これのおかげで関数の定義順序を自由にできる.

if __name__ == "__main__":
    sys.exit(main(sys.argv))