Ultralytics YOLO27: Встать в лист ожидания.
Get Started
За пределами детекции: сегментация, поза, OBB и классификация·Классифицируй, объединяй, отправляй·Урок 9/10
Уроксредний

Объединение задач в один конвейер

Создай каскад «детекция, затем классификация», классифицируй каждый отслеживаемый объект один раз, а не в каждом кадре, и преобразуй ключевые точки позы в временные ряды для каждого человека.

В самых полезных системах, построенных не только на детекции, обычно работают две модели, а не одна. Детектор находит каждого работника, а классификатор определяет, подходит ли тип каски; модель позы находит суставы, а трекер превращает их в временную шкалу, по которой можно считать повторения. Код короткий. Такие системы ломаются из-за того, что данные для второго этапа не соответствуют кропам первого этапа, а также из-за вычислительных затрат, растущих с каждым объектом в каждом кадре: при наивном подходе поток 30 FPS с десятью людьми требует 300 классификаций в секунду.

Результат

Запусти скрипт для детекции с последующей классификацией, его версию, классифицирующую каждого отслеживаемого человека только один раз, и трекер позы, сохраняющий углы коленей для каждого человека; обучающие данные классификатора подготовь из кропов самого детектора.

Ускоренное прохождение
Если ты уже освоился, вот краткая версия.
  1. Выполни детекцию, обрежь изображения с помощью result.orig_img[y1:y2, x1:x2] и классифицируй все кропы одним пакетным вызовом.

  2. Создай датасет классификатора из кропов своего детектора (save_crop=True), а не из студийных фотографий.

  3. Для видео используй persist=True для трекинга и классифицируй каждый result.boxes.id один раз, а затем повторно используй метку.

  4. При объединении позы и трекинга сохраняй историю ключевых точек каждого человека, используя ID трека в качестве ключа.

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

Два сценария, подходящие для большинства продуктов#

Оба сценария используют один и тот же кадр и один и тот же API Ultralytics; различие в том, что именно обрабатывает второй этап. Выбор и настройка трекера, а также обработка потоков для трекинга объектов рассматриваются в материале «Трекинг объектов с ByteTrack, BoT-SORT и TrackTrack»; здесь мы сосредоточимся на объединении задач.

Сначала детекция, затем классификация кропов#

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5)[0]
crops, labels = [], []
for box in result.boxes:
    x1, y1, x2, y2 = box.xyxy[0].int().tolist()
    crops.append(result.orig_img[y1:y2, x1:x2])  # BGR numpy crop, same as the detector saw
    labels.append(result.names[int(box.cls)])

for label, crop_result in zip(labels, classifier(crops, verbose=False)):  # one batched call for all crops
    probs = crop_result.probs
    print(f"{label:6s} -> {crop_result.names[probs.top1]} ({float(probs.top1conf):.2f})")
bus    -> bullet_train (0.31)
person -> sweatshirt (0.24)
person -> abaya (0.50)
person -> loudspeaker (0.49)
person -> frilled_lizard (0.24)

Здесь всё организовано правильно: кропы берутся из orig_img в полном разрешении, в пиксельных координатах самого детектора, а классификатор обрабатывает их все одним пакетным вызовом. Метки намеренно бессмысленны.

Качество второго этапа зависит от соответствия кропам

Классификатор ImageNet не обучался на плотно обрезанных фигурах на улице, поэтому он угадывает — обрати внимание, что уверенность ни в одном случае не превышает 0.50. То же происходит с классификатором, обученным на чётких фотографиях товаров, если подать ему размытые кропы детектора.

Создай датасет для второго этапа из собственных кропов#

Надёжный способ получить подходящие данные — поручить их создание детектору:

Сетка пронумерованных кропов чемоданов, сохранённых Ultralytics YOLO26 на видео с аэропортового конвейера; кропы готовы к сортировке по папкам классификатора

  1. Запусти детектор на репрезентативном видео с помощью save_crop=True, например yolo predict model=best.pt source=footage/ save_crop=True. Кропы появятся в runs/detect/predict/crops/<class>/.
  2. Распредели кропы каждого класса по папкам нужных подклассов (helmet_ok/, helmet_missing/) — это уже структура датасета для классификации из предыдущего урока.
  3. Обучи классификатор на этих папках и проверь его на кропах, полученных в другой день или с другой камеры.

Поскольку кропы создал детектор, в них сохранены реальные границы кадрирования, размытие и перекрытия — именно то, что увидит классификатор в рабочей системе; а дообучение на таких кропах — это обычное переносное обучение.

Классифицируй один раз для каждого ID трека#

Каска не меняется от кадра к кадру, поэтому классифицировать каждую рамку в каждом кадре — пустая трата ресурсов. Отслеживай детекции и классифицируй каждый новый ID трека один раз:

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"

labels = {}  # track id -> (label, confidence), filled once per person
frames = boxes_seen = 0
for result in detector.track(source, classes=[0], stream=True, persist=True, verbose=False):  # people only
    frames += 1
    if result.boxes.id is None:
        continue
    for track_id, (x1, y1, x2, y2) in zip(result.boxes.id.int().tolist(), result.boxes.xyxy.int().tolist()):
        boxes_seen += 1
        if track_id not in labels:  # first sighting: classify once, then reuse
            probs = classifier(result.orig_img[y1:y2, x1:x2], verbose=False)[0].probs
            labels[track_id] = (classifier.names[probs.top1], round(float(probs.top1conf), 2))

print(f"{frames} frames, {boxes_seen} person boxes, {len(labels)} classifier calls")
print(labels)
11 frames, 33 person boxes, 3 classifier calls
{1: ('basketball', 0.51), 2: ('maillot_(tights)', 0.14), 3: ('bulletproof_vest', 0.23)}

