Аугментация изображений для обучения CV-моделей
Датасет из 800 изображений стал датасетом из 24 000 — звучит красиво, но если аугментации неправильные, модель просто переобучится на артефакты самих трансформаций. Ключевой принцип: аугментации должны моделировать реальные вариации в production-данных, а не просто раздувать датасет.
Albumentations — основной инструмент
import albumentations as A
from albumentations.pytorch import ToTensorV2
def build_augmentation_pipeline(
task: str = 'detection', # 'classification' | 'detection' | 'segmentation'
domain: str = 'outdoor' # 'outdoor' | 'indoor' | 'medical' | 'satellite'
) -> A.Compose:
"""
Политика аугментации зависит от домена.
Outdoor — агрессивные изменения освещения и погоды.
Medical — только геометрические, цветовые противопоказаны.
"""
geometric = [
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(
shift_limit=0.05 if task == 'detection' else 0.1,
scale_limit=0.1,
rotate_limit=15,
border_mode=0, # constant padding
p=0.5
),
]
photometric_outdoor = [
A.RandomBrightnessContrast(
brightness_limit=0.3, contrast_limit=0.3, p=0.6
),
A.HueSaturationValue(
hue_shift_limit=15, sat_shift_limit=40, val_shift_limit=30,
p=0.4
),
A.RandomFog(fog_coef_lower=0.1, fog_coef_upper=0.35, p=0.15),
A.RandomRain(
slant_lower=-10, slant_upper=10,
drop_length=15, drop_width=1,
drop_color=(200, 200, 200), blur_value=2,
brightness_coefficient=0.85, p=0.1
),
A.RandomSunFlare(
flare_roi=(0, 0, 1, 0.5), angle_lower=0,
num_flare_circles_lower=3, num_flare_circles_upper=6,
src_radius=200, p=0.08
),
]
photometric_medical = [
# Для медицинских снимков — только contrast/gamma
A.RandomGamma(gamma_limit=(80, 120), p=0.4),
A.CLAHE(clip_limit=3.0, tile_grid_size=(8, 8), p=0.3),
]
photometric = (
photometric_outdoor if domain == 'outdoor'
else photometric_medical
)
noise = [
A.GaussNoise(var_limit=(10, 50), p=0.3),
A.ImageCompression(quality_lower=75, quality_upper=100, p=0.2),
A.Blur(blur_limit=3, p=0.1),
]
bbox_params = (
A.BboxParams(
format='yolo',
label_fields=['class_labels'],
min_visibility=0.3
)
if task == 'detection' else None
)
transforms = geometric + photometric + noise + [
A.Normalize(
mean=(0.485, 0.456, 0.406),
std=(0.229, 0.224, 0.225)
),
ToTensorV2()
]
return A.Compose(transforms, bbox_params=bbox_params)
MixUp и CutMix для регуляризации
Простые аугментации не убирают переобучение при малом датасете. MixUp и CutMix создают новые примеры смешением двух изображений — это существенная регуляризация:
import torch
import numpy as np
def mixup_batch(
images: torch.Tensor, # (B, C, H, W)
labels: torch.Tensor, # (B,) для классификации
alpha: float = 0.4
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]:
"""
MixUp: pixel-level смешение двух изображений.
Требует soft labels при вычислении лосса.
"""
lam = np.random.beta(alpha, alpha)
batch_size = images.size(0)
perm = torch.randperm(batch_size)
mixed_images = lam * images + (1 - lam) * images[perm]
labels_a = labels
labels_b = labels[perm]
return mixed_images, labels_a, labels_b, lam
def cutmix_batch(
images: torch.Tensor,
labels: torch.Tensor,
alpha: float = 1.0
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]:
"""
CutMix: вырезаем регион из одного изображения и вставляем в другое.
Лучше MixUp для задач с локальными признаками.
"""
lam = np.random.beta(alpha, alpha)
batch_size, _, H, W = images.shape
perm = torch.randperm(batch_size)
# Размер вырезаемого прямоугольника
cut_ratio = np.sqrt(1 - lam)
cut_h = int(H * cut_ratio)
cut_w = int(W * cut_ratio)
cx = np.random.randint(W)
cy = np.random.randint(H)
x1 = max(cx - cut_w // 2, 0)
y1 = max(cy - cut_h // 2, 0)
x2 = min(cx + cut_w // 2, W)
y2 = min(cy + cut_h // 2, H)
mixed = images.clone()
mixed[:, :, y1:y2, x1:x2] = images[perm, :, y1:y2, x1:x2]
# Фактический lam с учётом clipping
lam = 1 - (y2-y1) * (x2-x1) / (H * W)
return mixed, labels, labels[perm], lam
# Использование в training loop
def mixup_criterion(criterion, pred, y_a, y_b, lam):
return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)
Синтетические данные через Stable Diffusion
Когда реальных данных катастрофически мало (менее 100 примеров класса), синтетика через inpainting или ControlNet даёт прирост AP на 8–15%:
from diffusers import StableDiffusionInpaintPipeline
import torch
from PIL import Image
pipe = StableDiffusionInpaintPipeline.from_pretrained(
'runwayml/stable-diffusion-inpainting',
torch_dtype=torch.float16
).to('cuda')
def generate_synthetic_defect(
background_image: Image.Image,
mask_image: Image.Image, # где генерировать дефект
defect_type: str = 'surface crack'
) -> Image.Image:
prompt = (
f'industrial {defect_type}, high resolution, '
f'realistic texture, macro photography'
)
result = pipe(
prompt=prompt,
image=background_image,
mask_image=mask_image,
num_inference_steps=30,
guidance_scale=7.5,
strength=0.85
).images[0]
return result
На практике: 80 реальных примеров трещин + 400 синтетических подняли recall с 0.61 до 0.78 при сохранении precision > 0.80.
Сравнение стратегий аугментации
| Стратегия | Прирост accuracy | Вычислительная стоимость | Применимость |
|---|---|---|---|
| Базовые геометрические | +3–7% | Минимальная | Всегда |
| Фотометрические | +5–12% | Минимальная | Не для medical |
| MixUp / CutMix | +4–10% | Минимальная | Классификация |
| Mosaic (YOLO) | +7–15% | Низкая | Детекция мелких объектов |
| AutoAugment / RandAugment | +5–12% | Средняя | Общий случай |
| Синтетика (SD/ControlNet) | +8–20% | Высокая | Дефицит данных |
Сроки
| Работа | Срок |
|---|---|
| Настройка pipeline аугментации для задачи | 1–2 недели |
| Генерация синтетических данных (500–2000 примеров) | 2–4 недели |
| Полный эксперимент: baseline → аугментации → синтетика | 4–6 недель |







