eel4759_classification

Comparison of different ImageNet-based CNN models for classifying diabetic retinopathy images
Log | Files | Refs | README

dataset.py (6158B)


      1 import os
      2 from PIL import Image
      3 import numpy as np
      4 import cv2
      5 
      6 import torch
      7 from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler
      8 from torchvision import transforms
      9 from sklearn.model_selection import train_test_split
     10 
     11 IMAGENET_MEAN = [0.485, 0.456, 0.406]
     12 IMAGENET_STD = [0.229, 0.224, 0.225]
     13 
     14 
     15 class CLAHETransform:
     16     """Apply CLAHE to the L channel of LAB color space.
     17 
     18     Operates on PIL Images; returns a PIL Image.
     19     """
     20 
     21     def __init__(self, clip_limit: float = 2.0, tile_grid_size: tuple = (8, 8)):
     22         self.clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
     23 
     24     def __call__(self, img: Image.Image) -> Image.Image:
     25         img_np = np.array(img)  # RGB uint8
     26         lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB)
     27         lab[:, :, 0] = self.clahe.apply(lab[:, :, 0])
     28         result = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
     29         return Image.fromarray(result)
     30 
     31 
     32 class DRDataset(Dataset):
     33     """Diabetic Retinopathy dataset from flat class-folder layout."""
     34 
     35     def __init__(self, image_paths: list, labels: list, transform=None):
     36         self.image_paths = image_paths
     37         self.labels = labels
     38         self.transform = transform
     39 
     40     def __len__(self) -> int:
     41         return len(self.image_paths)
     42 
     43     def __getitem__(self, idx: int):
     44         img = Image.open(self.image_paths[idx]).convert('RGB')
     45         label = self.labels[idx]
     46         if self.transform is not None:
     47             img = self.transform(img)
     48         return img, label
     49 
     50 
     51 def split_dataset(data_root: str, seed: int = 42):
     52     """Scan class folders 0–4 and return stratified 70/15/15 splits.
     53 
     54     Returns:
     55         (train_paths, train_labels, val_paths, val_labels, test_paths, test_labels)
     56     """
     57     all_paths, all_labels = [], []
     58     for class_idx in range(5):
     59         class_dir = os.path.join(data_root, str(class_idx))
     60         for fname in sorted(os.listdir(class_dir)):
     61             if fname.lower().endswith(('.jpg', '.jpeg', '.png')):
     62                 all_paths.append(os.path.join(class_dir, fname))
     63                 all_labels.append(class_idx)
     64 
     65     # 70% train, 30% temp
     66     train_paths, temp_paths, train_labels, temp_labels = train_test_split(
     67         all_paths, all_labels, test_size=0.30, stratify=all_labels, random_state=seed
     68     )
     69     # 50/50 of temp -> 15% val, 15% test
     70     val_paths, test_paths, val_labels, test_labels = train_test_split(
     71         temp_paths, temp_labels, test_size=0.50, stratify=temp_labels, random_state=seed
     72     )
     73 
     74     print(f"Dataset split — train: {len(train_paths)}, val: {len(val_paths)}, test: {len(test_paths)}")
     75     return train_paths, train_labels, val_paths, val_labels, test_paths, test_labels
     76 
     77 
     78 def build_transforms(use_clahe: bool, training: bool, img_size: int = 224):
     79     """Build a transform pipeline for training or eval.
     80 
     81     Training path uses RandomResizedCrop for scale/position augmentation.
     82     Eval path uses a deterministic Resize to preserve comparability.
     83     CLAHE is applied before spatial transforms at a slightly larger size
     84     so the crop has room to operate.
     85     """
     86     ops = []
     87     if training:
     88         if use_clahe:
     89             # Resize larger so RandomResizedCrop still sees enough context after CLAHE
     90             ops.append(transforms.Resize((img_size + 32, img_size + 32)))
     91             ops.append(CLAHETransform(clip_limit=2.0, tile_grid_size=(8, 8)))
     92         ops += [
     93             transforms.RandomResizedCrop(img_size, scale=(0.8, 1.0), ratio=(0.9, 1.1)),
     94             transforms.RandomHorizontalFlip(p=0.5),
     95             transforms.RandomVerticalFlip(p=0.5),
     96             transforms.RandomRotation(degrees=30),
     97             transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.02),
     98             transforms.GaussianBlur(kernel_size=3, sigma=(0.1, 1.0)),
     99         ]
    100     else:
    101         ops.append(transforms.Resize((img_size, img_size)))
    102         if use_clahe:
    103             ops.append(CLAHETransform(clip_limit=2.0, tile_grid_size=(8, 8)))
    104     ops += [
    105         transforms.ToTensor(),
    106         transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
    107     ]
    108     return transforms.Compose(ops)
    109 
    110 
    111 def build_dataloaders(
    112     train_paths, train_labels,
    113     val_paths, val_labels,
    114     test_paths, test_labels,
    115     use_clahe: bool,
    116     batch_size: int = 32,
    117     num_workers: int = 4,
    118     img_size: int = 224,
    119 ):
    120     """Create DataLoaders for all three splits.
    121 
    122     The training DataLoader uses WeightedRandomSampler so that each class
    123     is sampled at approximately equal frequency, counteracting class imbalance.
    124     """
    125     train_ds = DRDataset(train_paths, train_labels, transform=build_transforms(use_clahe, training=True, img_size=img_size))
    126     val_ds = DRDataset(val_paths, val_labels, transform=build_transforms(use_clahe, training=False, img_size=img_size))
    127     test_ds = DRDataset(test_paths, test_labels, transform=build_transforms(use_clahe, training=False, img_size=img_size))
    128 
    129     # Per-sample weights: sqrt-inverse-frequency for moderate class balancing.
    130     # Full inverse (1/count) causes over-prediction of minorities; sqrt gives a
    131     # softer boost that retains enough majority-class signal.
    132     labels_arr = np.array(train_labels)
    133     class_counts = np.bincount(labels_arr, minlength=5)
    134     class_sample_weights = 1.0 / np.sqrt(class_counts.astype(float))
    135     sample_weights = class_sample_weights[labels_arr]
    136     sampler = WeightedRandomSampler(
    137         weights=torch.from_numpy(sample_weights).float(),
    138         num_samples=len(train_labels),
    139         replacement=True,
    140     )
    141 
    142     train_loader = DataLoader(
    143         train_ds, batch_size=batch_size, sampler=sampler,
    144         num_workers=num_workers, pin_memory=True,
    145         persistent_workers=True, prefetch_factor=4,
    146     )
    147     val_loader = DataLoader(
    148         val_ds, batch_size=batch_size * 2, shuffle=False,
    149         num_workers=num_workers, pin_memory=True,
    150         persistent_workers=True, prefetch_factor=4,
    151     )
    152     test_loader = DataLoader(
    153         test_ds, batch_size=batch_size * 2, shuffle=False,
    154         num_workers=num_workers, pin_memory=True,
    155         persistent_workers=True, prefetch_factor=4,
    156     )
    157     return train_loader, val_loader, test_loader