ミツバサンコーワ製バイク用ドラレコEDR-21で撮影した動画ファイル（30秒毎、0.5秒の重複あり）をpythonを使ってうまく結合する

# はじめに
　この記事は[ここ](https://adventar.org/calendars/6091)のアドベントカレンダーのやつです。

　6月からバイクを始めました。免許も取ったばかりなので安全運転を心がけたいです。

　このところあおり運転に関するニュースもよく聞きますので、自衛のためにもバイク用のドラレコを取り付けました。

[ミツバサンコーワ MITSUBA バイク専用ドライブレコーダー EDR-21 前後2カメラ EDR-21](https://www.amazon.co.jp/%E3%83%9F%E3%83%84%E3%83%90%E3%82%B5%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%AF-MITSUBA-%E3%83%90%E3%82%A4%E3%82%AF%E5%B0%82%E7%94%A8%E3%83%89%E3%83%A9%E3%82%A4%E3%83%96%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%80%E3%83%BC-EDR-21-%E5%89%8D%E5%BE%8C2%E3%82%AB%E3%83%A1%E3%83%A9/dp/B07VP9DG18/ref=sr_1_2?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%83%9F%E3%83%84%E3%83%90%E3%82%B5%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%AF&qid=1593322723&sr=8-2)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252429/7a959538-87e3-152a-b648-2fc34e749dbf.png)

　前後２カメラで、162°の超広角、暗いところも強く、256GBのSDカードも対応するいい感じのやつです。購入時点で23000円くらいだったと思います。バイクショップとかで購入＆取付をすると、工賃含めて４〜５万円くらいになりそうなので、Amazonで購入して自分で取り付けました。

　さて、このドラレコで撮影した動画はスマホ経由でも見ることができますし、SDカードをPCにつなげて直接見ることもできるのですが、30秒毎にファイルが細切れに保存される仕様となっており、結構扱いにくいです。
　さらに、ファイル間の前後0.5秒程度が重複して保存されており、１本に結合しようとしたらこの重複した0.5秒をカットしないとキレイにつながらず、とてもめんどくさいことになります。

　こんなイメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252429/adce26d4-9b38-ab15-b554-37eb2a05b1f2.png)

　重複分を考慮してキレイにつなげようとすると、こんな感じにしないといけなく手動で30秒毎にこの編集するのは手間がかかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252429/de4924cf-0e05-a3d8-81e7-16df5c2e5061.png)


　動画を１本に結合して何をするわけでもないですが、現時点でいい方法も見つからなかったので、お勉強も兼ねてpythonでやってみることにしました。

# 概要
　ドラレコの映像には音声も乗っているので、映像と音声両方を0.5秒カットしてつなげるイメージになるでしょう。pythonでやるには、映像と音声同時に編集するのは難しそうなので、別々に編集する必要がありそうです。


　使用するソフトウェア、ライブラリはこちらにしました。

- 映像と音声の分離・統合、動画エンコード：ffmpeg（Pythonからsubprocess.runで実行）
- 映像の0.5秒分カット＆結合：opencv
- 音声の0.5秒分カット＆結合：pydub

# 実装

## 準備
　ドラレコ動画のファイルはどのように作成されているのかみてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252429/8b311440-8ad9-d015-0cd1-a91bfe0679cb.png)

　フロント/リアカメラ毎に、撮影開始日時、30秒動画の開始日時、をファイル名に保持しているようです。（N/Eは……とりあえず無視しておきます。まあ大丈夫でしょう）

## 実装イメージ
　プログラム的には、カメラ毎×撮影開始日時単位にグルーピングして、動画開始日時順になるように塊を作って、塊毎に以下の処理をする感じで処理していくこととします。

1. 30秒動画を映像と音声に分離
2. 映像と音声のお尻0.5秒をカット
3. つなげる
4. 全部つながったら、映像と音声をくっつけて動画として再エンコード

## 実装

