튜토리얼

Twelve Labs API를 활용하여 비디오 내에서 브랜드 로고를 찾는 방법

안키트 카레 카레 (Ankit Khare)

개발자는 Twelve Labs API를 활용하여 별도의 로고 감지 인프라를 구축하거나 유지 관리할 필요 없이, 비디오에서 모든 브랜드 로고를 추출하고 인덱싱된 비디오 라이브러리 전체에서 특정 로고를 이름으로 검색할 수 있습니다. 본 튜토리얼은 비디오 레벨의 로고 추출과 인덱스 레벨의 로고 검색을 모두 다루며, 결과를 시각화하여 보여주는 Flask 애플리케이션 구축 방법을 함께 소개합니다.

개발자는 Twelve Labs API를 활용하여 별도의 로고 감지 인프라를 구축하거나 유지 관리할 필요 없이, 비디오에서 모든 브랜드 로고를 추출하고 인덱싱된 비디오 라이브러리 전체에서 특정 로고를 이름으로 검색할 수 있습니다. 본 튜토리얼은 비디오 레벨의 로고 추출과 인덱스 레벨의 로고 검색을 모두 다루며, 결과를 시각화하여 보여주는 Flask 애플리케이션 구축 방법을 함께 소개합니다.

목차

No headings found on page

뉴스레터 구독하기

뉴스레터 구독하기

영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.

영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.

AI로 영상을 검색하고, 분석하고, 탐색하세요.

2023. 6. 9.

13분

링크 복사하기

비디오 내 로고 감지(Logo detection)란 비디오 콘텐츠에 포함된 로고 또는 상표를 자동으로 식별하고 인식하는 프로세스를 말합니다. 이는 비디오 프레임이나 세그먼트를 분석하여 특정 브랜드와 관련된 로고 패턴이나 시각적 요소를 감지하고 위치를 찾아내는 작업을 포함합니다. 이 기술을 통해 비디오 콘텐츠를 빠르게 탐색하고, 특정 로고 패턴이 화면에 나타나는 정확한 순간을 정밀하게 식별할 수 있습니다. 비디오 내 로고 감지는 비디오 콘텐츠에 포함된 로고나 상표를 자동으로 식별하는 프로세스입니다. 프레임이나 세그먼트를 정밀 분석하여 브랜드와 결합된 시각적 패턴을 감지하므로, 화면 상에 특정 로고가 노출되는 시점을 빠르고 정확하게 파악할 수 있도록 돕습니다. 비디오 데이터 내의 로고 감지는 다양한 요소를 식별할 수 있어, 다음과 같이 여러 산업 분야에서 유용하게 활용됩니다:

  1. 광고 및 마케팅: 기업은 온라인과 오프라인을 아우르는 다양한 미디어 채널에서 자사 브랜드의 노출 빈도와 가시성을 모니터링할 수 있습니다. 마케팅 캠페인의 실질적인 효과를 측정하고, 로고의 무단 도용을 감지하며, 경쟁사의 마케팅 전략을 파악하는 데 유용합니다.

  2. 소셜 미디어 모니터링: 소비자가 작성한 콘텐츠(UGC) 내에서 자사 로고가 언제, 어디서, 얼마나 자주 등장하는지 분석합니다. 이를 통해 브랜드의 대중적 인기, 실제 사용 환경, 만족도 등의 감성 분석 데이터를 확보할 수 있습니다.

  3. 유통 및 이커머스: 유통사는 로고 감지 소프트웨어를 연동하여 재고와 공급망을 효율적으로 모니터링하고 관리할 수 있습니다. 예를 들어, 위조 상품(가품)이나 불법 유통 판매처를 식별하는 데 기여합니다.

  4. 스포츠 스폰서십: 스포츠 경기 라이브 중계 중 브랜드 노출 효과를 정밀하게 수치화합니다. 스폰서와 광고주에게 제공된 미디어 가치를 객관적으로 입증할 수 있는 통찰을 제시합니다.

  5. 미디어 및 엔터테인먼트: 영화나 TV 프로그램 내 PPL(간접광고)의 노출 수준을 추적할 수 있으며, 저작권 침해 사례를 식별하는 데에도 훌륭한 도구가 됩니다.

  6. 보안 및 관제: 보안 목적으로 기업 로고가 부착된 특정 차량이나 장비를 식별하고 이동 경로를 추적하는 일에 활용할 수 있습니다.

  7. 자동차: 자동차 제조사 및 모델명을 실시간으로 식별하여 교통 흐름 분석, 지능형 주차 관리, 나아가 자율주행 기술 고도화에 기여합니다.

본 가이드에서는 비디오 내 로고 감지의 메커니즘을 크게 두 가지 관점과 수준으로 나누어 살펴보겠습니다. 첫째는 비디오 레벨(Video-level)로, 전체 비디오 콘텐츠를 단일 파일 단위로 다루며 그 안에 포함된 모든 로고 정보를 포괄적으로 추출합니다. 둘째는 인덱스 레벨(Index-level)로, 시야를 좁혀 특정 로고 또는 특정 로고 그룹에 집중합니다. Twelve Labs 플랫폼에 인덱싱된 방대한 비디오 라이브러리 전체를 대상으로 자연어 쿼리를 활용해 고도의 검색을 수행하게 됩니다.

가장 핵심적인 이점은 이 모든 과정에 Twelve Labs API를 활용함으로써 모델 학습, 인프라 배포, 추론(inference) 최적화, 로드 스케일링 등 로고 감지를 구현하기 위해 수반되는 복잡한 작업들에 신경 쓰지 않아도 된다는 점입니다. 개발 단계에서 인프라 구측, 지속적인 엔지니어링 지원까지 Twelve Labs가 모두 담당합니다. 그럼 이제 본격적으로 비디오 로고 감지의 세계를 탐험하러 떠나보겠습니다.

비디오 내 텍스트 인식(Text-in-video) vs 로고 감지(Logo detection) - 중첩 가능성

특정 로고가 단순 브랜드명이나 회사명인 경우, 굳이 로고 감지를 쓰지 않고 비디오 내 텍스트 인식(OCR)만으로 충분하지 않은지 의문이 들 수 있습니다. 실제로 텍스트 기반 로고는 화면 상에서 일반 텍스트로 인식되어 검색 결과에 수집될 수 있습니다. 하지만, 텍스트가 노출되는 맥락에 따라 해석이 달라질 수 있는 상황에서는 인덱싱 및 검색 옵션을 정확히 '로고(logo)'로 구성하는 것이 매우 중요합니다.

예컨대 'Amazon'이라는 단어는 글로벌 기술 기업을 뜻할 수도 있고, 남미의 아마존강을 나타낼 수도 있습니다. 로고 감지 기능을 구축해 놓으면 시스템은 단순 텍스트 'Amazon'과 실제 아마존 브랜드 로고가 화면에 노출된 경우를 명확하게 구분합니다. 텍스트 형태의 브랜드 로고 때문에 두 기능이 다소 겹쳐 보일지라도, 정밀한 비즈니스 성과와 정확성을 확보하기 위해서는 명밀히 '로고 감지' 파이프라인을 의도적으로 선택해 구축해야 합니다.

사전 준비 사항

