Как мы сделали простой офлайн‑разметчик для YOLO и COCO с семантической и panoptic‑сегментацией

В настоящий момент я работаю над проектом по детекции дефектов дорожно‑транспортной инфраструктуры. Находимся на самом начальном этапе, подбираем модели для детекции.
Мы начинали работы с моделями детекции — как‑то по инерции с предыдущего проекта. Но этого опыта и знаний (а также обученных ранее моделей) оказалось недостаточно для решения поставленных задач. Тогда мы обратились к сегментационным моделям. И сразу возник вопрос об адекватном инструменте для разметки данных.
Если вы обучали свою модель детекции или сегментации, то знаете: самое долгое — не обучение, а разметка. Готовые инструменты делятся на две группы. Серверные (CVAT, Label Studio) умеют всё, но их нужно разворачивать и поддерживать. Эти инструменты замечательны, но очень непростые в освоении и затратные с аппаратной точки зрения.
Существующие простые десктопные вроде labelImg ставятся за минуту и очень удобные в освоении, но не обладают нужным функционалом — умеют в основном размечать прямоугольники (bbox). И стало понятно, что нам нужна была середина: программа, которая запускается на ноутбуке без сервера и делает разметку bbox и полигональную разметку.
Сделали простую программку, которая делала то, что надо. Но дальше — больше: сначала понадобились повёрнутые рамки (OBB), потом маски, затем многопользовательский режим, а под конец — семантическая и panoptic‑сегментация. И всё это нужно было выгружать в форматы, которые без доработки едят YOLO, Detectron2 и Mask2Former. Так появился YOLO Annotator. Код открыт (GPL-3.0), ниже — что он умеет и как устроен изнутри.
Сначала это был чисто внутренний инструмент, и продавать его мы не собирались. Но со временем стало понятно, что он может пригодиться не только нам: многим нужно что‑то посередине между labelImg и CVAT — без сервера, но с масками, сегментацией и нормальным экспортом. Поэтому решили просто поделиться им с сообществом: программа бесплатная, код открыт под GPL-3.0. Взамен буду рад обратной связи — что неудобно, чего не хватает, где ломается.
Что умеет
Тип разметки | Инструмент | Для чего |
|---|---|---|
Прямоугольник | BBox | детекция |
Полигон | Polygon | инстанс‑сегментация |
Ломаная | Polyline | дороги, трещины, кабели |
Повёрнутая рамка | OBB | аэрофото, документы |
Ключевые точки со скелетом | Pose | поза, ориентиры |
Точка | Point | подсчёт объектов |
Метка изображения | Classify | классификация |
Маска кистью | Brush | объекты сложной формы |
Область класса | Semantic | семантическая сегментация, фон |
Вокруг этого — импорт готовых YOLO‑датасетов, разбивка на train/val/test, проверка качества (пустые изображения, дубли, слишком мелкие полигоны), полный undo/redo, настраиваемые горячие клавиши, русский и английский интерфейс и совместная работа через общую сетевую папку.
Семантическая кисть: кисть — это класс
Обычная кисть рисует объект: каждая зафиксированная маска — отдельный экземпляр. Для семантической сегментации это неудобно: дорога на снимке — не «пять объектов‑дорог», а одна область, пусть и из нескольких кусков.
В семантическом режиме всё, что нарисовано классом road, сливается в одну область этого класса на изображении. Мазок сохраняется сразу при отпускании кнопки, а каждый пиксель принадлежит не более чем одному классу. Есть три режима:
overwrite — мазок забирает пиксели у других классов (закрасили край дороги «тротуаром» — он перешёл к тротуару);
keep — рисует только в пустые места, поэтому фон вокруг уже размеченного можно заливать широкой кистью, не боясь задеть соседей;
erase — снимает разметку.

