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')
Частые проблемы
- Относительные пути не работают: data.yaml лежит не там, где ожидается. Убедитесь, что пути разрешаются относительно data.yaml.
- Разные названия папок: `images/train`, но аннотации в `annotations/train`. Скрипт ищет `labels/train`. Переименуйте или поправьте логику.
- class_id начинается с 1: COCO использует id с 1, YOLO с 0. При конвертации вычитайте 1.
- Забыли test split: в data.yaml нет test — нормально для экспериментов, но для финальной оценки нужен.
- 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 (не удаляем файл).
- Запускаем валидацию перед обучением — ловит ошибки за секунды, экономит часы.