Twelve Labs 플랫폼은 현재 오픈 베타 단계로 운영 중이며, 회원 가입 시 최대 10시간 분량의 비디오를 무료로 인덱싱할 수 있는 크레딧을 드립니다. 튜토리얼을 시작하기에 앞서 Twelve Labs 플랫폼의 핵심 기능들을 가볍게 익혀두는 것을 추천합니다. 특히 비디오 인덱싱(video indexing), 인덱싱 옵션, 태스크 API(Task API), 검색 옵션에 대한 기본 개념을 이해하고 계시면 본 튜토리얼의 과정을 막힘없이 따라오실 수 있습니다. 첫 번째 튜토리얼에서 이 주제들을 심도 있게 다뤘으니 참고해 주세요. 진행 도중 어려움이 생기거나 의문이 드신다면 언제든 당사 기술 팀에 문의를 남겨주시기 바랍니다. 또한 당사 운영 중인 디스코드(Discord) 서버에 질문을 남겨주시면 매우 신속하게 답변을 받아보실 수 있습니다. 🚅🏎️⚡️

튜토리얼 퀵 가이드

이어서 로고 감지를 두 가지 접근 방식(비디오 레벨과 인덱스 레벨)으로 나누어 알아보겠습니다. 이번 프로젝트는 크게 두 개의 기술 섹션으로 구상되어 있으며, 최종적으로는 이 프로세스들을 결합하여 완전하게 동작하는 웹 애플리케이션 데모를 제작하는 흐름으로 마무리됩니다:

3단계로 간편하게 구현하는 로고 감지

특정 비디오에서 로고를 감지하고 추출하는 과정은 다음과 같이 3단계로 명확히 나뉩니다:

  1. 비디오 인덱싱 - 이전 튜토리얼을 읽어보셨다면 매우 익숙하고 자연스럽게 진행하실 수 있는 단계입니다.

  2. 비디오의 고유 식별자(ID) 조회 - Twelve Labs 플랫폼이 업로드한 비디오의 인덱싱 처리를 완료하면, 로고 감지를 수행하고자 하는 비디오 파일의 고유 ID를 가져옵니다.

  3. 화면에 나타나는 로고 정보 추출 - 생성한 인덱스 ID와 획득한 비디오 고유 ID를 타겟팅해 엔드포인트를 호출합니다. 복잡한 추론 연산 처리는 Twelve Labs API가 백엔드에서 모두 해결하며 최종 감지 결과물만 반환합니다.

로고 검색 - 인덱싱된 전체 비디오 내 특정 로고 탐색하기

개별 비디오 단위로 감지 처리를 실행하면 해당 비디오 내의 모든 로고 인스턴스를 세밀하게 식별할 수 있습니다. 반면, 이 '로고 검색' 기능을 활용하면 방대한 비디오 보관함 전체에서 우리가 찾고자 하는 특정 로고나 브랜드명이 실제로 노출되는 정확한 타임스탬프와 세그먼트들을 역으로 스캔하여 즉시 찾아낼 수 있습니다. 대규모 라이브러리를 일일이 수동 검색할 필요 없이, 영상 재생 중 로고 무늬가 식별된 최적의 편집 구간들만 즉각적으로 도출해 냅니다.

이전 글에서는 자연어 질의와 다양한 멀티모달 검색 옵션(예: 시각/오디오 중심 비디오 검색, 대화/스크립트 검색, OCR 기반 텍스트 검색 등)을 살펴보았습니다. 이번에는 전용 로고 감지 파이프라인(logo detection pipeline)만을 순수하게 연동하여 최적의 검색을 수행합니다. 필요 없는 연산을 건너뛰고 처리 효율 및 예산 낭비를 방지하기 위해 생성 시 인덱싱 옵션에 오직 'logo'만을 활성화하여 비디오 컬렉션을 인덱싱합니다. 이후 검색 API 요청을 보낼 때에도 동일한 'logo' 검색 옵션을 전송함으로써 화면 상에 일치하는 브랜드 로고 세그먼트 정보만을 깨끗하게 필터링해 가져오게 됩니다.

데모 애플리케이션 빌드

모든 기술 요소를 통합하기 위해 API 엔드포인트에서 반환된 원시 JSON 정보를 웹 브라우저 기반 화면으로 구성해 봅니다. 백엔드는 마이크로 프레임워크인 Python Flask를 사용하고 미니멀한 HTML로 렌더링하는 구조입니다. 로고 감지의 결과 데이터는 타임스탬프(시작 및 종료 시각)와 감지된 로고 명칭이 결합된 직관적인 표 형태로 시각화됩니다. 로고 검색 섹션에서는 사용자가 직접 입력한 브랜드 검색 쿼리와, 그에 대응하여 전체 라이브러리에서 검색된 최종 관련 영상 클립들이 목록 형태로 나타납니다.

3 단계로 구성하는 로고 감지 파이프라인

간단 명료한 테스트 진행을 위해, 기존 미리 생성해 둔 인덱스 계정을 이용해 총 5가지 비디오 파일을 사전에 등록해 두었습니다. 가입 시 기본 10시간의 인덱싱 용량이 제공되니 직접 따라 해 보실 수 있습니다. 테스트 규모를 넘어서 프로덕션 환경에 해당 기능을 확장하고 싶으시다면 공식 요금제 웹페이지에서 개발자 플랜(Developer Plan)으로 상향 전환이 가능합니다.

비디오 인덱싱

다음은 주피터 노트북(Jupyter notebook) 또는 개발 스크립트 작성에 반드시 포함되어야 하는 기본 환경 세팅 단계입니다. 필수 라이브러리(requests) 임포트 및 API 엔드포인트 도메인, 고유 API 키 주입 코드를 명시합니다:

<pre><code class="python">%env API_URL = https://api.twelvelabs.io/v1.1
%env API_KEY= <your API key>

!pip install requests

import os
import requests
import glob
from pprint import pprint

# Retrieve the URL of the API and my API key
API_URL = os.getenv("API_URL")
assert API_URL

API_KEY = os.getenv("API_KEY")
assert API_KEY
  </code></pre>
<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Set the header of the request
default_header = {
    "x-api-key": API_KEY
}

# Define a function to create an index with a given name
def create_index(index_name, index_options, engine):
    # Declare a dictionary named data
    data = {
        "engine_id": engine,
        "index_options": index_options,
        "index_name": index_name,
    }

    # Create an index
    response = requests.post(INDEXES_URL, headers=default_header, json=data)

    # Store the unique identifier of your index
    INDEX_ID = response.json().get('_id')

    # Check if the status code is 201 and print success
    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


# Create the indexes
index_id = create_index(index_name = "extract_text", index_options=["logo"], engine = "marengo2.5")

# Print the created index IDs
print(f"Created index IDs: {index_id}")
  </code></pre>

생성된 인덱스 폴더 내로 가상으로 다운로드된 F1 레이싱 관련 비디오 클립 5개를 전송하겠습니다. 로컬 저장 장치의 'static' 폴더에 사전에 MP4 사양으로 저장한 상태이며, 멀티스레드 비동기 처리를 가미해 동시에 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'  # folder containing the video files

