| import os
|
| import random
|
| import numpy as np
|
| from PIL import Image
|
| from collections import defaultdict
|
| from sklearn.model_selection import StratifiedShuffleSplit
|
| import torch
|
| from torch.utils.data import Dataset
|
| from torchvision import transforms
|
| from torchvision.transforms import InterpolationMode, functional as F
|
|
|
|
|
| IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
|
| DEFAULT_MEAN = [0.485, 0.456, 0.406]
|
| DEFAULT_STD = [0.229, 0.224, 0.225]
|
|
|
| def set_seed(seed=42):
|
| random.seed(seed)
|
| np.random.seed(seed)
|
| torch.manual_seed(seed)
|
| torch.cuda.manual_seed_all(seed)
|
|
|
| class SnakeDataset(Dataset):
|
| def __init__(self, image_paths, labels, transform=None):
|
| self.image_paths = image_paths
|
| self.labels = labels
|
| self.transform = transform
|
|
|
| def __len__(self):
|
| return len(self.image_paths)
|
|
|
| def __getitem__(self, idx):
|
| img_path = self.image_paths[idx]
|
| label = self.labels[idx]
|
|
|
| try:
|
| image = Image.open(img_path).convert('RGB')
|
| if self.transform:
|
| image = self.transform(image)
|
| return image, label
|
| except Exception as e:
|
| print(f"Error loading {img_path}: {e}")
|
|
|
|
|
|
|
| raise e
|
|
|
| def scan_dataset(root_dir):
|
| """
|
| Scans the directory for images.
|
| Structure expected: root_dir/class_name/image.jpg
|
| """
|
| species_to_files = defaultdict(list)
|
| for class_name in os.listdir(root_dir):
|
| class_dir = os.path.join(root_dir, class_name)
|
| if not os.path.isdir(class_dir):
|
| continue
|
|
|
| for f in os.listdir(class_dir):
|
| if os.path.splitext(f.lower())[1] in IMG_EXTS:
|
| species_to_files[class_name].append(os.path.join(class_dir, f))
|
| return species_to_files
|
|
|
| def filter_and_split_data(root_dir, threshold, seed=42):
|
| """
|
| Filters classes with < threshold images.
|
| Splits remaining data:
|
| 1. 80% (Train+Val) / 20% (Test)
|
| 2. Of the 80% Train+Val: 80% Train / 20% Val (Which is 64% total Train, 16% total Val)
|
| Returns:
|
| (train_paths, train_labels), (val_paths, val_labels), (test_paths, test_labels), class_to_idx
|
| """
|
| species_to_files = scan_dataset(root_dir)
|
|
|
|
|
| valid_species = sorted([sp for sp, files in species_to_files.items() if len(files) >= threshold])
|
| if not valid_species:
|
| return None, None, None, None
|
|
|
| class_to_idx = {sp: i for i, sp in enumerate(valid_species)}
|
|
|
| all_paths = []
|
| all_labels = []
|
|
|
| for sp in valid_species:
|
| files = species_to_files[sp]
|
| all_paths.extend(files)
|
| all_labels.extend([class_to_idx[sp]] * len(files))
|
|
|
| all_paths = np.array(all_paths)
|
| all_labels = np.array(all_labels)
|
|
|
|
|
| sss_test = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=seed)
|
| train_val_idx, test_idx = next(sss_test.split(all_paths, all_labels))
|
|
|
| X_train_val, y_train_val = all_paths[train_val_idx], all_labels[train_val_idx]
|
| X_test, y_test = all_paths[test_idx], all_labels[test_idx]
|
|
|
|
|
|
|
|
|
|
|
| sss_val = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=seed)
|
| train_idx, val_idx = next(sss_val.split(X_train_val, y_train_val))
|
|
|
| X_train, y_train = X_train_val[train_idx], y_train_val[train_idx]
|
| X_val, y_val = X_train_val[val_idx], y_train_val[val_idx]
|
|
|
| return (X_train, y_train), (X_val, y_val), (X_test, y_test), class_to_idx
|
|
|
| class RandomDiscreteTransform:
|
| """Apply a transform with a given probability."""
|
| def __init__(self, transform, p=0.5):
|
| self.transform = transform
|
| self.p = p
|
|
|
| def __call__(self, img):
|
| if random.random() < self.p:
|
| return self.transform(img)
|
| return img
|
|
|
| def get_transforms(intensity='none', input_size=224):
|
| """
|
| Returns training and validation transforms.
|
| Intensity profiles: none, low, medium, high.
|
| Base augmentation params (from previous codebase logic):
|
| - rotation: 10
|
| - shifts: 0.1
|
| - zoom: 0.1
|
| - shear: 0.1
|
| - flip: True (Horizontal)
|
|
|
| Multipliers:
|
| - low: 0.5
|
| - medium: 1.0
|
| - high: 1.5
|
| """
|
|
|
|
|
| val_transforms = transforms.Compose([
|
| transforms.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=DEFAULT_MEAN, std=DEFAULT_STD)
|
| ])
|
|
|
| if intensity == 'none':
|
| return val_transforms, val_transforms
|
|
|
|
|
| base_deg = 10
|
| base_trans = 0.1
|
| base_scale = 0.1
|
| base_shear = 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| multipliers = {'low': 0.5, 'medium': 1.0, 'high': 1.5}
|
| m = multipliers.get(intensity, 1.0)
|
|
|
|
|
| deg = 10 * m
|
| tx = 0.1 * m
|
| ty = 0.1 * m
|
| zm = 0.1 * m
|
| shr = 0.1 * m
|
| sh_deg = np.degrees(shr) if shr > 0 else 0.0
|
|
|
|
|
| val_transforms = transforms.Compose([
|
| transforms.Resize((input_size, input_size), interpolation=InterpolationMode.BILINEAR),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=DEFAULT_MEAN, std=DEFAULT_STD)
|
| ])
|
|
|
| if intensity == 'none':
|
| return val_transforms, val_transforms
|
|
|
|
|
| translate = (max(0.0, min(tx, 0.499)), max(0.0, min(ty, 0.499)))
|
| scale = (max(0.0, 1.0 - zm), 1.0 + zm)
|
|
|
|
|
| pad_px = int(round(0.12 * input_size))
|
|
|
| train_ops = [
|
| transforms.Resize((input_size, input_size), interpolation=InterpolationMode.BILINEAR),
|
| transforms.RandomHorizontalFlip(p=0.5),
|
| transforms.Pad(pad_px, padding_mode="edge"),
|
| transforms.RandomAffine(
|
| degrees=(-deg, deg),
|
| translate=translate,
|
| scale=scale,
|
| shear=(-sh_deg, sh_deg),
|
| interpolation=InterpolationMode.BILINEAR
|
| ),
|
| transforms.CenterCrop((input_size, input_size)),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=DEFAULT_MEAN, std=DEFAULT_STD)
|
| ]
|
|
|
| train_transforms = transforms.Compose(train_ops)
|
|
|
| return train_transforms, val_transforms
|
|
|