다중 객체 추적과 비디오 메모리(Multi-Object Tracking & Video Memory)

추적(Tracking)은 검출(Detection)에 연관(Association)이 더해진 것입니다. 매 프레임(frame)마다 검출하고, 이번 프레임의 검출 결과를 직전 프레임의 트랙(track) ID에 맞춥니다.

유형: Build 언어: Python 선수 지식: Phase 4 Lesson 06(YOLO Detection), Phase 4 Lesson 08(Mask R-CNN), Phase 4 Lesson 24(SAM 3) 예상 시간: 약 60분

학습 목표

  • 탐지 기반 추적(Tracking-by-detection)과 질의 기반 추적(query-based tracking)을 구분하고, 대표 알고리즘 계열인 SORT, DeepSORT, ByteTrack, BoT-SORT, SAM 2 메모리 추적기(memory tracker), SAM 3.1 Object Multiplex를 이름까지 말할 수 있습니다.
  • 전통적인 탐지 기반 추적을 위한 IoU + 헝가리안 배정(Hungarian assignment)을 처음부터 직접 구현합니다.
  • SAM 2의 메모리 뱅크(memory bank)를 설명하고, 그것이 왜 IoU 기반 연관보다 가림(occlusion)에 강한지 이해합니다.
  • 추적 평가 지표인 MOTA, IDF1, HOTA를 읽고, 특정 활용 사례(use case)에 어떤 지표가 더 중요한지 고를 수 있습니다.

문제

검출기(detector)는 한 장의 프레임 안에서 객체(object)가 어디에 있는지만 알려줍니다. 추적기(tracker)는 프레임 t의 검출 결과와 프레임 t-1의 검출 결과 중 어떤 것이 같은 객체인지 알려줍니다. 이 단계가 없다면 어떤 선을 가로지른 객체의 수를 셀 수 없고, 가림 구간을 통과하는 공을 따라갈 수도 없으며, "4번 차량이 차선에 8초 동안 머물렀다"라는 사실도 파악할 수 없습니다.

추적은 영상을 다루는 모든 제품에 필수적인 기능입니다. 스포츠 분석(sports analytics), 감시(surveillance), 자율 주행(autonomous driving), 의료 영상 분석(medical video analysis), 야생 동물 모니터링(wildlife monitoring), 워드마크 카운팅(wordmark counting)이 모두 여기에 해당합니다. 핵심 구성 요소는 공통입니다. 프레임별 검출기, 운동 모델(motion model; 칼만 필터(Kalman filter)나 그보다 풍부한 모델), 연관 단계(IoU나 코사인 유사도(cosine), 학습된 특징(learned feature) 위에서 동작하는 헝가리안 알고리즘(Hungarian algorithm)), 그리고 트랙 생애 주기(track lifecycle; 생성(birth), 갱신(update), 소멸(death))로 이루어집니다.

2026년에는 두 가지 새로운 패턴이 등장했습니다. SAM 2 기반의 메모리 추적(memory-based tracking) 은 운동 모델 연관 대신 특징 메모리(feature memory)를 사용합니다. SAM 3.1 Object Multiplex 는 같은 개념(concept)의 수많은 인스턴스(instance)를 위해 공유 메모리(shared memory)를 사용합니다. 이 레슨에서는 먼저 고전적인 스택을 살펴보고, 그다음 메모리 기반 접근으로 넘어갑니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.탐지 기반 추적(Tracking-by-detection)에서 헝가리안 알고리즘(Hungarian algorithm)은 무엇을 하나요?

2.ByteTrack의 차별화된 기여는 무엇인가요?

0/2 답변 완료

개념

탐지 기반 추적(Tracking-by-detection)

flowchart LR
    F1["Frame t"] --> DET["Detector"] --> D1["Detections at t"]
    PREV["Tracks up to t-1"] --> PREDICT["Motion predict<br/>(Kalman)"]
    PREDICT --> PRED["Predicted tracks at t"]
    D1 --> ASSOC["Hungarian assignment<br/>(IoU / cosine / motion)"]
    PRED --> ASSOC
    ASSOC --> UPDATE["Update matched tracks"]
    ASSOC --> NEW["Birth new tracks"]
    ASSOC --> DEAD["Age unmatched tracks; delete after N"]
    UPDATE --> NEXT["Tracks at t"]
    NEW --> NEXT
    DEAD --> NEXT

    style DET fill:#dbeafe,stroke:#2563eb
    style ASSOC fill:#fef3c7,stroke:#d97706
    style NEXT fill:#dcfce7,stroke:#16a34a