def upload_video(file_name):
    # Validate if a video already exists in the index
    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

    # Proceed further to create a new task to index the current video if the video didn't exist in the index already
    print("Entering task creation code for the file: ", file_name)
    
    if file_name.endswith('.mp4'):  # Make sure the file is an MP4 video
        file_path = os.path.join(video_folder, file_name)  # Get the full path of the video file
        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")),] #The video will be indexed on the platform using the same name as the video file itself.
            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)
            # Check if the status code is 201 and print success
            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")

# Get list of video files
video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')]

# Create a ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
    # Use executor to run upload_video in parallel for all video files
    executor.map(upload_video, video_files)
      </code></pre>

비디오 고유 식별자(ID) 조회

이제 현재 인덱스 인스턴스 전반에 활성화 처리된 리소스를 질의해 비디오 ID 및 각 메타데이터 정보를 호출하겠습니다. 고유 식별자 리스트를 키밸류 사양의 도메인으로 매핑 보관하여, 향후 Flask 템플릿 영역에 동적으로 렌더링하도록 저장합니다:

<pre><code class="python"># List all the videos in an index
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>

화면에 표시되는 로고 추출

이제 실제 로고 정보 추출 처리를 단행하겠습니다. API를 호출해 타겟 비디오 내에 존재하는 풍부한 로고 데이터를 디코딩해 옵니다:

<pre><code class="python">VIDEO_ID = '###a849b86daab572f349242'
LOGO_URL = f"{API_URL}/indexes/{INDEX_ID}/videos/{VIDEO_ID}/logo"

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 검색 엔진 설정을 직접 탑재한 API 요청을 전송해 업로드된 인덱스 비디오 전체에서 실제 매칭되는 로고 데이터를 역추적해 내겠습니다:

<pre><code class="python"># Construct the URL of the `/search` endpoint
SEARCH_URL = f"{API_URL}/search/"

# Declare a dictionary named `data`
data = {
    "index_id": INDEX_ID,
    "query": "honda",
    "search_options": [
        "logo"
    ]
}

# Extracting query to later pass it to flask application
input_query = data["query"]

# Make a search request
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>

훌륭합니다. 시스템 상의 전체 비디오 리포지토리를 대상으로 'Honda' 브랜드의 로고가 포함된 완치 구간을 빠짐없이 스캔한 후 관련된 연관 정보만을 고성능으로 리턴해 왔습니다.

자, 도출된 순수 응답 값을 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>

로고 감지 결과를 가공한 뒤, 주피터 노트북에서 가공한 원격 리소스를 보존하기 위해 표준적인 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

# Save data to a pickle file
with open('data.pkl', 'wb') as f:
    pickle.dump(data_to_save, f)
    </code></pre>

데모 애플리케이션 빌드

이제 지금까지의 결과물들을 시각적으로 구현해 줄 웹 데모 제작 마지막 관문에 도달했습니다. 로컬 디렉터리 경로 연결 및 Pickle 데이터를 복원 탑재하는 기본 처리를 바탕으로, 사용자의 가독성을 돕기 위해 원시 초 단위 시간 포맷(seconds)을 분/초(MM:SS) 포맷으로 변환해 주는 로직을 소량 추가합니다. 백엔드를 담당할 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__)

# Load data from a pickle file
with open('data.pkl', 'rb') as f:
    loaded_data = pickle.load(f)

# Access the data
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 템플릿

Flask에서 주입한 변수들을 브라우저 화면에 표출해 줄 최종 Jinja-2 규격의 HTML 코드 템플릿입니다. 전송받은 감지 정보 리스트를 순회하며 테이블(Table)을 형성하고, 미디어 플레이어를 하단에 함께 탑재합니다. 사용자가 추출된 특정 타임스탬프 링크를 클릭 시, JavaScript 기반의 playVideo 가 재생 위치를 동적으로 변경하도록 바인딩 처리했습니다. (비디오 플레이어 제어 API는 원시 초 포맷을 인식하므로 전달할 전달 인자 값은 원래의 초 포맷으로 변환해 처리하도록 설정해 둡니다.)

<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
    <link rel="shortcut icon" href="#" />
    <title>Logo Detection</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;
        }

        /* search style */
        .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>Logo detection in the entire Video</h1>
    <h3>Video file: <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">
        Your browser does not support the video tag.
    </video>
    <br /> <br /> <br />
    <table>
        <tr>
            <th>Start</th>
            <th>End</th>
            <th>Value</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>Logo search results</h1>  
        <h2>Video file: <i>{{ video_id_name_dict[video_id] }}</i></h2>
        <h2>Entered query: <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">
            Your browser does not support the video tag.
        </video>
        <table>
            <tr>
                <th>Start</th>
                <th>End</th>
                <th>Confidence</th>
                <th>Text</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 데모 앱 실행하기

완성되었습니다! 주피터의 코드 블록 마지막 셀에 명령어를 수행해 서버를 구동해 줍니다:



<pre><code class="python">%run app.py
</code></pre>

기대했던 대로 프로세스가 에러 없이 깨끗하게 기동되는 터미널 로그를 확인할 수 있습니다 😊:

초기 할당 바인딩 주소인 http://127.0.0.1:5000 링크를 클리해 접속해 보면 다음과 같은 멋진 웹 대시보드를 마주하시게 됩니다:

이번 가이드 내 소스 코드를 온전히 작성 및 테스트해 정리해 둔 구글 드라이브 다운로드 링크를 아래 제공합니다: https://drive.google.com/drive/folders/1D97_UU2Z0lvp3y52BHV5GKkSNOQKv3Xi?usp=share_link

마치며

Twelve Labs 개발팀은 여러분들을 위해 훨씬 더 유용하고 지능적인 멀티모달 AI 기능과 관련 튜토리얼을 지속적으로 선보일 예정입니다. 멀티모달 신경망 기술에 관심이 많으시다면 부담 없이 활기찬 당사 공식 디스코드 커뮤니티에 가입하셔서 동료 개발자, 연구원들과 따뜻한 교류를 이어나가 보세요.

