| from torchvision import transforms |
| import torch |
| from typing import Tuple, Dict, Optional |
| import numpy as np |
|
|
| |
| from ..models.model_configs import AUGMENTATION_CONFIGS, TRAINING_CONFIGS |
|
|
|
|
| def get_train_transforms( |
| input_size: int = 224, |
| augmentation_level: str = 'medium' |
| ) -> transforms.Compose: |
| """ |
| Get training data augmentation pipeline |
| |
| Args: |
| input_size: Target image size |
| augmentation_level: Augmentation strength ('light', 'medium', 'heavy') |
| |
| Returns: |
| Composed transform pipeline |
| """ |
| |
| aug_params = AUGMENTATION_CONFIGS.get(augmentation_level, AUGMENTATION_CONFIGS['medium']) |
|
|
| transform_list = [ |
| |
| transforms.RandomResizedCrop( |
| input_size, |
| scale=aug_params['scale'], |
| ratio=(0.9, 1.1), |
| ), |
|
|
| |
| transforms.RandomHorizontalFlip(p=aug_params['h_flip_p']), |
|
|
| |
| transforms.RandomRotation(degrees=aug_params['rotation']), |
|
|
| |
| transforms.ColorJitter( |
| brightness=aug_params['brightness'], |
| contrast=aug_params['contrast'], |
| saturation=aug_params['saturation'], |
| hue=aug_params['hue'] |
| ), |
|
|
| |
| transforms.RandomPerspective( |
| distortion_scale=aug_params['perspective'], |
| p=0.3 |
| ), |
|
|
| |
| transforms.ToTensor(), |
| transforms.Normalize( |
| mean=[0.485, 0.456, 0.406], |
| std=[0.229, 0.224, 0.225] |
| ) |
| ] |
|
|
| return transforms.Compose(transform_list) |
|
|
|
|
| def get_val_transforms(input_size: int = 224) -> transforms.Compose: |
| """ |
| Get validation/test transforms (no augmentation) |
| |
| Args: |
| input_size: Target image size |
| |
| Returns: |
| Composed transform pipeline |
| """ |
| return transforms.Compose([ |
| |
| transforms.Resize(int(input_size * 1.14)), |
| transforms.CenterCrop(input_size), |
|
|
| |
| transforms.ToTensor(), |
| transforms.Normalize( |
| mean=[0.485, 0.456, 0.406], |
| std=[0.229, 0.224, 0.225] |
| ) |
| ]) |
|
|
|
|
| def get_inference_transforms(input_size: int = 224) -> transforms.Compose: |
| """ |
| Get inference transforms (same as validation) |
| """ |
| return get_val_transforms(input_size) |
|
|
|
|
| def get_transforms_for_model( |
| model_name: str, |
| is_training: bool = True, |
| augmentation_level: str = 'medium' |
| ) -> transforms.Compose: |
| """ |
| Get appropriate transforms for a specific model |
| |
| Args: |
| model_name: Name of the model |
| is_training: Whether to include augmentations |
| augmentation_level: Strength of augmentations for training |
| |
| Returns: |
| Transform pipeline |
| """ |
| |
| config = TRAINING_CONFIGS.get(model_name, TRAINING_CONFIGS['resnet50']) |
| input_size = config['input_size'] |
|
|
| if is_training: |
| return get_train_transforms(input_size, augmentation_level) |
| else: |
| return get_val_transforms(input_size) |
|
|
|
|
| class MixUpTransform: |
| """ |
| MixUp augmentation for training |
| Reference: https://arxiv.org/abs/1710.09412 |
| """ |
|
|
| def __init__(self, alpha: float = 1.0, num_classes: int = 5): |
| self.alpha = alpha |
| self.num_classes = num_classes |
|
|
| def __call__( |
| self, |
| images: torch.Tensor, |
| labels: torch.Tensor |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]: |
| """ |
| Apply MixUp to a batch |
| |
| Args: |
| images: Batch of images (B, C, H, W) |
| labels: Batch of labels (B,) |
| |
| Returns: |
| mixed_images, labels_a, labels_b, lam |
| """ |
| batch_size = images.size(0) |
|
|
| |
| if self.alpha > 0: |
| lam = np.random.beta(self.alpha, self.alpha) |
| else: |
| lam = 1 |
|
|
| |
| index = torch.randperm(batch_size).to(images.device) |
|
|
| |
| mixed_images = lam * images + (1 - lam) * images[index] |
|
|
| |
| labels_a, labels_b = labels, labels[index] |
|
|
| return mixed_images, labels_a, labels_b, lam |
|
|
|
|
| class CutMixTransform: |
| """ |
| CutMix augmentation for training |
| Reference: https://arxiv.org/abs/1905.04899 |
| """ |
|
|
| def __init__(self, alpha: float = 1.0, num_classes: int = 5): |
| self.alpha = alpha |
| self.num_classes = num_classes |
|
|
| def __call__( |
| self, |
| images: torch.Tensor, |
| labels: torch.Tensor |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]: |
| """ |
| Apply CutMix to a batch |
| """ |
| batch_size, _, height, width = images.size() |
|
|
| |
| if self.alpha > 0: |
| lam = np.random.beta(self.alpha, self.alpha) |
| else: |
| lam = 1 |
|
|
| |
| index = torch.randperm(batch_size).to(images.device) |
|
|
| |
| cut_ratio = np.sqrt(1 - lam) |
| cut_h = int(height * cut_ratio) |
| cut_w = int(width * cut_ratio) |
|
|
| |
| cx = np.random.randint(width) |
| cy = np.random.randint(height) |
|
|
| |
| x1 = max(0, cx - cut_w // 2) |
| x2 = min(width, cx + cut_w // 2) |
| y1 = max(0, cy - cut_h // 2) |
| y2 = min(height, cy + cut_h // 2) |
|
|
| |
| mixed_images = images.clone() |
| mixed_images[:, :, y1:y2, x1:x2] = images[index, :, y1:y2, x1:x2] |
|
|
| |
| lam = 1 - ((x2 - x1) * (y2 - y1) / (width * height)) |
|
|
| labels_a, labels_b = labels, labels[index] |
|
|
| return mixed_images, labels_a, labels_b, lam |
|
|
|
|
| class RandAugmentTransform: |
| """ |
| RandAugment for automatic augmentation policy |
| Simplified version for product images |
| """ |
|
|
| def __init__(self, n: int = 2, m: int = 10): |
| """ |
| Args: |
| n: Number of augmentation transformations to apply |
| m: Magnitude of transformations |
| """ |
| self.n = n |
| self.m = m |
|
|
| |
| self.augmentations = [ |
| lambda img, mag: transforms.functional.rotate(img, mag * 3), |
| lambda img, mag: transforms.functional.adjust_brightness(img, 1 + mag * 0.05), |
| lambda img, mag: transforms.functional.adjust_contrast(img, 1 + mag * 0.05), |
| lambda img, mag: transforms.functional.adjust_saturation(img, 1 + mag * 0.05), |
| lambda img, mag: transforms.functional.adjust_sharpness(img, 1 + mag * 0.1), |
| ] |
|
|
| def __call__(self, img): |
| """Apply RandAugment to an image""" |
| |
| selected_augs = np.random.choice(self.augmentations, self.n, replace=False) |
|
|
| for aug in selected_augs: |
| img = aug(img, self.m) |
|
|
| return img |
|
|
|
|
| def get_advanced_train_transforms( |
| input_size: int = 224, |
| use_randaugment: bool = False, |
| randaugment_n: int = 2, |
| randaugment_m: int = 10 |
| ) -> transforms.Compose: |
| """ |
| Get advanced training transforms with optional RandAugment |
| |
| Args: |
| input_size: Target image size |
| use_randaugment: Whether to use RandAugment |
| randaugment_n: Number of augmentations |
| randaugment_m: Magnitude of augmentations |
| |
| Returns: |
| Transform pipeline |
| """ |
| transform_list = [ |
| transforms.RandomResizedCrop(input_size, scale=(0.7, 1.0)), |
| ] |
|
|
| if use_randaugment: |
| transform_list.append(RandAugmentTransform(n=randaugment_n, m=randaugment_m)) |
|
|
| transform_list.extend([ |
| transforms.RandomHorizontalFlip(p=0.5), |
| transforms.ToTensor(), |
| transforms.Normalize( |
| mean=[0.485, 0.456, 0.406], |
| std=[0.229, 0.224, 0.225] |
| ) |
| ]) |
|
|
| return transforms.Compose(transform_list) |
|
|
|
|
| |
| class DeNormalize: |
| """Denormalize tensor for visualization""" |
|
|
| def __init__(self, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]): |
| self.mean = torch.tensor(mean).view(3, 1, 1) |
| self.std = torch.tensor(std).view(3, 1, 1) |
|
|
| def __call__(self, tensor): |
| """ |
| Args: |
| tensor: Normalized image tensor |
| Returns: |
| Denormalized tensor |
| """ |
| return tensor * self.std + self.mean |
|
|
|
|
| def test_augmentations( |
| image_path: str, |
| model_name: str = 'efficientnet-b2', |
| num_samples: int = 8 |
| ): |
| """ |
| Test and visualize augmentations |
| |
| Args: |
| image_path: Path to test image |
| model_name: Model name for transforms |
| num_samples: Number of augmented samples to generate |
| """ |
| from PIL import Image |
| import matplotlib.pyplot as plt |
|
|
| |
| img = Image.open(image_path).convert('RGB') |
|
|
| |
| transform = get_transforms_for_model(model_name, is_training=True) |
|
|
| |
| fig, axes = plt.subplots(2, 4, figsize=(16, 8)) |
| axes = axes.flatten() |
|
|
| for i in range(num_samples): |
| augmented = transform(img) |
|
|
| |
| denorm = DeNormalize() |
| augmented = denorm(augmented) |
| augmented = torch.clamp(augmented, 0, 1) |
|
|
| |
| augmented = augmented.permute(1, 2, 0).numpy() |
|
|
| axes[i].imshow(augmented) |
| axes[i].axis('off') |
| axes[i].set_title(f'Augmented {i + 1}') |
|
|
| plt.suptitle(f'Augmentation samples for {model_name}') |
| plt.tight_layout() |
| plt.show() |
|
|
|
|
| |
| """ |
| Simple test script to quickly verify transforms.py functions |
| Add this to the bottom of transforms.py or run separately |
| """ |
|
|
| def quick_test_transforms(): |
| """Quick test of all major functions""" |
| import torch |
| import numpy as np |
| from PIL import Image |
|
|
| print("π§ͺ Quick Transforms Test") |
| print("-" * 40) |
|
|
| |
| test_img = Image.fromarray(np.random.randint(0, 255, (300, 300, 3), dtype=np.uint8)) |
| print(f"β Created test image: {test_img.size}") |
|
|
| |
| try: |
| train_transform = get_train_transforms(224, 'medium') |
| val_transform = get_val_transforms(224) |
|
|
| train_tensor = train_transform(test_img) |
| val_tensor = val_transform(test_img) |
|
|
| print(f"β Train transform: {train_tensor.shape}") |
| print(f"β Val transform: {val_tensor.shape}") |
| except Exception as e: |
| print(f"β Basic transforms failed: {e}") |
| return False |
|
|
| |
| try: |
| for model in ['resnet50', 'efficientnet-b2']: |
| transform = get_transforms_for_model(model, is_training=True) |
| tensor = transform(test_img) |
| print(f"β {model} transform: {tensor.shape}") |
| except Exception as e: |
| print(f"β Model-specific transforms failed: {e}") |
| return False |
|
|
| |
| try: |
| batch_size = 4 |
| images = torch.randn(batch_size, 3, 224, 224) |
| labels = torch.tensor([0, 1, 2, 3]) |
|
|
| |
| mixup = MixUpTransform(alpha=1.0, num_classes=5) |
| mixed_images, labels_a, labels_b, lam = mixup(images, labels) |
| print(f"β MixUp: lambda={lam:.3f}, shape={mixed_images.shape}") |
|
|
| |
| cutmix = CutMixTransform(alpha=1.0, num_classes=5) |
| mixed_images, labels_a, labels_b, lam = cutmix(images, labels) |
| print(f"β CutMix: lambda={lam:.3f}, shape={mixed_images.shape}") |
| except Exception as e: |
| print(f"β Advanced augmentations failed: {e}") |
| return False |
|
|
| |
| try: |
| randaug = RandAugmentTransform(n=2, m=10) |
| aug_img = randaug(test_img) |
| print(f"β RandAugment: {test_img.size} -> {aug_img.size}") |
| except Exception as e: |
| print(f"β RandAugment failed: {e}") |
| return False |
|
|
| |
| try: |
| denorm = DeNormalize() |
| normalized = val_transform(test_img) |
| denormalized = denorm(normalized) |
| print(f"β Denormalize: {normalized.shape} -> {denormalized.shape}") |
| except Exception as e: |
| print(f"β Denormalization failed: {e}") |
| return False |
|
|
| print("-" * 40) |
| print("π All transforms working correctly!") |
| return True |
|
|
| |
| if __name__ == "__main__": |
| |
| success = quick_test_transforms() |
|
|
| if success: |
| print("\nβ
transforms.py is ready to use!") |
| else: |
| print("\nβ transforms.py has issues that need fixing") |
|
|
| |
| """ |
| try: |
| # This requires matplotlib |
| test_augmentations('path/to/test/image.jpg', 'efficientnet-b2', 8) |
| except: |
| print("Visual test skipped (requires matplotlib and test image)") |
| """ |