Đếm, heatmap và ước tính tốc độ
Xây dựng bộ đếm đường, tính số đối tượng trong vùng, heatmap và ước tính tốc độ dựa trên tracking, đồng thời hiệu chuẩn một cách chính xác.
Phần lớn hệ thống thị giác máy tính trong production không chỉ là model.predict(). Chúng kết hợp tracker với một chút hình học: một đường để đối tượng đi qua, một vùng để đối tượng lưu lại, một tỷ lệ để chuyển pixel thành mét. Hình học là nơi kết quả thường sai: bộ ước tính tốc độ trong video demo của bài học này báo người đi bộ di chuyển với tốc độ 1 km/h, chỉ bằng một phần tư tốc độ thực, do một con số được ước đoán.
Thực hành
Tracking kết hợp hình học#
Mỗi class solution trong ultralytics.solutions chạy Chế độ tracking ở bên trong, sau đó áp dụng hình học riêng lên lịch sử track. Cài đặt các gói bổ sung một lần bằng pip install "ultralytics[solutions]".
| Lớp | Hình học | Kết quả chính |
|---|---|---|
ObjectCounter | 2 điểm = đường, từ 3 điểm trở lên = đa giác | in_count, out_count, classwise_count |
RegionCounter | Một hoặc nhiều đa giác được đặt tên | region_counts |
Heatmap | Tích lũy diện tích bbox theo thời gian | plot_im (cộng số đếm nếu được cung cấp một vùng) |
SpeedEstimator | Độ dịch chuyển của track × meter_per_pixel | speed_dict (km/h cho mỗi ID track) |
Theo mặc định, các solution dùng tracker="tracktrack.yaml"; hãy truyền tracker bạn đã xác thực ở bài học trước.
Đếm số lần băng qua đường#

Đường đếm là một đoạn thẳng. Một đối tượng được đếm một lần khi tâm bbox của nó cắt qua đường giữa hai khung hình; phía mà đối tượng đi từ đó quyết định hướng IN hay OUT. Vì vậy, băng chuyền ở trên cần tracking: nếu không có ID, thùng #015 sẽ bị đếm lại ở mọi khung hình mà nó chạm vào đường.
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}}Trong hai giây của đoạn video, có bốn người đi qua khu vực giữa quảng trường: hai người theo mỗi hướng. Với cấu hình TrackTrack mặc định, script tương tự báo in=0 out=1 vì số track ít hơn đồng nghĩa với số lần băng qua ít hơn. Mở counted.mp4 và kiểm tra trực quan từng lần băng qua trước khi tin vào con số.

Trên đường, cùng một đường đếm chia lưu lượng theo hướng: phương tiện đi lên được tính là IN, đi xuống là OUT. Đặt đường ở nơi đối tượng băng qua rõ ràng và vuông góc, tránh xa mép khung hình, vật che khuất và đầu đường phía xa nơi bbox rất nhỏ.
Đếm số đối tượng trong một vùng#
Vùng đa giác trả lời câu hỏi “hiện có bao nhiêu đối tượng bên trong?”. Hướng dẫn đếm trong vùng cung cấp sẵn RegionCounter; bên dưới, cơ chế này thực hiện một phép kiểm tra điểm nằm trong đa giác cho mỗi bbox được tracking:
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 zonexywh[:, :2] là tâm của mỗi bbox. Để tính hàng chờ và thời gian lưu lại, hãy lưu dấu thời gian một ID đi vào vùng cho từng ID.
Ước tính tốc độ#

