Computer Vision Engineer: от детекции и Doc-AI до продакшнаДанные и разметкаСбор данных и схема классов

Сбор данных и схема классов

Уроки курсаСбор данных и схема классов

Практика: подготовить data.yaml и валидировать пути

Собрали 300 изображений — теперь создаём data.yaml. Этот файл описывает датасет: пути к train/val/test, число классов, их имена. Без него фреймворк (YOLO, Detectron2) не знает, где искать данные. Валидация проверяет, что все пути существуют, нет опечаток, для каждого изображения есть аннотация. Один битый путь — обучение упадёт через час с ошибкой.

Ключевые идеи

  • data.yaml содержит: пути (train/val/test), nc (число классов), names (имена классов).
  • Пути относительные или абсолютные — лучше относительные (проект переносимый).
  • Валидация: все ли пути существуют, есть ли аннотации для изображений, корректны ли class_id.
  • Скрипт валидации ловит проблемы до обучения — экономит часы отладки.

Структура data.yaml

Минимальная версия:

# Пути к данным (относительно data.yaml)
train: images/train
val: images/val
test: images/test  # опционально

# Число классов
nc: 3

# Имена классов (индекс → имя)
names: ['car', 'person', 'dog']

Или с явным указанием индексов:

train: images/train
val: images/val
nc: 3
names:
  0: car
  1: person
  2: dog

Оба варианта работают одинаково, первый короче.

Скрипт создания data.yaml

import yaml
from pathlib import Path

def create_data_yaml(
    train_dir: str,
    val_dir: str,
    test_dir: str,
    class_names: list,
    output_path: str = 'data.yaml'
):
    """Создаёт data.yaml для датасета."""
    
    data = {
        'train': train_dir,
        'val': val_dir,
        'nc': len(class_names),
        'names': class_names
    }
    
    # Добавляем test, если указан
    if test_dir:
        data['test'] = test_dir
    
    # Сохраняем
    with open(output_path, 'w') as f:
        yaml.dump(data, f, default_flow_style=False, sort_keys=False)
    
    print(f"✓ Создан: {output_path}")
    print(f"  Train: {train_dir}")
    print(f"  Val: {val_dir}")
    print(f"  Classes: {class_names}")

# Пример использования
create_data_yaml(
    train_dir='data/images/train',
    val_dir='data/images/val',
    test_dir='data/images/test',
    class_names=['car', 'person', 'dog'],
    output_path='data/data.yaml'
)

Результат (`data/data.yaml`):

train: data/images/train
val: data/images/val
test: data/images/test
nc: 3
names:
- car
- person
- dog

Скрипт валидации путей

Проверяем, что все пути существуют, для каждого изображения есть аннотация:

import yaml
from pathlib import Path

def validate_dataset(data_yaml_path: str):
    """Валидирует датасет: пути, аннотации, class_id."""
    
    print(f"Валидация датасета: {data_yaml_path}")
    
    # Загружаем data.yaml
    with open(data_yaml_path) as f:
        data = yaml.safe_load(f)
    
    # Путь к data.yaml для разрешения относительных путей
    yaml_dir = Path(data_yaml_path).parent
    
    errors = []
    warnings = []
    
    # Проверяем поля
    required_fields = ['train', 'val', 'nc', 'names']
    for field in required_fields:
        if field not in data:
            errors.append(f"Отсутствует поле '{field}' в data.yaml")
    
    if errors:
        print("❌ Критические ошибки:")
        for err in errors:
            print(f"  {err}")
        return False
    
    # Проверяем число классов
    if data['nc'] != len(data['names']):
        errors.append(f"nc={data['nc']}, но names содержит {len(data['names'])} классов")
    
    # Проверяем пути к данным
    for split in ['train', 'val', 'test']:
        if split not in data:
            continue
        
        # Разрешаем относительный путь
        images_dir = yaml_dir / data[split]
        labels_dir = yaml_dir / data[split].replace('images', 'labels')
        
        print(f"\nПроверка {split}:")
        print(f"  Images: {images_dir}")
        print(f"  Labels: {labels_dir}")
        
        # Проверяем существование папок
        if not images_dir.exists():
            errors.append(f"{split}: папка не найдена - {images_dir}")
            continue
        
        if not labels_dir.exists():
            errors.append(f"{split}: папка labels не найдена - {labels_dir}")
            continue
        
        # Проверяем соответствие изображений и аннотаций
        image_files = list(images_dir.glob('*.jpg')) + list(images_dir.glob('*.png'))
        label_files = {f.stem: f for f in labels_dir.glob('*.txt')}
        
        print(f"  Изображений: {len(image_files)}")
        print(f"  Аннотаций: {len(label_files)}")
        
        missing_labels = []
        invalid_annotations = []
        
        for img_path in image_files:
            # Проверяем наличие аннотации
            if img_path.stem not in label_files:
                missing_labels.append(img_path.name)
                continue
            
            # Проверяем корректность аннотации
            label_path = label_files[img_path.stem]
            try:
                with open(label_path) as f:
                    for line_num, line in enumerate(f, 1):
                        parts = line.strip().split()
                        
                        if len(parts) != 5:
                            invalid_annotations.append(
                                f"{label_path.name}:{line_num} - неверное число значений ({len(parts)} вместо 5)"
                            )
                            continue
                        
                        class_id, x, y, w, h = map(float, parts)
                        
                        # Проверяем class_id
                        if not (0 <= class_id < data['nc']):
                            invalid_annotations.append(
                                f"{label_path.name}:{line_num} - class_id={int(class_id)} вне диапазона [0, {data['nc']-1}]"
                            )
                        
                        # Проверяем координаты
                        if not (0 <= x <= 1 and 0 <= y <= 1 and 0 < w <= 1 and 0 < h <= 1):
                            invalid_annotations.append(
                                f"{label_path.name}:{line_num} - координаты вне диапазона [0,1]"
                            )
                            
            except Exception as e:
                invalid_annotations.append(f"{label_path.name} - ошибка чтения: {e}")
        
        # Выводим результаты
        if missing_labels:
            warnings.append(f"{split}: {len(missing_labels)} изображений без аннотаций")
            print(f"  ⚠ Без аннотаций: {len(missing_labels)}")
            for name in missing_labels[:5]:
                print(f"    - {name}")
            if len(missing_labels) > 5:
                print(f"    ... и ещё {len(missing_labels) - 5}")
        
        if invalid_annotations:
            errors.extend(invalid_annotations[:10])  # Первые 10
    
    # Итоговый отчёт
    print("\n" + "="*50)
    if errors:
        print("❌ Найдены ошибки:")
        for err in errors:
            print(f"  {err}")
    
    if warnings:
        print("\n⚠ Предупреждения:")
        for warn in warnings:
            print(f"  {warn}")
    
    if not errors and not warnings:
        print("✅ Датасет валиден!")
        return True
    
    return len(errors) == 0

