Ultralytics YOLO27: 대기자 명단에 등록하세요.
Get Started
프로덕션 환경의 Ultralytics YOLO·추적 및 파이프라인 구축·레슨 6/9
강의중급

계수, 히트맵 및 속도 추정

추적을 바탕으로 라인 계수기, 영역 점유율, 히트맵, 속도 추정치를 만들고, 정확하게 보정합니다.

대부분의 프로덕션 비전 시스템은 model.predict()이 아닙니다. 추적기에 간단한 기하 계산을 더한 형태입니다. 객체가 통과하는 선, 객체가 점유하는 영역, 픽셀을 미터로 환산하는 척도가 그 예입니다. 결과가 잘못되는 원인은 기하 계산에 있습니다. 이 강의의 데모 클립에서 속도 추정기가 보행자의 속도를 실제 속도의 4분의 1인 시속 1km로 보고하는데, 이는 추측한 숫자 하나 때문입니다.

학습 성과

실제 동영상에서 라인 계수기, 영역 계수, 히트맵, 속도 추정치를 만들고 알려진 기준값과 비교해 속도를 보정합니다.

속성 과정
이미 사용법을 알고 계신다면, 간단히 요약해 드리겠습니다.
  1. 라인 통과: solutions.ObjectCounter(region=[p1, p2])을 사용합니다. 트랙의 중심이 선을 통과하면 IN 또는 OUT에 더합니다.

  2. 영역 점유율: 추적된 각 박스에 점-다각형 포함 여부 검사를 한 번씩 수행하면 현재 영역 안에 있는 객체를 파악할 수 있습니다.

  3. 히트맵: solutions.Heatmap은 추적된 객체가 머문 위치를 누적합니다.

  4. 속도: 픽셀 이동 거리 × 픽셀당 미터 ÷ 시간입니다. 장면의 축척을 측정하고 실제 FPS를 확인하세요.

실습형

추적과 기하 계산#

ultralytics.solutions의 각 솔루션 클래스는 내부적으로 추적 모드를 실행한 다음, 트랙 이력에 자체 기하 계산을 적용합니다. pip install "ultralytics[solutions]"로 추가 패키지를 한 번에 설치하세요.

클래스기하 형태주요 결과
ObjectCounter점 2개 = 선, 점 3개 이상 = 다각형in_count, out_count, classwise_count
RegionCounter이름이 지정된 다각형 하나 이상region_counts
Heatmap시간에 따른 박스 영역 누적plot_im(영역이 주어진 경우 계수도 수행)
SpeedEstimator트랙 이동 거리 × meter_per_pixelspeed_dict(트랙 ID별 km/h)

솔루션은 기본적으로 tracker="tracktrack.yaml"을 사용합니다. 이전 강의에서 검증한 추적기를 전달하세요.

라인 통과 계수하기#

