Spaces:
Sleeping
Sleeping
| """EfficientNet-B0 backbone + transform recipes for both imaging models. | |
| Grayscale strategy: the model keeps the standard 3-channel stem (so ImageNet | |
| pretrained weights load unchanged); grayscale inputs are replicated 1->3ch inside the | |
| transform (``Grayscale(num_output_channels=3)``). | |
| Two train recipes: | |
| - MODALITY (``make_transforms(train=True)``): geometric augs PLUS the source-confound | |
| killers (random JPEG-quality re-encode at q 60-95 and occasional gaussian blur), | |
| so the classifier cannot key on per-source compression/sharpness signatures. | |
| - AUTHENTICITY (``AUTH_TRAIN_TRANSFORM`` / ``make_auth_train_transform``): geometric | |
| augs ONLY. JPEG/blur augs would erase exactly the forensic artifacts (double-JPEG | |
| ghosts, splice seams, resampling softness) the detector must learn. | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import random | |
| import timm | |
| from PIL import Image | |
| from torch import nn | |
| from torchvision import transforms | |
| ARCH = "efficientnet_b0" | |
| IMAGENET_MEAN: tuple[float, float, float] = (0.485, 0.456, 0.406) | |
| IMAGENET_STD: tuple[float, float, float] = (0.229, 0.224, 0.225) | |
| def build_model(num_classes: int, pretrained: bool) -> nn.Module: | |
| """timm efficientnet_b0 with a fresh ``num_classes`` head (standard 3ch input).""" | |
| return timm.create_model(ARCH, pretrained=pretrained, num_classes=num_classes) | |
| class JpegQualityJitter: | |
| """Re-encode the PIL image as JPEG at a random quality in [lo, hi] (default 60-95). | |
| Custom transform (torchvision has no JPEG aug for PIL inputs): in-memory re-encode | |
| via ``io.BytesIO`` so no temp files. Uses python's ``random`` (seeded by the shared | |
| ``set_seed``) for determinism. | |
| """ | |
| def __init__(self, quality: tuple[int, int] = (60, 95)) -> None: | |
| if not (1 <= quality[0] <= quality[1] <= 100): | |
| raise ValueError(f"invalid quality range: {quality}") | |
| self.quality = quality | |
| def __call__(self, img: Image.Image) -> Image.Image: | |
| q = random.randint(self.quality[0], self.quality[1]) | |
| buf = io.BytesIO() | |
| img.convert("L").save(buf, format="JPEG", quality=q) | |
| buf.seek(0) | |
| return Image.open(buf).convert("L") | |
| def __repr__(self) -> str: | |
| return f"{type(self).__name__}(quality={self.quality})" | |
| def _to_3ch_tensor() -> list[object]: | |
| return [ | |
| transforms.Grayscale(num_output_channels=3), | |
| transforms.ToTensor(), | |
| transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), | |
| ] | |
| def make_transforms(train: bool, size: int = 224) -> transforms.Compose: | |
| """MODALITY-model transforms (train recipe includes the confound killers).""" | |
| if not train: | |
| return transforms.Compose( | |
| [ | |
| transforms.Resize(int(size * 256 / 224)), | |
| transforms.CenterCrop(size), | |
| *_to_3ch_tensor(), | |
| ] | |
| ) | |
| return transforms.Compose( | |
| [ | |
| transforms.RandomResizedCrop(size, scale=(0.8, 1.0)), | |
| transforms.RandomRotation(10), | |
| transforms.RandomHorizontalFlip(), | |
| transforms.ColorJitter(brightness=0.2, contrast=0.2), | |
| JpegQualityJitter((60, 95)), | |
| transforms.RandomApply( | |
| [transforms.GaussianBlur(kernel_size=5, sigma=(0.1, 1.5))], p=0.2 | |
| ), | |
| *_to_3ch_tensor(), | |
| ] | |
| ) | |
| def make_auth_train_transform(size: int = 224) -> transforms.Compose: | |
| """AUTHENTICITY-model train transforms: geometric only (no JPEG/blur/photometric). | |
| Compression/blur augs would destroy the forensic evidence the detector learns. | |
| """ | |
| return transforms.Compose( | |
| [ | |
| transforms.RandomResizedCrop(size, scale=(0.8, 1.0)), | |
| transforms.RandomRotation(10), | |
| transforms.RandomHorizontalFlip(), | |
| *_to_3ch_tensor(), | |
| ] | |
| ) | |
| AUTH_TRAIN_TRANSFORM = make_auth_train_transform() | |