# Использование
validate_dataset('data/data.yaml')

Вывод:

Валидация датасета: data/data.yaml

Проверка train:
  Images: data/images/train
  Labels: data/labels/train
  Изображений: 240
  Аннотаций: 240

Проверка val:
  Images: data/images/val
  Labels: data/labels/val
  Изображений: 60
  Аннотаций: 58
  ⚠ Без аннотаций: 2
    - image_045.jpg
    - image_089.jpg

==================================================
⚠ Предупреждения:
  val: 2 изображений без аннотаций

Автоматическое создание недостающих файлов

Если изображение без аннотации (нет объектов), создаём пустой .txt:

def create_missing_labels(images_dir, labels_dir):
    """Создаёт пустые .txt для изображений без аннотаций."""
    images_dir = Path(images_dir)
    labels_dir = Path(labels_dir)
    labels_dir.mkdir(parents=True, exist_ok=True)
    
    image_files = list(images_dir.glob('*.jpg')) + list(images_dir.glob('*.png'))
    existing_labels = {f.stem for f in labels_dir.glob('*.txt')}
    
    created = 0
    for img_path in image_files:
        if img_path.stem not in existing_labels:
            label_path = labels_dir / f"{img_path.stem}.txt"
            label_path.touch()  # Создаём пустой файл
            created += 1
    
    print(f"Создано {created} пустых аннотаций")

create_missing_labels('data/images/val', 'data/labels/val')

Частые проблемы

  1. Относительные пути не работают: data.yaml лежит не там, где ожидается. Убедитесь, что пути разрешаются относительно data.yaml.
  2. Разные названия папок: `images/train`, но аннотации в `annotations/train`. Скрипт ищет `labels/train`. Переименуйте или поправьте логику.
  3. class_id начинается с 1: COCO использует id с 1, YOLO с 0. При конвертации вычитайте 1.
  4. Забыли test split: в data.yaml нет test — нормально для экспериментов, но для финальной оценки нужен.
  5. nc не совпадает с names: nc=3, но names=['car', 'person'] (2 класса). Исправьте одно из значений.

Связанные понятия

  • YAML: человеко-читаемый формат конфигов. Строгий к отступам (как Python).
  • Относительные vs абсолютные пути: относительные переносимы, абсолютные привязаны к машине.
  • Sanity check: быстрая проверка данных перед длительным обучением.

Итоги

  • data.yaml описывает датасет: пути (train/val/test), nc, names.
  • Скрипт создания: генерирует data.yaml из путей и списка классов.
  • Валидация: проверяет пути, соответствие изображений и аннотаций, корректность class_id и координат.
  • Пустые аннотации: если объектов нет, создаём пустой .txt (не удаляем файл).
  • Запускаем валидацию перед обучением — ловит ошибки за секунды, экономит часы.