비디오 내 로고 감지(Logo detection)란 비디오 콘텐츠에 포함된 로고 또는 상표를 자동으로 식별하고 인식하는 프로세스를 말합니다. 이는 비디오 프레임이나 세그먼트를 분석하여 특정 브랜드와 관련된 로고 패턴이나 시각적 요소를 감지하고 위치를 찾아내는 작업을 포함합니다. 이 기술을 통해 비디오 콘텐츠를 빠르게 탐색하고, 특정 로고 패턴이 화면에 나타나는 정확한 순간을 정밀하게 식별할 수 있습니다. 비디오 내 로고 감지는 비디오 콘텐츠에 포함된 로고나 상표를 자동으로 식별하는 프로세스입니다. 프레임이나 세그먼트를 정밀 분석하여 브랜드와 결합된 시각적 패턴을 감지하므로, 화면 상에 특정 로고가 노출되는 시점을 빠르고 정확하게 파악할 수 있도록 돕습니다. 비디오 데이터 내의 로고 감지는 다양한 요소를 식별할 수 있어, 다음과 같이 여러 산업 분야에서 유용하게 활용됩니다:

  1. 광고 및 마케팅: 기업은 온라인과 오프라인을 아우르는 다양한 미디어 채널에서 자사 브랜드의 노출 빈도와 가시성을 모니터링할 수 있습니다. 마케팅 캠페인의 실질적인 효과를 측정하고, 로고의 무단 도용을 감지하며, 경쟁사의 마케팅 전략을 파악하는 데 유용합니다.

  2. 소셜 미디어 모니터링: 소비자가 작성한 콘텐츠(UGC) 내에서 자사 로고가 언제, 어디서, 얼마나 자주 등장하는지 분석합니다. 이를 통해 브랜드의 대중적 인기, 실제 사용 환경, 만족도 등의 감성 분석 데이터를 확보할 수 있습니다.

  3. 유통 및 이커머스: 유통사는 로고 감지 소프트웨어를 연동하여 재고와 공급망을 효율적으로 모니터링하고 관리할 수 있습니다. 예를 들어, 위조 상품(가품)이나 불법 유통 판매처를 식별하는 데 기여합니다.

  4. 스포츠 스폰서십: 스포츠 경기 라이브 중계 중 브랜드 노출 효과를 정밀하게 수치화합니다. 스폰서와 광고주에게 제공된 미디어 가치를 객관적으로 입증할 수 있는 통찰을 제시합니다.

  5. 미디어 및 엔터테인먼트: 영화나 TV 프로그램 내 PPL(간접광고)의 노출 수준을 추적할 수 있으며, 저작권 침해 사례를 식별하는 데에도 훌륭한 도구가 됩니다.

  6. 보안 및 관제: 보안 목적으로 기업 로고가 부착된 특정 차량이나 장비를 식별하고 이동 경로를 추적하는 일에 활용할 수 있습니다.

  7. 자동차: 자동차 제조사 및 모델명을 실시간으로 식별하여 교통 흐름 분석, 지능형 주차 관리, 나아가 자율주행 기술 고도화에 기여합니다.

본 가이드에서는 비디오 내 로고 감지의 메커니즘을 크게 두 가지 관점과 수준으로 나누어 살펴보겠습니다. 첫째는 비디오 레벨(Video-level)로, 전체 비디오 콘텐츠를 단일 파일 단위로 다루며 그 안에 포함된 모든 로고 정보를 포괄적으로 추출합니다. 둘째는 인덱스 레벨(Index-level)로, 시야를 좁혀 특정 로고 또는 특정 로고 그룹에 집중합니다. Twelve Labs 플랫폼에 인덱싱된 방대한 비디오 라이브러리 전체를 대상으로 자연어 쿼리를 활용해 고도의 검색을 수행하게 됩니다.

가장 핵심적인 이점은 이 모든 과정에 Twelve Labs API를 활용함으로써 모델 학습, 인프라 배포, 추론(inference) 최적화, 로드 스케일링 등 로고 감지를 구현하기 위해 수반되는 복잡한 작업들에 신경 쓰지 않아도 된다는 점입니다. 개발 단계에서 인프라 구측, 지속적인 엔지니어링 지원까지 Twelve Labs가 모두 담당합니다. 그럼 이제 본격적으로 비디오 로고 감지의 세계를 탐험하러 떠나보겠습니다.

비디오 내 텍스트 인식(Text-in-video) vs 로고 감지(Logo detection) - 중첩 가능성

특정 로고가 단순 브랜드명이나 회사명인 경우, 굳이 로고 감지를 쓰지 않고 비디오 내 텍스트 인식(OCR)만으로 충분하지 않은지 의문이 들 수 있습니다. 실제로 텍스트 기반 로고는 화면 상에서 일반 텍스트로 인식되어 검색 결과에 수집될 수 있습니다. 하지만, 텍스트가 노출되는 맥락에 따라 해석이 달라질 수 있는 상황에서는 인덱싱 및 검색 옵션을 정확히 '로고(logo)'로 구성하는 것이 매우 중요합니다.

예컨대 'Amazon'이라는 단어는 글로벌 기술 기업을 뜻할 수도 있고, 남미의 아마존강을 나타낼 수도 있습니다. 로고 감지 기능을 구축해 놓으면 시스템은 단순 텍스트 'Amazon'과 실제 아마존 브랜드 로고가 화면에 노출된 경우를 명확하게 구분합니다. 텍스트 형태의 브랜드 로고 때문에 두 기능이 다소 겹쳐 보일지라도, 정밀한 비즈니스 성과와 정확성을 확보하기 위해서는 명밀히 '로고 감지' 파이프라인을 의도적으로 선택해 구축해야 합니다.

사전 준비 사항

Twelve Labs 플랫폼은 현재 오픈 베타 단계로 운영 중이며, 회원 가입 시 최대 10시간 분량의 비디오를 무료로 인덱싱할 수 있는 크레딧을 드립니다. 튜토리얼을 시작하기에 앞서 Twelve Labs 플랫폼의 핵심 기능들을 가볍게 익혀두는 것을 추천합니다. 특히 비디오 인덱싱(video indexing), 인덱싱 옵션, 태스크 API(Task API), 검색 옵션에 대한 기본 개념을 이해하고 계시면 본 튜토리얼의 과정을 막힘없이 따라오실 수 있습니다. 첫 번째 튜토리얼에서 이 주제들을 심도 있게 다뤘으니 참고해 주세요. 진행 도중 어려움이 생기거나 의문이 드신다면 언제든 당사 기술 팀에 문의를 남겨주시기 바랍니다. 또한 당사 운영 중인 디스코드(Discord) 서버에 질문을 남겨주시면 매우 신속하게 답변을 받아보실 수 있습니다. 🚅🏎️⚡️

튜토리얼 퀵 가이드

이어서 로고 감지를 두 가지 접근 방식(비디오 레벨과 인덱스 레벨)으로 나누어 알아보겠습니다. 이번 프로젝트는 크게 두 개의 기술 섹션으로 구상되어 있으며, 최종적으로는 이 프로세스들을 결합하여 완전하게 동작하는 웹 애플리케이션 데모를 제작하는 흐름으로 마무리됩니다:

3단계로 간편하게 구현하는 로고 감지

특정 비디오에서 로고를 감지하고 추출하는 과정은 다음과 같이 3단계로 명확히 나뉩니다:

  1. 비디오 인덱싱 - 이전 튜토리얼을 읽어보셨다면 매우 익숙하고 자연스럽게 진행하실 수 있는 단계입니다.

  2. 비디오의 고유 식별자(ID) 조회 - Twelve Labs 플랫폼이 업로드한 비디오의 인덱싱 처리를 완료하면, 로고 감지를 수행하고자 하는 비디오 파일의 고유 ID를 가져옵니다.

  3. 화면에 나타나는 로고 정보 추출 - 생성한 인덱스 ID와 획득한 비디오 고유 ID를 타겟팅해 엔드포인트를 호출합니다. 복잡한 추론 연산 처리는 Twelve Labs API가 백엔드에서 모두 해결하며 최종 감지 결과물만 반환합니다.

로고 검색 - 인덱싱된 전체 비디오 내 특정 로고 탐색하기

개별 비디오 단위로 감지 처리를 실행하면 해당 비디오 내의 모든 로고 인스턴스를 세밀하게 식별할 수 있습니다. 반면, 이 '로고 검색' 기능을 활용하면 방대한 비디오 보관함 전체에서 우리가 찾고자 하는 특정 로고나 브랜드명이 실제로 노출되는 정확한 타임스탬프와 세그먼트들을 역으로 스캔하여 즉시 찾아낼 수 있습니다. 대규모 라이브러리를 일일이 수동 검색할 필요 없이, 영상 재생 중 로고 무늬가 식별된 최적의 편집 구간들만 즉각적으로 도출해 냅니다.

