Датасет для детекции средств индивидуальной защиты я собирал под диплом. Классов шесть, и они устроены несимметрично. Три положительных: Gloves, Safety Shoes, Welding mask. Три отрицательных, по которым и считается нарушение: Without Glove, Without Safety Shoes, Without welding mask.
На старте казалось, что проблема в модели. Оказалось, проблема в разметке.
Ниже про то, как я собрал свой автоаннотатор на SAM3, чем он отличается от Roboflow Auto Label и что из этого вышло.
Задача
Хуже всех в открытых датасетах представлена сварочная маска. Перчаток и касок много, масок после удаления дублей осталось около двух тысяч, а без аугментаций и того меньше. На таком объёме модель редкий класс не выучит.
Дальше я нарезал кадры из открытых видео про сварку. Кадров получились десятки тысяч, в пять-шесть раз больше финального набора: из десяти секунд записи полезными оказываются две-три, остальное заставки, титры и общие планы.
Размечать это руками долго. Автоматическая разметка выглядела очевидным выходом.
Почему не Auto Label
У Roboflow для этого есть готовый инструмент. Загружаешь кадры, задаёшь классы и текстовые промпты, выбираешь SAM 3, и модель сама расставляет маски и боксы. Есть отдельный порог уверенности на класс, есть просмотр результата до применения. Сделано хорошо, и подход мне понравился.
Не подошли две вещи. Первая — оплата. На свободном тарифе дают 10 кредитов в месяц, а кредит Auto Label это тысяча изображений. На десятки тысяч кадров уходит шесть-семь месяцев ожидания, и кредиты не копятся, они сгорают каждый месяц. Платный прогон по всему массиву по пакетному тарифу — около двухсот долларов. Не космос, но и не то, что хочется платить за диплом.
Вторая — то, чего в облачном инструменте нет:
-
конфликтов между классами. На них держится вся разметка нарушений, и это единственная часть, которую пришлось писать с нуля;
-
локального прогона. Датасет весит больше гигабайта, гонять его в облако и обратно на каждой итерации долго;
-
тестовой панели на двадцать кадров и режима merge, когда новые метки добавляются к старым, а не затирают их.
CVAT ускоряет ручную работу, но не убирает её. Label Studio требует поднимать бэкенд и конвертировать ответы. autodistill решает ровно мою задачу, но не умеет ни правил приоритета между классами, ни порогов на класс.
В итоге я выбрал свой GUI. Мне нужен был цикл «конфиг → тест на выборке → прогон по датасету» без промежуточных форматов и без сервера. Интерфейс оказался дешевле интеграции.
Что умеет SAM3
SAM3 — третья версия Segment Anything от Meta. Модель не требует обучения под твои классы: даёшь изображение и текстовый промпт, получаешь маски того, что промпт описывает. Обучать детектор на маленьком датасете, чтобы он разметил большой, не нужно.
Запускается через Ultralytics, класс называется SAM3SemanticPredictor. Настройки, которые у меня прижились:
-
imgsz=644— кратно 14, убирает предупреждение и заметно ускоряет работу; -
conf=0.01на входе — намеренно низкий порог, вся фильтрация происходит позже; -
max_det=100— больше сотни объектов на кадр в моей задаче не бывает; -
torch.inference_mode()вокруг цикла — градиенты не нужны, память не течёт.
Приложение
CLI-скриптом было бы быстрее, но правки всё равно делать руками, значит нужен интерфейс. Вышло пятнадцать файлов на PyQt6.
Что внутри:
-
загрузка
data.yamlот YOLO, классы подтягиваются автоматически; -
таблица классов: свой промпт (можно несколько через точку с запятой), свой порог уверенности, правила конфликтов;
-
общий порог NMS;
-
тестовая панель: прогон на N случайных изображениях до запуска разметки всего датасета;
-
два режима записи, replace стирает старые метки, merge сохраняет классы, которых нет в конфиге.
Модель грузится в отдельном потоке, обработка идёт в QThread, прогресс приходит сигналами. Без этого окно замирает на первой же секунде, и приложение выглядит сломанным.
Трюк с конфликтами классов
Самое интересное место.
Промпт bare hand работает, но понимает его модель как «рука», а не как «рука без перчатки»: на руку в перчатке он тоже срабатывает. Отрицание текстовым эмбеддингом не описывается. Поэтому нарушение нужно не найти, а вычислить: взять всё, что похоже на руку, и вычесть то, что перекрыто перчатками.
Отрицательные классы — это не отдельные объекты, а сумма нескольких промптов. Для Without Safety Shoes пришлось перечислять всё, что визуально попадает в категорию: bare foot, sneakers, sandals, dress shoes, socks. Причина в том, что тип спецобуви в ТЗ не зафиксирован, и под Safety Shoes попадает всё от берцев до кроссовок с толстой подошвой. Если бы речь шла о конкретном предприятии, промпт был бы точнее: у газовиков, насколько помню, это высокие оранжевые сапоги. Промпты английские, русские текстовые эмбеддинги у SAM3 работают заметно хуже.
В конфиге это выглядит как пара классов с порогом:
conflicts = { (WITHOUT_GLOVE_ID, GLOVES_ID): 0.25, (WITHOUT_SAFETY_SHOES_ID, SAFETY_SHOES_ID): 0.25,}
Первым идёт класс, который побеждает. Порядок выбран осознанно: если модель сомневается, перчатка это или голая рука, в разметку уходит нарушение. Ложное нарушение человек увидит на первом же просмотре и поправит, а ложная перчатка тихо уедет в датасет и будет там жить. Две пары показаны для примера, добавить третью про сварочную маску можно той же строкой, ограничений по количеству нет.
Логика простая. Собираем боксы приоритетного класса. Для каждого бокса вторичного класса считаем IoU со всеми приоритетными. Если максимум выше порога — вторичный бокс удаляется.
final_sec = []for sid, sbox, conf in sec_detections: if not any(compute_iou(pbox, sbox) > threshold for pbox in prio_boxes): final_sec.append((sid, sbox, conf))
После этого остаются только те руки, которые ничем не закрыты. Это и есть нарушения. Тот же код без изменений работает для обуви и для сварочной маски: пара классов и одно вычитание на каждую.
Приём требует, чтобы оба класса описывали одну и ту же область и чтобы модель уверенно находила каждый. Для СИЗ это так.
Маска, бокс, YOLO
SAM3 возвращает маски, а YOLO ждёт боксы в нормализованных координатах. Конвертация занимает восемь строк:
def mask_to_bbox(mask_np): contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None x, y, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea)) return (x, y, x + w, y + h)
Беру самый большой контур, а не объединение всех: маска почти всегда одна связная область, а осколки по краям дают лишнюю площадь.
Дальше NMS отдельно по каждому классу. Общий NMS тут вреден: рука и перчатка перекрываются законно, и общий проход съел бы одну из них. Порог 0.25 внутри класса, между классами сравнения нет.
Порог уверенности тоже по классам. У Without welding mask промпт «face» и один порог, у Welding mask другой: класс редкий, модель на нём менее уверена, и общий порог 0.5 просто выкинул бы половину полезных боксов.
Пайплайн целиком
Если собрать всё вместе, получается цепочка без ручных заготовок.
-
Скачиваю видео сварки из открытых источников и нарезаю кадры с шагом в несколько секунд, чтобы не было одинаковых соседних изображений.
-
Прогоняю кадры через автоаннотатор только по классу сварочной маски.
-
Удаляю те, где маска не нашлась. В видео попадаются заставки, титры и общие планы без сварки, держать их в датасете вредно. Выживает примерно каждый пятый кадр.
-
Оставшиеся кадры размечаю полностью, по всем шести классам.
-
Прогоняю второе приложение и правлю расхождения руками.
-
Обучаю модель и смотрю, где она ошибается.
Шаг третий оказался важнее, чем кажется. Наивный подход «нарезал и разметил» даёт датасет, где треть кадров не содержит целевого объекта, и модель учится на пустых разметках. Фильтр по одному классу отсекает балласт до обучения.
Скорость
На моей RTX 4060 Ti одно изображение с тремя-четырьмя промптами обрабатывается примерно за секунду. Промпты выполняются последовательно, каждый — отдельный проход модели по уже загруженному изображению.
Проходов два: первый по всему массиву, только по маске, второй — полная разметка по шести классам, уже по отобранным кадрам. Оба я запускал на ночь и утром получал готовый датасет. Финальный прогон по 12 576 кадрам — около трёх с половиной часов.
Для сравнения, ручная разметка того же объёма при десяти секундах на бокс — это десятки часов.
Как я проверял разметку
Автоматическая разметка не бывает правильной, она бывает черновой. Формальную метрику тут не посчитать: эталонной разметки на 12 тысяч изображений нет, иначе она была бы не нужна. Поэтому я пошёл обходным путём.
Обучил модель на получившемся датасете и написал второе приложение, которое показывает разногласия между датасетом и моделью: https://github.com/ilya-vor/yolo-annotation-tool
Прогоняет обученную модель по размеченным кадрам, сопоставляет с разметкой по IoU 0.5 и показывает те, где есть расхождение: класс не тот, бокс без предсказания, предсказание без бокса. Слева разметка, справа то, что видит модель.
Правки делаются не уходя с экрана: клик по предсказанию переносит его в разметку, боксы тянутся мышью. Есть фильтр «только кадры, где модель ничего не нашла».
Это не измерение качества, а способ быстро найти проблемные места: там, где модель расходится с разметкой, почти всегда стоит посмотреть глазами.
Где SAM3 врёт
Главная проблема — промпты.
Модель плохо понимает составные названия: «пожарная машина» даёт случайный результат, «красная машина с лестницей» уже понятно о чём. Работает описание внешнего вида, а не название объекта.
То же с классами СИЗ. «Welding mask» ловит маску сносно, «сварочная маска» может не найти ничего. Помогает перебор формулировок и несколько промптов на класс через точку с запятой: часть сработает на профиль, часть на фронтальный вид.
Второе — геометрия. Перчатки на дальнем плане модель либо пропускает, либо находит одну вместо двух. Рука за инструментом обрезается, и бокс выходит меньше реального. Тут спасает только ручная правка.
Третье, и самое неприятное — Safety Shoes. Рабочие ботинки визуально почти не отличаются от зимних или демисезонных: тот же высокий берц, та же толстая подошва. Модель уверенно находит обувь, но не различает, спецобувь это или просто ботинки. Часть таких боксов пришлось проверять глазами, а часть кадров выкинуть: по изображению ответить на этот вопрос невозможно в принципе, никаким промптом.
Четвёртое вытекает из порядка конфликтов. Раз побеждает отрицательный класс, часть сомнительных перчаток уйдёт в нарушения. Это осознанный размен: ложное нарушение человек увидит при проверке и поправит, а ложная перчатка проскочит молча.
Как запустить
Приложение не требует ничего, кроме видеокарты и пяти пакетов.
pip install PyQt6 torch torchvision opencv-python ultralytics PyYAMLpython gui_annotate.py
Веса sam3.pt кладутся в корень проекта, дальше выбирается data.yaml, классы подтягиваются сами. Перед полным прогоном стоит нажать тест на двадцати изображениях: если промпт не работает, это видно за минуту, а не через три часа.
Что получилось
В итоговом датасете 12 576 изображений. В публичном экспорте файлов больше, 22 637: тренировочная часть аугментирована в два варианта каждого кадра, а исходных изображений те же 12 576. Боксов на шесть классов 104 726. Стоимость — ноль рублей и одна ночь работы видеокарты. У Roboflow тот же прогон стоил бы около двухсот долларов, а бесплатно растянулся бы на полгода. Приложение осталось в виде инструмента, а не в виде счёта, и это оказалось важнее.
Датасет открыт: https://universe.roboflow.com/s-workspace-9g6go/ppe-last
Модель на нём показала mAP50 = 0.825. Это не рекорд, зато цифра честная: разметка черновая, часть ошибок в метрике — это ошибки разметки, а не модели.
|
Показатель |
Значение |
|---|---|
|
Изображений в датасете |
12 576 |
|
Промптов на изображение |
3–4 |
|
Время на изображение |
~1 с |
|
Полный прогон |
~3,5 часа |
|
Стоимость разметки |
0 ₽ |
|
Было бы у Roboflow |
≈200 $ или ~6 мес |
|
mAP50 обученной модели |
0.825 |
Ссылки
-
Автоаннотатор на SAM3: https://github.com/ilya-vor/sam3-auto-annotator
-
Инструмент сравнения разметки и предсказаний: https://github.com/ilya-vor/yolo-annotation-tool
-
Датасет: https://universe.roboflow.com/s-workspace-9g6go/ppe-last
Разметка перестала быть узким местом, как только перестала быть ручной. Не потому, что SAM3 идеален, а потому что черновик за ночь и правки за вечер дешевле, чем идеальный бокс за десять секунд.
ссылка на оригинал статьи https://habr.com/ru/articles/1101062/