```mitsuba.py
# -*- coding: utf-8 -*-
import os
import shutil
import cv2
import glob
import subprocess
from pydub import AudioSegment
from collections import defaultdict
from tqdm import tqdm, trange
from multiprocessing import Pool,Process, Queue, TimeoutError
from queue import Empty

DUP_FRAME = 14

# multi processing
WORKERS = 4
TIMEOUT = 10

def comb_movie(movie_files, out_path, num):
    # 作成済みならスキップ
    if os.path.exists(os.path.join("out",out_path)):
        return

    # 形式はmp4
    fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')

    #動画情報の取得
    movie = cv2.VideoCapture(movie_files[0])
    fps = movie.get(cv2.CAP_PROP_FPS)
    height = movie.get(cv2.CAP_PROP_FRAME_HEIGHT)
    width = movie.get(cv2.CAP_PROP_FRAME_WIDTH)

    # 出力先のファイルを開く
    out = cv2.VideoWriter(f"tmp/video_{num:02}.mp4", int(fourcc), fps,
                        (int(width), int(height)))

    audio_merged = None
    for movies in movie_files:
        # 動画ファイルの読み込み，引数はビデオファイルのパス
        movie = cv2.VideoCapture(movies)
        count = movie.get(cv2.CAP_PROP_FRAME_COUNT)
        frames = []
        if movie.isOpened() == False:  # 正常に動画ファイルを読み込めたか確認
            continue

        for _ in range(int(count)):
            ret, tmp_f = movie.read()  # read():1コマ分のキャプチャ画像データを読み込む
            if ret:
                frames.append(tmp_f)

        # 読み込んだフレームを書き込み
        for frame in frames[:-DUP_FRAME]:
            out.write(frame)

        command = f"ffmpeg -y -i {movies} -vn -loglevel quiet tmp/audio_{num:02}.wav"
        subprocess.run(command, shell=True)

        audio_tmp = AudioSegment.from_file(f"tmp/audio_{num:02}.wav", format="wav")
        audio_tmp = audio_tmp[:int((count-DUP_FRAME)/fps*1000)]

        if audio_merged is None:
            audio_merged = audio_tmp
        else:
            audio_merged += audio_tmp

    # 結合した音声書き出し
    audio_merged.export(f"tmp/audio_merged_{num:02}.wav", format="wav")
    out.release()

    # 動画と音声結合
    vf = ""  #ビデオフィルタはお好みで 例）ややソフト・彩度アップ・ノイズ除去の場合 "-vf smartblur=lr=1:ls=1:lt=0:cr=-0.9:cs=-2:ct=-31,eq=brightness=-0.06:saturation=1.4,hqdn3d,pp=ac"
    # 高速なエンコーダに対応していればお好みで 例）h264_videotoolbox, libx264, h264_nvenc
    cv = f"-c:v libx264"
    # ビットレートは解像度に応じて固定にしています。
    if height == 1080: # FHD
        bv = f"-b:v 11m"
    elif height == 720: # HD
        bv = f"-b:v 6m"
    else: # VGA
        bv = f"-b:v 3m"

    loglevel = "-loglevel quiet"
    command = f"ffmpeg -y -i tmp/video_{num:02}.mp4 -i tmp/audio_merged_{num:02}.wav {cv} {bv} {vf} -c:a aac {loglevel} out/{out_path}"
    subprocess.run(command, shell=True)


def wrapper(args):
    comb_movie(*args)

if __name__ == '__main__':
    os.makedirs("./tmp", exist_ok=True)
    os.makedirs("./out", exist_ok=True)

    # ディレクトリ内の動画を：フロント・リアカメラごと、撮影開始時間ごとにまとめる
    files_dict = defaultdict(list)
    for f in glob.glob("./in/*.MP4"):
        files_dict["_".join(f.split("/")[-1].split("_")[:2])].append(f)

    data = []
    for i, (key_name, files_list) in enumerate(files_dict.items()):
        data.append((sorted(files_list), key_name+".mp4", i))

    p = Pool(WORKERS)
    with tqdm(total=len(data)) as t:
        for _ in p.imap_unordered(wrapper, data):
            t.update(1)
    # tmp 削除
    shutil.rmtree('./tmp/')

```


## 補足
ソースコードは[Github](https://github.com/kiritan-pop/mitsuba)でも公開しております。

※[YouTube](https://www.youtube.com/watch?v=aqNSRVYN7N4) にサンプル動画上げました

- inフォルダにドラレコの生動画を入れておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252429/baf45881-0bfa-8b6d-68a5-4d2fd8e071cc.png)


- 実行するとプログレスバーが表示されます。（本当はもう少し細かく表示したい）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252429/77177d48-02d6-dd23-7492-87a00b108a91.png)


- outフォルダに結合した動画ファイルが出来上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252429/00f4fe7f-ba5b-85ca-3dce-40f094944332.png)

- DUP_FRAME = 14
　重複しているフレーム数なので変更不要です。（動画が28fps：28フレーム/s × 0.5s = 14フレーム）

- WORKERS = 4
　並列処理をして速くするオプションで、並列実行数を指定できます。環境に応じて変更してみてください。

- ビデオフィルタはお好みで
　元動画は（ドラレコなので）シャープネスがややきつい感じなので、ソフトにしてみたり色々フィルタを設定できます。詳細はffmpegのフィルタで調べてみてください。

- 高速なエンコーダに対応していればお好みで
　- libx264:大体どの環境でも使えます
　- h264_videotoolbox:mac用。libx264より数倍速いはず。
　- h264_nvenc:linux用（windowsもいけるかな？）libx264より数倍速いはず。

- ビットレート
　お好みですが、元の動画がそれほどキレイじゃないのでビットレート上げてもさほど効果はないかも


# おわりに
　GoPro8を買っちゃったんですが、まあキレイです。ドラレコはドラレコとして使って、車載動画はGoProで取るほうが良さそうです。