이전 글에서는 자연어 질의와 다양한 멀티모달 검색 옵션(예: 시각/오디오 중심 비디오 검색, 대화/스크립트 검색, OCR 기반 텍스트 검색 등)을 살펴보았습니다. 이번에는 전용 로고 감지 파이프라인(logo detection pipeline)만을 순수하게 연동하여 최적의 검색을 수행합니다. 필요 없는 연산을 건너뛰고 처리 효율 및 예산 낭비를 방지하기 위해 생성 시 인덱싱 옵션에 오직 'logo'만을 활성화하여 비디오 컬렉션을 인덱싱합니다. 이후 검색 API 요청을 보낼 때에도 동일한 'logo' 검색 옵션을 전송함으로써 화면 상에 일치하는 브랜드 로고 세그먼트 정보만을 깨끗하게 필터링해 가져오게 됩니다.

데모 애플리케이션 빌드

모든 기술 요소를 통합하기 위해 API 엔드포인트에서 반환된 원시 JSON 정보를 웹 브라우저 기반 화면으로 구성해 봅니다. 백엔드는 마이크로 프레임워크인 Python Flask를 사용하고 미니멀한 HTML로 렌더링하는 구조입니다. 로고 감지의 결과 데이터는 타임스탬프(시작 및 종료 시각)와 감지된 로고 명칭이 결합된 직관적인 표 형태로 시각화됩니다. 로고 검색 섹션에서는 사용자가 직접 입력한 브랜드 검색 쿼리와, 그에 대응하여 전체 라이브러리에서 검색된 최종 관련 영상 클립들이 목록 형태로 나타납니다.

3 단계로 구성하는 로고 감지 파이프라인

간단 명료한 테스트 진행을 위해, 기존 미리 생성해 둔 인덱스 계정을 이용해 총 5가지 비디오 파일을 사전에 등록해 두었습니다. 가입 시 기본 10시간의 인덱싱 용량이 제공되니 직접 따라 해 보실 수 있습니다. 테스트 규모를 넘어서 프로덕션 환경에 해당 기능을 확장하고 싶으시다면 공식 요금제 웹페이지에서 개발자 플랜(Developer Plan)으로 상향 전환이 가능합니다.

비디오 인덱싱

다음은 주피터 노트북(Jupyter notebook) 또는 개발 스크립트 작성에 반드시 포함되어야 하는 기본 환경 세팅 단계입니다. 필수 라이브러리(requests) 임포트 및 API 엔드포인트 도메인, 고유 API 키 주입 코드를 명시합니다:

<pre><code class="python">%env API_URL = https://api.twelvelabs.io/v1.1
%env API_KEY= <your API key>

!pip install requests

import os
import requests
import glob
from pprint import pprint

# Retrieve the URL of the API and my API key
API_URL = os.getenv("API_URL")
assert API_URL

API_KEY = os.getenv("API_KEY")
assert API_KEY
  </code></pre>
<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Set the header of the request
default_header = {
    "x-api-key": API_KEY
}

# Define a function to create an index with a given name
def create_index(index_name, index_options, engine):
    # Declare a dictionary named data
    data = {
        "engine_id": engine,
        "index_options": index_options,
        "index_name": index_name,
    }

    # Create an index
    response = requests.post(INDEXES_URL, headers=default_header, json=data)

    # Store the unique identifier of your index
    INDEX_ID = response.json().get('_id')

    # Check if the status code is 201 and print success
    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


# Create the indexes
index_id = create_index(index_name = "extract_text", index_options=["logo"], engine = "marengo2.5")

# Print the created index IDs
print(f"Created index IDs: {index_id}")
  </code></pre>

생성된 인덱스 폴더 내로 가상으로 다운로드된 F1 레이싱 관련 비디오 클립 5개를 전송하겠습니다. 로컬 저장 장치의 'static' 폴더에 사전에 MP4 사양으로 저장한 상태이며, 멀티스레드 비동기 처리를 가미해 동시에 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'  # folder containing the video files

def upload_video(file_name):
    # Validate if a video already exists in the index
    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

    # Proceed further to create a new task to index the current video if the video didn't exist in the index already
    print("Entering task creation code for the file: ", file_name)
    
    if file_name.endswith('.mp4'):  # Make sure the file is an MP4 video
        file_path = os.path.join(video_folder, file_name)  # Get the full path of the video file
        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")),] #The video will be indexed on the platform using the same name as the video file itself.
            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)
            # Check if the status code is 201 and print success
            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")

# Get list of video files
video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')]

# Create a ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
    # Use executor to run upload_video in parallel for all video files
    executor.map(upload_video, video_files)
      </code></pre>

비디오 고유 식별자(ID) 조회

이제 현재 인덱스 인스턴스 전반에 활성화 처리된 리소스를 질의해 비디오 ID 및 각 메타데이터 정보를 호출하겠습니다. 고유 식별자 리스트를 키밸류 사양의 도메인으로 매핑 보관하여, 향후 Flask 템플릿 영역에 동적으로 렌더링하도록 저장합니다:

<pre><code class="python"># List all the videos in an index
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>

화면에 표시되는 로고 추출

이제 실제 로고 정보 추출 처리를 단행하겠습니다. API를 호출해 타겟 비디오 내에 존재하는 풍부한 로고 데이터를 디코딩해 옵니다:

<pre><code class="python">VIDEO_ID = '###a849b86daab572f349242'
LOGO_URL = f"{API_URL}/indexes/{INDEX_ID}/videos/{VIDEO_ID}/logo"

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 검색 엔진 설정을 직접 탑재한 API 요청을 전송해 업로드된 인덱스 비디오 전체에서 실제 매칭되는 로고 데이터를 역추적해 내겠습니다:

<pre><code class="python"># Construct the URL of the `/search` endpoint
SEARCH_URL = f"{API_URL}/search/"

# Declare a dictionary named `data`
data = {
    "index_id": INDEX_ID,
    "query": "honda",
    "search_options": [
        "logo"
    ]
}

# Extracting query to later pass it to flask application
input_query = data["query"]

# Make a search request
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>

훌륭합니다. 시스템 상의 전체 비디오 리포지토리를 대상으로 'Honda' 브랜드의 로고가 포함된 완치 구간을 빠짐없이 스캔한 후 관련된 연관 정보만을 고성능으로 리턴해 왔습니다.

자, 도출된 순수 응답 값을 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>

로고 감지 결과를 가공한 뒤, 주피터 노트북에서 가공한 원격 리소스를 보존하기 위해 표준적인 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

# Save data to a pickle file
with open('data.pkl', 'wb') as f:
    pickle.dump(data_to_save, f)
    </code></pre>

데모 애플리케이션 빌드

이제 지금까지의 결과물들을 시각적으로 구현해 줄 웹 데모 제작 마지막 관문에 도달했습니다. 로컬 디렉터리 경로 연결 및 Pickle 데이터를 복원 탑재하는 기본 처리를 바탕으로, 사용자의 가독성을 돕기 위해 원시 초 단위 시간 포맷(seconds)을 분/초(MM:SS) 포맷으로 변환해 주는 로직을 소량 추가합니다. 백엔드를 담당할 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__)

