チュートリアル
Twelve Labs APIを使用して動画内からブランドロゴを見つける方法

アンキット・カレ
開発者はTwelve Labs APIを使用することで、独自のロゴ検出インフラを構築・維持することなく、ビデオからすべてのブランドロゴを抽出し、インデックス化されたビデオライブラリ全体から特定のロゴを名前で検索できます。このチュートリアルでは、ビデオレベルのロゴ抽出とインデックスレベルのロゴ検索の両方をカバーし、その結果を表示するFlaskアプリについても説明します。
開発者はTwelve Labs APIを使用することで、独自のロゴ検出インフラを構築・維持することなく、ビデオからすべてのブランドロゴを抽出し、インデックス化されたビデオライブラリ全体から特定のロゴを名前で検索できます。このチュートリアルでは、ビデオレベルのロゴ抽出とインデックスレベルのロゴ検索の両方をカバーし、その結果を表示するFlaskアプリについても説明します。

この記事の内容
ニュースレターに登録する
ニュースレターに登録する
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
AIを活用してビデオを検索、分析、探索します。
2023/06/09
13分
記事へのリンクをコピー
動画におけるロゴ検出とは、動画コンテンツ内に埋め込まれたロゴや商標を自動的に識別・認識するプロセスのことを指します。これには、動画のフレームやセグメントを分析して、ブランドに関連付けられた特定のロゴパターンや視覚的要素を検出および特定することが含まれます。この技術により、動画コンテンツ内を迅速にナビゲートし、特定のロゴパターンが画面に表示される具体的な瞬間を正確に特定することができます。動画におけるロゴ検出は、動画コンテンツ内に埋め込まれたロゴや商標を自動的に識別・認識するプロセスのことを指します。これには、動画のフレームやセグメントを分析して、ブランドに関連付けられた特定のロゴパターンや視覚的要素を検出および特定することが含まれます。この技術により、動画コンテンツ内を迅速にナビゲートし、特定のロゴパターンが画面に表示される具体的な瞬間を正確に特定することができます。動画データにおけるロゴ検出は、幅広い要素を識別できるため、さまざまな業界の垂直市場にわたって多くの応用分野があります。
広告とマーケティング: 企業は、オンラインとオフラインの両方で、さまざまなメディアにおける自社ブランドの存在感と視認性を監視できます。これにより、マーケティングキャンペーンの影響を評価し、ロゴの不正使用を特定し、競合他社のマーケティング戦略を理解するのに役立ちます。
ソーシャルメディアの監視: ロゴ検出は、ブランドのロゴがユーザー生成コンテンツのどこに、どのくらいの頻度で表示されるかを理解するのに役立ちます。これにより、ブランドの人気、使用文脈、感情分析に関する洞察を得ることができます。
小売とEコマース: 小売業者はロゴ検出を使用して在庫を監視および管理できます。たとえば、模倣品や無許可の販売者を特定するのに役立ちます。
スポーツスポンサーシップ: ロゴ検出は、スポーツイベントの生中継中のブランド露出を定量化できます。スポンサーや広告主に提供される価値に関する洞察を提供できます。
メディアとエンターテインメント: エンターテインメント業界では、ロゴ検出を使用して映画やテレビ番組でのプロダクトプレイスメントを追跡できます。また、著作権侵害の特定も可能です。
セキュリティと監視: ロゴ検出は、セキュリティ目的で企業のロゴに基づいて車両や物体を特定・追跡するのに役立ちます。
自動運転・自動車: 自動車業界では、ロゴ検出によって車のメーカーやモデルを特定し、交通分析、駐車管理、または自動運転システムを支援できます。
このチュートリアルでは、2つの異なる視点とレベルからロゴ検出の世界を探求していきます。1つ目は動画レベルで、動画コンテンツ全体を単一のエンティティとして扱い、そこに含まれるすべてのロゴ情報を掘り起こそうとします。2つ目のアプローチであるインデックスレベルでは、レンズの焦点を絞り、特定のロゴまたはロゴのグループに集中します。自然言語クエリを使用して、Twelve Labsプラットフォームにインデックス登録された豊富な動画ライブラリ全体で徹底的な検索を行います。
最も魅力的な部分は、Twelve Labs APIを使用することで、検出プロセスを構成するモデルのトレーニング、デプロイ、推論、または負荷のスケールなどの複雑な作業に煩わされることなく、これらすべてを達成できる点です。開発からインフラストラクチャまでカバーし、継続的なサポートも提供します。それでは、シートベルトを締めて、ロゴ検出の領域への魅力的な旅に一緒に出発しましょう。
動画内テキストとロゴ検出 - 潜在的な重複について
ロゴが単にブランド名や企業名である場合があり、これを画面上のテキストとして処理できるのか、動画内テキスト(OCR)のインデックス作成および検索オプションでロゴ検出に十分なのかという疑問が生じることがあります。動画再生中にそのようなテキストが画面に表示されたときに検出されるのは事実です。しかし、テキストが異なる文脈で異なる意味を持つ可能性がある場合には、ロゴを検出するためのインデックス作成および検索オプションを具体的に構成することが重要です。
たとえば、「Amazon」は多国籍テクノロジー企業を指すこともあれば、南アメリカの川を指すこともあります。ロゴ検出を採用している場合、システムはAmazonのブランドロゴの検索と、テキストとしての「Amazon」を区別します。したがって、ブランドロゴがテキストベースであり、動画内テキストとロゴ検出が表面的に重複する場合でも、正確な結果を確保するためにロゴ検出機能を意図的に選択して使用する必要があります。
前提条件
Twelve Labsプラットフォームは現在オープンベータ版であり、新規登録時に最大10時間分の無料動画インデックス作成クレジットを提供しています。このチュートリアルを開始する前に、Twelve Labsプラットフォームの主要な側面に慣れておくことをお勧めします。このチュートリアルをスムーズに進めるには、動画インデックス作成、インデックスオプション、Task API、検索オプションなどの概念をしっかりと理解していることが不可欠です。これらのトピックについては、私の最初のチュートリアルで詳しく説明しています。それにもかかわらず、障害に遭遇したり、どこかで立ち往生したりした場合は、いつでもお気軽にお問い合わせください。ロゴ検出の領域へのこのエキサイティングな旅で、私はいつでもあなたをサポートします。また、Discordがお好みのプラットフォームである場合、当社のDiscordサーバーでの応答時間は非常に高速です 🚅🏎️⚡️。
チュートリアルのクイックガイド
これまでの議論に引き続き、2つの異なる視点とレベルからロゴ検出について深く掘り下げていきます。このチュートリアルを2つの重要なセクションに分割し、最終的にすべての要素を完全に機能するウェブアプリに統合するデモで締めくくります。
3つの簡単なステップでのロゴ検出
特定の動画から認識されたロゴを抽出するには、次の3つのステップを実行します。
動画のインデックス作成 - ここに驚きはありません。以前のチュートリアルを読んできた方なら、このステップはもうすっかり慣れているはずです。
動画の一意識別子の取得 - Twelve Labsプラットフォームが動画のインデックス作成を完了したら、ロゴ検出が必要な動画の一意識別子を取得します。
画面に表示されるロゴの抽出 - 作成した特定のインデックスと、ロゴ検出が必要な動画に関連付けられたビデオIDを使用して、動画に焦点を絞ります。APIが面倒な処理を引き受け、求めている結果を提供してくれます。
ロゴ検索 - インデックス登録されたすべての動画から特定のロゴを検索する
動画全体にロゴ検出を適用することで、ロゴパターンのすべてのインスタンスを詳細に調査して抽出することができました。今度は、ロゴ検索機能を使用することで、入力または検索されたロゴやブランド名が具体的に現れる瞬間や動画スニペットをピンポイントで特定できます。これにより、大規模な動画カタログを調査する時間が大幅に短縮され、動画再生中に画面に表示されるロゴと検索用語の整合性に基づいた正確な検索結果が得られます。
これまでのチュートリアルでは、自然言語クエリと、ビジュアル(オーディオビジュアル検索)、会話(対話検索)、動画内テキスト(OCR)などのさまざまな検索オプションを利用して、インデックス登録された動画内のコンテンツ検索について説明しました。このチュートリアルでは、アプローチを切り替え、ロゴ検出パイプライン専用に活用して、インデックス登録された動画内のロゴを検索します。処理効率を最大化し、コストを最小限に抑えるために、「logo」インデックス作成オプションのみを使用してインデックスを作成します。その後、「logo」検索オプションを使用して検索クエリを実行し、インデックス登録された動画内で関連するロゴの一致を検出できるようにします。
デモアプリの構築
すべてを統合するために、APIエンドポイントによって生成されたデータを利用してウェブページに表示し、ミニマルなHTMLページをホストするFlaskベースのデモアプリを構築します。ロゴ検出プロセスの結果は、タイムスタンプと対応するロゴ名を示す表として系統的に整理されます。一方、ロゴ検索セクションには、使用したクエリと、それに対応して発見された関連動画セグメントが表示されます。
3つの簡単なステップでロゴを検出する
シンプルにするために、既存のアカウントを使用して5つの動画のみをインデックスにアップロードしました。現在はオープンベータ期間中のため、登録すれば最大10時間の動画コンテンツをインデックス作成できる無料のクレジットを受け取ることができます。それを超える容量が必要な場合は、Developerプランへのアップグレードについて当社の料金ページをご確認ください。
動画のインデックス作成
ここでは、Jupyter Notebookに含める必要のある重要な要素について説明します。これには、必要なインポート、API URLの定義、インデックスの作成、ローカルファイルシステムから動画をアップロードしてインデックス作成プロセスを開始することが含まれます。
<pre><code class="python">%env API_URL = https://api.twelvelabs.io/v1.1 %env API_KEY= <あなたのAPIキー> !pip install requests import os import requests import glob from pprint import pprint # APIのURLと自分のAPIキーを取得 API_URL = os.getenv("API_URL") assert API_URL API_KEY = os.getenv("API_KEY") assert API_KEY </code></pre>
<pre><code class="python"># `/indexes` エンドポイントのURLを構築 INDEXES_URL = f"{API_URL}/indexes" # リクエストのヘッダーを設定 default_header = { "x-api-key": API_KEY } # 指定された名前でインデックスを作成する関数を定義 def create_index(index_name, index_options, engine): # data という名前の辞書を宣言 data = { "engine_id": engine, "index_options": index_options, "index_name": index_name, } # インデックスを作成 response = requests.post(INDEXES_URL, headers=default_header, json=data) # インデックスの一意の識別子を保存 INDEX_ID = response.json().get('_id') # ステータスコードが201であるか確認し、成功を出力 if response.status_code == 201: print(f"Status code: {response.status_code} - The request was successful and a new index was created.") else: print(f"Status code: {response.status_code}") pprint(response.json()) return INDEX_ID # インデックスを作成 index_id = create_index(index_name = "extract_text", index_options=["logo"], engine = "marengo2.5") # 作成されたインデックスIDを出力 print(f"Created index IDs: {index_id}") </code></pre>
作成したばかりのインデックスに5つのF1レース動画をアップロードします。これらの動画はF1関連のYouTubeチャンネルからダウンロードし、ローカルハードドライブの「static」というフォルダーに保存してあります。これらのローカルファイルを使用して、Twelve Labsプラットフォームに動画のインデックスを作成します。
<pre><code class="python">import os import requests from concurrent.futures import ThreadPoolExecutor TASKS_URL = f"{API_URL}/tasks" TASK_ID_LIST = [] video_folder = 'static' # 動画ファイルを含むフォルダー def upload_video(file_name): # インデックスに動画が既に存在するか検証 task_list_response = requests.get( TASKS_URL, headers=default_header, params={"index_id": INDEX_ID, "filename": file_name}, ) if "data" in task_list_response.json(): task_list = task_list_response.json()["data"] if len(task_list) > 0: if task_list[0]['status'] == 'ready': print(f"Video '{file_name}' already exists in index {INDEX_ID}") else: print("task pending or validating") return # 動画がまだインデックスに存在しない場合、現在の動画のインデックス作成用の新しいタスク作成コードに進む print("Entering task creation code for the file: ", file_name) if file_name.endswith('.mp4'): # ファイルがMP4動画であることを確認 file_path = os.path.join(video_folder, file_name) # 動画ファイルのフルパスを取得 with open(file_path, "rb") as file_stream: data = { "index_id": INDEX_ID, "language": "en" } file_param = [ ("video_file", (file_name, file_stream, "application/octet-stream")),] #動画は動画ファイル自体と同じ名前でプラットフォーム上にインデックス登録されます。 response = requests.post(TASKS_URL, headers=default_header, data=data, files=file_param) TASK_ID = response.json().get("_id") TASK_ID_LIST.append(TASK_ID) # ステータスコードが201であるか確認し、成功を出力 if response.status_code == 201: print(f"Status code: {response.status_code} - The request was successful and a new resource was created.") else: print(f"Status code: {response.status_code}") print(f"File name: {file_name}") pprint(response.json()) print("\n") # 動画ファイルのリストを取得 video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')] # ThreadPoolExecutorを作成 with ThreadPoolExecutor() as executor: # 全動画ファイルに対してupload_videoを並行処理するようexecutorを使用 executor.map(upload_video, video_files) </code></pre>
動画の一意識別子の取得
次に、インデックス内のすべての動画を一覧表示してみましょう。これにより、特定の動画のビデオIDを保持することができ、その動画に埋め込まれたすべてのテキストを抽出することを可能にします。さらに、以前のチュートリアルでの方法と同様に、ビデオIDとそのタイトルのリストを組み立て、後で Flask アプリケーションに渡せるようにします。
<pre><code class="python"># インデックス内のすべての動画を一覧表示する default_header = { "x-api-key": API_KEY } INDEX_ID='##4a73aa8b1dd6cde172a9##' INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos" response = requests.get(INDEXES_VIDEOS_URL, headers=default_header) response_json = response.json() pprint(response_json) video_id_name_list = [{'video_id': video['_id'], 'video_name': video['metadata']['filename']} for video in response_json['data']] pprint(video_id_name_list) </code></pre>
出力結果:
<pre><code class="python">{'data': [{'_id': '##d978c86daab572f3481##', 'created_at': '2023-04-17T18:56:51Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T190000Z.mp4', 'fps': 25, 'height': 396, 'size': 415876158, 'width': 704}, 'updated_at': '2023-04-17T19:01:32Z'}, {'_id': '##3d975786daab572f3481##', 'created_at': '2023-04-17T18:56:44Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211114T170000Z.mp4', 'fps': 25, 'height': 396, 'size': 387273943, 'width': 704}, 'updated_at': '2023-04-17T19:00:39Z'}, {'_id': '##3d972e86daab572f3481##', 'created_at': '2023-04-17T18:56:38Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T193000Z.mp4', 'fps': 25, 'height': 396, 'size': 386209689, 'width': 704}, 'updated_at': '2023-04-17T18:59:58Z'}, {'_id': '##3d96d386daab572f3481##', 'created_at': '2023-04-17T18:56:28Z', 'metadata': {'duration': 1800.52, 'engine_id': 'marengo2.5', 'filename': '20211121T133000Z.mp4', 'fps': 25, 'height': 396, 'size': 348611416, 'width': 704}, 'updated_at': '2023-04-17T18:58:27Z'}, {'_id': '##3d96af86daab572f3481##', 'created_at': '2023-04-17T18:56:08Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T200000Z.mp4', 'fps': 25, 'height': 396, 'size': 327766175, 'width': 704}, 'updated_at': '2023-04-17T18:57:51Z'}], 'page_info': {'limit_per_page': 10, 'page': 1, 'total_duration': 9002.76, 'total_page': 1, 'total_results': 5}} [{'video_id': '##3d978c86daab572f3481##', 'video_name': '20211113T190000Z.mp4'}, {'video_id': '##3d975786daab572f3481##', 'video_name': '20211114T170000Z.mp4'}, {'video_id': '##3d972e86daab572f3481##', 'video_name': '20211113T193000Z.mp4'}, {'video_id': '##3d96d386daab572f3481##', 'video_name': '20211121T133000Z.mp4'}, {'video_id': '##3d96af86daab572f3481##', 'video_name': '20211113T200000Z.mp4'}] </code></pre>
画面に表示されるロゴの抽出
計画を実行に移す時です!選択した動画からすべてのロゴコンテンツの抽出を進めます。
<pre><code class="python">VIDEO_ID = '###a849b86daab572f349242' LOGO_URL = f"{API_URL}/indexes/{INDEX_ID}/videos/{VIDEO_ID}/logo" # GETリクエストを実行 response = requests.get(LOGO_URL, headers=default_header) print (f"Status code: {response.status_code}") logo_data = response.json() pprint (logo_data) </code></pre>
出力結果:
<pre><code class="python">Status code: 200 {'data': [{'end': 16, 'start': 15, 'value': 'Ducati Corse'}, {'end': 23, 'start': 22, 'value': 'Bank of Jordan'}, {'end': 23, 'start': 22, 'value': 'Han Chiang High School'}, {'end': 24, 'start': 23, 'value': 'Peugeot'}, {'end': 25, 'start': 24, 'value': 'Dr Lal Path Labs'}, {'end': 26, 'start': 25, 'value': 'Z8Games'}, {'end': 29, 'start': 27, 'value': 'Sky Sports'}, {'end': 29, 'start': 28, 'value': 'Tout'}, {'end': 31, 'start': 30, 'value': 'Sky UK'}, {'end': 31, 'start': 30, 'value': 'New Balance'}, {'end': 31, 'start': 30, 'value': 'Industria'}, {'end': 33, 'start': 32, 'value': 'Esport3'}, {'end': 35, 'start': 32, 'value': 'Nissan'}, {'end': 33, 'start': 32, 'value': 'GoCar'}, {'end': 34, 'start': 33, 'value': 'Land Bank of the Philippines'}, {'end': 34, 'start': 33, 'value': 'Z8Games'}, {'end': 37, 'start': 36, 'value': 'Tout'}, {'end': 37, 'start': 36, 'value': 'Zazzle'}, {'end': 39, 'start': 38, 'value': 'Z8Games'}, {'end': 39, 'start': 38, 'value': 'Giochi Preziosi'}, {'end': 41, 'start': 40, 'value': 'Mini'}], 'id': '###a849b86daab572f349242', 'index_id': '###a73aa8b1dd6cde172a933'} </code></pre>
明らかに、APIは画面上で確認されたすべてのロゴを一行ずつ巧みに抽出しました。この情報は、コンテンツのフィルタリング、分類、検索などの後続のワークフローのメタデータとして保存できます。簡潔にするため、ここに表示されている出力は一部省略されています。実際の出力はかなり広範囲におよびました。
ロゴ検索 - インデックス登録されたすべての動画から特定のロゴを検索する
アップロードしたインデックス動画コレクションの中から、関連するロゴパターンの不一致を検出するために、logo検索オプションを使用して検索クエリを実行します。
<pre><code class="python"># `/search` エンドポイントのURLを構築 SEARCH_URL = f"{API_URL}/search/" # `data` という名前の辞書を宣言 data = { "index_id": INDEX_ID, "query": "honda", "search_options": [ "logo" ] } # 後でflaskアプリケーションに渡すためにクエリを抽出 input_query = data["query"] # 検索リクエストを実行 response = requests.post(SEARCH_URL, headers=default_header, json=data) if response.status_code == 200: print(f"Status code: {response.status_code} - Success") else: print(f"Status code: {response.status_code}") pprint(response.json()) </code></pre>
出力結果:
<pre><code class="python">Status code: 200 - Success {'data': [{'confidence': 'high', 'end': 19, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 18, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 104, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 103, 'video_id': '###d978c86daab572f348###'}, {'confidence': 'high', 'end': 137, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 136, 'video_id': '###d978c86daab572f348###'}, {'confidence': 'high', 'end': 269, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 268, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 392, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 391, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 478, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 477, 'video_id': '###d975786daab572f348###'}, {'confidence': 'high', 'end': 483, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 482, 'video_id': '###d978c86daab572f348###'}], 'page_info': {'limit_per_page': 10, 'next_page_token': 'fc3c420a-5971-428a-8796-9e5c077754c0-1', 'page_expired_at': '2023-05-26T18:40:26Z', 'total_results': 47}, 'search_pool': {'index_id': '###d9556f607a5a7bd9ea###', 'total_count': 5, 'total_duration': 9003}} </code></pre>
もう一度、APIは入力されたロゴ名に対応するすべての画面表示ロゴを専門的に調査して取得しましたが、今回はアップロードした全インデックス動画にわたって実行されました。
結果がすっきりと表示されるように、Flaskアプリケーション用のデータを準備します。
<pre><code class="python">video_data = [{'start': d['start'], 'end': d['end'], 'confidence': d['confidence'], 'text': d['metadata'][0]['text']} for d in search_data['data']] video_search_dict = {} for vd in video_data: if search_data['data'][0]['video_id'] in video_search_dict: video_search_dict[search_data['data'][0]['video_id']].append(vd) else: video_search_dict[search_data['data'][0]['video_id']] = [vd] pprint(video_search_dict) </code></pre>
出力結果:
<pre><code class="python"> {'###d96af86daab572f348###': [{'confidence': 'high', 'end': 19, 'start': 18, 'text': 'Honda'}, {'confidence': 'high', 'end': 104, 'start': 103, 'text': 'Honda'}, {'confidence': 'high', 'end': 137, 'start': 136, 'text': 'Honda'}, {'confidence': 'high', 'end': 269, 'start': 268, 'text': 'Honda'}, {'confidence': 'high', 'end': 392, 'start': 391, 'text': 'Honda'}, {'confidence': 'high', 'end': 478, 'start': 477, 'text': 'Honda'}, {'confidence': 'high', 'end': 483, 'start': 482, 'text': 'Honda'}, {'confidence': 'high', 'end': 491, 'start': 487, 'text': 'Honda'}, {'confidence': 'high', 'end': 561, 'start': 560, 'text': 'Honda'}, {'confidence': 'high', 'end': 586, 'start': 585, 'text': 'Honda'}]} </code></pre>
ロゴ検出結果のための最終的なデータ準備を行い、その後にPythonオブジェクトをpickle化するいつもの手順に進みます。
<pre><code class="python">video_id = ocr_data.get('id') data_list = logo_data.get('data') data_to_save = { 'video_id': video_id, 'data_list': data_list, 'video_id_name_list': video_id_name_list, 'video_search_dict': video_search_dict } import pickle # データをpickleファイルに保存 with open('data.pkl', 'wb') as f: pickle.dump(data_to_save, f) </code></pre>
デモアプリの構築
さて、いよいよ今回の旅の最終段階に到達しました。出力を機能させるためにすべてを統合します。ローカルフォルダからの動画取得やJupyter Notebookから送信されたpickle化データの読み込みなどの標準的な設定に加えて、今回は独自の要件として、タイムスタンプを「秒のみ」の形式から「分:秒」の形式に変換する処理を行います。これにより、ウェブページ上でのデータの視認性が向上します。こちらがapp.pyファイルのコードです。
<pre><code class="python">from flask import Flask, render_template, send_from_directory import pickle import os from collections import defaultdict app = Flask(__name__) # pickleファイルからデータをロード with open('data.pkl', 'rb') as f: loaded_data = pickle.load(f) # データへのアクセス video_id = loaded_data['video_id'] data_list = loaded_data['data_list'] video_id_name_list = loaded_data['video_id_name_list'] video_search_dict = loaded_data['video_search_dict'] VIDEO_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "static") @app.route('/<path:filename>') def serve_video(filename): print(VIDEO_DIRECTORY, filename) return send_from_directory(directory=VIDEO_DIRECTORY, path=filename) @app.route('/') def home(): for item in data_list: if ":" not in str(item['start']): item['start'] = int(item['start']) item['start'] = f"{item['start'] // 60}:{item['start'] % 60:02}" if ":" not in str(item['end']): item['end'] = int(item['end']) item['end'] = f"{item['end'] // 60}:{item['end'] % 60:02}" video_id_name_dict = {video['video_id']: video['video_name'] for video in video_id_name_list} # video_name = video_id_name_dict.get(video_id) return render_template('index.html', data=data_list[:10], video_id_name_dict=video_id_name_dict, video_id=video_id, video_search_dict = video_search_dict) if __name__ == '__main__': app.run(debug=True) </code></pre>
HTML テンプレート
最後の一片である、Jinja-2ベースのHTMLテンプレートコードを織り交ぜる時です。これはFlaskのapp.pyファイルを通じて送信されたすべてのデータを統合します。まずはロゴの検出結果を示すことから始めます。ビデオプレーヤーは動画の全時間をカバーし、そのすぐ下に、その特定のデュレーション内に画面上で識別された「開始」、「終了」、および「テキスト」を表示する表が配置されます。わかりやすさを向上させるために、タイムスタンプは「分:秒」形式で表示され、対話型になります。クリックすることで正確なタイムスタンプにジャンプし、そこから動画再生を開始できます。JavaScript関数 `playVideo` に渡すときには、タイムスタンプを秒のみの形式に戻していることに留意してください。この関数は動画再生のために秒のみのタイムスタンプを受け取る仕様になっているためです。
<pre><code class="language-html"><!DOCTYPE html> <html> <head> <link rel="shortcut icon" href="#" /> <title>ロゴ検出</title> <style> body { text-align: center; font-family: Arial, sans-serif; color: #333; background-color: #f5f5f5; } h1, h2 { color: #444; } table { margin: 0 auto; border-collapse: collapse; width: 80%; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 8px; text-align: center; } th { padding-top: 12px; padding-bottom: 12px; text-decoration: underline; color: black; } video { width: 40%; height: auto; margin-top: 20px; } /* 検索スタイル */ .video-container { text-align: center; margin-bottom: 2em; padding: 1em; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } table { margin: 0 auto; margin-bottom: 1em; } th, td { padding: 0.5em; border: 1px solid #ddd; } </style> <script> function playVideo(timeString) { var timeParts = timeString.split(":"); var time = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]); var video = document.querySelector('#mainVideo'); video.currentTime = time; video.play(); } </script> </head> <body> <h1>動画全体のロゴ検出</h1> <h3>動画ファイル: <i>{{ video_id_name_dict[video_id]}}</i></h3> <video id="mainVideo" controls> <source src="{{ url_for('static', filename=video_id_name_dict[video_id]|string) }}" type="video/mp4"> お使いのブラウザはビデオタグをサポートしていません。 </video> <br /> <br /> <br /> <table> <tr> <th>開始</th> <th>終了</th> <th>値</th> </tr> {% for item in data %} <tr> <td><a href="javascript:void(0)" onclick="playVideo('{{ item['start'] }}')">{{ item['start'] }}</a></td> <td>{{ item['end'] }}</td> <td>{{ item['value'] }}</td> </tr> {% endfor %} </table> <br /> <br /> {% for video_id, results in video_search_dict.items() %} <div class="video-container"> <h1>ロゴ検索結果</h1> <h2>動画ファイル: <i>{{ video_id_name_dict[video_id] }}</i></h2> <h2>入力されたクエリ: <i>{{input_query}}</i></h2> {% for result in results %} <video controls preload="metadata" style="width: 40%;"> <source src="{{ url_for('static', filename=video_id_name_dict[video_id]) }}#t={{ result['start'] }},{{ result['end'] }}" type="video/mp4"> お使いのブラウザはビデオタグをサポートしていません。 </video> <table> <tr> <th>開始</th> <th>終了</th> <th>信頼度</th> <th>テキスト</th> </tr> <tr> <td>{{ result['start'] }}</td> <td>{{ result['end'] }}</td> <td>{{ result['confidence'] }}</td> <td>{{ result['text'] }}</td> </tr> </table> {% endfor %} </div> {% endfor %} </body> </html> </code></pre>
Flask アプリの実行
素晴らしいです! Jupyter Notebookの最後のセルを実行してFlaskアプリを起動しましょう。
<pre><code class="python">%run app.py </code></pre>
以下のような出力が表示され、すべて想定通りに進行したことが確認できます 😊。

URLリンク http://127.0.0.1:5000 をクリックすると、以下のウェブページが表示されます。


このチュートリアルを通じて構築した完全なコードが記載された Jupyter Notebook はこちらから入手可能です:https://drive.google.com/drive/folders/1D97_UU2Z0lvp3y52BHV5GKkSNOQKv3Xi?usp=share_link
結び
今後もさらにエキサイティングなコンテンツをお届けしていきます。まだ参加していない方は、マルチモーダルAIの情熱を共有する活発な仲間たちが集まる当社の Discordコミュニティ に心から歓迎します。
動画におけるロゴ検出とは、動画コンテンツ内に埋め込まれたロゴや商標を自動的に識別・認識するプロセスのことを指します。これには、動画のフレームやセグメントを分析して、ブランドに関連付けられた特定のロゴパターンや視覚的要素を検出および特定することが含まれます。この技術により、動画コンテンツ内を迅速にナビゲートし、特定のロゴパターンが画面に表示される具体的な瞬間を正確に特定することができます。動画におけるロゴ検出は、動画コンテンツ内に埋め込まれたロゴや商標を自動的に識別・認識するプロセスのことを指します。これには、動画のフレームやセグメントを分析して、ブランドに関連付けられた特定のロゴパターンや視覚的要素を検出および特定することが含まれます。この技術により、動画コンテンツ内を迅速にナビゲートし、特定のロゴパターンが画面に表示される具体的な瞬間を正確に特定することができます。動画データにおけるロゴ検出は、幅広い要素を識別できるため、さまざまな業界の垂直市場にわたって多くの応用分野があります。
広告とマーケティング: 企業は、オンラインとオフラインの両方で、さまざまなメディアにおける自社ブランドの存在感と視認性を監視できます。これにより、マーケティングキャンペーンの影響を評価し、ロゴの不正使用を特定し、競合他社のマーケティング戦略を理解するのに役立ちます。
ソーシャルメディアの監視: ロゴ検出は、ブランドのロゴがユーザー生成コンテンツのどこに、どのくらいの頻度で表示されるかを理解するのに役立ちます。これにより、ブランドの人気、使用文脈、感情分析に関する洞察を得ることができます。
小売とEコマース: 小売業者はロゴ検出を使用して在庫を監視および管理できます。たとえば、模倣品や無許可の販売者を特定するのに役立ちます。
スポーツスポンサーシップ: ロゴ検出は、スポーツイベントの生中継中のブランド露出を定量化できます。スポンサーや広告主に提供される価値に関する洞察を提供できます。
メディアとエンターテインメント: エンターテインメント業界では、ロゴ検出を使用して映画やテレビ番組でのプロダクトプレイスメントを追跡できます。また、著作権侵害の特定も可能です。
セキュリティと監視: ロゴ検出は、セキュリティ目的で企業のロゴに基づいて車両や物体を特定・追跡するのに役立ちます。
自動運転・自動車: 自動車業界では、ロゴ検出によって車のメーカーやモデルを特定し、交通分析、駐車管理、または自動運転システムを支援できます。
このチュートリアルでは、2つの異なる視点とレベルからロゴ検出の世界を探求していきます。1つ目は動画レベルで、動画コンテンツ全体を単一のエンティティとして扱い、そこに含まれるすべてのロゴ情報を掘り起こそうとします。2つ目のアプローチであるインデックスレベルでは、レンズの焦点を絞り、特定のロゴまたはロゴのグループに集中します。自然言語クエリを使用して、Twelve Labsプラットフォームにインデックス登録された豊富な動画ライブラリ全体で徹底的な検索を行います。
最も魅力的な部分は、Twelve Labs APIを使用することで、検出プロセスを構成するモデルのトレーニング、デプロイ、推論、または負荷のスケールなどの複雑な作業に煩わされることなく、これらすべてを達成できる点です。開発からインフラストラクチャまでカバーし、継続的なサポートも提供します。それでは、シートベルトを締めて、ロゴ検出の領域への魅力的な旅に一緒に出発しましょう。
動画内テキストとロゴ検出 - 潜在的な重複について
ロゴが単にブランド名や企業名である場合があり、これを画面上のテキストとして処理できるのか、動画内テキスト(OCR)のインデックス作成および検索オプションでロゴ検出に十分なのかという疑問が生じることがあります。動画再生中にそのようなテキストが画面に表示されたときに検出されるのは事実です。しかし、テキストが異なる文脈で異なる意味を持つ可能性がある場合には、ロゴを検出するためのインデックス作成および検索オプションを具体的に構成することが重要です。
たとえば、「Amazon」は多国籍テクノロジー企業を指すこともあれば、南アメリカの川を指すこともあります。ロゴ検出を採用している場合、システムはAmazonのブランドロゴの検索と、テキストとしての「Amazon」を区別します。したがって、ブランドロゴがテキストベースであり、動画内テキストとロゴ検出が表面的に重複する場合でも、正確な結果を確保するためにロゴ検出機能を意図的に選択して使用する必要があります。
前提条件
Twelve Labsプラットフォームは現在オープンベータ版であり、新規登録時に最大10時間分の無料動画インデックス作成クレジットを提供しています。このチュートリアルを開始する前に、Twelve Labsプラットフォームの主要な側面に慣れておくことをお勧めします。このチュートリアルをスムーズに進めるには、動画インデックス作成、インデックスオプション、Task API、検索オプションなどの概念をしっかりと理解していることが不可欠です。これらのトピックについては、私の最初のチュートリアルで詳しく説明しています。それにもかかわらず、障害に遭遇したり、どこかで立ち往生したりした場合は、いつでもお気軽にお問い合わせください。ロゴ検出の領域へのこのエキサイティングな旅で、私はいつでもあなたをサポートします。また、Discordがお好みのプラットフォームである場合、当社のDiscordサーバーでの応答時間は非常に高速です 🚅🏎️⚡️。
チュートリアルのクイックガイド
これまでの議論に引き続き、2つの異なる視点とレベルからロゴ検出について深く掘り下げていきます。このチュートリアルを2つの重要なセクションに分割し、最終的にすべての要素を完全に機能するウェブアプリに統合するデモで締めくくります。
3つの簡単なステップでのロゴ検出
特定の動画から認識されたロゴを抽出するには、次の3つのステップを実行します。
動画のインデックス作成 - ここに驚きはありません。以前のチュートリアルを読んできた方なら、このステップはもうすっかり慣れているはずです。
動画の一意識別子の取得 - Twelve Labsプラットフォームが動画のインデックス作成を完了したら、ロゴ検出が必要な動画の一意識別子を取得します。
画面に表示されるロゴの抽出 - 作成した特定のインデックスと、ロゴ検出が必要な動画に関連付けられたビデオIDを使用して、動画に焦点を絞ります。APIが面倒な処理を引き受け、求めている結果を提供してくれます。
ロゴ検索 - インデックス登録されたすべての動画から特定のロゴを検索する
動画全体にロゴ検出を適用することで、ロゴパターンのすべてのインスタンスを詳細に調査して抽出することができました。今度は、ロゴ検索機能を使用することで、入力または検索されたロゴやブランド名が具体的に現れる瞬間や動画スニペットをピンポイントで特定できます。これにより、大規模な動画カタログを調査する時間が大幅に短縮され、動画再生中に画面に表示されるロゴと検索用語の整合性に基づいた正確な検索結果が得られます。
これまでのチュートリアルでは、自然言語クエリと、ビジュアル(オーディオビジュアル検索)、会話(対話検索)、動画内テキスト(OCR)などのさまざまな検索オプションを利用して、インデックス登録された動画内のコンテンツ検索について説明しました。このチュートリアルでは、アプローチを切り替え、ロゴ検出パイプライン専用に活用して、インデックス登録された動画内のロゴを検索します。処理効率を最大化し、コストを最小限に抑えるために、「logo」インデックス作成オプションのみを使用してインデックスを作成します。その後、「logo」検索オプションを使用して検索クエリを実行し、インデックス登録された動画内で関連するロゴの一致を検出できるようにします。
デモアプリの構築
すべてを統合するために、APIエンドポイントによって生成されたデータを利用してウェブページに表示し、ミニマルなHTMLページをホストするFlaskベースのデモアプリを構築します。ロゴ検出プロセスの結果は、タイムスタンプと対応するロゴ名を示す表として系統的に整理されます。一方、ロゴ検索セクションには、使用したクエリと、それに対応して発見された関連動画セグメントが表示されます。
3つの簡単なステップでロゴを検出する
シンプルにするために、既存のアカウントを使用して5つの動画のみをインデックスにアップロードしました。現在はオープンベータ期間中のため、登録すれば最大10時間の動画コンテンツをインデックス作成できる無料のクレジットを受け取ることができます。それを超える容量が必要な場合は、Developerプランへのアップグレードについて当社の料金ページをご確認ください。
動画のインデックス作成
ここでは、Jupyter Notebookに含める必要のある重要な要素について説明します。これには、必要なインポート、API URLの定義、インデックスの作成、ローカルファイルシステムから動画をアップロードしてインデックス作成プロセスを開始することが含まれます。
<pre><code class="python">%env API_URL = https://api.twelvelabs.io/v1.1 %env API_KEY= <あなたのAPIキー> !pip install requests import os import requests import glob from pprint import pprint # APIのURLと自分のAPIキーを取得 API_URL = os.getenv("API_URL") assert API_URL API_KEY = os.getenv("API_KEY") assert API_KEY </code></pre>
<pre><code class="python"># `/indexes` エンドポイントのURLを構築 INDEXES_URL = f"{API_URL}/indexes" # リクエストのヘッダーを設定 default_header = { "x-api-key": API_KEY } # 指定された名前でインデックスを作成する関数を定義 def create_index(index_name, index_options, engine): # data という名前の辞書を宣言 data = { "engine_id": engine, "index_options": index_options, "index_name": index_name, } # インデックスを作成 response = requests.post(INDEXES_URL, headers=default_header, json=data) # インデックスの一意の識別子を保存 INDEX_ID = response.json().get('_id') # ステータスコードが201であるか確認し、成功を出力 if response.status_code == 201: print(f"Status code: {response.status_code} - The request was successful and a new index was created.") else: print(f"Status code: {response.status_code}") pprint(response.json()) return INDEX_ID # インデックスを作成 index_id = create_index(index_name = "extract_text", index_options=["logo"], engine = "marengo2.5") # 作成されたインデックスIDを出力 print(f"Created index IDs: {index_id}") </code></pre>
作成したばかりのインデックスに5つのF1レース動画をアップロードします。これらの動画はF1関連のYouTubeチャンネルからダウンロードし、ローカルハードドライブの「static」というフォルダーに保存してあります。これらのローカルファイルを使用して、Twelve Labsプラットフォームに動画のインデックスを作成します。
<pre><code class="python">import os import requests from concurrent.futures import ThreadPoolExecutor TASKS_URL = f"{API_URL}/tasks" TASK_ID_LIST = [] video_folder = 'static' # 動画ファイルを含むフォルダー def upload_video(file_name): # インデックスに動画が既に存在するか検証 task_list_response = requests.get( TASKS_URL, headers=default_header, params={"index_id": INDEX_ID, "filename": file_name}, ) if "data" in task_list_response.json(): task_list = task_list_response.json()["data"] if len(task_list) > 0: if task_list[0]['status'] == 'ready': print(f"Video '{file_name}' already exists in index {INDEX_ID}") else: print("task pending or validating") return # 動画がまだインデックスに存在しない場合、現在の動画のインデックス作成用の新しいタスク作成コードに進む print("Entering task creation code for the file: ", file_name) if file_name.endswith('.mp4'): # ファイルがMP4動画であることを確認 file_path = os.path.join(video_folder, file_name) # 動画ファイルのフルパスを取得 with open(file_path, "rb") as file_stream: data = { "index_id": INDEX_ID, "language": "en" } file_param = [ ("video_file", (file_name, file_stream, "application/octet-stream")),] #動画は動画ファイル自体と同じ名前でプラットフォーム上にインデックス登録されます。 response = requests.post(TASKS_URL, headers=default_header, data=data, files=file_param) TASK_ID = response.json().get("_id") TASK_ID_LIST.append(TASK_ID) # ステータスコードが201であるか確認し、成功を出力 if response.status_code == 201: print(f"Status code: {response.status_code} - The request was successful and a new resource was created.") else: print(f"Status code: {response.status_code}") print(f"File name: {file_name}") pprint(response.json()) print("\n") # 動画ファイルのリストを取得 video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')] # ThreadPoolExecutorを作成 with ThreadPoolExecutor() as executor: # 全動画ファイルに対してupload_videoを並行処理するようexecutorを使用 executor.map(upload_video, video_files) </code></pre>
動画の一意識別子の取得
次に、インデックス内のすべての動画を一覧表示してみましょう。これにより、特定の動画のビデオIDを保持することができ、その動画に埋め込まれたすべてのテキストを抽出することを可能にします。さらに、以前のチュートリアルでの方法と同様に、ビデオIDとそのタイトルのリストを組み立て、後で Flask アプリケーションに渡せるようにします。
<pre><code class="python"># インデックス内のすべての動画を一覧表示する default_header = { "x-api-key": API_KEY } INDEX_ID='##4a73aa8b1dd6cde172a9##' INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos" response = requests.get(INDEXES_VIDEOS_URL, headers=default_header) response_json = response.json() pprint(response_json) video_id_name_list = [{'video_id': video['_id'], 'video_name': video['metadata']['filename']} for video in response_json['data']] pprint(video_id_name_list) </code></pre>
出力結果:
<pre><code class="python">{'data': [{'_id': '##d978c86daab572f3481##', 'created_at': '2023-04-17T18:56:51Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T190000Z.mp4', 'fps': 25, 'height': 396, 'size': 415876158, 'width': 704}, 'updated_at': '2023-04-17T19:01:32Z'}, {'_id': '##3d975786daab572f3481##', 'created_at': '2023-04-17T18:56:44Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211114T170000Z.mp4', 'fps': 25, 'height': 396, 'size': 387273943, 'width': 704}, 'updated_at': '2023-04-17T19:00:39Z'}, {'_id': '##3d972e86daab572f3481##', 'created_at': '2023-04-17T18:56:38Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T193000Z.mp4', 'fps': 25, 'height': 396, 'size': 386209689, 'width': 704}, 'updated_at': '2023-04-17T18:59:58Z'}, {'_id': '##3d96d386daab572f3481##', 'created_at': '2023-04-17T18:56:28Z', 'metadata': {'duration': 1800.52, 'engine_id': 'marengo2.5', 'filename': '20211121T133000Z.mp4', 'fps': 25, 'height': 396, 'size': 348611416, 'width': 704}, 'updated_at': '2023-04-17T18:58:27Z'}, {'_id': '##3d96af86daab572f3481##', 'created_at': '2023-04-17T18:56:08Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T200000Z.mp4', 'fps': 25, 'height': 396, 'size': 327766175, 'width': 704}, 'updated_at': '2023-04-17T18:57:51Z'}], 'page_info': {'limit_per_page': 10, 'page': 1, 'total_duration': 9002.76, 'total_page': 1, 'total_results': 5}} [{'video_id': '##3d978c86daab572f3481##', 'video_name': '20211113T190000Z.mp4'}, {'video_id': '##3d975786daab572f3481##', 'video_name': '20211114T170000Z.mp4'}, {'video_id': '##3d972e86daab572f3481##', 'video_name': '20211113T193000Z.mp4'}, {'video_id': '##3d96d386daab572f3481##', 'video_name': '20211121T133000Z.mp4'}, {'video_id': '##3d96af86daab572f3481##', 'video_name': '20211113T200000Z.mp4'}] </code></pre>
画面に表示されるロゴの抽出
計画を実行に移す時です!選択した動画からすべてのロゴコンテンツの抽出を進めます。
<pre><code class="python">VIDEO_ID = '###a849b86daab572f349242' LOGO_URL = f"{API_URL}/indexes/{INDEX_ID}/videos/{VIDEO_ID}/logo" # GETリクエストを実行 response = requests.get(LOGO_URL, headers=default_header) print (f"Status code: {response.status_code}") logo_data = response.json() pprint (logo_data) </code></pre>
出力結果:
<pre><code class="python">Status code: 200 {'data': [{'end': 16, 'start': 15, 'value': 'Ducati Corse'}, {'end': 23, 'start': 22, 'value': 'Bank of Jordan'}, {'end': 23, 'start': 22, 'value': 'Han Chiang High School'}, {'end': 24, 'start': 23, 'value': 'Peugeot'}, {'end': 25, 'start': 24, 'value': 'Dr Lal Path Labs'}, {'end': 26, 'start': 25, 'value': 'Z8Games'}, {'end': 29, 'start': 27, 'value': 'Sky Sports'}, {'end': 29, 'start': 28, 'value': 'Tout'}, {'end': 31, 'start': 30, 'value': 'Sky UK'}, {'end': 31, 'start': 30, 'value': 'New Balance'}, {'end': 31, 'start': 30, 'value': 'Industria'}, {'end': 33, 'start': 32, 'value': 'Esport3'}, {'end': 35, 'start': 32, 'value': 'Nissan'}, {'end': 33, 'start': 32, 'value': 'GoCar'}, {'end': 34, 'start': 33, 'value': 'Land Bank of the Philippines'}, {'end': 34, 'start': 33, 'value': 'Z8Games'}, {'end': 37, 'start': 36, 'value': 'Tout'}, {'end': 37, 'start': 36, 'value': 'Zazzle'}, {'end': 39, 'start': 38, 'value': 'Z8Games'}, {'end': 39, 'start': 38, 'value': 'Giochi Preziosi'}, {'end': 41, 'start': 40, 'value': 'Mini'}], 'id': '###a849b86daab572f349242', 'index_id': '###a73aa8b1dd6cde172a933'} </code></pre>
明らかに、APIは画面上で確認されたすべてのロゴを一行ずつ巧みに抽出しました。この情報は、コンテンツのフィルタリング、分類、検索などの後続のワークフローのメタデータとして保存できます。簡潔にするため、ここに表示されている出力は一部省略されています。実際の出力はかなり広範囲におよびました。
ロゴ検索 - インデックス登録されたすべての動画から特定のロゴを検索する
アップロードしたインデックス動画コレクションの中から、関連するロゴパターンの不一致を検出するために、logo検索オプションを使用して検索クエリを実行します。
<pre><code class="python"># `/search` エンドポイントのURLを構築 SEARCH_URL = f"{API_URL}/search/" # `data` という名前の辞書を宣言 data = { "index_id": INDEX_ID, "query": "honda", "search_options": [ "logo" ] } # 後でflaskアプリケーションに渡すためにクエリを抽出 input_query = data["query"] # 検索リクエストを実行 response = requests.post(SEARCH_URL, headers=default_header, json=data) if response.status_code == 200: print(f"Status code: {response.status_code} - Success") else: print(f"Status code: {response.status_code}") pprint(response.json()) </code></pre>
出力結果:
<pre><code class="python">Status code: 200 - Success {'data': [{'confidence': 'high', 'end': 19, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 18, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 104, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 103, 'video_id': '###d978c86daab572f348###'}, {'confidence': 'high', 'end': 137, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 136, 'video_id': '###d978c86daab572f348###'}, {'confidence': 'high', 'end': 269, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 268, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 392, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 391, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 478, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 477, 'video_id': '###d975786daab572f348###'}, {'confidence': 'high', 'end': 483, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 482, 'video_id': '###d978c86daab572f348###'}], 'page_info': {'limit_per_page': 10, 'next_page_token': 'fc3c420a-5971-428a-8796-9e5c077754c0-1', 'page_expired_at': '2023-05-26T18:40:26Z', 'total_results': 47}, 'search_pool': {'index_id': '###d9556f607a5a7bd9ea###', 'total_count': 5, 'total_duration': 9003}} </code></pre>
もう一度、APIは入力されたロゴ名に対応するすべての画面表示ロゴを専門的に調査して取得しましたが、今回はアップロードした全インデックス動画にわたって実行されました。
結果がすっきりと表示されるように、Flaskアプリケーション用のデータを準備します。
<pre><code class="python">video_data = [{'start': d['start'], 'end': d['end'], 'confidence': d['confidence'], 'text': d['metadata'][0]['text']} for d in search_data['data']] video_search_dict = {} for vd in video_data: if search_data['data'][0]['video_id'] in video_search_dict: video_search_dict[search_data['data'][0]['video_id']].append(vd) else: video_search_dict[search_data['data'][0]['video_id']] = [vd] pprint(video_search_dict) </code></pre>
出力結果:
<pre><code class="python"> {'###d96af86daab572f348###': [{'confidence': 'high', 'end': 19, 'start': 18, 'text': 'Honda'}, {'confidence': 'high', 'end': 104, 'start': 103, 'text': 'Honda'}, {'confidence': 'high', 'end': 137, 'start': 136, 'text': 'Honda'}, {'confidence': 'high', 'end': 269, 'start': 268, 'text': 'Honda'}, {'confidence': 'high', 'end': 392, 'start': 391, 'text': 'Honda'}, {'confidence': 'high', 'end': 478, 'start': 477, 'text': 'Honda'}, {'confidence': 'high', 'end': 483, 'start': 482, 'text': 'Honda'}, {'confidence': 'high', 'end': 491, 'start': 487, 'text': 'Honda'}, {'confidence': 'high', 'end': 561, 'start': 560, 'text': 'Honda'}, {'confidence': 'high', 'end': 586, 'start': 585, 'text': 'Honda'}]} </code></pre>
ロゴ検出結果のための最終的なデータ準備を行い、その後にPythonオブジェクトをpickle化するいつもの手順に進みます。
<pre><code class="python">video_id = ocr_data.get('id') data_list = logo_data.get('data') data_to_save = { 'video_id': video_id, 'data_list': data_list, 'video_id_name_list': video_id_name_list, 'video_search_dict': video_search_dict } import pickle # データをpickleファイルに保存 with open('data.pkl', 'wb') as f: pickle.dump(data_to_save, f) </code></pre>
デモアプリの構築
さて、いよいよ今回の旅の最終段階に到達しました。出力を機能させるためにすべてを統合します。ローカルフォルダからの動画取得やJupyter Notebookから送信されたpickle化データの読み込みなどの標準的な設定に加えて、今回は独自の要件として、タイムスタンプを「秒のみ」の形式から「分:秒」の形式に変換する処理を行います。これにより、ウェブページ上でのデータの視認性が向上します。こちらがapp.pyファイルのコードです。
<pre><code class="python">from flask import Flask, render_template, send_from_directory import pickle import os from collections import defaultdict app = Flask(__name__) # pickleファイルからデータをロード with open('data.pkl', 'rb') as f: loaded_data = pickle.load(f) # データへのアクセス video_id = loaded_data['video_id'] data_list = loaded_data['data_list'] video_id_name_list = loaded_data['video_id_name_list'] video_search_dict = loaded_data['video_search_dict'] VIDEO_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "static") @app.route('/<path:filename>') def serve_video(filename): print(VIDEO_DIRECTORY, filename) return send_from_directory(directory=VIDEO_DIRECTORY, path=filename) @app.route('/') def home(): for item in data_list: if ":" not in str(item['start']): item['start'] = int(item['start']) item['start'] = f"{item['start'] // 60}:{item['start'] % 60:02}" if ":" not in str(item['end']): item['end'] = int(item['end']) item['end'] = f"{item['end'] // 60}:{item['end'] % 60:02}" video_id_name_dict = {video['video_id']: video['video_name'] for video in video_id_name_list} # video_name = video_id_name_dict.get(video_id) return render_template('index.html', data=data_list[:10], video_id_name_dict=video_id_name_dict, video_id=video_id, video_search_dict = video_search_dict) if __name__ == '__main__': app.run(debug=True) </code></pre>
HTML テンプレート
最後の一片である、Jinja-2ベースのHTMLテンプレートコードを織り交ぜる時です。これはFlaskのapp.pyファイルを通じて送信されたすべてのデータを統合します。まずはロゴの検出結果を示すことから始めます。ビデオプレーヤーは動画の全時間をカバーし、そのすぐ下に、その特定のデュレーション内に画面上で識別された「開始」、「終了」、および「テキスト」を表示する表が配置されます。わかりやすさを向上させるために、タイムスタンプは「分:秒」形式で表示され、対話型になります。クリックすることで正確なタイムスタンプにジャンプし、そこから動画再生を開始できます。JavaScript関数 `playVideo` に渡すときには、タイムスタンプを秒のみの形式に戻していることに留意してください。この関数は動画再生のために秒のみのタイムスタンプを受け取る仕様になっているためです。
<pre><code class="language-html"><!DOCTYPE html> <html> <head> <link rel="shortcut icon" href="#" /> <title>ロゴ検出</title> <style> body { text-align: center; font-family: Arial, sans-serif; color: #333; background-color: #f5f5f5; } h1, h2 { color: #444; } table { margin: 0 auto; border-collapse: collapse; width: 80%; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 8px; text-align: center; } th { padding-top: 12px; padding-bottom: 12px; text-decoration: underline; color: black; } video { width: 40%; height: auto; margin-top: 20px; } /* 検索スタイル */ .video-container { text-align: center; margin-bottom: 2em; padding: 1em; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } table { margin: 0 auto; margin-bottom: 1em; } th, td { padding: 0.5em; border: 1px solid #ddd; } </style> <script> function playVideo(timeString) { var timeParts = timeString.split(":"); var time = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]); var video = document.querySelector('#mainVideo'); video.currentTime = time; video.play(); } </script> </head> <body> <h1>動画全体のロゴ検出</h1> <h3>動画ファイル: <i>{{ video_id_name_dict[video_id]}}</i></h3> <video id="mainVideo" controls> <source src="{{ url_for('static', filename=video_id_name_dict[video_id]|string) }}" type="video/mp4"> お使いのブラウザはビデオタグをサポートしていません。 </video> <br /> <br /> <br /> <table> <tr> <th>開始</th> <th>終了</th> <th>値</th> </tr> {% for item in data %} <tr> <td><a href="javascript:void(0)" onclick="playVideo('{{ item['start'] }}')">{{ item['start'] }}</a></td> <td>{{ item['end'] }}</td> <td>{{ item['value'] }}</td> </tr> {% endfor %} </table> <br /> <br /> {% for video_id, results in video_search_dict.items() %} <div class="video-container"> <h1>ロゴ検索結果</h1> <h2>動画ファイル: <i>{{ video_id_name_dict[video_id] }}</i></h2> <h2>入力されたクエリ: <i>{{input_query}}</i></h2> {% for result in results %} <video controls preload="metadata" style="width: 40%;"> <source src="{{ url_for('static', filename=video_id_name_dict[video_id]) }}#t={{ result['start'] }},{{ result['end'] }}" type="video/mp4"> お使いのブラウザはビデオタグをサポートしていません。 </video> <table> <tr> <th>開始</th> <th>終了</th> <th>信頼度</th> <th>テキスト</th> </tr> <tr> <td>{{ result['start'] }}</td> <td>{{ result['end'] }}</td> <td>{{ result['confidence'] }}</td> <td>{{ result['text'] }}</td> </tr> </table> {% endfor %} </div> {% endfor %} </body> </html> </code></pre>
Flask アプリの実行
素晴らしいです! Jupyter Notebookの最後のセルを実行してFlaskアプリを起動しましょう。
<pre><code class="python">%run app.py </code></pre>
以下のような出力が表示され、すべて想定通りに進行したことが確認できます 😊。

URLリンク http://127.0.0.1:5000 をクリックすると、以下のウェブページが表示されます。


このチュートリアルを通じて構築した完全なコードが記載された Jupyter Notebook はこちらから入手可能です:https://drive.google.com/drive/folders/1D97_UU2Z0lvp3y52BHV5GKkSNOQKv3Xi?usp=share_link
結び
今後もさらにエキサイティングなコンテンツをお届けしていきます。まだ参加していない方は、マルチモーダルAIの情熱を共有する活発な仲間たちが集まる当社の Discordコミュニティ に心から歓迎します。
動画におけるロゴ検出とは、動画コンテンツ内に埋め込まれたロゴや商標を自動的に識別・認識するプロセスのことを指します。これには、動画のフレームやセグメントを分析して、ブランドに関連付けられた特定のロゴパターンや視覚的要素を検出および特定することが含まれます。この技術により、動画コンテンツ内を迅速にナビゲートし、特定のロゴパターンが画面に表示される具体的な瞬間を正確に特定することができます。動画におけるロゴ検出は、動画コンテンツ内に埋め込まれたロゴや商標を自動的に識別・認識するプロセスのことを指します。これには、動画のフレームやセグメントを分析して、ブランドに関連付けられた特定のロゴパターンや視覚的要素を検出および特定することが含まれます。この技術により、動画コンテンツ内を迅速にナビゲートし、特定のロゴパターンが画面に表示される具体的な瞬間を正確に特定することができます。動画データにおけるロゴ検出は、幅広い要素を識別できるため、さまざまな業界の垂直市場にわたって多くの応用分野があります。
広告とマーケティング: 企業は、オンラインとオフラインの両方で、さまざまなメディアにおける自社ブランドの存在感と視認性を監視できます。これにより、マーケティングキャンペーンの影響を評価し、ロゴの不正使用を特定し、競合他社のマーケティング戦略を理解するのに役立ちます。
ソーシャルメディアの監視: ロゴ検出は、ブランドのロゴがユーザー生成コンテンツのどこに、どのくらいの頻度で表示されるかを理解するのに役立ちます。これにより、ブランドの人気、使用文脈、感情分析に関する洞察を得ることができます。
小売とEコマース: 小売業者はロゴ検出を使用して在庫を監視および管理できます。たとえば、模倣品や無許可の販売者を特定するのに役立ちます。
スポーツスポンサーシップ: ロゴ検出は、スポーツイベントの生中継中のブランド露出を定量化できます。スポンサーや広告主に提供される価値に関する洞察を提供できます。
メディアとエンターテインメント: エンターテインメント業界では、ロゴ検出を使用して映画やテレビ番組でのプロダクトプレイスメントを追跡できます。また、著作権侵害の特定も可能です。
セキュリティと監視: ロゴ検出は、セキュリティ目的で企業のロゴに基づいて車両や物体を特定・追跡するのに役立ちます。
自動運転・自動車: 自動車業界では、ロゴ検出によって車のメーカーやモデルを特定し、交通分析、駐車管理、または自動運転システムを支援できます。
このチュートリアルでは、2つの異なる視点とレベルからロゴ検出の世界を探求していきます。1つ目は動画レベルで、動画コンテンツ全体を単一のエンティティとして扱い、そこに含まれるすべてのロゴ情報を掘り起こそうとします。2つ目のアプローチであるインデックスレベルでは、レンズの焦点を絞り、特定のロゴまたはロゴのグループに集中します。自然言語クエリを使用して、Twelve Labsプラットフォームにインデックス登録された豊富な動画ライブラリ全体で徹底的な検索を行います。
最も魅力的な部分は、Twelve Labs APIを使用することで、検出プロセスを構成するモデルのトレーニング、デプロイ、推論、または負荷のスケールなどの複雑な作業に煩わされることなく、これらすべてを達成できる点です。開発からインフラストラクチャまでカバーし、継続的なサポートも提供します。それでは、シートベルトを締めて、ロゴ検出の領域への魅力的な旅に一緒に出発しましょう。
動画内テキストとロゴ検出 - 潜在的な重複について
ロゴが単にブランド名や企業名である場合があり、これを画面上のテキストとして処理できるのか、動画内テキスト(OCR)のインデックス作成および検索オプションでロゴ検出に十分なのかという疑問が生じることがあります。動画再生中にそのようなテキストが画面に表示されたときに検出されるのは事実です。しかし、テキストが異なる文脈で異なる意味を持つ可能性がある場合には、ロゴを検出するためのインデックス作成および検索オプションを具体的に構成することが重要です。
たとえば、「Amazon」は多国籍テクノロジー企業を指すこともあれば、南アメリカの川を指すこともあります。ロゴ検出を採用している場合、システムはAmazonのブランドロゴの検索と、テキストとしての「Amazon」を区別します。したがって、ブランドロゴがテキストベースであり、動画内テキストとロゴ検出が表面的に重複する場合でも、正確な結果を確保するためにロゴ検出機能を意図的に選択して使用する必要があります。
前提条件
Twelve Labsプラットフォームは現在オープンベータ版であり、新規登録時に最大10時間分の無料動画インデックス作成クレジットを提供しています。このチュートリアルを開始する前に、Twelve Labsプラットフォームの主要な側面に慣れておくことをお勧めします。このチュートリアルをスムーズに進めるには、動画インデックス作成、インデックスオプション、Task API、検索オプションなどの概念をしっかりと理解していることが不可欠です。これらのトピックについては、私の最初のチュートリアルで詳しく説明しています。それにもかかわらず、障害に遭遇したり、どこかで立ち往生したりした場合は、いつでもお気軽にお問い合わせください。ロゴ検出の領域へのこのエキサイティングな旅で、私はいつでもあなたをサポートします。また、Discordがお好みのプラットフォームである場合、当社のDiscordサーバーでの応答時間は非常に高速です 🚅🏎️⚡️。
チュートリアルのクイックガイド
これまでの議論に引き続き、2つの異なる視点とレベルからロゴ検出について深く掘り下げていきます。このチュートリアルを2つの重要なセクションに分割し、最終的にすべての要素を完全に機能するウェブアプリに統合するデモで締めくくります。
3つの簡単なステップでのロゴ検出
特定の動画から認識されたロゴを抽出するには、次の3つのステップを実行します。
動画のインデックス作成 - ここに驚きはありません。以前のチュートリアルを読んできた方なら、このステップはもうすっかり慣れているはずです。
動画の一意識別子の取得 - Twelve Labsプラットフォームが動画のインデックス作成を完了したら、ロゴ検出が必要な動画の一意識別子を取得します。
画面に表示されるロゴの抽出 - 作成した特定のインデックスと、ロゴ検出が必要な動画に関連付けられたビデオIDを使用して、動画に焦点を絞ります。APIが面倒な処理を引き受け、求めている結果を提供してくれます。
ロゴ検索 - インデックス登録されたすべての動画から特定のロゴを検索する
動画全体にロゴ検出を適用することで、ロゴパターンのすべてのインスタンスを詳細に調査して抽出することができました。今度は、ロゴ検索機能を使用することで、入力または検索されたロゴやブランド名が具体的に現れる瞬間や動画スニペットをピンポイントで特定できます。これにより、大規模な動画カタログを調査する時間が大幅に短縮され、動画再生中に画面に表示されるロゴと検索用語の整合性に基づいた正確な検索結果が得られます。
これまでのチュートリアルでは、自然言語クエリと、ビジュアル(オーディオビジュアル検索)、会話(対話検索)、動画内テキスト(OCR)などのさまざまな検索オプションを利用して、インデックス登録された動画内のコンテンツ検索について説明しました。このチュートリアルでは、アプローチを切り替え、ロゴ検出パイプライン専用に活用して、インデックス登録された動画内のロゴを検索します。処理効率を最大化し、コストを最小限に抑えるために、「logo」インデックス作成オプションのみを使用してインデックスを作成します。その後、「logo」検索オプションを使用して検索クエリを実行し、インデックス登録された動画内で関連するロゴの一致を検出できるようにします。
デモアプリの構築
すべてを統合するために、APIエンドポイントによって生成されたデータを利用してウェブページに表示し、ミニマルなHTMLページをホストするFlaskベースのデモアプリを構築します。ロゴ検出プロセスの結果は、タイムスタンプと対応するロゴ名を示す表として系統的に整理されます。一方、ロゴ検索セクションには、使用したクエリと、それに対応して発見された関連動画セグメントが表示されます。
3つの簡単なステップでロゴを検出する
シンプルにするために、既存のアカウントを使用して5つの動画のみをインデックスにアップロードしました。現在はオープンベータ期間中のため、登録すれば最大10時間の動画コンテンツをインデックス作成できる無料のクレジットを受け取ることができます。それを超える容量が必要な場合は、Developerプランへのアップグレードについて当社の料金ページをご確認ください。
動画のインデックス作成
ここでは、Jupyter Notebookに含める必要のある重要な要素について説明します。これには、必要なインポート、API URLの定義、インデックスの作成、ローカルファイルシステムから動画をアップロードしてインデックス作成プロセスを開始することが含まれます。
<pre><code class="python">%env API_URL = https://api.twelvelabs.io/v1.1 %env API_KEY= <あなたのAPIキー> !pip install requests import os import requests import glob from pprint import pprint # APIのURLと自分のAPIキーを取得 API_URL = os.getenv("API_URL") assert API_URL API_KEY = os.getenv("API_KEY") assert API_KEY </code></pre>
<pre><code class="python"># `/indexes` エンドポイントのURLを構築 INDEXES_URL = f"{API_URL}/indexes" # リクエストのヘッダーを設定 default_header = { "x-api-key": API_KEY } # 指定された名前でインデックスを作成する関数を定義 def create_index(index_name, index_options, engine): # data という名前の辞書を宣言 data = { "engine_id": engine, "index_options": index_options, "index_name": index_name, } # インデックスを作成 response = requests.post(INDEXES_URL, headers=default_header, json=data) # インデックスの一意の識別子を保存 INDEX_ID = response.json().get('_id') # ステータスコードが201であるか確認し、成功を出力 if response.status_code == 201: print(f"Status code: {response.status_code} - The request was successful and a new index was created.") else: print(f"Status code: {response.status_code}") pprint(response.json()) return INDEX_ID # インデックスを作成 index_id = create_index(index_name = "extract_text", index_options=["logo"], engine = "marengo2.5") # 作成されたインデックスIDを出力 print(f"Created index IDs: {index_id}") </code></pre>
作成したばかりのインデックスに5つのF1レース動画をアップロードします。これらの動画はF1関連のYouTubeチャンネルからダウンロードし、ローカルハードドライブの「static」というフォルダーに保存してあります。これらのローカルファイルを使用して、Twelve Labsプラットフォームに動画のインデックスを作成します。
<pre><code class="python">import os import requests from concurrent.futures import ThreadPoolExecutor TASKS_URL = f"{API_URL}/tasks" TASK_ID_LIST = [] video_folder = 'static' # 動画ファイルを含むフォルダー def upload_video(file_name): # インデックスに動画が既に存在するか検証 task_list_response = requests.get( TASKS_URL, headers=default_header, params={"index_id": INDEX_ID, "filename": file_name}, ) if "data" in task_list_response.json(): task_list = task_list_response.json()["data"] if len(task_list) > 0: if task_list[0]['status'] == 'ready': print(f"Video '{file_name}' already exists in index {INDEX_ID}") else: print("task pending or validating") return # 動画がまだインデックスに存在しない場合、現在の動画のインデックス作成用の新しいタスク作成コードに進む print("Entering task creation code for the file: ", file_name) if file_name.endswith('.mp4'): # ファイルがMP4動画であることを確認 file_path = os.path.join(video_folder, file_name) # 動画ファイルのフルパスを取得 with open(file_path, "rb") as file_stream: data = { "index_id": INDEX_ID, "language": "en" } file_param = [ ("video_file", (file_name, file_stream, "application/octet-stream")),] #動画は動画ファイル自体と同じ名前でプラットフォーム上にインデックス登録されます。 response = requests.post(TASKS_URL, headers=default_header, data=data, files=file_param) TASK_ID = response.json().get("_id") TASK_ID_LIST.append(TASK_ID) # ステータスコードが201であるか確認し、成功を出力 if response.status_code == 201: print(f"Status code: {response.status_code} - The request was successful and a new resource was created.") else: print(f"Status code: {response.status_code}") print(f"File name: {file_name}") pprint(response.json()) print("\n") # 動画ファイルのリストを取得 video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')] # ThreadPoolExecutorを作成 with ThreadPoolExecutor() as executor: # 全動画ファイルに対してupload_videoを並行処理するようexecutorを使用 executor.map(upload_video, video_files) </code></pre>
動画の一意識別子の取得
次に、インデックス内のすべての動画を一覧表示してみましょう。これにより、特定の動画のビデオIDを保持することができ、その動画に埋め込まれたすべてのテキストを抽出することを可能にします。さらに、以前のチュートリアルでの方法と同様に、ビデオIDとそのタイトルのリストを組み立て、後で Flask アプリケーションに渡せるようにします。
<pre><code class="python"># インデックス内のすべての動画を一覧表示する default_header = { "x-api-key": API_KEY } INDEX_ID='##4a73aa8b1dd6cde172a9##' INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos" response = requests.get(INDEXES_VIDEOS_URL, headers=default_header) response_json = response.json() pprint(response_json) video_id_name_list = [{'video_id': video['_id'], 'video_name': video['metadata']['filename']} for video in response_json['data']] pprint(video_id_name_list) </code></pre>
出力結果:
<pre><code class="python">{'data': [{'_id': '##d978c86daab572f3481##', 'created_at': '2023-04-17T18:56:51Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T190000Z.mp4', 'fps': 25, 'height': 396, 'size': 415876158, 'width': 704}, 'updated_at': '2023-04-17T19:01:32Z'}, {'_id': '##3d975786daab572f3481##', 'created_at': '2023-04-17T18:56:44Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211114T170000Z.mp4', 'fps': 25, 'height': 396, 'size': 387273943, 'width': 704}, 'updated_at': '2023-04-17T19:00:39Z'}, {'_id': '##3d972e86daab572f3481##', 'created_at': '2023-04-17T18:56:38Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T193000Z.mp4', 'fps': 25, 'height': 396, 'size': 386209689, 'width': 704}, 'updated_at': '2023-04-17T18:59:58Z'}, {'_id': '##3d96d386daab572f3481##', 'created_at': '2023-04-17T18:56:28Z', 'metadata': {'duration': 1800.52, 'engine_id': 'marengo2.5', 'filename': '20211121T133000Z.mp4', 'fps': 25, 'height': 396, 'size': 348611416, 'width': 704}, 'updated_at': '2023-04-17T18:58:27Z'}, {'_id': '##3d96af86daab572f3481##', 'created_at': '2023-04-17T18:56:08Z', 'metadata': {'duration': 1800.56, 'engine_id': 'marengo2.5', 'filename': '20211113T200000Z.mp4', 'fps': 25, 'height': 396, 'size': 327766175, 'width': 704}, 'updated_at': '2023-04-17T18:57:51Z'}], 'page_info': {'limit_per_page': 10, 'page': 1, 'total_duration': 9002.76, 'total_page': 1, 'total_results': 5}} [{'video_id': '##3d978c86daab572f3481##', 'video_name': '20211113T190000Z.mp4'}, {'video_id': '##3d975786daab572f3481##', 'video_name': '20211114T170000Z.mp4'}, {'video_id': '##3d972e86daab572f3481##', 'video_name': '20211113T193000Z.mp4'}, {'video_id': '##3d96d386daab572f3481##', 'video_name': '20211121T133000Z.mp4'}, {'video_id': '##3d96af86daab572f3481##', 'video_name': '20211113T200000Z.mp4'}] </code></pre>
画面に表示されるロゴの抽出
計画を実行に移す時です!選択した動画からすべてのロゴコンテンツの抽出を進めます。
<pre><code class="python">VIDEO_ID = '###a849b86daab572f349242' LOGO_URL = f"{API_URL}/indexes/{INDEX_ID}/videos/{VIDEO_ID}/logo" # GETリクエストを実行 response = requests.get(LOGO_URL, headers=default_header) print (f"Status code: {response.status_code}") logo_data = response.json() pprint (logo_data) </code></pre>
出力結果:
<pre><code class="python">Status code: 200 {'data': [{'end': 16, 'start': 15, 'value': 'Ducati Corse'}, {'end': 23, 'start': 22, 'value': 'Bank of Jordan'}, {'end': 23, 'start': 22, 'value': 'Han Chiang High School'}, {'end': 24, 'start': 23, 'value': 'Peugeot'}, {'end': 25, 'start': 24, 'value': 'Dr Lal Path Labs'}, {'end': 26, 'start': 25, 'value': 'Z8Games'}, {'end': 29, 'start': 27, 'value': 'Sky Sports'}, {'end': 29, 'start': 28, 'value': 'Tout'}, {'end': 31, 'start': 30, 'value': 'Sky UK'}, {'end': 31, 'start': 30, 'value': 'New Balance'}, {'end': 31, 'start': 30, 'value': 'Industria'}, {'end': 33, 'start': 32, 'value': 'Esport3'}, {'end': 35, 'start': 32, 'value': 'Nissan'}, {'end': 33, 'start': 32, 'value': 'GoCar'}, {'end': 34, 'start': 33, 'value': 'Land Bank of the Philippines'}, {'end': 34, 'start': 33, 'value': 'Z8Games'}, {'end': 37, 'start': 36, 'value': 'Tout'}, {'end': 37, 'start': 36, 'value': 'Zazzle'}, {'end': 39, 'start': 38, 'value': 'Z8Games'}, {'end': 39, 'start': 38, 'value': 'Giochi Preziosi'}, {'end': 41, 'start': 40, 'value': 'Mini'}], 'id': '###a849b86daab572f349242', 'index_id': '###a73aa8b1dd6cde172a933'} </code></pre>
明らかに、APIは画面上で確認されたすべてのロゴを一行ずつ巧みに抽出しました。この情報は、コンテンツのフィルタリング、分類、検索などの後続のワークフローのメタデータとして保存できます。簡潔にするため、ここに表示されている出力は一部省略されています。実際の出力はかなり広範囲におよびました。
ロゴ検索 - インデックス登録されたすべての動画から特定のロゴを検索する
アップロードしたインデックス動画コレクションの中から、関連するロゴパターンの不一致を検出するために、logo検索オプションを使用して検索クエリを実行します。
<pre><code class="python"># `/search` エンドポイントのURLを構築 SEARCH_URL = f"{API_URL}/search/" # `data` という名前の辞書を宣言 data = { "index_id": INDEX_ID, "query": "honda", "search_options": [ "logo" ] } # 後でflaskアプリケーションに渡すためにクエリを抽出 input_query = data["query"] # 検索リクエストを実行 response = requests.post(SEARCH_URL, headers=default_header, json=data) if response.status_code == 200: print(f"Status code: {response.status_code} - Success") else: print(f"Status code: {response.status_code}") pprint(response.json()) </code></pre>
出力結果:
<pre><code class="python">Status code: 200 - Success {'data': [{'confidence': 'high', 'end': 19, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 18, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 104, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 103, 'video_id': '###d978c86daab572f348###'}, {'confidence': 'high', 'end': 137, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 136, 'video_id': '###d978c86daab572f348###'}, {'confidence': 'high', 'end': 269, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 268, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 392, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 391, 'video_id': '###d96af86daab572f348###'}, {'confidence': 'high', 'end': 478, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 477, 'video_id': '###d975786daab572f348###'}, {'confidence': 'high', 'end': 483, 'metadata': [{'text': 'Honda', 'type': 'logo'}], 'score': 92.28, 'start': 482, 'video_id': '###d978c86daab572f348###'}], 'page_info': {'limit_per_page': 10, 'next_page_token': 'fc3c420a-5971-428a-8796-9e5c077754c0-1', 'page_expired_at': '2023-05-26T18:40:26Z', 'total_results': 47}, 'search_pool': {'index_id': '###d9556f607a5a7bd9ea###', 'total_count': 5, 'total_duration': 9003}} </code></pre>
もう一度、APIは入力されたロゴ名に対応するすべての画面表示ロゴを専門的に調査して取得しましたが、今回はアップロードした全インデックス動画にわたって実行されました。
結果がすっきりと表示されるように、Flaskアプリケーション用のデータを準備します。
<pre><code class="python">video_data = [{'start': d['start'], 'end': d['end'], 'confidence': d['confidence'], 'text': d['metadata'][0]['text']} for d in search_data['data']] video_search_dict = {} for vd in video_data: if search_data['data'][0]['video_id'] in video_search_dict: video_search_dict[search_data['data'][0]['video_id']].append(vd) else: video_search_dict[search_data['data'][0]['video_id']] = [vd] pprint(video_search_dict) </code></pre>
出力結果:
<pre><code class="python"> {'###d96af86daab572f348###': [{'confidence': 'high', 'end': 19, 'start': 18, 'text': 'Honda'}, {'confidence': 'high', 'end': 104, 'start': 103, 'text': 'Honda'}, {'confidence': 'high', 'end': 137, 'start': 136, 'text': 'Honda'}, {'confidence': 'high', 'end': 269, 'start': 268, 'text': 'Honda'}, {'confidence': 'high', 'end': 392, 'start': 391, 'text': 'Honda'}, {'confidence': 'high', 'end': 478, 'start': 477, 'text': 'Honda'}, {'confidence': 'high', 'end': 483, 'start': 482, 'text': 'Honda'}, {'confidence': 'high', 'end': 491, 'start': 487, 'text': 'Honda'}, {'confidence': 'high', 'end': 561, 'start': 560, 'text': 'Honda'}, {'confidence': 'high', 'end': 586, 'start': 585, 'text': 'Honda'}]} </code></pre>
ロゴ検出結果のための最終的なデータ準備を行い、その後にPythonオブジェクトをpickle化するいつもの手順に進みます。
<pre><code class="python">video_id = ocr_data.get('id') data_list = logo_data.get('data') data_to_save = { 'video_id': video_id, 'data_list': data_list, 'video_id_name_list': video_id_name_list, 'video_search_dict': video_search_dict } import pickle # データをpickleファイルに保存 with open('data.pkl', 'wb') as f: pickle.dump(data_to_save, f) </code></pre>
デモアプリの構築
さて、いよいよ今回の旅の最終段階に到達しました。出力を機能させるためにすべてを統合します。ローカルフォルダからの動画取得やJupyter Notebookから送信されたpickle化データの読み込みなどの標準的な設定に加えて、今回は独自の要件として、タイムスタンプを「秒のみ」の形式から「分:秒」の形式に変換する処理を行います。これにより、ウェブページ上でのデータの視認性が向上します。こちらがapp.pyファイルのコードです。
<pre><code class="python">from flask import Flask, render_template, send_from_directory import pickle import os from collections import defaultdict app = Flask(__name__) # pickleファイルからデータをロード with open('data.pkl', 'rb') as f: loaded_data = pickle.load(f) # データへのアクセス video_id = loaded_data['video_id'] data_list = loaded_data['data_list'] video_id_name_list = loaded_data['video_id_name_list'] video_search_dict = loaded_data['video_search_dict'] VIDEO_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "static") @app.route('/<path:filename>') def serve_video(filename): print(VIDEO_DIRECTORY, filename) return send_from_directory(directory=VIDEO_DIRECTORY, path=filename) @app.route('/') def home(): for item in data_list: if ":" not in str(item['start']): item['start'] = int(item['start']) item['start'] = f"{item['start'] // 60}:{item['start'] % 60:02}" if ":" not in str(item['end']): item['end'] = int(item['end']) item['end'] = f"{item['end'] // 60}:{item['end'] % 60:02}" video_id_name_dict = {video['video_id']: video['video_name'] for video in video_id_name_list} # video_name = video_id_name_dict.get(video_id) return render_template('index.html', data=data_list[:10], video_id_name_dict=video_id_name_dict, video_id=video_id, video_search_dict = video_search_dict) if __name__ == '__main__': app.run(debug=True) </code></pre>
HTML テンプレート
最後の一片である、Jinja-2ベースのHTMLテンプレートコードを織り交ぜる時です。これはFlaskのapp.pyファイルを通じて送信されたすべてのデータを統合します。まずはロゴの検出結果を示すことから始めます。ビデオプレーヤーは動画の全時間をカバーし、そのすぐ下に、その特定のデュレーション内に画面上で識別された「開始」、「終了」、および「テキスト」を表示する表が配置されます。わかりやすさを向上させるために、タイムスタンプは「分:秒」形式で表示され、対話型になります。クリックすることで正確なタイムスタンプにジャンプし、そこから動画再生を開始できます。JavaScript関数 `playVideo` に渡すときには、タイムスタンプを秒のみの形式に戻していることに留意してください。この関数は動画再生のために秒のみのタイムスタンプを受け取る仕様になっているためです。
<pre><code class="language-html"><!DOCTYPE html> <html> <head> <link rel="shortcut icon" href="#" /> <title>ロゴ検出</title> <style> body { text-align: center; font-family: Arial, sans-serif; color: #333; background-color: #f5f5f5; } h1, h2 { color: #444; } table { margin: 0 auto; border-collapse: collapse; width: 80%; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 8px; text-align: center; } th { padding-top: 12px; padding-bottom: 12px; text-decoration: underline; color: black; } video { width: 40%; height: auto; margin-top: 20px; } /* 検索スタイル */ .video-container { text-align: center; margin-bottom: 2em; padding: 1em; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } table { margin: 0 auto; margin-bottom: 1em; } th, td { padding: 0.5em; border: 1px solid #ddd; } </style> <script> function playVideo(timeString) { var timeParts = timeString.split(":"); var time = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]); var video = document.querySelector('#mainVideo'); video.currentTime = time; video.play(); } </script> </head> <body> <h1>動画全体のロゴ検出</h1> <h3>動画ファイル: <i>{{ video_id_name_dict[video_id]}}</i></h3> <video id="mainVideo" controls> <source src="{{ url_for('static', filename=video_id_name_dict[video_id]|string) }}" type="video/mp4"> お使いのブラウザはビデオタグをサポートしていません。 </video> <br /> <br /> <br /> <table> <tr> <th>開始</th> <th>終了</th> <th>値</th> </tr> {% for item in data %} <tr> <td><a href="javascript:void(0)" onclick="playVideo('{{ item['start'] }}')">{{ item['start'] }}</a></td> <td>{{ item['end'] }}</td> <td>{{ item['value'] }}</td> </tr> {% endfor %} </table> <br /> <br /> {% for video_id, results in video_search_dict.items() %} <div class="video-container"> <h1>ロゴ検索結果</h1> <h2>動画ファイル: <i>{{ video_id_name_dict[video_id] }}</i></h2> <h2>入力されたクエリ: <i>{{input_query}}</i></h2> {% for result in results %} <video controls preload="metadata" style="width: 40%;"> <source src="{{ url_for('static', filename=video_id_name_dict[video_id]) }}#t={{ result['start'] }},{{ result['end'] }}" type="video/mp4"> お使いのブラウザはビデオタグをサポートしていません。 </video> <table> <tr> <th>開始</th> <th>終了</th> <th>信頼度</th> <th>テキスト</th> </tr> <tr> <td>{{ result['start'] }}</td> <td>{{ result['end'] }}</td> <td>{{ result['confidence'] }}</td> <td>{{ result['text'] }}</td> </tr> </table> {% endfor %} </div> {% endfor %} </body> </html> </code></pre>
Flask アプリの実行
素晴らしいです! Jupyter Notebookの最後のセルを実行してFlaskアプリを起動しましょう。
<pre><code class="python">%run app.py </code></pre>
以下のような出力が表示され、すべて想定通りに進行したことが確認できます 😊。

URLリンク http://127.0.0.1:5000 をクリックすると、以下のウェブページが表示されます。


このチュートリアルを通じて構築した完全なコードが記載された Jupyter Notebook はこちらから入手可能です:https://drive.google.com/drive/folders/1D97_UU2Z0lvp3y52BHV5GKkSNOQKv3Xi?usp=share_link
結び
今後もさらにエキサイティングなコンテンツをお届けしていきます。まだ参加していない方は、マルチモーダルAIの情熱を共有する活発な仲間たちが集まる当社の Discordコミュニティ に心から歓迎します。




