映像データでデータ分析したいみなさん、 こんにちは、システムアーキテクトの伊勢です。 特定用途で映像を解析するAIモデルを作るのは大変です。 本記事では、映像をそのまま解析するのではなく、 生成AIでフレームをテキストに要約し、CSVとして分析するアプローチを紹介します。 はじめに 映像のAI解析 コンセプト 映像フレームのテキスト要約 全体構成図 インストール クライアントライブラリ OpenAIパッケージ やってみた ローカル線の車窓映像 分析課題 プロンプト データ分析 雪国の市バス映像 分析課題 プロンプト データ分析 起動オプション サンプルプログラム説明 要約入力データ量・頻度削減 グリッド画像生成 JPEG圧縮率指定 キューサイズ上限指定 トークン数超過判定 OpenAI問い合わせ アップストリーム おわりに リンク はじめに 映像のAI解析 本ブログでは、事前学習したAIモデルでの物体検出をご紹介しました。 SDK入門④でYOLOで物体検出・矩形描画・人数カウントしています。 tech.aptpod.co.jp ただし、これらのAIモデルは事前学習による開発が必要です。 軽めのPoCなど利用シーンに応じて、本格的な専用モデル開発を伴わず、対象物の検出や文字読み取りが実現できると便利です。 コンセプト 本ブログでは、計測結果をChatGPTでデータ分析してきました。 tech.aptpod.co.jp tech.aptpod.co.jp 数値や文字列をCSVファイルでChatGPTに与え、検索や統計ができます。 しかし、映像はバイナリデータであるため、単純に同じ方法は使えません。 そこで、映像フレームをテキストデータ化してCSVでデータ分析してみます。 映像フレームのテキスト要約 映像フレームをAIモデルに要約・記述させ、テキストデータに変換します。 1 OpenAIの chat.completions APIに 映像フレームをBase64エンコードしてリクエストします。 qiita.com 生成AI活用で重要なのは、すべてを送るのではなく「何を送らないか」の設計です。 精度・安定性のため、映像フレームを間引き・集約してデータ量と頻度を抑えます。 2 # 工夫 効果 1 映像フレームを縮小 データ量低減 2 前フレームとの差分が一定以上のときだけ 頻度低減 3 4×4グリッド画像に時系列圧縮 頻度低減 4 JPEG圧縮 データ量低減 5 リクエスト待ちを2件までに制限 頻度低減 6 トークン数超過したらスキップ 頻度低減 これらは前処理が必要なのでintdash SDKを使って実装します。 3 全体構成図 intdash リアルタイムAPIからダウンストリームした映像フレームを グリッド状に並べた画像と要約結果をアップストリームします。 要約結果は、あとでCSVでダウンロードして計測全体の分析を行います。 全体構成図 インストール クライアントライブラリ REST API、リアルタイムAPIの使い方は SDK入門④ と同じです。 OpenAIパッケージ 追加でOpenAIライブラリをインストールします。 pip install openai やってみた ローカル線の車窓映像 分析課題 2024年11月、房総半島を走る久留里線の一部区間(久留里駅~上総亀山駅間)が廃止されると発表されました。 利用客の激減による不採算が理由だそうです。 www.sankei.com 実際に乗車してきました。 4 木更津駅を出発して、廃線予定の久留里駅〜終点上総亀山駅までを走る区間です。 5 計測と要約結果を可視化しました。 6 youtu.be プロンプト 画像を要約させるプロンプトを chat.completions APIに渡します。 あなたは列車のカメラ映像(4x4グリッドの時系列画像)を要約するAIです。 遠景を含む、人物・居住・移動・インフラ管理から人間生活の兆候を検出します。 出力は必ず以下のJSON形式で返してください。 { "person": 人物の数 int, "group": 集団の有無 無/有, "view": 視野 無/狭/中/広, "area": エリア種別 市街地/住宅地/農地/草地/山野 etc, "building": 住宅密度 無/疎/密, "road": 道路幅 無/細/太, "infra": 設備の整備度 無/荒/整, "text": 画像から読み取れる文字(グリッド左上の時刻は除く), "description": 見える風景の説明 } 有効なJSONのみを出力してください。 このようなJSONが返ってきます。 7 { " person ": 3 , " group ": " 無 ", " view ": " 広 ", " area ": " 市街地 ", " building ": " 疎 ", " road ": " 太 ", " infra ": " 整 ", " text ": " 28 ", " description ": " 市街地の道路沿いにある建物と車両、バイクが見える風景。周囲には草木が生えている。 " } リアルタイムで要約した結果をintdashにアップストリームします。 python ./lesson10/src/analyze_video.py --api_url https://example.intdash.jp --api_token < YOUR_API_TOKEN > --project_uuid < YOUR_PROJECT_UUID > --edge_uuid < YOUR_EDGE_UUID > --openai_key < YOUR_OPENA_KEY > プログラム起動 Data Visualizerでリアルタイム可視化 以下の項目が表示されています 入力データ:画面左上 GNSSデータ H.264映像 出力データ:画面中央・右側・下側 プレビュー画像:グリッドを埋めている途中の画像(JPEG) 要約対象画像:グリッドが埋まって要約対象になった画像(JPEG) 要約結果:JSON内の項目 データ分析 それでは、分析を行います。 課題 ローカル線の利用者数はそんなに少ないのか。 Data Visualizerでデータを選択してCSVファイルをダウンロードします。 intdash Motionアプリ GNSSデータ グリッド画像の要約結果 JSONの各項目 https://example.intdash.jp/vm2m/?playMode=storedData&startTime=2026-01-12T14:54:44.000%2B09:00&endTime=2026-01-12T16:05:04.000%2B09:00 CSVダウンロード CSVファイル ChatGPTにCSVファイルをアップロードして、基本情報を確認します。 ローカル線走行中のGNSSデータ、カメラ映像の要約結果のCSVデータをアップロードします。 GNSSデータ(緯度経度、速度、高度、方位)、要約結果(人物数、集団有無、視界の広さ、エリア種別、建物密度、道路広さ、インフラ整備度、文字起こし、説明)が含まれます。 課題:ローカル線の利用者数はそんなに少ないのか。 のためにデータ分析を行います。 以下を確認してください。 - 項目の一覧 - データ行数 - タイムスタンプ範囲 データの確認 データ項目を整えます。 データを整えてください。 - 見やすさのため、項目名の"GNSS_"と"@"以降を削除してください。 - 空の値が含まれます。GNSSデータについては直前の値を使用して穴埋め(forward fill)してください。 また、 - 分析結果のグラフは英語で、本文は日本語で表記してください。 データクレンジング まずはシンプルに聞いてみます。 検出した人数の最大値、集団を検出したか、 を教えてください。 人物・集団検出の結果 グラフや地図で可視化してみます。 人数が 0人 / 1–2人 / 3人以上 の時間割合 集団が 無 / 有 の時間割合 をそれぞれグラフにしてください。 人数、集団有無の割合グラフ GNSSの緯度経度データを、PythonのFoliumを使って以下をプロットしてください。 以下を可視化してください。 ・人物検出箇所・人数 ・集団検出箇所 人数・集団の地図プロット 検出されたのはいずれも木更津駅発の前半区間であり、廃線予定の久留里駅以南では人物・集団が見られません。 8 ただし、誤検出の箇所があり、精度は専用AIモデルに劣りそうです。 墓石を人間と間違えてそう 今度は地図上に、エリア種別を色分けしてプロットしてください。 エリア種別 赤 :市街地 オレンジ :住宅地 緑 :草地 濃緑 :農地 青 :山野 久留里駅以南ははっきりと山野が多いのがわかります。 9 同じく、各要約結果をプロットしました。 建物密度 赤 :密 濃緑 :疎 青 :無 道路幅 赤 :太 濃緑 :細 グレー :他 インフラ整備度 濃緑 :整 黒 :荒 グレー :無 文字起こしの精度も見てみます。 文字起こしできた文字列をリスト化してください。 文字起こしリスト 沿線に看板などがそれほど多くはないようです。 複数行の場合は改行コード付きです。 JPEGの画質を落としているため、"俵田"駅の誤字が目立ちます。 結論 1回の計測では久留里駅以南で"混雑"を確認できなかった。 一方で、沿線環境の特徴を読み解くのには有効な情報が得られた。 車窓から広がる自然の眺めは、思わず時間を忘れてしまう魅力がある。 雪国の市バス映像 プロンプトを差し替えて別のデータ分析を行います。 北海道 千歳市街〜新千歳空港までのバス映像です。 分析課題 2026年1月13日、寒波到来により、積雪による交通麻痺等が懸念されていました。 www.uhb.jp 実際に行って確認してきました。 10 千歳市街でバスに乗り込み、新千歳空港まで走ります。 11 計測と要約結果を可視化しました。 12 youtu.be プロンプト プロンプトをこのように差し替えます。 あなたは豪雪地帯の市バスのカメラ映像(4x4グリッドの時系列画像)を要約するAIです。 交通・積雪・除雪状態から生活の支障具合を検出します。 出力は必ず以下のJSON形式で返してください。 { "person": 人物の数 int, "vehicle": 車両の数 int, "view": 視野 無/狭/中/広, "area": エリア種別 市街地/住宅地/郊外/トンネル/空港 etc, "depth": 建物・樹木・車上の積雪 無/少/多, "sidewalk": 歩道の除雪 未/中/済, "pile": 路肩の堆雪 無/少/多, "text": 画像から読み取れる文字(グリッド左上の時刻は除く), "description": 見える風景の説明 } 有効なJSONのみを出力してください。 このようなJSONが返ってきます。 { " person ": 1 , " vehicle ": 1 , " view ": " 中 ", " area ": " 市街地 ", " depth ": " 多 ", " sidewalk ": " 中 ", " pile ": " 多 ", " text ": " 消火栓 ", " description ": " 雪が積もった市街地の道路に、歩道と車道があり、人の人物と1台の車が見える。 " } プロンプト設定ファイルを指定してプログラムを起動します。 python ./lesson10/src/analyze_video.py --api_url https://example.intdash.jp --api_token < YOUR_API_TOKEN > --project_uuid < YOUR_PROJECT_UUID > --edge_uuid < YOUR_EDGE_UUID > --openai_key < YOUR_OPENA_KEY > --prompt_path ./lesson10/config/snow.txt プロンプト差し替えで起動 プロンプト差し替えで可視化 データ分析 分析を行います。 課題 豪雪地帯の除雪は行き届いているか。 Data Visualizerでデータを選択してCSVファイルをダウンロードします。 CSVダウンロード CSVファイル ChatGPTにCSVファイルをアップロードして、基本情報を確認します。 豪雪地域の市バス走行中のGNSSデータ、カメラ映像の要約結果のCSVデータをアップロードします。 GNSSデータ(緯度経度、速度、高度、方位)、要約結果(人物数、車両数、視界の広さ、エリア種別、建物・樹木・車上の積雪状況、歩道の除雪状況、路肩の堆雪状況、文字起こし、説明)が含まれます。 課題:豪雪地帯の除雪は行き届いているか。 のためにデータ分析を行います。 以下を確認してください。 - 項目の一覧 - データ行数 - タイムスタンプ範囲 データの確認 データクレンジング 今回はいきなりグラフ化してみます。 歩道の除雪状況の割合 をグラフで表示してください。 歩道の除雪状況グラフ 文字化けしていますが、左から 未 / 中 / 済 の順で並んでいます。 済 と判断されたのは40%以下、 未 は50%超です。 ただし、除雪後も完全に歩道が見えないケースでは画像で白く見えるため、判断の精度は高くなさそうです。 除雪状況が判断が難しそうなケース もう少し詳しく見てみましょう。 GNSSの緯度経度データを、PythonのFoliumを使って以下をプロットしてください。 以下を可視化してください。 ・歩道の除雪状況 歩道の除雪状況のプロット 赤 :未 オレンジ :中 濃緑 :済 グレー :他 市街や交差点、空港構内はおおむね除雪率が高そうです。 他の項目も可視化してみましょう。 今度は地図上に、エリア種別を色分けしてプロットしてください。 市街地/住宅地/郊外/トンネル/空港 etc で色分けしてください。 エリア種別 赤 :市街地 青 :住宅地 濃緑 :郊外 紫 :トンネル オレンジ :空港 ピンク :他 エリア種別ごとの除雪率を確認してみます。 エリア別の除雪状況 未/中/済 を積み上げグラフ化してください。 グラフ内の文言は英語に訳して表示してください。 エリア別除雪状況 空港と判断されたエリアは除雪率が80%超、市街地は部分的も含めると70%弱です。 郊外の除雪率は20%に留まっており、歩行者が多いエリアを優先して除雪されているのがわかります。 トンネルはそもそも積雪がありませんが、除雪も 未 と判断されています。 トンネル内は 積雪:無 かつ 除雪:未 他の観点でも見てみましょう。 路肩の堆雪と視界の広さを組み合わせて、 路肩の堆雪により、視界を確保できないと思われるポイントをプロットしてください。 視界確保リスクポイント Data Visualizerで見ると確かに該当時刻は大きな堆雪が映っており、歩道の状況がほとんど見えません。 13 該当時刻の映像 結論 除雪は限られたリソースで極めて効率的に行われていることが伺える。 大動脈である空港は"最優先"で守られている。 起動オプション --api_url required :サーバーURL --api_token required :APIトークン --project_uuid :プロジェクトUUID(省略時は Global Project) --edge_uuid required :ダウンストリームエッジUUID --dst_edge_uuid :アップストリームエッジUUID --openai_key required :OpenAI アクセスキー --prompt_path :システムプロンプトファイルパス サンプルプログラム説明 複雑に見えますが、 SDK入門④ と同じく各処理をシンプルな非同期ステージに分けて並列起しています。 クラスアーキテクチャ 要約入力データ量・頻度削減 グリッド画像生成 フレームを縮小します。 img = np.frombuffer(frame, dtype=np.uint8).reshape((self._in_h, self._in_w, 3 )) tile_img = cv2.resize( img, (self._tile_w, self._tile_h), interpolation=cv2.INTER_AREA ) 前フレームと現フレームの差異が閾値未満なら、スキップします。 ヒストグラム化して比較していますが、場合によってはもっと厳密に判定する方がいいかもしれません。 if self._prev_tile_hist is not None : d = cv2.compareHist(self._prev_tile_hist, cur_h, cv2.HISTCMP_BHATTACHARYYA) if d < self._diff_threshold: return None , False self._prev_tile_hist = cur_h 4x4グリッドに配置します。 for i, t in enumerate (self._tiles[: self._grid_size]): c = i % self._cols x0 = c * self._tile_w r = i // self._cols y0 = r * self._tile_h grid[y0 : y0 + self._tile_h, x0 : x0 + self._tile_w] = t JPEG圧縮率指定 JPEGエンコードの GStreamer パイプラインに設定しています。 QUALITY = 50 ... quality=QUALITY, キューサイズ上限指定 self.prompt_queue: asyncio.Queue[ int ] = asyncio.Queue(maxsize=chat_maxsize) トークン数超過判定 try : answer = await asyncio.to_thread(self.chatter.chat, frame) ... except RateLimitError as e: logging.info(f "RateLimitError! {e}" ) await asyncio.sleep( 0.5 ) OpenAI問い合わせ JPEG画像をBase64エンコードしてリクエストに付与しています。 MODEL = "gpt-4o-mini" ... image_b64 = base64.b64encode(image).decode( "utf-8" ) messages: list [ChatCompletionMessageParam] = [ { "role" : "system" , "content" : self.system_prompt}, { "role" : "user" , "content" : [ { "type" : "image_url" , "image_url" : { "url" : f "data:image/jpeg;base64,{image_b64}" }, }, ], }, ] resp = self.client.chat.completions.create( model=MODEL, messages=messages, max_tokens= 400 , temperature= 0.3 , response_format={ "type" : "json_object" }, ) アップストリーム 要約結果をアップストリームしています。 await self.up.write_data_points( iscp.DataID(name=self.data_name_answer, type = "string" ), iscp.DataPoint( elapsed_time=elapsed_time, payload=payload.encode( "utf-8" ), ), ) await self.up.flush() おわりに SDKを利用して映像データをテキスト要約、生成AIでデータ分析しました。 また、プロンプトを差し替えて要約内容を変更できました。 AI開発が不要になるため、スマートメンテや巡回点検PoCを試しやすくなりそうです。 今回はインターネット経由でAPIエンドポイントを使用しています。 14 生成AI利用のネットワーク遅延やトークン数に縛られないため、Ollama などの検討も有効そうです。 tech.aptpod.co.jp tech.aptpod.co.jp リンク 本シリーズの過去記事はこちらからご覧ください。 SDK入門①〜社用車で走ったとこ全部見せます〜 :REST APIでデータ取得 SDK入門②〜データ移行ツールの作り方〜 :REST APIでデータ送信 SDK入門③〜RTSPで映像配信するぞ〜 :リアルタイムAPIでデータ取得 SDK入門④〜YOLOで物体検知しちゃう〜 :リアルタイムAPIでデータ送信 SDK入門⑤〜iPadでData Visualizerを見る会〜 :リアルタイムAPIでキャプチャデータ送信 SDK入門⑥〜最速最高度で計測する日〜 : AWS LambdaでREST APIデータ送信 SDK入門⑦〜計測リプレイツールの作り方〜 : REST APIでデータ取得、リアルタイムAPIでデータ送信 SDK入門⑧〜動画アップロードツールの作り方〜 :REST APIで映像データ送信 SDK入門⑨〜動画ダウンロードツールの作り方〜 :REST APIでデータ取得、ファイル化 今回、モデルはOpenAIの gpt-4o-mini を使用します。テキストも画像も入力できるマルチモーダルモデルです。視覚精度ではVLMが有利ですが、intdashから映像以外のデータも渡せることを想定して、マルチモーダルモデルを選択しています。 ↩ 同時にOpenAIのAPIトークン消費量も低減しています。 ↩ サンプルプログラムを GitHub にて公開しています。 ↩ 2024年2月に計測を行いました。 ↩ 一度収集した計測データを SDK入門⑦〜計測リプレイツールの作り方〜 でリアルタイムで再アップストリームしています。 ↩ 1時間ほどあります。飛ばしながら見てください。 ↩ たまに 道路幅:細 のように指定から崩れる場合があります。 ↩ 進行方向右側の映像のみで判定しています。左側にホームある駅が多かったので実態より少なく検出されているかもしれません。 ↩ プロンプトで与えた例 市街地/住宅地/農地/草地/山野 の区分けに引きずられてそうです。プロンプトの与え方次第でもっと有意義な分類ができるかもしれません。 ↩ 吹雪の数日後の2026年1月18日に計測を行いました。 ↩ こちらも計測データを SDK入門⑦〜計測リプレイツールの作り方〜 で再アップストリームしています。 ↩ 16分ほどです。再生速度を調整などして見てください。 ↩ 他の該当ポイントでは大きなフェンスが映っていたりするため、厳密性は高くなさそうです。 ↩ 消費トークンの従量課金です。開発を通じて、長時間データを流しっぱなしにしないように気をつけて使い、$4.40分を消費しました。 ↩