# Load data from a pickle file
with open('data.pkl', 'rb') as f:
    loaded_data = pickle.load(f)

# Access the data
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 템플릿

Flask에서 주입한 변수들을 브라우저 화면에 표출해 줄 최종 Jinja-2 규격의 HTML 코드 템플릿입니다. 전송받은 감지 정보 리스트를 순회하며 테이블(Table)을 형성하고, 미디어 플레이어를 하단에 함께 탑재합니다. 사용자가 추출된 특정 타임스탬프 링크를 클릭 시, JavaScript 기반의 playVideo 가 재생 위치를 동적으로 변경하도록 바인딩 처리했습니다. (비디오 플레이어 제어 API는 원시 초 포맷을 인식하므로 전달할 전달 인자 값은 원래의 초 포맷으로 변환해 처리하도록 설정해 둡니다.)

<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
    <link rel="shortcut icon" href="#" />
    <title>Logo Detection</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;
        }

        /* search style */
        .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>Logo detection in the entire Video</h1>
    <h3>Video file: <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">
        Your browser does not support the video tag.
    </video>
    <br /> <br /> <br />
    <table>
        <tr>
            <th>Start</th>
            <th>End</th>
            <th>Value</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>Logo search results</h1>  
        <h2>Video file: <i>{{ video_id_name_dict[video_id] }}</i></h2>
        <h2>Entered query: <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">
            Your browser does not support the video tag.
        </video>
        <table>
            <tr>
                <th>Start</th>
                <th>End</th>
                <th>Confidence</th>
                <th>Text</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 데모 앱 실행하기

완성되었습니다! 주피터의 코드 블록 마지막 셀에 명령어를 수행해 서버를 구동해 줍니다:



<pre><code class="python">%run app.py
</code></pre>

기대했던 대로 프로세스가 에러 없이 깨끗하게 기동되는 터미널 로그를 확인할 수 있습니다 😊:

초기 할당 바인딩 주소인 http://127.0.0.1:5000 링크를 클리해 접속해 보면 다음과 같은 멋진 웹 대시보드를 마주하시게 됩니다:

이번 가이드 내 소스 코드를 온전히 작성 및 테스트해 정리해 둔 구글 드라이브 다운로드 링크를 아래 제공합니다: https://drive.google.com/drive/folders/1D97_UU2Z0lvp3y52BHV5GKkSNOQKv3Xi?usp=share_link

마치며

Twelve Labs 개발팀은 여러분들을 위해 훨씬 더 유용하고 지능적인 멀티모달 AI 기능과 관련 튜토리얼을 지속적으로 선보일 예정입니다. 멀티모달 신경망 기술에 관심이 많으시다면 부담 없이 활기찬 당사 공식 디스코드 커뮤니티에 가입하셔서 동료 개발자, 연구원들과 따뜻한 교류를 이어나가 보세요.

비디오 내 로고 감지(Logo detection)란 비디오 콘텐츠에 포함된 로고 또는 상표를 자동으로 식별하고 인식하는 프로세스를 말합니다. 이는 비디오 프레임이나 세그먼트를 분석하여 특정 브랜드와 관련된 로고 패턴이나 시각적 요소를 감지하고 위치를 찾아내는 작업을 포함합니다. 이 기술을 통해 비디오 콘텐츠를 빠르게 탐색하고, 특정 로고 패턴이 화면에 나타나는 정확한 순간을 정밀하게 식별할 수 있습니다. 비디오 내 로고 감지는 비디오 콘텐츠에 포함된 로고나 상표를 자동으로 식별하는 프로세스입니다. 프레임이나 세그먼트를 정밀 분석하여 브랜드와 결합된 시각적 패턴을 감지하므로, 화면 상에 특정 로고가 노출되는 시점을 빠르고 정확하게 파악할 수 있도록 돕습니다. 비디오 데이터 내의 로고 감지는 다양한 요소를 식별할 수 있어, 다음과 같이 여러 산업 분야에서 유용하게 활용됩니다:

  1. 광고 및 마케팅: 기업은 온라인과 오프라인을 아우르는 다양한 미디어 채널에서 자사 브랜드의 노출 빈도와 가시성을 모니터링할 수 있습니다. 마케팅 캠페인의 실질적인 효과를 측정하고, 로고의 무단 도용을 감지하며, 경쟁사의 마케팅 전략을 파악하는 데 유용합니다.

  2. 소셜 미디어 모니터링: 소비자가 작성한 콘텐츠(UGC) 내에서 자사 로고가 언제, 어디서, 얼마나 자주 등장하는지 분석합니다. 이를 통해 브랜드의 대중적 인기, 실제 사용 환경, 만족도 등의 감성 분석 데이터를 확보할 수 있습니다.

  3. 유통 및 이커머스: 유통사는 로고 감지 소프트웨어를 연동하여 재고와 공급망을 효율적으로 모니터링하고 관리할 수 있습니다. 예를 들어, 위조 상품(가품)이나 불법 유통 판매처를 식별하는 데 기여합니다.

  4. 스포츠 스폰서십: 스포츠 경기 라이브 중계 중 브랜드 노출 효과를 정밀하게 수치화합니다. 스폰서와 광고주에게 제공된 미디어 가치를 객관적으로 입증할 수 있는 통찰을 제시합니다.

  5. 미디어 및 엔터테인먼트: 영화나 TV 프로그램 내 PPL(간접광고)의 노출 수준을 추적할 수 있으며, 저작권 침해 사례를 식별하는 데에도 훌륭한 도구가 됩니다.

  6. 보안 및 관제: 보안 목적으로 기업 로고가 부착된 특정 차량이나 장비를 식별하고 이동 경로를 추적하는 일에 활용할 수 있습니다.

  7. 자동차: 자동차 제조사 및 모델명을 실시간으로 식별하여 교통 흐름 분석, 지능형 주차 관리, 나아가 자율주행 기술 고도화에 기여합니다.

본 가이드에서는 비디오 내 로고 감지의 메커니즘을 크게 두 가지 관점과 수준으로 나누어 살펴보겠습니다. 첫째는 비디오 레벨(Video-level)로, 전체 비디오 콘텐츠를 단일 파일 단위로 다루며 그 안에 포함된 모든 로고 정보를 포괄적으로 추출합니다. 둘째는 인덱스 레벨(Index-level)로, 시야를 좁혀 특정 로고 또는 특정 로고 그룹에 집중합니다. Twelve Labs 플랫폼에 인덱싱된 방대한 비디오 라이브러리 전체를 대상으로 자연어 쿼리를 활용해 고도의 검색을 수행하게 됩니다.

가장 핵심적인 이점은 이 모든 과정에 Twelve Labs API를 활용함으로써 모델 학습, 인프라 배포, 추론(inference) 최적화, 로드 스케일링 등 로고 감지를 구현하기 위해 수반되는 복잡한 작업들에 신경 쓰지 않아도 된다는 점입니다. 개발 단계에서 인프라 구측, 지속적인 엔지니어링 지원까지 Twelve Labs가 모두 담당합니다. 그럼 이제 본격적으로 비디오 로고 감지의 세계를 탐험하러 떠나보겠습니다.

