Почему разметка данных до сих пор всех бесит
Вспомни последний проект по компьютерному зрению. Собрал тысячу изображений? Отлично. Теперь их надо разметить. Ты открываешь CVAT или Label Studio, берешь мышку и начинаешь обводить. Первые десять картинок - нормально. Сотня - уже скучно. Тысяча - хочется выбросить монитор в окно.
Особенно в робототехнике, где нужны специфичные объекты: детали на конвейере, инструменты в цеху, упаковки на складе. Готовых датасетов нет. Собирать руками - ад. Нанимать аутсорс - дорого и долго. А если проект экспериментальный, и нужно быстро проверить гипотезу?
Статистика, от которой плачут: на разметку одного изображения с 5-10 объектами уходит 2-3 минуты. Для 10 000 изображений - это 300-500 часов чистой ручной работы. Месяц полного рабочего времени одного человека.
В моей предыдущей статье "Автоматизация разметки датасетов: от недель работы к нескольким минутам без GPU" я рассказывал про базовые подходы. Сегодня - конкретный пайплайн для сегментации. Работает на обычном CPU, не требует дорогой видеокарты.
Секрет в трех слоях автоматизации
Весь фокус в том, чтобы не размечать с нуля, а использовать то, что уже умеют современные модели. Даже маленькие. Даже медленные. Главное - правильно выстроить процесс.
Наш пайплайн состоит из трех ключевых этапов:
- Сбор и предварительная фильтрация сырых данных
- Автоматическая предварительная разметка lightweight-моделями
- Постобработка и валидация разметки
Звучит просто? Так и есть. Сложность в деталях, которые определяют, будет ли твой датасет мусором или золотом.
1 Где брать данные, если их нет
Первая ошибка - пытаться собрать "идеальный" датасет с первого раза. Не надо. Собери всё, что можешь:
- Скриншоты с камер видеонаблюдения (если есть доступ)
- Фотографии со смартфона в разных условиях освещения
- Публичные датасеты со схожими объектами
- Сгенерированные изображения (Blender, Unity)
Важное правило: собирай в 3-5 раз больше, чем нужно. Потому что 80% отсеется на следующих этапах.
# Структура папок для сбора
raw_data/
├── source_1/ # Данные с первой камеры
│ ├── images/ # Исходные изображения
│ └── metadata.json # Инфо о времени, условиях
├── source_2/ # Данные со смартфона
├── synthetic/ # Сгенерированные данные
└── public/ # Скачанные публичные датасеты
2 Автоматический отсев мусора
Ты собрал 20 000 изображений. Половина - брак: слишком темные, размытые, без целевых объектов. Ручная проверка займет вечность.
Вот что делаем автоматически:
| Фильтр | Что делает | Порог отсева |
|---|---|---|
| Контрастность | Отбрасывает слишком темные/светлые | Нижние 10% |
| Размытие | Детектирует смазанные кадры | Выше 0.3 (Laplacian variance) |
| Дубликаты | Находит почти идентичные | Хэш-расстояние < 5 |
| Размер объекта | Проверяет, что объект достаточно крупный | > 5% площади изображения |
import cv2
import numpy as np
from PIL import Image
import imagehash
def filter_blurry(image_path, threshold=0.3):
"""Отбрасываем размытые изображения"""
image = cv2.imread(image_path)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
fm = cv2.Laplacian(gray, cv2.CV_64F).var()
return fm > threshold # True если не размыто
def filter_duplicate(image_path, seen_hashes, threshold=5):
"""Находим дубликаты через perceptual hash"""
img = Image.open(image_path)
img_hash = imagehash.phash(img)
for seen_hash in seen_hashes:
if img_hash - seen_hash < threshold:
return False # Дубликат
seen_hashes.append(img_hash)
return True # Уникальное
После этой фильтрации из 20 000 останется 8-10 тысяч качественных изображений. Это нормально. Лучше меньше, но лучше.
Магия автоматической разметки на CPU
Вот где начинается самое интересное. Ты мог слышать, что для разметки нужны мощные GPU и огромные модели типа Segment Anything. Неправда.
YOLO-NAS nano весит 4 МБ. Запускается на CPU за 200-300 мс на изображение. Да, она менее точная, чем большие модели. Но нам не нужна идеальная разметка - нужна хорошая основа для ручной доработки.
3 Настраиваем YOLO-NAS для предразметки
Первое: не используй предобученные веса из коробки. Они обучены на COCO - там другие объекты. Нужен transfer learning, даже минимальный.
from super_gradients.training import models
from super_gradients.training import training_hyperparams
# Загружаем tiny модель (самую легкую)
model = models.get('yolo_nas_s',
num_classes=1, # Только наш класс
pretrained_weights='coco')
# Мини-дообучение на 100 ручных примерах
# Это займет 30 минут на CPU
hyperparams = training_hyperparams.get('training_hyperparams')
hyperparams['max_epochs'] = 10
hyperparams['batch_size'] = 8
hyperparams['warmup_initial_lr'] = 0.001
# Тренируем
model.train(...)
После 10 эпох дообучения на 100 ручных примерах модель уже понимает, что мы от нее хотим. Точность будет 60-70% mAP. Для предразметки - достаточно.
Не пытайся добиться 90% точности на этом этапе. Это предразметка, а не финальная модель. Цель - сократить ручную работу в 10 раз, а не заменить её полностью.
4 Пайплайн разметки: от изображения к маске
Вот полный скрипт, который запускается на CPU и обрабатывает тысячу изображений за ночь:
import os
from tqdm import tqdm
import cv2
import json
class AutoSegmentationPipeline:
def __init__(self, model_path):
self.model = self.load_model(model_path)
self.results_dir = "auto_annotations"
os.makedirs(self.results_dir, exist_ok=True)
def process_folder(self, images_folder):
"""Обрабатываем всю папку с изображениями"""
image_files = [f for f in os.listdir(images_folder)
if f.endswith(('.jpg', '.png', '.jpeg'))]
for filename in tqdm(image_files, desc="Processing"):
image_path = os.path.join(images_folder, filename)
# Пропускаем если уже обработано
json_path = os.path.join(self.results_dir,
filename.replace('.jpg', '.json'))
if os.path.exists(json_path):
continue
# Детекция
predictions = self.model.predict(image_path)
# Конвертируем в COCO формат
annotations = self.predictions_to_coco(predictions, filename)
# Сохраняем
with open(json_path, 'w') as f:
json.dump(annotations, f, indent=2)
# Генерируем предпросмотр с bounding boxes
self.save_preview(image_path, predictions, filename)
def predictions_to_coco(self, predictions, image_id):
"""Конвертируем предсказания в COCO формат"""
annotations = []
for i, pred in enumerate(predictions):
if pred.confidence > 0.3: # Порог уверенности
annotation = {
"id": i,
"image_id": image_id,
"category_id": 1,
"bbox": pred.bbox.tolist(),
"segmentation": self.bbox_to_polygon(pred.bbox),
"area": (pred.bbox[2] * pred.bbox[3]),
"iscrowd": 0
}
annotations.append(annotation)
return annotations
def bbox_to_polygon(self, bbox):
"""Преобразуем bounding box в полигон (пока упрощенно)"""
x, y, w, h = bbox
# Простой прямоугольный полигон
return [[x, y, x+w, y, x+w, y+h, x, y+h]]
Этот скрипт за ночь на 8-ядерном процессоре обработает 5-8 тысяч изображений. На выходе - JSON файлы с разметкой в COCO формате.
Постобработка: превращаем сырую разметку в чистый датасет
Автоматическая разметка сделала 70% работы. Остальные 30% - самая важная часть. Потому что если их пропустить, получится мусор.
5 Фильтрация плохих предсказаний
Модель иногда глючит: рисует bounding boxes на пустом месте, путает объекты, делает слишком маленькие или слишком большие боксы.
def filter_bad_predictions(annotations, image_width, image_height):
"""Фильтруем явно ошибочные предсказания"""
good_annotations = []
for ann in annotations:
bbox = ann["bbox"] # [x, y, width, height]
# 1. Слишком маленькие объекты (< 0.5% изображения)
area_ratio = (bbox[2] * bbox[3]) / (image_width * image_height)
if area_ratio < 0.005:
continue
# 2. Слишком большие (> 80% изображения)
if area_ratio > 0.8:
continue
# 3. Выходят за границы изображения
if (bbox[0] < -10 or bbox[1] < -10 or
bbox[0] + bbox[2] > image_width + 10 or
bbox[1] + bbox[3] > image_height + 10):
continue
# 4. Неверное соотношение сторон (вероятно ошибка)
aspect_ratio = bbox[2] / bbox[3]
if aspect_ratio > 10 or aspect_ratio < 0.1:
continue
good_annotations.append(ann)
return good_annotations
Эта фильтрация убирает 20-30% мусорных предсказаний. Автоматически.
6 Интеллектуальная ручная проверка
Теперь нужно проверить то, что осталось. Но не всё подряд. Используем приоритизацию:
| Приоритет | Критерий | Что проверять |
|---|---|---|
| Высокий | Низкая уверенность модели | Confidence 0.3-0.6 |
| Высокий | Много объектов на изображении | > 5 bounding boxes |
| Средний | Сложный фон | Высокая энтропия текстуры |
| Низкий | Высокая уверенность, один объект | Confidence > 0.8, 1-2 boxes |
Сначала проверяем высокоприоритетные - там больше всего ошибок. Низкоприоритетные часто можно принять как есть.
Интеграция в Label Studio: полуавтоматическая доработка
Label Studio - не просто инструмент для ручной разметки. Это платформа для human-in-the-loop. Мы загружаем туда автоматически сгенерированные разметки, а человек только правит ошибки.
import label_studio_sdk
# Подключаемся к Label Studio
ls = label_studio_sdk.Client('http://localhost:8080',
api_key='your-api-key')
project = ls.start_project(
title='Auto-segmentation review',
label_config='''
'''
)
# Импортируем задачи с предразметкой
for json_file in auto_annotation_files:
with open(json_file) as f:
annotation = json.load(f)
task = {
"data": {
"image": f"/data/{annotation['image_id']}"
},
"predictions": [{
"model_version": "yolo_nas_auto",
"result": annotation["segmentation"]
}]
}
project.import_tasks([task])
Теперь разметчики видят уже готовые полигоны. Их задача - поправить границы, удалить ложные срабатывания, добавить пропущенные объекты. Вместо 3 минут на изображение - 30 секунд.
Не экономь на этом этапе. Даже 10% ручной проверки повышают качество датасета в 2 раза. Как в продакшн-готовых агентах, важен баланс автоматизации и человеческого контроля.
Ошибки, которые все совершают (и как их избежать)
Я видел десятки попыток автоматизировать разметку. 90% проваливаются из-за одних и тех же ошибок.
Ошибка 1: Жадность
Пытаются разметить всё и сразу. Собирают 50 000 изображений, запускают пайплайн, через неделю получают гору мусора. Разочаровываются.
Правильно: начать с 1000 изображений. Настроить пайплайн, проверить качество. Потом масштабировать.
Ошибка 2: Перфекционизм
Хотят, чтобы автоматическая разметка была идеальной. Трава не должна пробиваться за контур объекта. Тень должна быть исключена из маски. Тратят месяцы на доработку алгоритмов.
Правильно: приемлемое качество + быстрая ручная правка. 80/20 правило.
Ошибка 3: Игнорирование class imbalance
В робототехнике часто: 95% кадров - конвейер пустой, 5% - с деталями. Если не балансировать, модель научится только детектировать "пустоту".
Правильно: oversampling редких классов на этапе сбора данных.
# Балансировка датасета
def balance_dataset(image_files, target_ratio=0.3):
"""Добиваемся, чтобы целевые объекты были в target_ratio изображений"""
# Определяем, есть ли объекты на изображении
has_objects = []
no_objects = []
for img_file in image_files:
if has_object(img_file): # Детектим хотя бы один объект
has_objects.append(img_file)
else:
no_objects.append(img_file)
# Рассчитываем, сколько "пустых" оставить
current_ratio = len(has_objects) / len(image_files)
if current_ratio < target_ratio:
# Нужно добавить больше с объектами
# (ищем дополнительные данные)
pass
else:
# Отбираем подмножество без объектов
keep_count = int(len(has_objects) * (1-target_ratio) / target_ratio)
no_objects = np.random.choice(no_objects, keep_count, replace=False)
return list(has_objects) + list(no_objects)
Сколько времени это реально занимает
Давай посчитаем на примере датасета в 10 000 изображений:
| Этап | Ручной способ | Наш пайплайн | Экономия |
|---|---|---|---|
| Сбор данных | 3-5 дней | 2-3 дня | 30% |
| Фильтрация | Не делается (или неделя) | 4 часа (авто) | 95% |
| Разметка | 300-500 часов | 50 часов (80% авто) | 85% |
| Проверка | 50-100 часов | 20 часов (приоритетная) | 60% |
| ИТОГО | 1-2 месяца | 3-5 дней | 90-95% |
Из 2 месяцев в 5 дней. На обычном процессоре. Без GPU. Без тысяч долларов на аутсорс.
Что дальше? Итеративное улучшение
Самый крутой трюк, о котором мало кто говорит: твой первый датасет - это не конец, а начало.
1. Тренируешь модель на автоматически размеченных данных
2. Запускаешь её на новых данных
3. Собираешь случаи, где модель ошибается
4. Добавляешь их в датасет и переразмечаешь
5. Повторяешь
Каждая итерация улучшает и модель, и качество разметки. Через 3-4 цикла у тебя будет датасет, который не отличить от ручного. Но потратишь ты не месяцы, а недели.
Самый частый вопрос: "А если у меня специфичные объекты, которых нет в COCO?"
Ответ: неважно. Даже на 100 ручных примерах YOLO-NAS nano дообучается за час. Потом используешь её для разметки следующих 1000. Это как замена ETL-конвейера на агентов - начинаешь с малого, потом система масштабируется сама.
В следующий раз, когда услышишь "нам нужен датасет, но размечать некому", покажи эту статью. Пайплайн работает. Проверено на робототехнических проектах, медицинских изображениях, сельском хозяйстве.
CPU - не приговор. Это просто ещё один параметр, который нужно учитывать. Как время, бюджет или качество данных.