📹

o1で動画解析したいなら、こう!!

に公開

はじめまして、ふっきーです
普段いろいろなLLMアプリ開発をしています。

画像を入力可能なLLM APIはいくつかあるのですが、動画を入力できるものは少ない印象です。
フレームを切り出して画像として入力する方法もありますが、その方法のベストプラクティスがわかりませんでした。
対象フレームと前フレームの差分画像を入力するとか、いろいろ考えられますが、なんだか面倒くさいです。
探したらOpen AIのCookBookにあったので、それを真似してみます。

こう

セットアップして、

使うライブラリのimportと、APIキーの設定をします。

import cv2  # We're using OpenCV to read video, to install !pip install opencv-python
import base64
import time
from openai import OpenAI
import os

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<your OpenAI API key if not set as env var>"))

フレーム画像の取得して、

動画をフレーム画像分け、base64エンコードして、OpenAI APIに渡せるようにします。

video = cv2.VideoCapture("data/input_video.mp4")

base64Frames = []
while video.isOpened():
    success, frame = video.read()
    if not success:
        break
    _, buffer = cv2.imencode(".jpg", frame)
    base64Frames.append(base64.b64encode(buffer).decode("utf-8"))

video.release()

OpenAI APIに渡す

エンコードしたフレーム画像をOpenAI APIに渡します。
フレームの渡し方はそのままいっき渡しでした。なんの工夫もありません。

response = client.responses.create(
    model="o1",
    input=[
        {
            "role": "user",
            "content": [
                {
                    "type": "input_text",
                    "text": (
                        "These are frames from a video. Generate a detail description."
                    )
                },
                *[
                    {
                        "type": "input_image",
                        "image_url": f"data:image/jpeg;base64,{frame}"
                    }
                    for frame in base64Frames[0::25]
                ]
            ]
        }
    ],
)

print(response.output_text)

めっちゃシンプルですね
フレームを切り出してbase64エンコードして、OpenAI APIに一度に渡すだけです。

試してみた

入力動画は、バスケの試合の動画です。
冒頭10秒ぐらいまでを入力としています
https://www.youtube.com/watch?v=Szgvdf2oO6I

結果

動画の長さ: 8.47 秒
実行時間: 36.04 秒
These frames show a fast-paced indoor basketball game between two teams: one in teal jerseys, the other in white. The court is a typical wooden surface with green out-of-bounds areas, and the stands are crowded with spectators on one side. In the opening moments, a teal player at the top of the key dribbles while a teammate sets a screen. Two white-uniformed defenders step up to pressure the ball. As the ball handler tries to thread a path around the defenders, contact occurs, and the ball gets knocked loose onto the floor.

Several players from both teams immediately dive in, creating a scramble at the free-throw line area. Teal- and white-jersey players wrestle for possession, with one white-uniformed player briefly pinning the ball on the hardwood. Eventually, another white player recovers it and passes quickly ahead to a teammate leaking out in transition.

In subsequent frames, the white team surges down the floor while teal defenders scramble to catch up. One white player is well ahead of everyone and receives a long outlet pass, setting up a wide-open finish at the opposite basket. Spectators can be seen reacting in the stands as the white player rises to attempt an easy layup. Meanwhile, teal players hustle back, but by the time they reach the paint, the white team has already put up the shot.

Later frames briefly show the teal team regrouping on defense and then attempting a counterattack. Numbered jerseys on both sides are visible—(for example, “6,” “13,” “18,” “41” in teal)—though the specific details of their names or identities are not shown. The sequence emphasizes quick ball movement, aggressive defense, and a fierce fight for loose balls, culminating in a successful fast-break basket for the team in white.

日本語指定を忘れていたので、翻訳しました。

これらのフレームには、ティール(青緑)色のユニフォームのチームと白いユニフォームのチームによる、テンポの速い屋内バスケットボールの試合が映し出されています。コートは木製の一般的な床で、アウト・オブ・バウンズの部分は緑色。観客席は片側が満員の状態です。

序盤のシーンでは、ティールの選手がトップ・オブ・ザ・キー(スリーポイントラインの中央)でドリブルしながら、味方がスクリーンをセットします。そこに白いユニフォームのディフェンダー2人がボールにプレッシャーをかけるように前に出てきます。ボールハンドラーがその間を抜けようとした瞬間、接触が起こり、ボールが床にこぼれます。

すぐに両チームの選手たちが飛び込み、フリースローライン付近でルーズボールをめぐる激しい争奪戦が始まります。ティールと白のユニフォームの選手がもみ合い、一時的に白い選手がボールを床に押さえ込みますが、最終的には別の白い選手がボールを確保し、素早く前方にパスを出します。そこにはすでに速攻に走り出していた味方がいました。

次のフレームでは、白チームが一気にコートを駆け下り、ティールの選手たちは懸命に戻ろうとします。しかし白の選手の1人が他の選手より大きく前に出ており、ロングパスを受けて、対面のゴールでイージーなレイアップを狙います。観客席では、その瞬間に反応する観客たちの姿も見られます。一方、ティールの選手たちはペイントエリアに戻ってきますが、その頃には白チームのシュートはすでに放たれていました。

その後のフレームでは、ティールチームが守備を立て直し、逆襲を狙う場面が短く描かれています。両チームの背番号も確認でき(ティールでは「6」「13」「18」「41」など)、ただし選手名や個人の詳細は表示されていません。全体を通じて、素早いパス回し、激しいディフェンス、ルーズボールをめぐる気迫のこもった攻防、そして白チームの速攻による得点という展開が描かれています。

状況も動きも両方と描写できていそうです。(ちょっと大袈裟な表現が目立ちますが)
複数のフレームを同時に渡している効果は出ていそうですね。

実装

import cv2
import base64
import time
import sys
import os
from pip 
from openai import OpenAI

def main():
    start_time = time.time()  # 実行開始時間
    video_path = "input_video.mp4"
    
    # 動画ファイルをオープン
    video = cv2.VideoCapture(video_path)
    if not video.isOpened():
        print("動画ファイルのオープンに失敗しました:", video_path)
        sys.exit(1)
    
    # FPSと総フレーム数を取得して動画時間を計算
    fps = video.get(cv2.CAP_PROP_FPS)
    frame_count = video.get(cv2.CAP_PROP_FRAME_COUNT)
    duration = frame_count / fps if fps > 0 else 0
    
    # フレームをbase64エンコード
    base64_frames = []
    while True:
        ret, frame = video.read()
        if not ret:
            break
        success, buffer = cv2.imencode('.jpg', frame)
        if not success:
            continue
        base64_str = base64.b64encode(buffer).decode("utf-8")
        base64_frames.append(base64_str)
    
    video.release()
    

    client = OpenAI()
    response = client.responses.create(
        model="o1",
        input=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "input_text",
                        "text": (
                            "These are frames from a video. Generate a detail description."
                        )
                    },
                    *[
                        {
                            "type": "input_image",
                            "image_url": f"data:image/jpeg;base64,{frame}"
                        }
                        for frame in base64_frames[0::25]
                    ]
                ]
            }
        ],
    )


    end_time = time.time()  # 実行終了時間
    execution_time = end_time - start_time

    # 結果出力
    print("動画の長さ: {:.2f} 秒".format(duration))
    print("実行時間: {:.2f} 秒".format(execution_time))

    print(response.output_text)

if __name__ == "__main__":
    main()

最後に

動画解析を意外と簡単にできました。出力も不満ないですし、いい感じです。
なんかいろいろな活用方法がありそうですね

Discussion