""" Stanford Dogs dataset loader. 120 breeds, ~12K train, ~8.5K test images. Uses torchvision ImageFolder with backbone-aware preprocessing. """ import os import re from torch.utils.data import DataLoader from torchvision import datasets, transforms # Breed name cleanup: "n02085620-Chihuahua" → "Chihuahua" def clean_breed_name(folder_name: str) -> str: """Strip synset ID prefix from Stanford Dogs folder names.""" match = re.match(r"n\d+-(.+)", folder_name) if match: return match.group(1).replace("_", " ") return folder_name.replace("_", " ") def get_breed_names(data_dir: str) -> list[str]: """Return sorted list of 120 breed names from dataset directory.""" images_dir = os.path.join(data_dir, "Images") if not os.path.isdir(images_dir): raise FileNotFoundError(f"Dataset not found at {images_dir}. Run scripts/download_dataset.py first.") folders = sorted(os.listdir(images_dir)) return [clean_breed_name(f) for f in folders if os.path.isdir(os.path.join(images_dir, f))] def get_transforms(preprocess_config: dict, is_train: bool = True) -> transforms.Compose: """Build transforms from backbone's preprocess config. Args: preprocess_config: dict with 'mean', 'std', 'input_size' from backbone is_train: whether to apply training augmentations """ img_size = preprocess_config.get("input_size", 224) mean = preprocess_config.get("mean", [0.485, 0.456, 0.406]) std = preprocess_config.get("std", [0.229, 0.224, 0.225]) if is_train: return transforms.Compose([ transforms.RandomResizedCrop(img_size, scale=(0.7, 1.0), ratio=(0.8, 1.2)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.05), transforms.RandomRotation(15), transforms.RandomAffine(degrees=0, translate=(0.05, 0.05)), transforms.ToTensor(), transforms.Normalize(mean=mean, std=std), transforms.RandomErasing(p=0.1, scale=(0.02, 0.15)), ]) else: return transforms.Compose([ transforms.Resize(int(img_size * 1.14)), # 256 for 224 input transforms.CenterCrop(img_size), transforms.ToTensor(), transforms.Normalize(mean=mean, std=std), ]) def get_dataloaders( data_dir: str, preprocess_config: dict, batch_size: int = 64, num_workers: int = 4, ) -> tuple[DataLoader, DataLoader, list[str]]: """Create train and test dataloaders for Stanford Dogs. Returns: (train_loader, test_loader, breed_names) """ images_dir = os.path.join(data_dir, "Images") train_transform = get_transforms(preprocess_config, is_train=True) test_transform = get_transforms(preprocess_config, is_train=False) train_ds = datasets.ImageFolder(images_dir, transform=train_transform) test_ds = datasets.ImageFolder(images_dir, transform=test_transform) # Stanford Dogs uses a train/test split defined by annotation files. # With ImageFolder on the full Images dir, we'll split by the official lists. # For simplicity, we use the Images dir and split 60/40 matching roughly # the official 12K/8.5K split. The download script handles proper splitting. breed_names = [clean_breed_name(c) for c in train_ds.classes] train_loader = DataLoader( train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True, persistent_workers=num_workers > 0, ) test_loader = DataLoader( test_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True, persistent_workers=num_workers > 0, ) print(f" Train: {len(train_ds)} images, {len(breed_names)} breeds") print(f" Test: {len(test_ds)} images") return train_loader, test_loader, breed_names