Spaces:
Sleeping
Sleeping
File size: 5,560 Bytes
58027ad 2ea8a2d 58027ad 2ea8a2d 58027ad fa0ce5b 2ea8a2d 58027ad 2ea8a2d 58027ad 2ea8a2d 58027ad fa0ce5b 2ea8a2d 58027ad fa0ce5b 2ea8a2d fa0ce5b 58027ad 2ea8a2d fa0ce5b 58027ad fa0ce5b 58027ad | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | # src/data/augmentor.py
# Augmentation pipelines for SmartVision CNN training.
#
# Pipeline design principles:
#
# PIL-space first (indices 0-4, before ToImage at index 5):
# Geometric/color transforms on PIL input. PIL handles uint8 arithmetic
# and boundary conditions correctly. Cost is CPU-bound but negligible
# at 224x224 images.
#
# Float tensor space (indices 7-9, after ToDtype at index 6):
# RandomZoomOut requires float32 input with fill values in [0.0, 1.0].
# fill=IMAGENET_MEAN: after Normalize, padded pixels become exactly 0.0.
# Proof: (IMAGENET_MEAN - IMAGENET_MEAN) / IMAGENET_STD = [0, 0, 0]
# fill=0.0: (0 - mean) / std = [-2.12, -2.04, -1.80] after Normalize (wrong)
# fill=0.5: gives [+0.065, +0.196, +0.418] per channel (wrong, colored border)
#
# RandomPerspective border artifact (accepted tradeoff):
# PIL RandomPerspective default fill=0 -> black border in uint8.
# After full pipeline: (0/255 - mean) / std ~= [-2.12, -2.04, -1.80].
# At distortion_scale=0.2, p=0.3: border <5% of pixels on 30% of samples.
# Net gradient impact is small. Mitigation if needed: move after ToDtype
# and set fill=IMAGENET_MEAN.
#
# Why RandomGrayscale(p=0.05):
# COCO household items (cup/bowl/bottle) are confused via memorized color
# patterns from specific training images. 5% grayscale forces color-agnostic
# feature learning occasionally without degrading vehicle/animal color signal.
#
# Why RandomPerspective (not RandomErasing):
# COCO ground-truth boxes reflect real camera perspective variation.
# Perspective augmentation models this directly.
# RandomErasing destroys local texture patches -- the primary discriminator
# for fine-grained categories. Do not add it.
#
# Round 3 changes from Round 2:
# RandomRotation: 15 -> 20 degrees
# ColorJitter brightness/contrast: 0.2 -> 0.3
# ColorJitter saturation: 0.1 -> 0.2
# RandomGrayscale(p=0.05): ADDED (PIL-space)
# RandomPerspective(distortion_scale=0.2, p=0.3): ADDED (PIL-space)
# RandomZoomOut side_range: (1.0, 1.3) -> (1.0, 1.4)
import torch
import torchvision.transforms.v2 as T
IMAGENET_MEAN: list[float] = [0.485, 0.456, 0.406]
IMAGENET_STD: list[float] = [0.229, 0.224, 0.225]
def get_train_transforms(image_size: int = 224) -> T.Compose:
"""
Training augmentation pipeline -- 10 transforms.
Ordered execution (order is load-bearing):
[0] RandomHorizontalFlip(p=0.5) PIL geometric
[1] RandomRotation(degrees=20) PIL geometric
[2] ColorJitter(0.3, 0.3, 0.2, 0.05) PIL color
[3] RandomGrayscale(p=0.05) PIL color NEW Round 3
[4] RandomPerspective(scale=0.2, p=0.3) PIL geometric NEW Round 3
[5] ToImage() PIL -> uint8 CHW tensor
[6] ToDtype(torch.float32, scale=True) uint8 -> float32 [0, 1]
[7] RandomZoomOut(fill=IMAGENET_MEAN, ...) float32 tensor
[8] Resize((image_size, image_size))
[9] Normalize(IMAGENET_MEAN, IMAGENET_STD)
Hard ordering constraints:
[3], [4] < [5]: PIL ops must run before ToImage conversion
[6] < [7]: ZoomOut fill is float32 [0, 1] space
[9] = last: Normalize must be final transform
"""
return T.Compose(
[
# PIL-space geometric
T.RandomHorizontalFlip(p=0.5),
T.RandomRotation(degrees=20),
# PIL-space color
T.ColorJitter(
brightness=0.3,
contrast=0.3,
saturation=0.2,
hue=0.05,
),
T.RandomGrayscale(p=0.05),
# PIL-space geometric (perspective)
# fill=0 default: black border in uint8 -> ~[-2.12,-2.04,-1.80] after Normalize.
# Accepted: borders are small (<5% of pixels) and infrequent (p=0.3).
T.RandomPerspective(distortion_scale=0.2, p=0.3),
# Convert to float32 tensor
T.ToImage(),
T.ToDtype(torch.float32, scale=True),
# Float32 tensor augmentation
# fill=IMAGENET_MEAN: padded pixels normalize to 0.0 per channel.
T.RandomZoomOut(
fill=IMAGENET_MEAN,
side_range=(1.0, 1.4),
p=0.3,
),
# Finalize (single resize after all spatial augmentation)
T.Resize((image_size, image_size), antialias=True),
# Normalize last
T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
]
)
def get_eval_transforms(image_size: int = 224) -> T.Compose:
"""
Evaluation/test pipeline -- fully deterministic.
Matches inference-time preprocessing exactly.
Contains zero stochastic transforms.
"""
return T.Compose(
[
T.ToImage(),
T.ToDtype(torch.float32, scale=True),
T.Resize((image_size, image_size), antialias=True),
T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
]
)
def denormalize(tensor: torch.Tensor) -> "np.ndarray": # noqa: F821
"""Reverse ImageNet normalization. Returns HWC uint8 numpy array [0, 255]."""
import numpy as np
mean = torch.tensor(IMAGENET_MEAN, dtype=torch.float32).view(3, 1, 1)
std = torch.tensor(IMAGENET_STD, dtype=torch.float32).view(3, 1, 1)
img = tensor.clone().cpu() * std + mean
return (img.clamp(0.0, 1.0).permute(1, 2, 0).numpy() * 255).astype(np.uint8)
|