- BrainTools - https://www.braintools.ru -
Датасет для детекции средств индивидуальной защиты я собирал под диплом. Классов шесть, и они устроены несимметрично. Три положительных: Gloves, Safety Shoes, Welding mask. Три отрицательных, по которым и считается нарушение: Without Glove, Without Safety Shoes, Without welding mask.
На старте казалось, что проблема в модели. Оказалось, проблема в разметке.
Ниже про то, как я собрал свой автоаннотатор на SAM3, чем он отличается от Roboflow Auto Label и что из этого вышло.
Хуже всех в открытых датасетах представлена сварочная маска. Перчаток и касок много, масок после удаления дублей осталось около двух тысяч, а без аугментаций и того меньше. На таком объёме модель редкий класс не выучит.
Дальше я нарезал кадры из открытых видео про сварку. Кадров получились десятки тысяч, в пять-шесть раз больше финального набора: из десяти секунд записи полезными оказываются две-три, остальное заставки, титры и общие планы.
Размечать это руками долго. Автоматическая разметка выглядела очевидным выходом.
У Roboflow для этого есть готовый инструмент. Загружаешь кадры, задаёшь классы и текстовые промпты, выбираешь SAM 3, и модель сама расставляет маски и боксы. Есть отдельный порог уверенности на класс, есть просмотр результата до применения. Сделано хорошо, и подход мне понравился.
Не подошли две вещи. Первая — оплата. На свободном тарифе дают 10 кредитов в месяц, а кредит Auto Label это тысяча изображений. На десятки тысяч кадров уходит шесть-семь месяцев ожидания, и кредиты не копятся, они сгорают каждый месяц. Платный прогон по всему массиву по пакетному тарифу — около двухсот долларов. Не космос, но и не то, что хочется платить за диплом.
Вторая — то, чего в облачном инструменте нет:
конфликтов между классами. На них держится вся разметка нарушений, и это единственная часть, которую пришлось писать с нуля;
локального прогона. Датасет весит больше гигабайта, гонять его в облако и обратно на каждой итерации долго;
тестовой панели на двадцать кадров и режима merge, когда новые метки добавляются к старым, а не затирают их.
CVAT ускоряет ручную работу, но не убирает её. Label Studio требует поднимать бэкенд и конвертировать ответы. autodistill решает ровно мою задачу, но не умеет ни правил приоритета между классами, ни порогов на класс.
В итоге я выбрал свой GUI. Мне нужен был цикл «конфиг → тест на выборке → прогон по датасету» без промежуточных форматов и без сервера. Интерфейс оказался дешевле интеграции.
SAM3 — третья версия Segment Anything от Meta. Модель не требует обучения [1] под твои классы: даёшь изображение и текстовый промпт, получаешь маски того, что промпт описывает. Обучать детектор на маленьком датасете, чтобы он разметил большой, не нужно.
Запускается через Ultralytics, класс называется SAM3SemanticPredictor. Настройки, которые у меня прижились:
imgsz=644 — кратно 14, убирает предупреждение и заметно ускоряет работу;
conf=0.01 на входе — намеренно низкий порог, вся фильтрация происходит позже;
max_det=100 — больше сотни объектов на кадр в моей задаче не бывает;
torch.inference_mode() вокруг цикла — градиенты не нужны, память [2] не течёт.
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,
}
Первым идёт класс, который побеждает. Порядок выбран осознанно: если модель сомневается, перчатка это или голая рука, в разметку уходит нарушение. Ложное нарушение человек увидит на первом же просмотре и поправит, а ложная перчатка тихо уедет в датасет и будет там жить. Две пары показаны для примера, добавить третью про сварочную маску можно той же строкой, ограничений по количеству нет.
Логика [3] простая. Собираем боксы приоритетного класса. Для каждого бокса вторичного класса считаем 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))
После этого остаются только те руки, которые ничем не закрыты. Это и есть нарушения. Тот же код без изменений работает для обуви и для сварочной маски: пара классов и одно вычитание на каждую.
Приём требует, чтобы оба класса описывали одну и ту же область и чтобы модель уверенно находила каждый. Для СИЗ это так.
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 [4]
Прогоняет обученную модель по размеченным кадрам, сопоставляет с разметкой по IoU 0.5 и показывает те, где есть расхождение: класс не тот, бокс без предсказания, предсказание без бокса. Слева разметка, справа то, что видит модель.
Правки делаются не уходя с экрана: клик по предсказанию переносит его в разметку, боксы тянутся мышью. Есть фильтр «только кадры, где модель ничего не нашла».
Это не измерение качества, а способ быстро найти проблемные места: там, где модель расходится с разметкой, почти всегда стоит посмотреть глазами.
Главная проблема — промпты.
Модель плохо понимает составные названия: «пожарная машина» даёт случайный результат, «красная машина с лестницей» уже понятно о чём. Работает описание внешнего вида, а не название объекта.
То же с классами СИЗ. «Welding mask» ловит маску сносно, «сварочная маска» может не найти ничего. Помогает перебор формулировок и несколько промптов на класс через точку с запятой: часть сработает на профиль, часть на фронтальный вид.
Второе — геометрия. Перчатки на дальнем плане модель либо пропускает, либо находит одну вместо двух. Рука за инструментом обрезается, и бокс выходит меньше реального. Тут спасает только ручная правка.
Третье, и самое неприятное — Safety Shoes. Рабочие ботинки визуально почти не отличаются от зимних или демисезонных: тот же высокий берц, та же толстая подошва. Модель уверенно находит обувь, но не различает, спецобувь это или просто ботинки. Часть таких боксов пришлось проверять глазами, а часть кадров выкинуть: по изображению ответить на этот вопрос невозможно в принципе, никаким промптом.
Четвёртое вытекает из порядка конфликтов. Раз побеждает отрицательный класс, часть сомнительных перчаток уйдёт в нарушения. Это осознанный размен: ложное нарушение человек увидит при проверке и поправит, а ложная перчатка проскочит молча.
Приложение не требует ничего, кроме видеокарты и пяти пакетов.
pip install PyQt6 torch torchvision opencv-python ultralytics PyYAML
python 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 [5]
Модель на нём показала mAP50 = 0.825. Это не рекорд, зато цифра честная: разметка черновая, часть ошибок в метрике — это ошибки [6] разметки, а не модели.
|
Показатель |
Значение |
|---|---|
|
Изображений в датасете |
12 576 |
|
Промптов на изображение |
3–4 |
|
Время на изображение |
~1 с |
|
Полный прогон |
~3,5 часа |
|
Стоимость разметки |
0 ₽ |
|
Было бы у Roboflow |
≈200 $ или ~6 мес |
|
mAP50 обученной модели |
0.825 |
Автоаннотатор на SAM3: https://github.com/ilya-vor/sam3-auto-annotator [7]
Инструмент сравнения разметки и предсказаний: https://github.com/ilya-vor/yolo-annotation-tool [4]
Датасет: https://universe.roboflow.com/s-workspace-9g6go/ppe-last [5]
Разметка перестала быть узким местом, как только перестала быть ручной. Не потому, что SAM3 идеален, а потому что черновик за ночь и правки за вечер дешевле, чем идеальный бокс за десять секунд.
Автор: ilya-vor
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/37705
URLs in this post:
[1] обучения: http://www.braintools.ru/article/5125
[2] память: http://www.braintools.ru/article/4140
[3] Логика: http://www.braintools.ru/article/7640
[4] https://github.com/ilya-vor/yolo-annotation-tool: https://github.com/ilya-vor/yolo-annotation-tool
[5] https://universe.roboflow.com/s-workspace-9g6go/ppe-last: https://universe.roboflow.com/s-workspace-9g6go/ppe-last
[6] ошибки: http://www.braintools.ru/article/4192
[7] https://github.com/ilya-vor/sam3-auto-annotator: https://github.com/ilya-vor/sam3-auto-annotator
[8] Источник: https://habr.com/ru/articles/1101062/?utm_campaign=1101062&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.