Три вызова классификатора вместо 33 на клипе из 11 кадров; на длинном потоке экономия растёт с каждым кадром, в котором человек остаётся в поле зрения. Метки пока представляют собой догадки ImageNet — замени классификатор на дообученный, и получишь ответ для каждого человека.

Правило выбора

Классифицируй объект один раз на трек, если его атрибут не может измениться, пока объект в кадре (тип шлема, марка автомобиля, артикул товара). Если атрибут может меняться (открытая дверь, поднятая рука), выполняй классификацию каждые N кадров. Если при первом появлении объект обычно маленький и размытый, вместо этого классифицируй его на кадре, где рамка трека наибольшая.

Поза и трекинг#

Оценка позы Ultralytics YOLO при подсчёте отжиманий: скелет тела, угол локтя 110 градусов и счётчик повторений

Режим трекинга присваивает каждому человеку постоянный result.boxes.id, превращая ключевые точки во временной ряд. Этот скрипт записывает угол левого колена каждого человека на том же коротком видео:

from collections import defaultdict

import numpy as np

from ultralytics import YOLO

def angle(a, b, c):
    """Angle at b, in degrees, formed by points a-b-c."""
    ba, bc = a - b, c - b
    return np.degrees(np.arccos(np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))))

model = YOLO("yolo26n-pose.pt")
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"
HIP, KNEE, ANKLE = 11, 13, 15  # left leg, COCO keypoint order
knee_angles = defaultdict(list)  # track id -> knee angle per frame

for result in model.track(source, stream=True, persist=True, verbose=False):
    if result.boxes.id is None:  # no confirmed tracks in this frame
        continue
    kpts = result.keypoints.cpu().numpy()
    for track_id, xy, conf in zip(result.boxes.id.int().tolist(), kpts.xy, kpts.conf):
        if conf[[HIP, KNEE, ANKLE]].min() > 0.5:  # skip occluded legs
            knee_angles[track_id].append(angle(xy[HIP], xy[KNEE], xy[ANKLE]))

for track_id, angles in sorted(knee_angles.items()):
    print(f"id {track_id}: {len(angles)} frames, left knee {min(angles):.0f}-{max(angles):.0f} deg")
id 1: 11 frames, left knee 170-176 deg
id 2: 11 frames, left knee 96-118 deg
id 3: 11 frames, left knee 177-180 deg

Согнутое колено объекта с ID 2 соответствует человеку, сидящему в кадре; двое других стоят, выпрямив ноги почти до 180°. Теперь счётчик повторений можно построить на пересечении порогов в ряду каждого ID: угол опускается ниже одного значения, а затем поднимается выше другого. Именно такую функцию предоставляет решение для мониторинга тренировок.

Учти затраты на цепочку#

Фактор затратСначала детекция, затем классификацияПоза и трекинг
Масштабируется в зависимости отколичества объектов в кадрекадров
Главный способ оптимизациисобирать кропы; классифицировать один раз для каждого ID трека и кэшировать результатуменьши imgsz или vid_stride, если движение медленное
Сбойклассификатор получает кропы, не похожие на данные, на которых его обучалипереключения ID разделяют историю одного человека на две части
Смена ID нарушает историю объекта

Симптом: счётчик повторений сбрасывается в середине подхода или один человек появляется под двумя ID. Причина: трекер потерял человека (из-за перекрытия или резкого поворота) и присвоил ему новый ID. Решение: настрой трекер, как описано в курсе по рабочему развертыванию, и сделай логику для каждого ID устойчивой к коротким пропускам — кэшированная метка классификатора просто вычисляется заново для нового ID.

Попробовать

Запусти two_stage.py, добавив save_crop=True в вызов детектора, и убедись, что кропы появляются в папках для каждого класса. Запусти classify_per_track.py и сравни число вызовов классификатора с числом рамок людей. Затем дополни pose_track.py, чтобы он выводил для каждого ID трека количество кадров, в которых угол колена меньше 120°.

Критерии готовности
Урок считается завершённым, если выполнены все условия ниже.
  • Кропы сохранены в crops/<class>/, и ты можешь описать, как распределить их по папкам для классификатора.

  • Ты можешь назвать, сколько вызовов классификатора удалось сэкономить благодаря кэшированию для каждого трека на тестовом клипе.

  • pose_track.py выводит по одной строке на каждый ID трека: количество кадров и количество кадров с углом меньше 120°.

Показать решение

Добавь save_crop=True в вызов детектора:

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5, save_crop=True)[0]

Кропы появляются в runs/detect/predict*/crops/person/ и crops/bus/ (имя папки запуска увеличивается при каждом запуске предсказания). Кэширование для каждого трека позволило выполнить 3 вызова классификатора для 33 рамок людей, сэкономив 30 вызовов на клипе из 11 кадров.

Чтобы подсчитать угол колена, замени последний цикл в pose_track.py:

for track_id, angles in sorted(knee_angles.items()):
    bent = sum(a < 120 for a in angles)
    print(f"id {track_id}: {len(angles)} frames, {bent} with knee below 120 deg")
id 1: 11 frames, 0 with knee below 120 deg
id 2: 11 frames, 11 with knee below 120 deg
id 3: 11 frames, 0 with knee below 120 deg

У сидящего человека с ID 2 угол меньше 120° во всех 11 кадрах; у объектов с ID 1 и 3 такого не бывает.

Что дальше

В завершение мы оцениваем каждую задачу по подходящей метрике и экспортируем все задачи для развёртывания.

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