import torch import torchvision.transforms.v2 as v2 import numpy as np from PIL import Image import albumentations as A from config import Config class Transform: """Unified image transform pipeline for both train and validation. Pass ``train=True`` for augmented training transforms, ``train=False`` for deterministic validation transforms. """ def __init__(self, train: bool = True): self._train = train img_size = Config.get_model_config()['image_size'] aug = Config.get_augmentation_config() if train: # albumentations: only for transforms without a torchvision.v2 equivalent self._alb = A.Compose([ A.CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), p=0.3), A.OneOf([ A.GaussianBlur(blur_limit=(3, 7), p=0.5), A.MedianBlur(blur_limit=5, p=0.5), # no torchvision equivalent ], p=0.3), A.RandomRotate90(p=0.5), # snaps to 0/90/180/270°; v2 has no equivalent ]) self._tv = v2.Compose([ v2.Resize((img_size, img_size)), v2.RandomHorizontalFlip(p=aug.get('horizontal_flip_prob', 0.5)), v2.RandomVerticalFlip(p=aug.get('vertical_flip_prob', 0.5)), v2.RandomAffine( degrees=aug.get('rotation', 20), translate=(0.0625, 0.0625), scale=(0.85, 1.15), ), v2.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.25, hue=0.083), v2.ToImage(), v2.ToDtype(torch.float32, scale=True), v2.RandomErasing(p=0.5, scale=(0.004, 0.016), ratio=(0.3, 3.3), value=0), v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) else: self._alb = None self._tv = v2.Compose([ v2.Resize((img_size, img_size)), v2.ToImage(), v2.ToDtype(torch.float32, scale=True), v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) def __call__(self, pil_img): if self._train: assert self._alb is not None pil_img = Image.fromarray(self._alb(image=np.array(pil_img))['image']) return self._tv(pil_img) # TTA transforms applied to PIL images before the standard pipeline. # Image.Transpose enum avoids the deprecated integer constants. tta_transforms = { 'original': lambda img: img, 'hflip': lambda img: img.transpose(Image.Transpose.FLIP_LEFT_RIGHT), 'vflip': lambda img: img.transpose(Image.Transpose.FLIP_TOP_BOTTOM), 'rot90': lambda img: img.rotate(90, expand=False), 'rot180': lambda img: img.rotate(180, expand=False), 'rot270': lambda img: img.rotate(270, expand=False), }