Ultralytics YOLO27: Встать в лист ожидания.
Get Started
Ultralytics YOLO в продакшене·Трекинг объектов и создание конвейеров·Урок 6/9
Уроксредний

Подсчёт, тепловые карты и оценка скорости

Создавай счётчики пересечения линий, подсчёт объектов в зонах, тепловые карты и оценки скорости на основе трекинга и честно калибруй их.

Большинство производственных систем компьютерного зрения — это не model.predict(). Это трекер и немного геометрии: линия, которую пересекают объекты, зона, в которой они находятся, масштаб для перевода пикселей в метры. Именно на геометрии возникают ошибки: оценка скорости на демонстрационном ролике из этого урока показывает для идущих пешеходов 1 км/ч — четверть их реальной скорости — из-за одного числа, выбранного наугад.

Результат

Создай на реальном видео счётчик пересечения линии, подсчёт объектов в зоне, тепловую карту и оценку скорости, а скорость откалибруй по известному ориентиру.

Ускоренное прохождение
Если ты уже освоился, вот краткая версия.
  1. Пересечение линии: solutions.ObjectCounter(region=[p1, p2]); пересечение центром трека линии увеличивает счётчик IN или OUT.

  2. Нахождение в зоне: проверка попадания одной точки в многоугольник для каждого отслеживаемого бокса показывает, кто находится внутри прямо сейчас.

  3. Тепловая карта: solutions.Heatmap накапливает данные о том, где отслеживаемые объекты проводят время.

  4. Скорость: перемещение в пикселях × метры на пиксель ÷ время. Измерь масштаб сцены и проверь фактический FPS.

Практические занятия

Трекинг и геометрия#

Каждый класс решений в ultralytics.solutions внутри себя запускает режим Track, а затем применяет к истории трека собственные геометрические вычисления. Установи дополнительные зависимости один раз с помощью pip install "ultralytics[solutions]".

КлассГеометрияОсновные результаты
ObjectCounter2 точки = линия, 3+ точек = многоугольникin_count, out_count, classwise_count
RegionCounterОдин или несколько именованных многоугольниковregion_counts
HeatmapНакапливает площади боксов с течением времениplot_im (плюс подсчёт, если задан регион)
SpeedEstimatorПеремещение трека × meter_per_pixelspeed_dict (км/ч для каждого ID трека)

По умолчанию решения используют 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}}

За две секунды ролика через середину площади прошли четыре человека: по двое в каждом направлении. С конфигурацией TrackTrack по умолчанию тот же скрипт выдаёт in=0 out=1, потому что меньшее количество треков означает меньше пересечений. Открой counted.mp4 и проверь каждое пересечение визуально, прежде чем доверять результату.

Подсчёт пересечений линии на дороге с помощью Ultralytics YOLO: отслеживаемые автомобили и грузовик пересекают голубую линию, стрелки показывают направление, а счётчик транспортных средств показывает 24 въезда и 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 метку времени входа в зону.

Оценивай скорость#

Оценка скорости на шоссе: у каждого отслеживаемого автомобиля и грузовика указана расчётная скорость в км/ч (от 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 км/ч, поэтому значение 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 раза мимо по той же причине: масштаб оценён по размеру тела. Центр бокса подходит для таких камер с видом сверху; для камер под углом лучше использовать цикл с нижней центральной точкой или гомографию.

Накапливаем тепловую карту#

Тепловая карта 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 для инференса в реальном времени.

Подводные камни#

Двойной запуск модели на каждом кадре

Симптом: конвейер подсчёта работает с половиной ожидаемого FPS. Решение: ObjectCounter, Heatmap и SpeedEstimator выполняют детекцию и трекинг внутри себя. Не оборачивай их в собственный цикл model.track() и не передавай r.orig_img; подавай им исходные кадры.

Доверие некалиброванной скорости

Симптом: скорость пешеходов — 1 км/ч, а автомобилей — 300 км/ч. Решение: измерь 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 км/ч, значит, масштаб измерен на неверной глубине или указано неправильное значение FPS: замедление ролика вдвое уменьшает каждое значение скорости вдвое. В демонстрационном ролике оба скрипта использовали один и тот же приблизительный масштаб (0,03 м на пиксель — это 33 пикселя на метр) и показывали скорость в 2–4 раза ниже. Значит, здесь на пиксель приходится большее расстояние, чем предполагалось, либо реальная частота захвата ролика не равна 30 FPS. Только измерение в самой сцене поможет определить, что именно неверно.

Что дальше

Одна модель и один поток — вполне управляемая задача. Дальше — много потоков параллельно и правила потокобезопасного инференса, которые для этого нужны.

Войди, чтобы сохранять прогресс