2026년에 마주치는 거의 모든 추적기는 이 루프의 변형입니다. 차이점은 다음과 같습니다.

  • SORT(2016): 칼만 필터에 IoU 기반 헝가리안 매칭을 결합합니다. 단순하고 빠르지만, 외형 모델(appearance model)이 없습니다.
  • DeepSORT(2017): SORT에 트랙별 합성곱 신경망(CNN) 기반 외형 특징(ReID embedding)을 추가합니다. 객체가 서로 교차하는 상황을 더 잘 처리합니다.
  • ByteTrack(2021): 낮은 신뢰도의 검출 결과를 2단계로 연관시키는 방식을 도입했습니다. 외형 특징 없이도 MOT17에서 최고 성능(top performer)을 달성합니다.
  • BoT-SORT(2022): ByteTrack에 카메라 움직임 보정(camera motion compensation)과 ReID를 더했습니다.
  • StrongSORT / OC-SORT: 더 나은 운동 모델과 외형 특징을 갖춘 ByteTrack 계열의 후속 모델입니다.

칼만 필터(Kalman filter)를 한 문단으로

칼만 필터는 트랙별로 (x, y, w, h, dx, dy, dw, dh) 상태(state)와 공분산(covariance)을 유지합니다. 각 프레임에서 등속도 모델(constant-velocity model)로 상태를 예측(predict) 하고, 매칭된 검출 결과로 상태를 갱신(update) 합니다. 예측 불확실성이 클수록 갱신 단계에서는 검출 결과를 더 신뢰합니다. 이를 통해 궤적이 매끄럽게 이어지고, 짧은 가림(1~5 프레임) 구간에서도 트랙이 끊기지 않고 계속 이어집니다.

모든 고전적 추적기는 운동 예측 단계에서 칼만 필터를 사용합니다.

헝가리안 알고리즘(Hungarian algorithm)

M x N 비용 행렬(cost matrix; 행은 트랙, 열은 검출 결과)이 주어졌을 때, 총 비용을 최소화하는 일대일 배정(one-to-one assignment)을 찾는 알고리즘입니다. 비용은 보통 1 - IoU(track_bbox, detection_bbox)로 정의하거나, 외형 특징 사이의 음의 코사인 유사도(negative cosine similarity)로 정의합니다. 시간 복잡도는 O((M+N)^3)이며, M과 N이 약 1000 정도까지는 scipy.optimize.linear_sum_assignment를 사용하면 Python에서도 충분히 빠르게 처리됩니다.

ByteTrack의 핵심 아이디어

일반적인 추적기는 신뢰도가 낮은 검출 결과(< 0.5)를 그냥 버립니다. ByteTrack은 이를 2단계 후보(second-stage candidate) 로 남겨 둡니다. 먼저 트랙을 신뢰도가 높은 검출 결과에 매칭한 뒤, 매칭에 실패한 트랙은 조금 더 느슨한 IoU 임계값으로 신뢰도가 낮은 검출 결과와의 매칭을 한 번 더 시도합니다. 이렇게 하면 짧은 가림과 혼잡한 군중 근처에서의 ID 전환(ID switch)을 복구할 수 있습니다.

SAM 2 기반의 메모리 추적(SAM 2 memory-based tracking)

SAM 2는 영상을 처리할 때 인스턴스별 시공간 특징(spatio-temporal feature)을 보관하는 메모리 뱅크(memory bank) 를 유지합니다. 한 프레임에서 프롬프트(prompt; 클릭, 박스, 텍스트)를 주면 해당 인스턴스가 메모리에 인코딩됩니다. 이후 프레임에서는 새 프레임의 특징과 메모리 사이에 교차 어텐션(cross-attention)이 수행되고, 디코더(decoder)가 같은 인스턴스에 해당하는 마스크(mask)를 만들어 냅니다.

이 방식에는 칼만 필터도, 헝가리안 배정도 등장하지 않습니다. 연관 작업은 메모리 어텐션 연산 안에 암묵적으로 녹아 있습니다.

장점:

  • 긴 가림 구간에 강합니다. 메모리가 여러 프레임에 걸쳐 인스턴스의 정체성(identity)을 유지하기 때문입니다.
  • SAM 3의 텍스트 프롬프트(text prompt)와 결합하면 개방형 어휘(open-vocabulary)로 동작할 수 있습니다.
  • 별도의 운동 모델 없이도 동작합니다.