비디오 내 텍스트 인식(Text-in-video) vs 로고 감지(Logo detection) - 중첩 가능성

특정 로고가 단순 브랜드명이나 회사명인 경우, 굳이 로고 감지를 쓰지 않고 비디오 내 텍스트 인식(OCR)만으로 충분하지 않은지 의문이 들 수 있습니다. 실제로 텍스트 기반 로고는 화면 상에서 일반 텍스트로 인식되어 검색 결과에 수집될 수 있습니다. 하지만, 텍스트가 노출되는 맥락에 따라 해석이 달라질 수 있는 상황에서는 인덱싱 및 검색 옵션을 정확히 '로고(logo)'로 구성하는 것이 매우 중요합니다.

예컨대 'Amazon'이라는 단어는 글로벌 기술 기업을 뜻할 수도 있고, 남미의 아마존강을 나타낼 수도 있습니다. 로고 감지 기능을 구축해 놓으면 시스템은 단순 텍스트 'Amazon'과 실제 아마존 브랜드 로고가 화면에 노출된 경우를 명확하게 구분합니다. 텍스트 형태의 브랜드 로고 때문에 두 기능이 다소 겹쳐 보일지라도, 정밀한 비즈니스 성과와 정확성을 확보하기 위해서는 명밀히 '로고 감지' 파이프라인을 의도적으로 선택해 구축해야 합니다.

사전 준비 사항

Twelve Labs 플랫폼은 현재 오픈 베타 단계로 운영 중이며, 회원 가입 시 최대 10시간 분량의 비디오를 무료로 인덱싱할 수 있는 크레딧을 드립니다. 튜토리얼을 시작하기에 앞서 Twelve Labs 플랫폼의 핵심 기능들을 가볍게 익혀두는 것을 추천합니다. 특히 비디오 인덱싱(video indexing), 인덱싱 옵션, 태스크 API(Task API), 검색 옵션에 대한 기본 개념을 이해하고 계시면 본 튜토리얼의 과정을 막힘없이 따라오실 수 있습니다. 첫 번째 튜토리얼에서 이 주제들을 심도 있게 다뤘으니 참고해 주세요. 진행 도중 어려움이 생기거나 의문이 드신다면 언제든 당사 기술 팀에 문의를 남겨주시기 바랍니다. 또한 당사 운영 중인 디스코드(Discord) 서버에 질문을 남겨주시면 매우 신속하게 답변을 받아보실 수 있습니다. 🚅🏎️⚡️

튜토리얼 퀵 가이드

이어서 로고 감지를 두 가지 접근 방식(비디오 레벨과 인덱스 레벨)으로 나누어 알아보겠습니다. 이번 프로젝트는 크게 두 개의 기술 섹션으로 구상되어 있으며, 최종적으로는 이 프로세스들을 결합하여 완전하게 동작하는 웹 애플리케이션 데모를 제작하는 흐름으로 마무리됩니다:

3단계로 간편하게 구현하는 로고 감지

특정 비디오에서 로고를 감지하고 추출하는 과정은 다음과 같이 3단계로 명확히 나뉩니다:

  1. 비디오 인덱싱 - 이전 튜토리얼을 읽어보셨다면 매우 익숙하고 자연스럽게 진행하실 수 있는 단계입니다.

  2. 비디오의 고유 식별자(ID) 조회 - Twelve Labs 플랫폼이 업로드한 비디오의 인덱싱 처리를 완료하면, 로고 감지를 수행하고자 하는 비디오 파일의 고유 ID를 가져옵니다.

  3. 화면에 나타나는 로고 정보 추출 - 생성한 인덱스 ID와 획득한 비디오 고유 ID를 타겟팅해 엔드포인트를 호출합니다. 복잡한 추론 연산 처리는 Twelve Labs API가 백엔드에서 모두 해결하며 최종 감지 결과물만 반환합니다.

로고 검색 - 인덱싱된 전체 비디오 내 특정 로고 탐색하기

개별 비디오 단위로 감지 처리를 실행하면 해당 비디오 내의 모든 로고 인스턴스를 세밀하게 식별할 수 있습니다. 반면, 이 '로고 검색' 기능을 활용하면 방대한 비디오 보관함 전체에서 우리가 찾고자 하는 특정 로고나 브랜드명이 실제로 노출되는 정확한 타임스탬프와 세그먼트들을 역으로 스캔하여 즉시 찾아낼 수 있습니다. 대규모 라이브러리를 일일이 수동 검색할 필요 없이, 영상 재생 중 로고 무늬가 식별된 최적의 편집 구간들만 즉각적으로 도출해 냅니다.

이전 글에서는 자연어 질의와 다양한 멀티모달 검색 옵션(예: 시각/오디오 중심 비디오 검색, 대화/스크립트 검색, OCR 기반 텍스트 검색 등)을 살펴보았습니다. 이번에는 전용 로고 감지 파이프라인(logo detection pipeline)만을 순수하게 연동하여 최적의 검색을 수행합니다. 필요 없는 연산을 건너뛰고 처리 효율 및 예산 낭비를 방지하기 위해 생성 시 인덱싱 옵션에 오직 'logo'만을 활성화하여 비디오 컬렉션을 인덱싱합니다. 이후 검색 API 요청을 보낼 때에도 동일한 'logo' 검색 옵션을 전송함으로써 화면 상에 일치하는 브랜드 로고 세그먼트 정보만을 깨끗하게 필터링해 가져오게 됩니다.

데모 애플리케이션 빌드

모든 기술 요소를 통합하기 위해 API 엔드포인트에서 반환된 원시 JSON 정보를 웹 브라우저 기반 화면으로 구성해 봅니다. 백엔드는 마이크로 프레임워크인 Python Flask를 사용하고 미니멀한 HTML로 렌더링하는 구조입니다. 로고 감지의 결과 데이터는 타임스탬프(시작 및 종료 시각)와 감지된 로고 명칭이 결합된 직관적인 표 형태로 시각화됩니다. 로고 검색 섹션에서는 사용자가 직접 입력한 브랜드 검색 쿼리와, 그에 대응하여 전체 라이브러리에서 검색된 최종 관련 영상 클립들이 목록 형태로 나타납니다.

3 단계로 구성하는 로고 감지 파이프라인

간단 명료한 테스트 진행을 위해, 기존 미리 생성해 둔 인덱스 계정을 이용해 총 5가지 비디오 파일을 사전에 등록해 두었습니다. 가입 시 기본 10시간의 인덱싱 용량이 제공되니 직접 따라 해 보실 수 있습니다. 테스트 규모를 넘어서 프로덕션 환경에 해당 기능을 확장하고 싶으시다면 공식 요금제 웹페이지에서 개발자 플랜(Developer Plan)으로 상향 전환이 가능합니다.

비디오 인덱싱

다음은 주피터 노트북(Jupyter notebook) 또는 개발 스크립트 작성에 반드시 포함되어야 하는 기본 환경 세팅 단계입니다. 필수 라이브러리(requests) 임포트 및 API 엔드포인트 도메인, 고유 API 키 주입 코드를 명시합니다:

<pre><code class="python">%env API_URL = https://api.twelvelabs.io/v1.1
%env API_KEY= <your API key>

!pip install requests

import os
import requests
import glob
from pprint import pprint