Ultralytics YOLO로 창고 컨베이어 위의 소포를 계수하는 모습: 각 상자에 트랙 ID(#013~#018)가 표시되고, 청록색 계수선을 통과하는 지점의 IN 계수기가 12를 나타냅니다

계수선은 선분입니다. 객체는 두 프레임 사이에서 박스 중심이 선을 통과할 때 한 번 계산되며, 어느 쪽에서 왔는지에 따라 IN 또는 OUT이 결정됩니다. 따라서 위 컨베이어 예시에는 추적이 필요합니다. ID가 없다면 상자 #015가 선에 닿는 매 프레임마다 다시 계산되기 때문입니다.

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))  # 640x360, 30 FPS, 62 frames
w, h, fps = (int(cap.get(p)) for p in (cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT, cv2.CAP_PROP_FPS))
writer = cv2.VideoWriter("counted.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))

counter = solutions.ObjectCounter(
    model="yolo26n.pt",
    region=[(0, 180), (640, 180)],  # two points: a horizontal line across the middle of the frame
    classes=[0],  # person
    tracker="bytetrack.yaml",
    verbose=False,
)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    results = counter(frame)  # detect + track + count in one call
    writer.write(results.plot_im)

cap.release()
writer.release()
print(f"in={results.in_count} out={results.out_count} classwise={results.classwise_count}")
in=2 out=2 classwise={'person': {'IN': 2, 'OUT': 2}}

클립의 2초 동안 네 명이 광장 중앙을 가로질렀으며, 양방향으로 두 명씩 이동했습니다. 기본 TrackTrack 구성으로 같은 스크립트를 실행하면 in=0 out=1을 보고합니다. 트랙 수가 적으면 통과 횟수도 적기 때문입니다. counted.mp4을 열고 수치를 신뢰하기 전에 각 통과 장면을 눈으로 확인하세요.

Ultralytics YOLO로 분리 도로의 라인 통과를 계수하는 모습: 추적된 승용차와 트럭이 청록색 선을 통과하고, 화살표가 이동 방향을 표시하며 차량 통과 계수는 IN 24대, OUT 18대입니다

도로에서는 동일한 선으로 교통량을 방향별로 나눕니다. 위쪽으로 선을 통과하는 차량은 IN, 아래쪽으로 통과하는 차량은 OUT으로 계산합니다. 객체가 선을 깨끗하게 직각으로 통과하는 위치에 선을 배치하고, 프레임 가장자리, 가림 객체, 박스가 작아지는 도로 끝부분은 피하세요.

영역 내 점유 수 계산하기#

다각형 영역을 사용하면 "현재 안에 객체가 몇 개 있나요?"라는 질문에 답할 수 있습니다. 영역 계수 가이드에는 바로 사용할 수 있는 RegionCounter이 포함되어 있습니다. 내부적으로는 추적된 각 박스에 대해 점-다각형 포함 여부를 한 번 검사합니다.

from shapely.geometry import Point, Polygon

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
zone = Polygon([(100, 100), (500, 100), (500, 300), (100, 300)])  # pixel coordinates

peak = 0
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
    if not r.boxes.is_track:
        continue
    inside = sum(zone.contains(Point(cx, cy)) for cx, cy in r.boxes.xywh[:, :2].tolist())
    peak = max(peak, inside)
print(f"peak occupancy: {peak} people inside the zone")
peak occupancy: 10 people inside the zone

xywh[:, :2]은 각 박스의 중심입니다. 대기열과 체류 시간을 계산하려면 영역에 들어온 시각을 ID별 타임스탬프로 저장하세요.

속도 추정하기#

고속도로 속도 추정: 추적된 각 승용차와 트럭에 추정 속도(km/h, 48~65)가 표시되고 짧은 이동 궤적이 나타납니다

위의 각 레이블은 차량 뒤의 궤적에서 계산됩니다. 즉, 알려진 프레임 수 동안 트랙이 이동한 픽셀 거리입니다. 속도 = 픽셀 거리 × 픽셀당 미터 ÷ 경과 시간입니다. 두 입력값에 따라 결과의 정확도가 결정됩니다.

  1. 픽셀당 미터. 장면에서 알려진 길이(바닥 타일, 차선 표시, 주차 공간 등)를 객체가 움직이는 깊이에서 측정하세요. 카메라가 기울어져 있다면 대신 네 점 호모그래피로 지면을 매핑하세요.
  2. 실제 캡처 프레임률. 속도는 FPS에 비례합니다. 영상을 재인코딩하거나 느리게 재생하면 모든 속도 값도 같은 비율로 달라집니다.

이 루프는 각 박스의 하단 중앙(객체와 지면이 맞닿는 지점)을 기준으로 측정하고, 각 트랙의 값을 10프레임에 걸쳐 평균 냅니다:

from collections import defaultdict, deque

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
fps = 30  # the real capture rate, not the playback rate
pixels_per_meter = 33  # example: a 1.2 m floor feature spans 40 px; measure your own scene

prev = {}  # track ID -> last bottom-center point
window = defaultdict(lambda: deque(maxlen=10))  # about 0.3 s of speeds per track at 30 FPS
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
    if not r.boxes.is_track:
        continue
    for (x1, y1, x2, y2), tid in zip(r.boxes.xyxy.tolist(), r.boxes.id.int().tolist()):
        cx, cy = (x1 + x2) / 2, y2  # bottom-center sits on the ground plane
        if tid in prev:
            px, py = prev[tid]
            meters = ((cx - px) ** 2 + (cy - py) ** 2) ** 0.5 / pixels_per_meter
            window[tid].append(meters * fps * 3.6)  # km/h
        prev[tid] = (cx, cy)

smoothed = sorted(sum(s) / len(s) for s in window.values() if len(s) == s.maxlen)
print(f"{len(smoothed)} tracks; median {smoothed[len(smoothed) // 2]:.1f} km/h")
29 tracks; median 2.1 km/h

사람은 4–5 km/h로 걸으므로, 2.1 km/h라는 값은 이 장면에 적용한 예시 스케일이 잘못되었음을 보여줍니다. pixels_per_meter 값은 여기서 측정한 값이 아닙니다. 중앙값을 알고 있는 속도와 비교하는 이 타당성 검사가 중요한 습관입니다.

solutions.SpeedEstimator은 같은 아이디어를 프로덕션용으로 구현합니다. 각 트랙의 박스 중심을 max_hist 프레임에 걸쳐 측정하고, meter_per_pixel를 사용해 변환한 뒤, ID마다 하나의 속도 값으로 고정합니다(상한은 max_speed):

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
speed = solutions.SpeedEstimator(
    model="yolo26n.pt",
    classes=[0],
    tracker="bytetrack.yaml",
    fps=30,  # the real capture rate, not the playback rate
    meter_per_pixel=0.03,  # a guess: a ~0.45 m shoulder width spans ~15 px here
    max_hist=15,  # 0.5 s of history before a speed is locked
    verbose=False,
)

speeds = {}
while True:
    ok, frame = cap.read()
    if not ok:
        break
    speeds.update(speed(frame).speed_dict)  # {track_id: km/h} once locked
cap.release()

kmh = sorted(speeds.values())
print(f"{len(kmh)} tracks; median {kmh[len(kmh) // 2]} km/h, range {kmh[0]}-{kmh[-1]} km/h")
25 tracks; median 1 km/h, range 0-4 km/h

같은 이유로 다시 약 4배 차이가 납니다. 신체 크기를 기준으로 스케일을 추정했기 때문입니다. 이와 같은 탑다운 카메라에서는 박스 중심을 사용해도 괜찮지만, 비스듬한 각도의 카메라에서는 하단 중앙 루프나 호모그래피를 사용하는 것이 좋습니다.

히트맵 누적하기#

도시 고속도로에서 Ultralytics YOLO 히트맵을 보여주는 이미지: 파란색에서 빨간색으로 이어지는 오버레이가 교통량이 가장 많은 차선을 따라 가장 밝게 표시되고, 승용차와 트럭 박스 및 승용차 12대와 트럭 2대의 누적 개수가 표시됩니다

히트맵은 매 프레임에서 추적 중인 각 박스 아래에 열을 누적하므로, 교통량이 가장 많은 차선이나 통로가 가장 밝게 표시됩니다. region을 지정하면 위 고속도로 예시처럼 객체 수도 셉니다:

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
heatmap = solutions.Heatmap(
    model="yolo26n.pt",
    classes=[0],
    tracker="bytetrack.yaml",
    colormap=cv2.COLORMAP_PARULA,
    verbose=False,
)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    results = heatmap(frame)
cap.release()
cv2.imwrite("heatmap.jpg", results.plot_im)  # 누적된 히트 오버레이가 표시된 마지막 프레임

여러 시간 분량의 영상에서 같은 오버레이를 통해 혼잡한 통로, 사용되지 않는 구역, 그리고 사람들이 실제로 이동하는 경로를 확인할 수 있습니다. 운동 모니터링과 Streamlit 실시간 추론 UI도 같은 구성 요소로 만들 수 있습니다.

주의할 점#

프레임당 모델을 두 번 실행하기

증상: 카운터 파이프라인의 FPS가 예상치의 절반입니다. 해결 방법: ObjectCounter, Heatmap, SpeedEstimator는 내부적으로 탐지와 추적을 수행합니다. 이러한 함수를 자체 model.track() 루프로 감싸 r.orig_img를 전달하지 말고, 원본 프레임을 입력하세요.

보정되지 않은 속도를 신뢰하기

증상: 보행자가 시속 1km로 이동하거나 차량이 시속 300km로 이동하는 것으로 표시됩니다. 해결 방법: 객체가 움직이는 구간에서 알려진 길이를 기준으로 meter_per_pixel을 측정하고, 카메라의 실제 FPS를 확인한 다음, 중앙값을 알려진 속도(보행 속도 또는 제한 속도)와 비교합니다.

각도 있는 카메라에서는 박스의 하단 중앙을 사용합니다

객체가 카메라에 가까워지고 박스가 커지면 기하학적 중심은 위로 이동합니다. 하단 중앙은 객체가 지면에 닿는 위치를 근사하므로, 하단 중앙에서 측정한 거리는 실제 움직임과 훨씬 더 가깝게 대응합니다.

사용해 보기

자체 도메인의 동영상(또는 데모 클립)을 사용해 라인 카운터를 만들고, counted.mp4에서 모든 교차를 눈으로 확인하세요. 그런 다음 장면에서 실제 길이를 하나 측정하고, 솔루션 스크립트로 스케일을 계산한 뒤 speed_loop.py을 다시 실행하여 중앙값 속도를 알려진 기준값과 비교하세요. 개수, 스케일, 보정 전후의 중앙값 속도를 제출하세요.

완료 조건
다음 조건을 모두 충족하면 레슨을 완료한 것입니다.
  • 라인 카운터의 IN 및 OUT 값이 동일한 클립을 직접 센 결과와 일치합니다.

  • 속도 추정에 장면에서 측정한 스케일을 사용하고, 중앙값이 알려진 기준 속도에서 25% 이내로 차이 납니다.

  • 비스듬한 각도의 카메라에서 속도를 측정할 때 박스의 하단 중앙이 적합한 지점인 이유를 설명할 수 있습니다.

정답 보기

프레임에서 직접 측정할 수 있는 대상을 기준으로 보정한 다음, 타당성을 확인하세요:

# Two points along a floor feature whose real length you know (read them off a saved frame)
(x1, y1), (x2, y2) = (100, 300), (140, 300)
real_length_m = 1.2  # e.g. measured tile width or lane marking

pixels = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print(f"pixels_per_meter = {pixels / real_length_m:.1f}, meter_per_pixel = {real_length_m / pixels:.4f}")
pixels_per_meter = 33.3, meter_per_pixel = 0.0300

pixels_per_meter을 speed_loop.py에 설정하거나, meter_per_pixel를 SpeedEstimator에 설정하고 다시 실행하세요. 보행자의 속도가 여전히 4–5 km/h와 크게 다르면 스케일을 잘못된 깊이에서 측정했거나 FPS가 잘못된 것입니다. 클립을 절반 속도로 느리게 재생하면 모든 측정값도 절반으로 줄어듭니다. 데모 클립에서는 두 스크립트 모두 같은 추정 스케일(픽셀당 0.03 m, 즉 미터당 33픽셀)을 사용했고, 실제보다 2–4배 낮은 값을 얻었습니다. 따라서 이 영상의 픽셀 하나가 추정치보다 더 넓은 지면을 나타내거나, 클립의 실제 캡처 프레임률이 30 FPS가 아닐 수 있습니다. 어느 쪽인지는 장면에서 직접 측정해야만 알 수 있습니다.

다음 단계

모델 하나와 스트림 하나라면 처리하기 쉽습니다. 다음 단계에서는 여러 스트림을 병렬로 처리하고, 그에 필요한 스레드 안전 추론 원칙을 살펴봅니다.