단점:

  • 다객체 추적(many-object tracking)에서는 ByteTrack보다 느립니다.
  • 메모리 뱅크가 점점 커지므로 컨텍스트 윈도(context window)에 제한이 생깁니다.

SAM 3.1 Object Multiplex

이전 세대의 SAM 2와 SAM 3 추적은 인스턴스마다 별도의 메모리 뱅크를 유지했습니다. 객체가 50개면 메모리 뱅크도 50개가 됩니다. Object Multiplex(2026년 3월)는 이들을 인스턴스별 질의 토큰(per-instance query token) 을 사용하는 하나의 공유 메모리(shared memory)로 묶습니다. 그 덕분에 비용이 인스턴스 수에 대해 1차 이하(sub-linear)로만 증가합니다.

Multiplex는 2026년 군중 추적(crowd tracking)의 새로운 기본 선택지가 되었습니다. 콘서트 군중, 창고 작업자, 교차로 차량 같은 장면에 잘 어울립니다.

알아 두어야 할 세 가지 지표

  • MOTA(Multi-Object Tracking Accuracy): 1 - (FN + FP + ID switches) / GT로 정의합니다. 오류 유형별 가중치가 들어 있고, 검출 실패와 연관 실패를 하나의 숫자로 섞어 보여 줍니다.
  • IDF1(ID F1): ID 정밀도(ID precision)와 재현율(recall)의 조화 평균(harmonic mean)입니다. 각 정답 트랙(ground-truth track)이 시간이 흘러도 자신의 ID를 얼마나 잘 유지하는지를 집중적으로 봅니다. ID 전환에 민감한 작업에서는 MOTA보다 낫습니다.
  • HOTA(Higher Order Tracking Accuracy): 검출 정확도(DetA)와 연관 정확도(AssA)로 분해되는 지표입니다. 2020년 이후 커뮤니티(community) 표준으로 자리 잡았으며, 가장 종합적인 평가 지표입니다.

감시 영상(surveillance)처럼 "누가 누구인가"가 중요한 경우에는 IDF1을 보고합니다. 스포츠 분석처럼 패스 수 세기(counting passes)가 중요한 경우에는 HOTA를 사용합니다. 일반적인 학술 비교(academic comparison)에서도 보통 HOTA를 씁니다.

직접 만들기

Step 1: IoU 기반 비용 행렬

import numpy as np


def bbox_iou(a, b):
    """
    a, b: [x1, y1, x2, y2] 형태의 (N, 4) 배열입니다.
    (N_a, N_b) 형태의 IoU 행렬을 반환합니다.
    """
    ax1, ay1, ax2, ay2 = a[:, 0], a[:, 1], a[:, 2], a[:, 3]
    bx1, by1, bx2, by2 = b[:, 0], b[:, 1], b[:, 2], b[:, 3]
    inter_x1 = np.maximum(ax1[:, None], bx1[None, :])
    inter_y1 = np.maximum(ay1[:, None], by1[None, :])
    inter_x2 = np.minimum(ax2[:, None], bx2[None, :])
    inter_y2 = np.minimum(ay2[:, None], by2[None, :])
    inter = np.clip(inter_x2 - inter_x1, 0, None) * np.clip(inter_y2 - inter_y1, 0, None)
    area_a = (ax2 - ax1) * (ay2 - ay1)
    area_b = (bx2 - bx1) * (by2 - by1)
    union = area_a[:, None] + area_b[None, :] - inter
    return inter / np.clip(union, 1e-8, None)

Step 2: 최소한의 SORT 스타일 추적기

분량을 줄이기 위해 고정 등속도 칼만 필터(fixed constant-velocity Kalman)는 생략하고, 여기서는 단순한 IoU 기반 연관만 사용합니다. 실제 운영(production) 환경에서는 칼만 예측 단계가 필수이며, 완전한 구현은 sort Python 패키지에서 제공합니다.

from scipy.optimize import linear_sum_assignment


class Track:
    def __init__(self, tid, bbox, frame):
        self.id = tid
        self.bbox = bbox
        self.last_frame = frame
        self.hits = 1

    def update(self, bbox, frame):
        self.bbox = bbox
        self.last_frame = frame
        self.hits += 1


