How to handle Video and Audio with OpenCV

OpenCV

OpenCVで画像を処理する際に、同時に音声も処理したい場合があります。OpenCVのCVはComputer Visionですので、音声を処理する機能は持っていません。音声は別のPythonライブラリで処理します。

この記事ではOpenCVで映像を表示しながら音声を処理する際に気をつける点について解説します。

Video frame and Audio frame

以下にmain関数を示します。

def main(video_file, audio_file):
    print(f"{video_file=},{audio_file=},{use_pyaudio=}")
    cap = cv2.VideoCapture(video_file)

    player = WithPyAudio(audio_file)
    start_time = time.time()

    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break

        cv2.imshow(video_file, frame)

        elapsed = (time.time() - start_time) * 1000  # msec
        play_time = int(cap.get(cv2.CAP_PROP_POS_MSEC))
        sleep = max(1, int(play_time - elapsed))
        if cv2.waitKey(sleep) & 0xFF == ord("q"):
            break

    player.close()
    cap.release()
    cv2.destroyAllWindows()Code language: Python (python)

main関数には映像ファイル(video_file)と音声ファイル(audio_file)を別々に渡します。通常の映像ファイルは音声も含まれますが、予め分割して渡す必要があります。映像から音声を分割する方法はffmpegを使用し以下を実行します。

ffmpeg -i test.mp4 -vn -f wav test.wavCode language: Bash (bash)

cap.read()で映像フレームを取得します。音声データはループの中で特別な処理は不要です。

        ret, frame = cap.read()
        if not ret:
            breakCode language: Python (python)

elapsedに再生が開始されてからの実時間を設定します。play_timeに現在のビデオの再生位置を表す時間を設定します。cv2.CAP_PROP_POS_MSECを使用することで再生位置の時間を取得することができます。このplay_timeをelapsedに一致させるように調整することで、ビデオの再生速度を実時間に合わせることができます。なお、音声の再生速度を実時間に合わせる特別な処理は不要です。

        play_time = int(cap.get(cv2.CAP_PROP_POS_MSEC))
        sleep = max(1, int(play_time - elapsed))
        if cv2.waitKey(sleep) & 0xFF == ord("q"):
            breakCode language: Python (python)

最後に、pyaudioの処理を説明します。

class WithPyAudio:
    def __init__(self, audio_file):
        super().__init__()
        wf = wave.open(audio_file, "rb")
        p = pyaudio.PyAudio()

        self.wf = wf
        self.p = p
        print(f"{wf.getsampwidth()=},{wf.getnchannels()=},{wf.getframerate()=}")

        self.stream = p.open(
            format=p.get_format_from_width(wf.getsampwidth()),
            channels=wf.getnchannels(),
            rate=wf.getframerate(),
            output=True,
            stream_callback=self._stream_cb,
        )

    def close(self):
        self.stream.stop_stream()
        self.stream.close
        self.p.terminate()
        self.wf.close()

    def _stream_cb(self, in_data, frame_count, time_info, status):
        data = self.wf.readframes(frame_count)
        # process audio here
        return (data, pyaudio.paContinue)Code language: Python (python)

waveファイルオブジェクトwf、PyAudioオブジェクトpを作成し、p.openを呼び出すとstream_callbackに渡したコールバック関数_stream_cbが定期的に呼び出されます。

    def __init__(self, audio_file):
        super().__init__()
        wf = wave.open(audio_file, "rb")
        p = pyaudio.PyAudio()

        self.wf = wf
        self.p = p
        print(f"{wf.getsampwidth()=},{wf.getnchannels()=},{wf.getframerate()=}")

        self.stream = p.open(
            format=p.get_format_from_width(wf.getsampwidth()),
            channels=wf.getnchannels(),
            rate=wf.getframerate(),
            output=True,
            stream_callback=self._stream_cb,
        )Code language: Python (python)

_stream_cbではwaveファイルオブジェクトwfから音声フレームを読み出し、そのフレームをリターンします。音声ファイルに処理を加えたい場合は、このコールバック関数内で取得した音声フレームを処理します。

    def _stream_cb(self, in_data, frame_count, time_info, status):
        data = self.wf.readframes(frame_count)
        # process audio here
        return (data, pyaudio.paContinue)Code language: Python (python)

ソースコードはhttps://github.com/otamajakusi/opencv_video_with_audioにあります。


応用例

応用例として、映像の物体認識を実行しながら音声認識を実行し、特定の音声を検出したい場合、該当の映像を強調表示される例を考えます。説明がわかりにくいで以下のビデオを参照してくだいさい。

これは将棋の盤面をYOLOv5で認識しながら、解説者の「45歩」、「88玉」、「79玉」などの音声を認識し、将棋盤にその座標の位置を強調表示させる例です。音声認識はwav2vec2を使用しています。

以上です。

参照