CoolFace
Apppublic

Droid210/FleetVision

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
data.py362 linesDownload Raw Back to model_b
1"""Data loading and augmentation utilities."""2from pathlib import Path3from typing import List, Tuple4 5import torch6from torch.utils.data import DataLoader, Dataset7from torchvision import transforms8from transformers import AutoImageProcessor9 10from .config import EXPECTED_CLASSES, IMAGE_SIZE, MODEL_ID11 12 13class DamagedImageAugmentation:14    """Heavy augmentation for Damaged class to simulate different angles."""15 16    def __call__(self, img):17        """Apply random augmentations."""18        img = transforms.RandomResizedCrop(IMAGE_SIZE, scale=(0.7, 1.0))(img)19        img = transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.1)(img)20        img = transforms.RandomRotation(25)(img)21        img = transforms.RandomAffine(degrees=0, translate=(0.1, 0.1))(img)22        img = transforms.GaussianBlur(kernel_size=3, sigma=(0.1, 0.5))(img)23        return img24 25 26class DamageDataset(Dataset):27    """Custom dataset for damage detection with class-specific augmentation."""28 29    def __init__(30        self,31        data_dir: Path,32        processor,33        is_train: bool = True,34    ):35        """Initialize dataset.36 37        Args:38            data_dir: Path to dataset root (parent of Whole/Damaged folders).39            processor: Hugging Face image processor for preprocessing.40            is_train: Whether this is training set.41        """42        self.processor = processor43        self.is_train = is_train44        self.images = []45        self.labels = []46 47        # Load images and labels48        for class_idx, class_name in enumerate(EXPECTED_CLASSES):49            class_dir = data_dir / class_name50            if not class_dir.exists():51                raise FileNotFoundError(f"Class directory not found: {class_dir}")52 53            for img_path in class_dir.glob("*.jpg"):54                self.images.append(img_path)55                self.labels.append(class_idx)56 57            for img_path in class_dir.glob("*.png"):58                self.images.append(img_path)59                self.labels.append(class_idx)60 61        if not self.images:62            raise ValueError(f"No images found in {data_dir}")63 64        # Drop stale paths if files were moved/deleted after listing.65        filtered = [(p, y) for p, y in zip(self.images, self.labels) if p.exists() and p.is_file()]66        self.images = [p for p, _ in filtered]67        self.labels = [y for _, y in filtered]68 69        if not self.images:70            raise ValueError(f"No readable image files found in {data_dir}")71 72        # Augmentation for training73        self.damaged_aug = DamagedImageAugmentation() if is_train else None74 75    def __len__(self) -> int:76        return len(self.images)77 78    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:79        """Get image and label.80 81        Args:82            idx: Index of image.83 84        Returns:85            Tuple of (image_tensor, label_tensor).86        """87        from PIL import Image88 89        max_tries = min(16, len(self.images))90        for offset in range(max_tries):91            safe_idx = (idx + offset) % len(self.images)92            img_path = self.images[safe_idx]93            label = self.labels[safe_idx]94 95            if not img_path.exists():96                continue97 98            try:99                image = Image.open(img_path).convert("RGB")100            except (FileNotFoundError, OSError):101                continue102 103            # Apply heavy augmentation to Damaged class during training104            if self.is_train and label == 1:  # Damaged class105                image = self.damaged_aug(image)106 107            # Process with ViTImageProcessor108            processed = self.processor(image, return_tensors="pt")109            pixel_values = processed["pixel_values"].squeeze(0)110            return pixel_values, torch.tensor(label, dtype=torch.long)111 112        raise FileNotFoundError("Failed to load a valid image sample after multiple attempts.")113 114 115class BinaryDamageDataset(Dataset):116    """Binary dataset built from explicit (image_path, label) pairs."""117 118    def __init__(119        self,120        samples: List[Tuple[Path, int, bool]],121        processor,122        is_train: bool = True,123    ):124        self.samples = [(p, y, syn) for p, y, syn in samples if p.exists() and p.is_file()]125        self.processor = processor126        self.is_train = is_train127        self.damaged_aug = DamagedImageAugmentation() if is_train else None128        self.synthetic_negative_aug = transforms.Compose(129            [130                transforms.RandomResizedCrop(IMAGE_SIZE, scale=(0.2, 0.6)),131                transforms.ColorJitter(brightness=0.6, contrast=0.6, saturation=0.6, hue=0.2),132                transforms.RandomGrayscale(p=0.4),133                transforms.GaussianBlur(kernel_size=5, sigma=(0.3, 1.5)),134            ]135        )136 137        if not self.samples:138            raise ValueError("No valid image files found after filtering sample paths.")139 140    def __len__(self) -> int:141        return len(self.samples)142 143    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:144        from PIL import Image145 146        max_tries = min(16, len(self.samples))147        for offset in range(max_tries):148            safe_idx = (idx + offset) % len(self.samples)149            img_path, label, is_synthetic_negative = self.samples[safe_idx]150 151            if not img_path.exists():152                continue153 154            try:155                image = Image.open(img_path).convert("RGB")156            except (FileNotFoundError, OSError):157                continue158 159            if self.is_train and label == 1:  # Damaged class160                image = self.damaged_aug(image)161            elif is_synthetic_negative:162                # Damaged-only fallback: synthesize non-damage-like negatives.163                image = self.synthetic_negative_aug(image)164 165            processed = self.processor(image, return_tensors="pt")166            pixel_values = processed["pixel_values"].squeeze(0)167            return pixel_values, torch.tensor(label, dtype=torch.long)168 169        raise FileNotFoundError("Failed to load a valid image sample after multiple attempts.")170 171 172def _collect_images(folder: Path) -> List[Path]:173    """Collect images from a folder recursively."""174    exts = ("*.jpg", "*.jpeg", "*.png", "*.bmp", "*.webp")175    files: List[Path] = []176    for ext in exts:177        files.extend(folder.rglob(ext))178    return sorted(files)179 180 181def _split_items(items: List[Path], val_ratio: float = 0.2, seed: int = 42) -> Tuple[List[Path], List[Path]]:182    """Deterministically split items into train/validation."""183    if len(items) < 2:184        return items, []185 186    g = torch.Generator().manual_seed(seed)187    perm = torch.randperm(len(items), generator=g).tolist()188    split_at = max(1, int(len(items) * (1.0 - val_ratio)))189    train_idx = perm[:split_at]190    val_idx = perm[split_at:]191 192    train_items = [items[i] for i in train_idx]193    val_items = [items[i] for i in val_idx]194    return train_items, val_items195 196 197def _resolve_damage_assessment_root(data_dir: Path) -> Path | None:198    """Resolve CarDD/FiftyOne-style dataset root."""199    if (data_dir / "samples.json").exists() and (data_dir / "data").exists():200        return data_dir201 202    nested = data_dir / "damage_assessment"203    if (nested / "samples.json").exists() and (nested / "data").exists():204        return nested205 206    return None207 208 209def _build_from_damage_assessment(210    data_dir: Path,211    batch_size: int,212    num_workers: int,213    processor,214) -> Tuple[DataLoader, DataLoader, object]:215    """Build loaders from CarDD/FiftyOne-style layout.216 217    Expected positives: <root>/damage_assessment/data or <root>/data218    Expected negatives: any of219      - <data_dir>/whole_pool220      - <data_dir>/Whole221      - <data_dir>/train/Whole + <data_dir>/valid/Whole222    """223    assessment_root = _resolve_damage_assessment_root(data_dir)224    if assessment_root is None:225        raise ValueError("Could not resolve damage_assessment dataset root.")226 227    damaged_images = _collect_images(assessment_root / "data")228    if not damaged_images:229        raise ValueError(f"No images found in {assessment_root / 'data'}")230 231    whole_candidates = [232        data_dir / "whole_pool",233        data_dir / "Whole",234        assessment_root.parent / "whole_pool",235        assessment_root.parent / "Whole",236    ]237 238    whole_images: List[Path] = []239    for folder in whole_candidates:240        if folder.exists():241            whole_images.extend(_collect_images(folder))242 243    train_whole = data_dir / "train" / "Whole"244    valid_whole = data_dir / "valid" / "Whole"245    parent_train_whole = assessment_root.parent / "train" / "Whole"246    parent_valid_whole = assessment_root.parent / "valid" / "Whole"247    if train_whole.exists():248        whole_images.extend(_collect_images(train_whole))249    if valid_whole.exists():250        whole_images.extend(_collect_images(valid_whole))251    if parent_train_whole.exists():252        whole_images.extend(_collect_images(parent_train_whole))253    if parent_valid_whole.exists():254        whole_images.extend(_collect_images(parent_valid_whole))255 256    whole_images = sorted(set(whole_images))257    train_damaged, val_damaged = _split_items(damaged_images, val_ratio=0.2, seed=42)258    train_samples: List[Tuple[Path, int, bool]]259    val_samples: List[Tuple[Path, int, bool]]260 261    if whole_images:262        train_whole_split, val_whole_split = _split_items(whole_images, val_ratio=0.2, seed=42)263        train_samples = [(p, 1, False) for p in train_damaged] + [(p, 0, False) for p in train_whole_split]264        val_samples = [(p, 1, False) for p in val_damaged] + [(p, 0, False) for p in val_whole_split]265    else:266        print(267            "WARNING: No Whole images found. Using damaged-only fallback with synthetic Whole negatives."268        )269        # Each damaged image is used twice: once as Damaged, once as synthetic Whole.270        train_samples = [(p, 1, False) for p in train_damaged] + [(p, 0, True) for p in train_damaged]271        val_samples = [(p, 1, False) for p in val_damaged] + [(p, 0, True) for p in val_damaged]272 273    if not train_samples or not val_samples:274        raise ValueError("Insufficient samples after split. Need at least one train and one validation sample per run.")275 276    g = torch.Generator().manual_seed(42)277    train_perm = torch.randperm(len(train_samples), generator=g).tolist()278    val_perm = torch.randperm(len(val_samples), generator=g).tolist()279    train_samples = [train_samples[i] for i in train_perm]280    val_samples = [val_samples[i] for i in val_perm]281 282    train_dataset = BinaryDamageDataset(train_samples, processor, is_train=True)283    val_dataset = BinaryDamageDataset(val_samples, processor, is_train=False)284 285    train_loader = DataLoader(286        train_dataset,287        batch_size=batch_size,288        shuffle=True,289        num_workers=num_workers,290        pin_memory=torch.cuda.is_available(),291    )292 293    val_loader = DataLoader(294        val_dataset,295        batch_size=batch_size,296        shuffle=False,297        num_workers=num_workers,298        pin_memory=torch.cuda.is_available(),299    )300 301    return train_loader, val_loader, processor302 303 304def build_dataloaders(305    data_dir: Path,306    batch_size: int,307    num_workers: int,308) -> Tuple[DataLoader, DataLoader, object]:309    """Build train and validation dataloaders.310 311    Args:312        data_dir: Path to dataset root.313        batch_size: Batch size.314        num_workers: Number of workers.315 316    Returns:317        Tuple of (train_loader, val_loader, processor).318    """319    # Load processor320    processor = AutoImageProcessor.from_pretrained(MODEL_ID)321 322    # Layout A: split folders (existing format)323    train_dir = data_dir / "train"324    val_dir = data_dir / "valid"325 326    if train_dir.exists() and val_dir.exists():327        train_dataset = DamageDataset(train_dir, processor, is_train=True)328        val_dataset = DamageDataset(val_dir, processor, is_train=False)329 330        train_loader = DataLoader(331            train_dataset,332            batch_size=batch_size,333            shuffle=True,334            num_workers=num_workers,335            pin_memory=torch.cuda.is_available(),336        )337 338        val_loader = DataLoader(339            val_dataset,340            batch_size=batch_size,341            shuffle=False,342            num_workers=num_workers,343            pin_memory=torch.cuda.is_available(),344        )345 346        return train_loader, val_loader, processor347 348    # Layout B: CarDD/FiftyOne damage_assessment format349    assessment_root = _resolve_damage_assessment_root(data_dir)350    if assessment_root is not None:351        return _build_from_damage_assessment(352            data_dir=data_dir,353            batch_size=batch_size,354            num_workers=num_workers,355            processor=processor,356        )357 358    raise ValueError(359        f"Unsupported dataset layout in {data_dir}. Expected either train/valid folders "360        "or a damage_assessment dataset with data/ and samples.json."361    )362