class SimpleTracker:
    def __init__(self, iou_threshold=0.3, max_age=5):
        self.tracks = []
        self.next_id = 1
        self.iou_threshold = iou_threshold
        self.max_age = max_age

    def step(self, detections, frame):
        if not self.tracks:
            for d in detections:
                self.tracks.append(Track(self.next_id, d, frame))
                self.next_id += 1
            return [(t.id, t.bbox) for t in self.tracks]

        track_boxes = np.array([t.bbox for t in self.tracks])
        det_boxes = np.array(detections) if len(detections) else np.empty((0, 4))

        iou = bbox_iou(track_boxes, det_boxes) if len(det_boxes) else np.zeros((len(track_boxes), 0))
        cost = 1 - iou
        cost[iou < self.iou_threshold] = 1e6

        matched_track = set()
        matched_det = set()
        if cost.size > 0:
            row, col = linear_sum_assignment(cost)
            for r, c in zip(row, col):
                if cost[r, c] < 1.0:
                    self.tracks[r].update(det_boxes[c], frame)
                    matched_track.add(r); matched_det.add(c)

        for i, d in enumerate(det_boxes):
            if i not in matched_det:
                self.tracks.append(Track(self.next_id, d, frame))
                self.next_id += 1

        self.tracks = [t for t in self.tracks if frame - t.last_frame <= self.max_age]
        return [(t.id, t.bbox) for t in self.tracks]

60줄 분량입니다. 프레임별 검출 결과를 받아 프레임별 트랙 ID를 돌려줍니다. 실제 시스템에서는 여기에 칼만 예측 단계, ByteTrack의 2단계 재매칭(second-stage re-match), 그리고 외형 특징까지 더해 갑니다.

Step 3: 합성 궤적(synthetic trajectory) 테스트

def synthetic_frames(num_frames=20, num_objects=3, H=240, W=320, seed=0):
    rng = np.random.default_rng(seed)
    starts = rng.uniform(20, 200, size=(num_objects, 2))
    velocities = rng.uniform(-5, 5, size=(num_objects, 2))
    frames = []
    for f in range(num_frames):
        dets = []
        for i in range(num_objects):
            cx, cy = starts[i] + f * velocities[i]
            dets.append([cx - 10, cy - 10, cx + 10, cy + 10])
        frames.append(dets)
    return frames


tracker = SimpleTracker()
for f, dets in enumerate(synthetic_frames()):
    tracks = tracker.step(dets, f)

직선으로 움직이는 세 객체는 20 프레임 동안 자신의 ID를 그대로 유지해야 합니다.

Step 4: ID 전환(ID switch) 지표

def count_id_switches(tracks_per_frame, gt_per_frame):
    """
    tracks_per_frame:  프레임별 (track_id, bbox) 리스트의 리스트
    gt_per_frame:      프레임별 (gt_id, bbox) 리스트의 리스트
    ID 전환 횟수를 반환합니다.
    """
    prev_assignment = {}
    switches = 0
    for tracks, gts in zip(tracks_per_frame, gt_per_frame):
        if not tracks or not gts:
            continue
        t_boxes = np.array([b for _, b in tracks])
        g_boxes = np.array([b for _, b in gts])
        iou = bbox_iou(g_boxes, t_boxes)
        for g_idx, (gt_id, _) in enumerate(gts):
            j = iou[g_idx].argmax()
            if iou[g_idx, j] > 0.5:
                t_id = tracks[j][0]
                if gt_id in prev_assignment and prev_assignment[gt_id] != t_id:
                    switches += 1
                prev_assignment[gt_id] = t_id
    return switches

이는 IDF1에 가까운(IDF1-adjacent) 단순화된 지표입니다. 정답 객체에 할당된 예측 트랙 ID가 몇 번이나 바뀌는지를 세어 줍니다. 실제로 사용되는 MOTA, IDF1, HOTA 평가 도구는 py-motmetricsTrackEval에 들어 있습니다.

사용해 보기

2026년에 운영 환경에서 자주 쓰이는 추적기는 다음과 같습니다.

  • ultralytics: YOLOv8에 ByteTrack과 BoT-SORT가 기본 내장되어 있습니다. results = model.track(source, tracker="bytetrack.yaml") 한 줄로 사용할 수 있는 기본 선택지입니다.
  • supervision(Roboflow): ByteTrack 래퍼(wrapper)와 주석(annotation) 유틸리티를 함께 제공합니다.
  • SAM 2 / SAM 3.1: processor.track()을 통한 메모리 기반 추적을 지원합니다.
  • 사용자 정의 스택(custom stack): 검출기(YOLOv8 / RT-DETR)에 sort-tracker, OC-SORT, StrongSORT 같은 추적기를 조합합니다.