Mỗi nhãn ở trên được tính từ đường theo vết phía sau phương tiện: quãng đường track di chuyển tính bằng pixel trong một số khung hình đã biết. Tốc độ = khoảng cách tính bằng pixel × số mét trên mỗi pixel ÷ thời gian đã trôi qua. Hai đầu vào quyết định kết quả có chính xác hay không:
- Số mét trên mỗi pixel. Đo một độ dài đã biết trong cảnh (một viên gạch lát sàn, vạch kẻ làn đường, chỗ đỗ xe) ở độ sâu mà các đối tượng di chuyển. Với camera đặt nghiêng, thay vào đó hãy ánh xạ mặt phẳng mặt đất bằng phép biến đổi homography bốn điểm.
- Tốc độ ghi hình thực tế. Tốc độ tỷ lệ thuận với FPS; video được mã hóa lại hoặc làm chậm sẽ khiến mọi tốc độ thay đổi theo cùng một hệ số.
Vòng lặp này đo từ tâm cạnh dưới của mỗi hộp (nơi đối tượng tiếp xúc với mặt đất) và lấy trung bình từng track trong 10 frame:
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/hMọi người đi bộ với tốc độ 4–5 km/h, nên mức 2.1 km/h cho thấy scale ví dụ không phù hợp với cảnh này: giá trị pixels_per_meter chưa được đo tại đây. Phép kiểm tra hợp lý này, tức so sánh giá trị trung vị với tốc độ bạn đã biết, mới là thói quen quan trọng.
solutions.SpeedEstimator đóng gói cùng ý tưởng này để dùng trong production: đo tâm hộp của mỗi track qua max_hist frame, quy đổi bằng meter_per_pixel, rồi cố định một tốc độ cho mỗi ID (giới hạn ở 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/hLại lệch khoảng 4×, vì cùng một lý do: scale được ước đoán từ kích thước cơ thể. Tâm hộp phù hợp với camera nhìn từ trên xuống như camera này; với camera đặt nghiêng, hãy ưu tiên vòng lặp dùng tâm cạnh dưới hoặc phép biến đổi homography.
Tích lũy Heatmap#

Heatmap cộng nhiệt bên dưới mỗi hộp được theo dõi ở từng frame, vì vậy những làn đường hoặc lối đi có lưu lượng cao nhất sẽ sáng nhất. Cung cấp region và heatmap cũng đếm đối tượng, như ví dụ đường cao tốc ở trên:
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) # frame cuối cùng với lớp phủ nhiệt đã tích lũyTrong nhiều giờ video, cùng một lớp phủ cho thấy các lối đi bị ùn tắc, khu vực không được sử dụng và những tuyến đường mọi người thực sự đi qua. Tính năng giám sát buổi tập và giao diện suy luận trực tiếp bằng Streamlit được xây dựng từ cùng các thành phần.
Các lỗi thường gặp#
Triệu chứng: pipeline đếm chạy ở một nửa FPS dự kiến. Cách khắc phục: ObjectCounter, Heatmap và SpeedEstimator tự thực hiện detection và tracking. Đừng bọc chúng trong vòng lặp model.track() của riêng bạn rồi truyền r.orig_img; hãy đưa trực tiếp các frame thô vào.
Triệu chứng: người đi bộ có tốc độ 1 km/h hoặc ô tô chạy 300 km/h. Cách khắc phục: đo meter_per_pixel từ một đoạn có độ dài đã biết mà các đối tượng di chuyển qua, xác nhận FPS thực của camera và kiểm tra giá trị trung vị so với tốc độ đã biết (tốc độ đi bộ hoặc giới hạn tốc độ được niêm yết).
Tâm hình học sẽ nâng lên khi đối tượng tiến gần camera và box của đối tượng lớn dần. Tâm dưới xấp xỉ vị trí đối tượng tiếp xúc với mặt đất, vì vậy khoảng cách đo từ điểm này phản ánh chuyển động thực chính xác hơn nhiều.
Hiển thị lời giải
Hiệu chuẩn dựa trên một vật có thể đo được trong frame, rồi kiểm tra tính hợp lý:
# 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Đặt pixels_per_meter vào speed_loop.py (hoặc meter_per_pixel vào SpeedEstimator) rồi chạy lại. Nếu tốc độ người đi bộ vẫn chênh lệch nhiều so với 4–5 km/h, scale đã được đo ở độ sâu không phù hợp hoặc FPS không chính xác: clip bị làm chậm còn một nửa tốc độ sẽ làm mọi kết quả giảm một nửa. Trong clip demo, cả hai script đều dùng cùng một scale ước đoán (0.03 m mỗi pixel tương đương 33 pixel mỗi mét) và cho kết quả thấp hơn 2–4×; nghĩa là mỗi pixel ở đây bao phủ khoảng cách lớn hơn mức ước đoán hoặc tốc độ ghi hình thực tế của clip không phải 30 FPS. Chỉ phép đo trong cảnh mới xác định được nguyên nhân.