Подсчёт, тепловые карты и оценка скорости
Создавай счётчики пересечения линий, подсчёт объектов в зонах, тепловые карты и оценки скорости на основе трекинга и честно калибруй их.
Большинство производственных систем компьютерного зрения — это не model.predict(). Это трекер и немного геометрии: линия, которую пересекают объекты, зона, в которой они находятся, масштаб для перевода пикселей в метры. Именно на геометрии возникают ошибки: оценка скорости на демонстрационном ролике из этого урока показывает для идущих пешеходов 1 км/ч — четверть их реальной скорости — из-за одного числа, выбранного наугад.
Практические занятия
Трекинг и геометрия#
Каждый класс решений в ultralytics.solutions внутри себя запускает режим Track, а затем применяет к истории трека собственные геометрические вычисления. Установи дополнительные зависимости один раз с помощью pip install "ultralytics[solutions]".
| Класс | Геометрия | Основные результаты |
|---|---|---|
ObjectCounter | 2 точки = линия, 3+ точек = многоугольник | in_count, out_count, classwise_count |
RegionCounter | Один или несколько именованных многоугольников | region_counts |
Heatmap | Накапливает площади боксов с течением времени | plot_im (плюс подсчёт, если задан регион) |
SpeedEstimator | Перемещение трека × meter_per_pixel | speed_dict (км/ч для каждого ID трека) |
По умолчанию решения используют tracker="tracktrack.yaml"; передай трекер, который ты проверил в предыдущем уроке.
Подсчитывай пересечения линии#

Линия подсчёта — это отрезок. Объект учитывается один раз, когда центр его бокса пересекает линию между двумя кадрами, а направление определяет, попадёт ли он в 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}}За две секунды ролика через середину площади прошли четыре человека: по двое в каждом направлении. С конфигурацией TrackTrack по умолчанию тот же скрипт выдаёт in=0 out=1, потому что меньшее количество треков означает меньше пересечений. Открой counted.mp4 и проверь каждое пересечение визуально, прежде чем доверять результату.

На дороге та же линия разделяет транспортные потоки по направлению: транспортные средства, пересекающие линию вверх, учитываются как 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 zonexywh[:, :2] — это центр каждого бокса. Для очередей и времени пребывания храни для каждого ID метку времени входа в зону.
Оценивай скорость#

Каждая метка выше рассчитывается по следу позади транспортного средства: расстоянию, которое его трек прошёл в пикселях за известное число кадров. Скорость = расстояние в пикселях × метры на пиксель ÷ прошедшее время. Правильность результата зависит от двух параметров:
- Метры на пиксель. Измерь известную длину в сцене (плитку на полу, дорожную разметку, парковочное место) на той глубине, где движутся объекты. Для камер под углом вместо этого преобразуй плоскость земли с помощью гомографии по четырём точкам.
- Реальная частота захвата. Скорость масштабируется вместе с 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 км/ч, поэтому значение 2,1 км/ч говорит о том, что примерный масштаб для этой сцены неверен: значение 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 раза мимо по той же причине: масштаб оценён по размеру тела. Центр бокса подходит для таких камер с видом сверху; для камер под углом лучше использовать цикл с нижней центральной точкой или гомографию.
Накапливаем тепловую карту#

Тепловая карта добавляет тепловые значения под каждым отслеживаемым боксом в каждом кадре, поэтому полосы или проходы с самым интенсивным движением светятся ярче всего. Передай ей 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 для инференса в реальном времени.
Подводные камни#
Симптом: конвейер подсчёта работает с половиной ожидаемого FPS. Решение: ObjectCounter, Heatmap и SpeedEstimator выполняют детекцию и трекинг внутри себя. Не оборачивай их в собственный цикл model.track() и не передавай r.orig_img; подавай им исходные кадры.
Симптом: скорость пешеходов — 1 км/ч, а автомобилей — 300 км/ч. Решение: измерь meter_per_pixel по известному расстоянию, на котором перемещаются объекты, проверь фактический FPS камеры и сравни медианное значение с известной скоростью (скоростью ходьбы или установленным ограничением).
Геометрический центр поднимается по мере приближения объекта к камере и увеличения его рамки. Нижняя центральная точка приблизительно соответствует месту соприкосновения объекта с землёй, поэтому расстояния, измеренные от неё, гораздо точнее отражают реальное движение.
Показать решение
Откалибруй масштаб по объекту, длину которого можно измерить в кадре, а затем проверь результат на здравый смысл:
# 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 км/ч, значит, масштаб измерен на неверной глубине или указано неправильное значение FPS: замедление ролика вдвое уменьшает каждое значение скорости вдвое. В демонстрационном ролике оба скрипта использовали один и тот же приблизительный масштаб (0,03 м на пиксель — это 33 пикселя на метр) и показывали скорость в 2–4 раза ниже. Значит, здесь на пиксель приходится большее расстояние, чем предполагалось, либо реальная частота захвата ролика не равна 30 FPS. Только измерение в самой сцене поможет определить, что именно неверно.