상황별 선택 기준은 다음과 같습니다.

  • 보행자, 차량, 박스 등 일반 객체를 30 fps 이상으로 처리: ultralytics의 ByteTrack
  • 군중 속에서 같은 클래스의 객체가 매우 많은 경우: SAM 3.1 Object Multiplex
  • 외형 식별이 가능한 무거운 가림 상황: DeepSORT / StrongSORT(ReID 특징 활용)
  • 스포츠 등 복잡한 상호 작용: BoT-SORT 또는 학습 기반 추적기(MOTRv3 등)

산출물 만들기

이 레슨에서는 다음 산출물을 만듭니다.

  • outputs/prompt-tracker-picker.md: 장면 유형(scene type), 가림 패턴(occlusion pattern), 지연 시간 예산(latency budget)에 따라 SORT / ByteTrack / BoT-SORT / SAM 2 / SAM 3.1 중에서 적합한 추적기를 골라 주는 프롬프트입니다.
  • outputs/skill-mot-evaluator.md: 정답 트랙에 대해 MOTA / IDF1 / HOTA를 평가하는 완전한 평가 하네스(evaluation harness)를 작성하는 스킬입니다.

연습문제

  1. (쉬움) 위의 합성 궤적 추적기를 객체 3개, 10개, 30개로 각각 실행해 봅니다. 각 경우에 나타난 ID 전환 횟수를 보고하고, 단순한 IoU 기반 연관이 어디서부터 흔들리기 시작하는지 찾아봅니다.
  2. (중간) 연관 단계 앞에 등속도 칼만 예측 단계를 추가해 봅니다. 그러면 짧은 2~3 프레임의 가림이 더 이상 ID 전환을 일으키지 않는지를 확인합니다.
  3. (어려움) transformers를 통해 SAM 2의 메모리 기반 추적기를 대안 백엔드(alternative backend)로 통합합니다. 군중을 담은 30초짜리 클립에서 SimpleTracker와 SAM 2를 모두 실행하고, 눈에 띄는 인물 5명의 정답 ID를 직접 라벨링해 두 추적기의 ID 전환 횟수를 비교합니다.

핵심 용어

용어흔한 설명실제 의미
탐지 기반 추적(Tracking-by-detection)"검출한 뒤 연관시킨다"프레임별 검출기 위에 IoU 또는 외형 특징을 사용하는 헝가리안 배정을 얹은 구조
칼만 필터(Kalman filter)"운동 예측"매끄러운 트랙 예측과 가림 처리를 위한 선형 동역학과 공분산의 결합
헝가리안 알고리즘(Hungarian algorithm)"최적 배정"최소 비용 이분 매칭(minimum-cost bipartite matching)을 푸는 알고리즘. scipy.optimize.linear_sum_assignment 사용
ByteTrack"낮은 신뢰도의 2차 패스"매칭에 실패한 트랙을 신뢰도가 낮은 검출 결과에 다시 매칭해 짧은 가림을 복구한다
DeepSORT"SORT + 외형"프레임 간 매칭을 위한 ReID 특징을 추가해 ID 보존을 개선한다
메모리 뱅크(Memory bank)"SAM 2의 트릭"프레임 간에 저장되는 인스턴스별 시공간 특징. 명시적 연관 단계를 교차 어텐션으로 대체한다
Object Multiplex"SAM 3.1의 공유 메모리"인스턴스별 질의(query)와 결합된 단일 공유 메모리로 다객체 추적을 빠르게 만든다
HOTA"현대적 추적 지표"검출 정확도와 연관 정확도로 분해되는 커뮤니티 표준 지표

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 2개

skill-mot-evaluator

Write a complete evaluation harness for MOTA / IDF1 / HOTA against ground-truth tracks

Skill
prompt-tracker-picker

Pick SORT / ByteTrack / BoT-SORT / SAM 2 / SAM 3.1 given scene type, occlusion patterns, and latency budget

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.SAM 2의 메모리 기반 추적기는 어떻게 명시적인 헝가리안 식 연관을 피하나요?

2.각 인물의 ID 일관성이 핵심인 감시(surveillance) 영상에서는 어떤 지표(metric)를 보고해야 하나요?

3.SAM 3.1 Object Multiplex(2026년 3월)는 추적 중인 여러 인스턴스에 대한 공유 메모리(shared memory)를 도입했습니다. 이것은 무엇을 가능하게 하나요?

0/3 답변 완료