# Retrieve the URL of the API and my API key
API_URL = os.getenv("API_URL")
assert API_URL

API_KEY = os.getenv("API_KEY")
assert API_KEY
  </code></pre>
<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Set the header of the request
default_header = {
    "x-api-key": API_KEY
}

# Define a function to create an index with a given name
def create_index(index_name, index_options, engine):
    # Declare a dictionary named data
    data = {
        "engine_id": engine,
        "index_options": index_options,
        "index_name": index_name,
    }

    # Create an index
    response = requests.post(INDEXES_URL, headers=default_header, json=data)

    # Store the unique identifier of your index
    INDEX_ID = response.json().get('_id')

    # Check if the status code is 201 and print success
    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


# Create the indexes
index_id = create_index(index_name = "extract_text", index_options=["logo"], engine = "marengo2.5")

# Print the created index IDs
print(f"Created index IDs: {index_id}")
  </code></pre>

생성된 인덱스 폴더 내로 가상으로 다운로드된 F1 레이싱 관련 비디오 클립 5개를 전송하겠습니다. 로컬 저장 장치의 'static' 폴더에 사전에 MP4 사양으로 저장한 상태이며, 멀티스레드 비동기 처리를 가미해 동시에 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'  # folder containing the video files

def upload_video(file_name):
    # Validate if a video already exists in the index
    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

    # Proceed further to create a new task to index the current video if the video didn't exist in the index already
    print("Entering task creation code for the file: ", file_name)
    
    if file_name.endswith('.mp4'):  # Make sure the file is an MP4 video
        file_path = os.path.join(video_folder, file_name)  # Get the full path of the video file
        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")),] #The video will be indexed on the platform using the same name as the video file itself.
            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)
            # Check if the status code is 201 and print success
            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")

# Get list of video files
video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')]

# Create a ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
    # Use executor to run upload_video in parallel for all video files
    executor.map(upload_video, video_files)
      </code></pre>

비디오 고유 식별자(ID) 조회

이제 현재 인덱스 인스턴스 전반에 활성화 처리된 리소스를 질의해 비디오 ID 및 각 메타데이터 정보를 호출하겠습니다. 고유 식별자 리스트를 키밸류 사양의 도메인으로 매핑 보관하여, 향후 Flask 템플릿 영역에 동적으로 렌더링하도록 저장합니다:

<pre><code class="python"># List all the videos in an index
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>

화면에 표시되는 로고 추출

이제 실제 로고 정보 추출 처리를 단행하겠습니다. API를 호출해 타겟 비디오 내에 존재하는 풍부한 로고 데이터를 디코딩해 옵니다:

<pre><code class="python">VIDEO_ID = '###a849b86daab572f349242'
LOGO_URL = f"{API_URL}/indexes/{INDEX_ID}/videos/{VIDEO_ID}/logo"

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 검색 엔진 설정을 직접 탑재한 API 요청을 전송해 업로드된 인덱스 비디오 전체에서 실제 매칭되는 로고 데이터를 역추적해 내겠습니다:

<pre><code class="python"># Construct the URL of the `/search` endpoint
SEARCH_URL = f"{API_URL}/search/"

# Declare a dictionary named `data`
data = {
    "index_id": INDEX_ID,
    "query": "honda",
    "search_options": [
        "logo"
    ]
}

# Extracting query to later pass it to flask application
input_query = data["query"]

# Make a search request
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>

훌륭합니다. 시스템 상의 전체 비디오 리포지토리를 대상으로 'Honda' 브랜드의 로고가 포함된 완치 구간을 빠짐없이 스캔한 후 관련된 연관 정보만을 고성능으로 리턴해 왔습니다.

자, 도출된 순수 응답 값을 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>

로고 감지 결과를 가공한 뒤, 주피터 노트북에서 가공한 원격 리소스를 보존하기 위해 표준적인 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

# Save data to a pickle file
with open('data.pkl', 'wb') as f:
    pickle.dump(data_to_save, f)
    </code></pre>

데모 애플리케이션 빌드

이제 지금까지의 결과물들을 시각적으로 구현해 줄 웹 데모 제작 마지막 관문에 도달했습니다. 로컬 디렉터리 경로 연결 및 Pickle 데이터를 복원 탑재하는 기본 처리를 바탕으로, 사용자의 가독성을 돕기 위해 원시 초 단위 시간 포맷(seconds)을 분/초(MM:SS) 포맷으로 변환해 주는 로직을 소량 추가합니다. 백엔드를 담당할 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__)

# Load data from a pickle file
with open('data.pkl', 'rb') as f:
    loaded_data = pickle.load(f)

# Access the data
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 템플릿

Flask에서 주입한 변수들을 브라우저 화면에 표출해 줄 최종 Jinja-2 규격의 HTML 코드 템플릿입니다. 전송받은 감지 정보 리스트를 순회하며 테이블(Table)을 형성하고, 미디어 플레이어를 하단에 함께 탑재합니다. 사용자가 추출된 특정 타임스탬프 링크를 클릭 시, JavaScript 기반의 playVideo 가 재생 위치를 동적으로 변경하도록 바인딩 처리했습니다. (비디오 플레이어 제어 API는 원시 초 포맷을 인식하므로 전달할 전달 인자 값은 원래의 초 포맷으로 변환해 처리하도록 설정해 둡니다.)

<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
    <link rel="shortcut icon" href="#" />
    <title>Logo Detection</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;
        }

        /* search style */
        .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>Logo detection in the entire Video</h1>
    <h3>Video file: <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">
        Your browser does not support the video tag.
    </video>
    <br /> <br /> <br />
    <table>
        <tr>
            <th>Start</th>
            <th>End</th>
            <th>Value</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>Logo search results</h1>  
        <h2>Video file: <i>{{ video_id_name_dict[video_id] }}</i></h2>
        <h2>Entered query: <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">
            Your browser does not support the video tag.
        </video>
        <table>
            <tr>
                <th>Start</th>
                <th>End</th>
                <th>Confidence</th>
                <th>Text</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 데모 앱 실행하기

완성되었습니다! 주피터의 코드 블록 마지막 셀에 명령어를 수행해 서버를 구동해 줍니다:



<pre><code class="python">%run app.py
</code></pre>

기대했던 대로 프로세스가 에러 없이 깨끗하게 기동되는 터미널 로그를 확인할 수 있습니다 😊:

초기 할당 바인딩 주소인 http://127.0.0.1:5000 링크를 클리해 접속해 보면 다음과 같은 멋진 웹 대시보드를 마주하시게 됩니다:

이번 가이드 내 소스 코드를 온전히 작성 및 테스트해 정리해 둔 구글 드라이브 다운로드 링크를 아래 제공합니다: https://drive.google.com/drive/folders/1D97_UU2Z0lvp3y52BHV5GKkSNOQKv3Xi?usp=share_link

마치며

Twelve Labs 개발팀은 여러분들을 위해 훨씬 더 유용하고 지능적인 멀티모달 AI 기능과 관련 튜토리얼을 지속적으로 선보일 예정입니다. 멀티모달 신경망 기술에 관심이 많으시다면 부담 없이 활기찬 당사 공식 디스코드 커뮤니티에 가입하셔서 동료 개발자, 연구원들과 따뜻한 교류를 이어나가 보세요.