Как устроено. Каждый слой — PNG‑маска «изображение + класс». Самое интересное — undo: при каждом мазке пишется новый файл, а старый не трогается. Отмена просто возвращает аннотации ссылку на предыдущий файл — никаких копий битмапов в памяти. Мазок, затронувший сразу несколько классов (например, отобравший пиксели у соседнего), откатывается одной командой — изменения группируются в макрос QUndoStack.
Платить за это приходится лишними файлами, поэтому при открытии проекта работает уборка: файлы, на которые больше нет ссылок, сначала переносятся в _orphaned/ и удаляются только при следующем открытии. Плюс отсрочка в 24 часа — в многопользовательском режиме коллега мог только что нарисовать маску, а его JSON ещё не автосохранился.
Panoptic: объекты и фон в одном проекте
Panoptic‑сегментация объединяет два вида разметки: счётные объекты (things — машины, люди) и аморфные области (stuff — дорога, небо). Каждый пиксель принадлежит ровно одному сегменту: конкретному объекту, области класса‑фона или «пустоте».
У каждого класса есть роль auto / thing / stuff (по умолчанию семантические классы — stuff, остальные — things). Экспорт COCO Panoptic строит карту сегментов: сначала фон, поверх — объекты; area и bbox считаются по видимой части, а объект, полностью закрытый другими, в разметку не попадает. id сегмента кодируется в цвете пикселя (R + 256·G + 256²·B), причём id выбираются рядом с цветом класса, так что PNG можно открыть и проверить глазами.

COCO RLE без pycocotools
Маски в COCO можно отдать полигонами или RLE. Полигоны теряют дырки (дорога вокруг люка станет сплошной), RLE — точная попиксельная маска.
Стандартный способ закодировать RLE — pycocotools, но тянуть C‑расширение ради одной функции не хотелось. Формат несложный: маска обходится по столбцам, записываются длины чередующихся серий «фон/объект», каждая длина — как разность с длиной двумя позициями ранее, и упаковывается кусками по 5 бит в печатные символы, начиная с '0':
def _counts_to_string(counts):
out = []
for i, x in enumerate(counts):
if i > 2:
x -= counts[i - 2]
more = True
while more:
c = x & 0x1F
x >>= 5 # арифметический сдвиг, как в C-оригинале
more = (x != -1) if (c & 0x10) else (x != 0)
if more:
c |= 0x20
out.append(chr(c + 48))
return "".join(out)Длины серий считаются векторно в NumPy. Проверка — побайтовое сравнение с pycocotools на 300 случайных масках: результат совпал полностью, а итоговый JSON целиком загружается через pycocotools.coco.COCO — тот же загрузчик, на который опираются Detectron2 и MMDetection.
Какой экспорт для какой модели
Модель | Формат |
|---|---|
Ultralytics YOLO (detect / seg / OBB / pose) | соответствующий YOLO‑формат |
U‑Net, DeepLab, SegFormer | Semantic Masks, режим Index |
Detectron2, MMDetection | COCO Instances + RLE |
Mask2Former, OneFormer | COCO Panoptic |
Всего форматов 12: YOLO Detect / Segment / OBB / Pose / Point / Classify, COCO Instances / Keypoints / Panoptic, Pascal VOC, LabelMe и PNG‑маски (бинарные, индексные, цветные).
Как устроен проект
Внутренний формат — собственный, а не YOLO или COCO: папка
.annprojс JSON на каждое изображение и PNG для масок. Координаты нормализованы, поэтому экспорт в любой формат — чистая функция от модели данных.Слои разделены: домен (аннотации и классы) → хранение → экспортёры → инструменты → UI. Экспортёр не знает про Qt, инструмент не знает про форматы.
Все изменения идут через команды
QUndoStack— undo/redo работает для любого инструмента.Новые инструменты подключаются плагинами:
.py‑файл в папкеplugins/— и кнопка появляется в меню.561 автотест, включая регрессионные на найденные баги.
Установка
Скачать архив релиза: https://github.com/ILYAGRISH/yolo‑annotator/releases/tag/v1.5
Поставить Python 3.10+ (рекомендую 3.13) с галкой «Add python.exe to PATH».
Запустить
setup_venv.bat— он создаст окружение и поставит зависимости с проверенными версиями.Запускать
run.bat.
Ограничения и планы
Честно о том, чего пока нет:
AI‑авторазметки — следующий большой шаг: SAM и YOLO в отдельном процессе, чтобы тяжёлые зависимости не попадали в основную программу;
разметки видео — в планах, через нарезку кадров;
проверено только на Windows (PyQt6 кроссплатформенный, но на Linux и macOS я не тестировал).
Ссылки
Каких форматов или функций не хватает в вашей разметке? Пишите в комментариях или в Issues — pull request'ы тоже приветствуются.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.