Spaces:
Sleeping
Sleeping
Download dataset.py from SMSD75/SynthScope: direct link, hf CLI and curl.
- Browser
- Download file 132 kB
-
https://huggingface.co/spaces/SMSD75/SynthScope/resolve/main/dataset.py
- Command line
-
hf download hf://spaces/SMSD75/SynthScope/dataset.py
-
curl -L -o dataset.py https://huggingface.co/spaces/SMSD75/SynthScope/resolve/main/dataset.py
132 kB
| from torch.utils.data import Dataset | |
| from typing import Literal | |
| from pathlib import Path | |
| import hashlib | |
| import os | |
| import re | |
| import zipfile | |
| import tarfile | |
| import shutil | |
| import subprocess | |
| import csv | |
| import time | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| from matplotlib import pyplot as plt | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from decord import VideoReader, cpu | |
| from torch.utils.data import DataLoader | |
| from transformations import AIGITransformations | |
| from torchvision.io import decode_image, ImageReadMode | |
| from PIL import Image | |
| from config_utils import compose_config | |
| def _decode_rgb(path) -> torch.Tensor: | |
| """Decode an image to a uint8 [3, H, W] RGB tensor. | |
| Uses torchvision's fast native decoder, falling back to PIL for the rare files | |
| its backend rejects (e.g. CMYK / progressive / slightly-malformed JPEGs that | |
| raise "Internal error"). Keeps the uint8 CHW contract the transform pipeline expects. | |
| """ | |
| try: | |
| return decode_image(str(path), mode=ImageReadMode.RGB) | |
| except Exception: | |
| pass | |
| try: | |
| img = Image.open(str(path)).convert("RGB") | |
| return torch.from_numpy(np.array(img)).permute(2, 0, 1).contiguous() | |
| except Exception: | |
| # Corrupt / truncated / empty file (some GenImage PNGs are 0-byte): don't kill the | |
| # whole run — return a black 256x256 image so this sample is just uninformative. | |
| return torch.zeros(3, 256, 256, dtype=torch.uint8) | |
| IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".jfif", ".png", ".webp") | |
| VIDEO_EXTENSIONS = (".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v", ".gif") | |
| def _iter_images(directory: Path): | |
| return (p for p in directory.rglob("*") if p.is_file() and p.suffix.lower() in IMAGE_EXTENSIONS) | |
| def _has_images(directory: Path) -> bool: | |
| return directory.is_dir() and any(_iter_images(directory)) | |
| def _iter_videos(directory: Path): | |
| return (p for p in directory.rglob("*") if p.is_file() and p.suffix.lower() in VIDEO_EXTENSIONS) | |
| def _has_videos(directory: Path) -> bool: | |
| return directory.is_dir() and any(_iter_videos(directory)) | |
| def sample_video_frames( | |
| path: str | Path, | |
| num_frames: int, | |
| fps: float | None = None, | |
| window_seconds: float = 2.0, | |
| size: int = 224, | |
| window_center: bool = False, | |
| interp: str = "bilinear", | |
| edge_crop: float = 0.0, | |
| edge_crop_long_side: bool = False, | |
| short_clip_frames: int = 0, | |
| fps_nearest: bool = False, | |
| bgr: bool = False, | |
| ) -> torch.Tensor: | |
| """Decode a clip of exactly ``num_frames`` frames and return ``[T, 3, size, size]`` float in ``[0, 1]``. | |
| Frames are sampled over a ``window_seconds`` window. By default the window starts at the | |
| beginning of the video; set ``window_center=True`` to center it at the video midpoint (this is | |
| what the official ReStraV pipeline does). When ``fps`` is given (e.g. D3 samples at 8 fps), the | |
| window is shortened to ``num_frames / fps`` seconds so the effective sampling rate matches | |
| ``fps``; when ``fps`` is ``None`` the frames are spread evenly across ``window_seconds``. The | |
| resize interpolation is ``interp`` (``"bilinear"`` default; ``"bicubic"`` for ReStraV, with | |
| antialiasing, matching the official preprocessing). The output frame count is always | |
| ``num_frames`` so the default collate can stack clips into ``[B, T, 3, size, size]``. | |
| Two options reproduce the official D3 preprocessing: ``edge_crop_long_side`` trims ``edge_crop`` from the two ends | |
| of the LONGER side only (D3's ``crop_center_by_percentage``), and ``short_clip_frames`` (D3: 8) returns that many | |
| frames, instead of repeating frames up to ``num_frames``, when the video holds fewer than ``num_frames`` frames at | |
| ``fps`` (D3 reads 16 frames, or 8 when fewer than 16 exist). With it the frame count can be below ``num_frames``. | |
| ``fps_nearest`` takes the source frame nearest to each time ``k / fps`` (what ffmpeg's ``fps`` filter writes) instead | |
| of spreading frames evenly over the window, and ``bgr`` returns the channels in BGR order (D3 reads its frames with | |
| ``cv2.imread`` and never converts them to RGB, so its model sees BGR). | |
| """ | |
| try: | |
| reader = VideoReader(str(path), ctx=cpu(0)) | |
| num_source = len(reader) | |
| except Exception: | |
| return torch.zeros(num_frames, 3, size, size) | |
| if num_source == 0: | |
| return torch.zeros(num_frames, 3, size, size) | |
| avg_fps = float(reader.get_avg_fps()) or 30.0 | |
| duration = num_source / avg_fps | |
| window = min(window_seconds, duration) | |
| if fps and fps > 0: | |
| if short_clip_frames and int(duration * fps) < num_frames: # too short for num_frames at this fps | |
| num_frames = short_clip_frames | |
| window = min(window, num_frames / float(fps)) | |
| n_window = max(int(round(window * avg_fps)), 1) # frames spanning the window | |
| start_index = 0 | |
| if window_center: # center the window at the midpoint | |
| start_index = int(round((duration / 2.0 - window / 2.0) * avg_fps)) | |
| start_index = min(max(start_index, 0), num_source - 1) | |
| end_index = min(start_index + n_window - 1, num_source - 1) | |
| if fps_nearest and fps and fps > 0: | |
| indices = start_index + np.rint(np.arange(num_frames) * avg_fps / float(fps)) | |
| else: | |
| indices = np.linspace(start_index, end_index, num=num_frames) | |
| indices = np.clip(np.rint(indices).astype(np.int64), 0, num_source - 1) | |
| # decord's batched get_batch raises a size-mismatch DECORDError on videos whose frames | |
| # vary in resolution (or are partly corrupt); fall back to per-frame decode+resize, and | |
| # finally to a black clip, so one bad video never crashes the whole DataLoader worker. | |
| def _resize(t): # t: [T,3,H,W] float | |
| if edge_crop and edge_crop > 0.0: | |
| # Crop the central (1 - 2*edge_crop) region off each frame BEFORE resizing — the official | |
| # D3 preprocessing trims borders/letterboxing that otherwise dominate the frozen-feature | |
| # trajectory. Default 0.0 leaves D3-without-crop / ReStraV / STALL untouched. | |
| h, w = t.shape[-2], t.shape[-1] | |
| ch, cw = int(round(h * edge_crop)), int(round(w * edge_crop)) | |
| if edge_crop_long_side: # D3: crop only along the longer side | |
| ch, cw = (0, int(w * edge_crop)) if w > h else (int(h * edge_crop), 0) | |
| if h - 2 * ch >= 8 and w - 2 * cw >= 8: | |
| t = t[..., ch:h - ch, cw:w - cw] | |
| if t.shape[-2:] != (size, size): | |
| # Default ("bilinear") preserves the original no-antialias behavior (D3/STALL unchanged); | |
| # "bicubic" enables antialiasing to match the official ReStraV resize. | |
| t = F.interpolate(t, size=(size, size), mode=interp, align_corners=False, | |
| antialias=(interp == "bicubic")) | |
| return t | |
| try: | |
| frames = reader.get_batch(indices.tolist()).asnumpy() # [T, H, W, 3] uint8 | |
| clip = torch.from_numpy(frames).permute(0, 3, 1, 2).float() / 255.0 | |
| clip = _resize(clip) | |
| except Exception: | |
| per_frame = [] | |
| for i in indices.tolist(): | |
| try: | |
| f = reader[i].asnumpy() # [H, W, 3] | |
| ft = torch.from_numpy(f).permute(2, 0, 1).unsqueeze(0).float() / 255.0 | |
| per_frame.append(_resize(ft)) | |
| except Exception: | |
| per_frame.append(torch.zeros(1, 3, size, size)) | |
| clip = torch.cat(per_frame, dim=0) if per_frame else torch.zeros(num_frames, 3, size, size) | |
| # Coerce to exactly 3 channels: some clips decode to 1 (grayscale), 4 (RGBA) or more | |
| # (e.g. packed/stereo streams) channels, which breaks the 3-channel Normalize downstream. | |
| c = clip.shape[1] | |
| if c == 1: | |
| clip = clip.repeat(1, 3, 1, 1) | |
| elif c != 3: | |
| clip = clip[:, :3] | |
| if bgr: | |
| clip = clip[:, [2, 1, 0]] | |
| return clip.clamp_(0.0, 1.0) | |
| def _safe_extract_zip(zip_path: str | Path, destination_dir: str | Path) -> None: | |
| """Extract a zip file without allowing path traversal outside destination_dir.""" | |
| zip_path = Path(zip_path) | |
| destination_dir = Path(destination_dir) | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| destination_root = destination_dir.resolve() | |
| with zipfile.ZipFile(zip_path, "r") as zf: | |
| for member in zf.infolist(): | |
| member_path = (destination_dir / member.filename).resolve() | |
| if not member_path.is_relative_to(destination_root): | |
| raise RuntimeError(f"Refusing to extract unsafe zip member: {member.filename}") | |
| zf.extractall(destination_dir) | |
| def _safe_extract_tar(tar_path: str | Path, destination_dir: str | Path) -> None: | |
| """Extract a tar file without allowing path traversal outside destination_dir.""" | |
| tar_path = Path(tar_path) | |
| destination_dir = Path(destination_dir) | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| destination_root = destination_dir.resolve() | |
| with tarfile.open(tar_path, "r:*") as tf: | |
| for member in tf.getmembers(): | |
| member_path = (destination_dir / member.name).resolve() | |
| if not member_path.is_relative_to(destination_root): | |
| raise RuntimeError(f"Refusing to extract unsafe tar member: {member.name}") | |
| tf.extractall(destination_dir) | |
| def _download_hf_file( | |
| repo_id: str, | |
| filename: str, | |
| download_dir: str | Path, | |
| repo_type: str = "dataset", | |
| ) -> Path: | |
| """Download one file from the Hugging Face Hub into download_dir and return its local path.""" | |
| from huggingface_hub import hf_hub_download | |
| download_dir = Path(download_dir) | |
| download_dir.mkdir(parents=True, exist_ok=True) | |
| return Path( | |
| hf_hub_download( | |
| repo_id=repo_id, | |
| filename=filename, | |
| repo_type=repo_type, | |
| local_dir=download_dir, | |
| ) | |
| ) | |
| class CoSpyDataset(Dataset): | |
| """CO-SPY-Bench test-only dataset (Cheng et al., CVPR 2025). | |
| The benchmark lives in two HuggingFace dataset repos: | |
| * Synthetic images: ``ruojiruoli/Co-Spy-Bench`` as | |
| ``{source_dataset}/{model}/images_with_metadata.tar.gz`` archives, | |
| with five real-image source datasets and 22 generator models. | |
| * Real images: ``ruojiruoli/Co-Spy-Misc`` as a single | |
| ``real_image_examples.zip`` archive (one subfolder per source). | |
| After download, ``prepare_split`` restructures the data into the exact | |
| layout used by ``CNNDetectionDataset`` (``cnn_synth_testset``):: | |
| <root>/test/ | |
| ├── <source>__<model>/ | |
| │ ├── 0_real/*.png (hardlinks into a single shared real pool) | |
| │ └── 1_fake/*.png (moved from the extracted tarball) | |
| ├── ... | |
| └── (5 * 22 = 110 class folders) | |
| The class name uses ``<source>__<model>`` (separator ``__``), which gives | |
| up to 110 unique ``folder_name``s matching the granularity of Table 2 in | |
| the paper. Real images are physically stored once per source under | |
| ``<root>/test/_real_pool/<source>/`` and exposed inside each per-model | |
| ``0_real/`` directory via ``os.link`` (zero extra disk usage); a | |
| ``shutil.copy2`` fallback is used on filesystems that do not support | |
| hardlinks. With this layout, label and folder extraction reuse the | |
| same convention as ``CNNDetectionDataset``: the leaf folder name | |
| (``0_real``/``1_fake``) drives the label and the grandparent folder | |
| (``<source>__<model>``) drives ``folder_name``. | |
| Note: ``ruojiruoli/Co-Spy-Misc/real_image_examples.zip`` is a *partial* | |
| release that ships reals for ``CC3M``/``SBU``/``TextCaps`` only (with | |
| that exact casing). Reals for ``mscoco`` and ``flickr`` must be obtained | |
| from their original sources and placed under | |
| ``<root>/test/_real_pool/<source>/``; class folders without reals will | |
| contain only ``1_fake/``. | |
| """ | |
| repo_id = "ruojiruoli/Co-Spy-Bench" | |
| real_repo_id = "ruojiruoli/Co-Spy-Misc" | |
| real_filename = "real_image_examples.zip" | |
| default_source_datasets: tuple[str, ...] = ( | |
| "mscoco", | |
| "flickr", | |
| "cc3m", | |
| "textcaps", | |
| "sbu", | |
| ) | |
| default_generator_models: tuple[str, ...] = ( | |
| "CompVis@ldm-text2im-large-256", | |
| "CompVis@stable-diffusion-v1-4", | |
| "runwayml@stable-diffusion-v1-5", | |
| "segmind@SSD-1B", | |
| "segmind@tiny-sd", | |
| "segmind@SegMoE-SD-4x2-v0", | |
| "segmind@small-sd", | |
| "stabilityai@stable-diffusion-2-1", | |
| "stabilityai@stable-diffusion-3-medium-diffusers", | |
| "stabilityai@sdxl-turbo", | |
| "stabilityai@stable-diffusion-2", | |
| "stabilityai@stable-diffusion-xl-base-1.0", | |
| "playgroundai@playground-v2.5-1024px-aesthetic", | |
| "playgroundai@playground-v2-1024px-aesthetic", | |
| "playgroundai@playground-v2-512px-base", | |
| "playgroundai@playground-v2-256px-base", | |
| "PixArt-alpha@PixArt-XL-2-1024-MS", | |
| "PixArt-alpha@PixArt-XL-2-512x512", | |
| "latent-consistency@lcm-lora-sdxl", | |
| "latent-consistency@lcm-lora-sdv1-5", | |
| "black-forest-labs@FLUX.1-schnell", | |
| "black-forest-labs@FLUX.1-dev", | |
| ) | |
| CLASS_SEPARATOR = "__" | |
| REAL_LABEL_DIR = "0_real" | |
| FAKE_LABEL_DIR = "1_fake" | |
| REAL_POOL_DIR = "_real_pool" | |
| def __init__( | |
| self, | |
| root_dir: str | Path, | |
| split: Literal["test"] = "test", | |
| transform: AIGITransformations | None = None, | |
| prefetch: bool = False, | |
| source_datasets: tuple[str, ...] | list[str] | None = None, | |
| generator_models: tuple[str, ...] | list[str] | None = None, | |
| include_real: bool = True, | |
| repo_id: str | None = None, | |
| real_repo_id: str | None = None, | |
| real_filename: str | None = None, | |
| ): | |
| super().__init__() | |
| if split != "test": | |
| raise ValueError("CoSpyDataset only supports the 'test' split.") | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.transform = transform | |
| self.repo_id = repo_id or self.repo_id | |
| self.real_repo_id = real_repo_id or self.real_repo_id | |
| self.real_filename = real_filename or self.real_filename | |
| self.source_datasets = tuple(source_datasets) if source_datasets else self.default_source_datasets | |
| self.generator_models = tuple(generator_models) if generator_models else self.default_generator_models | |
| self.include_real = include_real | |
| self.destination_dir = self.root_dir / self.split | |
| if prefetch and not _has_images(self.destination_dir): | |
| print("Downloading CO-SPY-Bench test split from Hugging Face...") | |
| self._download_data() | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def prepare_split( | |
| cls, | |
| root_dir: str | Path, | |
| split: Literal["test"] = "test", | |
| source_datasets: tuple[str, ...] | list[str] | None = None, | |
| generator_models: tuple[str, ...] | list[str] | None = None, | |
| include_real: bool = True, | |
| repo_id: str = "ruojiruoli/Co-Spy-Bench", | |
| real_repo_id: str = "ruojiruoli/Co-Spy-Misc", | |
| real_filename: str = "real_image_examples.zip", | |
| force: bool = False, | |
| keep_downloads: bool = False, | |
| ) -> None: | |
| """Download CO-SPY-Bench and lay it out in CNNDetection-style ``test/`` tree.""" | |
| if split != "test": | |
| raise ValueError("CoSpyDataset only supports the 'test' split.") | |
| from huggingface_hub import hf_hub_download, snapshot_download | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if not force and _has_images(destination_dir): | |
| return | |
| if force: | |
| shutil.rmtree(destination_dir, ignore_errors=True) | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| sources = tuple(source_datasets) if source_datasets else cls.default_source_datasets | |
| models = tuple(generator_models) if generator_models else cls.default_generator_models | |
| bench_download_dir = root_dir / "_downloads" / "Co-Spy-Bench" | |
| patterns = [ | |
| f"{ds}/{model}/images_with_metadata.tar.gz" | |
| for ds in sources | |
| for model in models | |
| ] | |
| snapshot_download( | |
| repo_id=repo_id, | |
| repo_type="dataset", | |
| local_dir=bench_download_dir, | |
| allow_patterns=patterns, | |
| ) | |
| for tar_path in sorted(bench_download_dir.rglob("images_with_metadata.tar.gz")): | |
| rel = tar_path.relative_to(bench_download_dir) | |
| if len(rel.parts) < 2: | |
| continue | |
| source_name = rel.parts[0] | |
| extract_target = bench_download_dir / source_name | |
| _safe_extract_tar(tar_path, extract_target) | |
| real_extracted_root: Path | None = None | |
| if include_real: | |
| real_download_dir = root_dir / "_downloads" / "Co-Spy-Misc" | |
| real_download_dir.mkdir(parents=True, exist_ok=True) | |
| real_path = hf_hub_download( | |
| repo_id=real_repo_id, | |
| filename=real_filename, | |
| repo_type="dataset", | |
| local_dir=real_download_dir, | |
| ) | |
| real_extract_dir = real_download_dir / "extracted" | |
| _safe_extract_zip(real_path, real_extract_dir) | |
| real_extracted_root = cls._find_real_root(real_extract_dir, sources) | |
| cls._restructure_to_cnndet_layout( | |
| synthetic_root=bench_download_dir, | |
| real_root=real_extracted_root, | |
| destination_dir=destination_dir, | |
| sources=sources, | |
| models=models, | |
| ) | |
| if not _has_images(destination_dir): | |
| raise RuntimeError( | |
| "CO-SPY-Bench download did not produce any recognizable images. " | |
| f"Check that the HuggingFace repo {repo_id!r} is reachable and " | |
| "that at least one (source_dataset, model) pair has been requested." | |
| ) | |
| if not keep_downloads: | |
| shutil.rmtree(root_dir / "_downloads", ignore_errors=True) | |
| def prepare_dataset( | |
| cls, | |
| root_dir: str | Path, | |
| source_datasets: tuple[str, ...] | list[str] | None = None, | |
| generator_models: tuple[str, ...] | list[str] | None = None, | |
| include_real: bool = True, | |
| repo_id: str = "ruojiruoli/Co-Spy-Bench", | |
| real_repo_id: str = "ruojiruoli/Co-Spy-Misc", | |
| real_filename: str = "real_image_examples.zip", | |
| force: bool = False, | |
| keep_downloads: bool = False, | |
| ) -> None: | |
| """Download CO-SPY-Bench as a test-only dataset under ``root_dir/test``.""" | |
| cls.prepare_split( | |
| root_dir=root_dir, | |
| split="test", | |
| source_datasets=source_datasets, | |
| generator_models=generator_models, | |
| include_real=include_real, | |
| repo_id=repo_id, | |
| real_repo_id=real_repo_id, | |
| real_filename=real_filename, | |
| force=force, | |
| keep_downloads=keep_downloads, | |
| ) | |
| def _case_insensitive_subdir(parent: Path, name: str) -> Path | None: | |
| """Return ``parent/<entry>`` whose lowercase name equals ``name.lower()``.""" | |
| if not parent.is_dir(): | |
| return None | |
| target = name.lower() | |
| for entry in parent.iterdir(): | |
| if entry.is_dir() and entry.name.lower() == target: | |
| return entry | |
| return None | |
| def _find_real_root(cls, real_extract_dir: Path, sources: tuple[str, ...]) -> Path | None: | |
| """Locate the directory that holds per-source real-image subfolders. | |
| The upstream ``real_image_examples.zip`` only ships reals for a subset | |
| of the five Co-Spy sources (currently ``CC3M``/``SBU``/``TextCaps``) | |
| and uses an inconsistent casing for the per-source folder names, so | |
| the search is case-insensitive and accepts a partial source set. | |
| """ | |
| candidates: list[Path] = [ | |
| real_extract_dir / "real_image_examples", | |
| real_extract_dir, | |
| ] | |
| candidates.extend(p for p in real_extract_dir.rglob("real_image_examples") if p.is_dir()) | |
| for candidate in candidates: | |
| if not candidate.is_dir(): | |
| continue | |
| if any(cls._case_insensitive_subdir(candidate, src) is not None for src in sources): | |
| return candidate | |
| for candidate in real_extract_dir.rglob("*"): | |
| if not candidate.is_dir(): | |
| continue | |
| if any(cls._case_insensitive_subdir(candidate, src) is not None for src in sources): | |
| return candidate | |
| return None | |
| def _restructure_to_cnndet_layout( | |
| cls, | |
| synthetic_root: Path, | |
| real_root: Path | None, | |
| destination_dir: Path, | |
| sources: tuple[str, ...], | |
| models: tuple[str, ...], | |
| ) -> None: | |
| """Move synthetic + hardlink real images into ``<source>__<model>/{0_real,1_fake}``.""" | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| real_pool_dir = destination_dir / cls.REAL_POOL_DIR | |
| if real_root is not None: | |
| for source in sources: | |
| source_real_dir = cls._case_insensitive_subdir(real_root, source) | |
| if source_real_dir is None: | |
| continue | |
| pool_target = real_pool_dir / source | |
| pool_target.mkdir(parents=True, exist_ok=True) | |
| for img in _iter_images(source_real_dir): | |
| target = cls._dedup_name(pool_target / img.name, img) | |
| if target.exists(): | |
| continue | |
| shutil.move(str(img), str(target)) | |
| report: list[tuple[str, str, int, int]] = [] | |
| for source in sources: | |
| for model in models: | |
| model_synthetic_dir = synthetic_root / source / model | |
| model_images = ( | |
| list(_iter_images(model_synthetic_dir)) | |
| if model_synthetic_dir.is_dir() | |
| else [] | |
| ) | |
| source_real_pool = real_pool_dir / source | |
| pool_images = ( | |
| list(_iter_images(source_real_pool)) | |
| if source_real_pool.is_dir() | |
| else [] | |
| ) | |
| if not model_images and not pool_images: | |
| continue | |
| class_dir = destination_dir / f"{source}{cls.CLASS_SEPARATOR}{model}" | |
| if model_images: | |
| fake_dir = class_dir / cls.FAKE_LABEL_DIR | |
| fake_dir.mkdir(parents=True, exist_ok=True) | |
| for img in model_images: | |
| target = cls._dedup_name(fake_dir / img.name, img) | |
| if target.exists(): | |
| continue | |
| shutil.move(str(img), str(target)) | |
| if pool_images: | |
| real_dir = class_dir / cls.REAL_LABEL_DIR | |
| real_dir.mkdir(parents=True, exist_ok=True) | |
| for img in pool_images: | |
| target = cls._dedup_name(real_dir / img.name, img) | |
| if target.exists(): | |
| continue | |
| cls._hardlink_or_copy(img, target) | |
| report.append((source, model, len(model_images), len(pool_images))) | |
| if report: | |
| sources_missing_reals = sorted({src for src, _, _, n_real in report if n_real == 0}) | |
| sources_with_reals = sorted({src for src, _, _, n_real in report if n_real > 0}) | |
| print(f"[CoSpyDataset] reals available for sources : {sources_with_reals}") | |
| if sources_missing_reals: | |
| print( | |
| f"[CoSpyDataset] reals MISSING for sources : {sources_missing_reals} " | |
| f"(class folders will have only '{cls.FAKE_LABEL_DIR}/'; provide reals " | |
| f"under '{real_pool_dir}/<source>/' to include them)" | |
| ) | |
| def _hardlink_or_copy(src: Path, dst: Path) -> None: | |
| """Create ``dst`` as a hardlink to ``src``; fall back to a copy on failure.""" | |
| try: | |
| os.link(src, dst) | |
| except OSError: | |
| shutil.copy2(src, dst) | |
| def _dedup_name(target: Path, source: Path) -> Path: | |
| """Append a short SHA-1 prefix to ``target`` when its name already exists.""" | |
| if not target.exists(): | |
| return target | |
| digest = hashlib.sha1(str(source).encode("utf-8")).hexdigest()[:10] | |
| return target.with_name(f"{target.stem}_{digest}{target.suffix}") | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split( | |
| root_dir=self.root_dir, | |
| split=self.split, | |
| source_datasets=self.source_datasets, | |
| generator_models=self.generator_models, | |
| include_real=self.include_real, | |
| repo_id=self.repo_id, | |
| real_repo_id=self.real_repo_id, | |
| real_filename=self.real_filename, | |
| force=force, | |
| ) | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _get_label(filepath: Path) -> int: | |
| name = filepath.parent.name.lower() | |
| if "real" in name: | |
| return 0 | |
| if "fake" in name or "false" in name: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from filename: {filepath}") | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| split_dir = self.destination_dir | |
| pool_marker = self.REAL_POOL_DIR | |
| for filepath in _iter_images(split_dir): | |
| if pool_marker in filepath.parts: | |
| continue | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append( | |
| filepath.parent.parent.name | |
| if filepath.parent.parent != split_dir.parent | |
| else self.split | |
| ) | |
| return images, labels, folder_names | |
| class GenImageDataset(Dataset): | |
| google_drive_folder_url = "https://drive.google.com/drive/folders/1jGt10bwTbhEZuGXLyvrCuxOI0cBqQ1FS" | |
| hf_repo_id = None | |
| hf_subdir = None | |
| split_aliases = { | |
| "train": "train", | |
| "val": "test", | |
| "test": "test", | |
| } | |
| source_split_markers = { | |
| "train": ("train",), | |
| "test": ("val", "validation", "test"), | |
| } | |
| class_dir_to_label = { | |
| "nature": "real", | |
| "real": "real", | |
| "ai": "fake", | |
| "fake": "fake", | |
| "false": "fake", | |
| } | |
| def __init__(self, root_dir: str | Path, split: Literal["train", "val", "test"], transform: AIGITransformations | None = None, prefetch: bool = False): | |
| super().__init__() | |
| self.root_dir = Path(root_dir) | |
| self.requested_split = split | |
| self.split = self.split_aliases[split] | |
| self.transform = transform | |
| self.prefetch = prefetch | |
| self.destination_dir = self.root_dir / self.split | |
| if prefetch and not _has_images(self.destination_dir): | |
| print(f"Downloading {self.split} split from Google Drive...") | |
| self._download_data() | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def _tokens(path: Path) -> list[str]: | |
| tokens: list[str] = [] | |
| for part in path.parts: | |
| tokens.extend(token for token in re.split(r"[^a-z0-9]+", part.lower()) if token) | |
| return tokens | |
| def download_files( | |
| cls, | |
| download_dir: str | Path, | |
| url: str | None = None, | |
| force: bool = False, | |
| ) -> Path: | |
| """Download the shared GenImage Google Drive folder into download_dir.""" | |
| download_dir = Path(download_dir) | |
| if download_dir.exists() and any(download_dir.iterdir()) and not force: | |
| return download_dir | |
| if force: | |
| shutil.rmtree(download_dir, ignore_errors=True) | |
| download_dir.mkdir(parents=True, exist_ok=True) | |
| if cls.hf_repo_id: | |
| from huggingface_hub import hf_hub_download, list_repo_files | |
| repo_files = [f for f in list_repo_files(cls.hf_repo_id, repo_type="dataset") if not f.endswith("/")] | |
| if cls.hf_subdir: | |
| prefix = str(cls.hf_subdir).rstrip("/") + "/" | |
| repo_files = [f for f in repo_files if f.startswith(prefix)] | |
| if not repo_files: | |
| raise RuntimeError( | |
| f"No files found in HF dataset {cls.hf_repo_id!r} under subdir {cls.hf_subdir!r}." | |
| ) | |
| for filename in sorted(repo_files): | |
| hf_hub_download( | |
| repo_id=cls.hf_repo_id, | |
| filename=filename, | |
| repo_type="dataset", | |
| local_dir=str(download_dir), | |
| ) | |
| return download_dir | |
| try: | |
| import gdown | |
| except ImportError as exc: | |
| raise ImportError( | |
| "Downloading the GenImage Google Drive folder requires `gdown`. " | |
| "Install it with `pip install gdown`." | |
| ) from exc | |
| gdown.download_folder( | |
| url=url or cls.google_drive_folder_url, | |
| output=str(download_dir), | |
| quiet=False, | |
| use_cookies=False, | |
| ) | |
| return download_dir | |
| def _extract_downloaded_archives(cls, download_dir: Path) -> None: | |
| for zip_path in sorted(download_dir.rglob("*.zip")): | |
| extract_dir = zip_path.with_suffix("") | |
| if _has_images(extract_dir): | |
| continue | |
| if zip_path.with_suffix(".z01").exists(): | |
| cls._extract_spanned_zip(zip_path, extract_dir) | |
| else: | |
| _safe_extract_zip(zip_path, extract_dir) | |
| def _extract_spanned_zip(zip_path: Path, destination_dir: Path) -> None: | |
| """Extract a multi-part (spanned) zip (.z01..+.zip) using 7z.""" | |
| seven_zip = shutil.which("7z") or shutil.which("7za") | |
| if seven_zip is None: | |
| raise RuntimeError( | |
| "Extracting multi-part (spanned) zip archives requires the `7z` command. Install p7zip-full." | |
| ) | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| subprocess.run( | |
| [seven_zip, "x", str(zip_path), f"-o{destination_dir}", "-y"], | |
| cwd=zip_path.parent, | |
| check=True, | |
| ) | |
| def _source_split_for_path(cls, filepath: Path, destination_split: Literal["train", "test"]) -> bool: | |
| tokens = cls._tokens(filepath.parent) | |
| return any(marker in tokens for marker in cls.source_split_markers[destination_split]) | |
| def _label_dir_for_path(cls, filepath: Path) -> str | None: | |
| tokens = cls._tokens(filepath.parent) | |
| for source_name, label_dir in cls.class_dir_to_label.items(): | |
| if source_name in tokens: | |
| return label_dir | |
| return None | |
| def _relative_image_path(cls, filepath: Path) -> Path: | |
| parts = list(filepath.parts) | |
| lower_parts = [part.lower() for part in parts] | |
| marker_index = -1 | |
| for idx, part in enumerate(lower_parts[:-1]): | |
| tokens = cls._tokens(Path(part)) | |
| if any(token in cls.source_split_markers["train"] + cls.source_split_markers["test"] for token in tokens): | |
| marker_index = idx | |
| if any(token in cls.class_dir_to_label for token in tokens): | |
| marker_index = idx | |
| relative_parts = parts[marker_index + 1 :] if marker_index >= 0 else [filepath.name] | |
| return Path(*relative_parts) | |
| def _deduplicated_destination(destination_path: Path, source_path: Path) -> Path: | |
| if not destination_path.exists(): | |
| return destination_path | |
| digest = hashlib.sha1(str(source_path).encode("utf-8")).hexdigest()[:10] | |
| return destination_path.with_name(f"{destination_path.stem}_{digest}{destination_path.suffix}") | |
| def prepare_dataset( | |
| cls, | |
| root_dir: str | Path, | |
| url: str | None = None, | |
| force: bool = False, | |
| keep_downloads: bool = True, | |
| ) -> None: | |
| """Download GenImage and normalize it to root/train and root/test.""" | |
| root_dir = Path(root_dir) | |
| train_dir = root_dir / "train" | |
| test_dir = root_dir / "test" | |
| if not force and _has_images(train_dir) and _has_images(test_dir): | |
| return | |
| if force: | |
| shutil.rmtree(train_dir, ignore_errors=True) | |
| shutil.rmtree(test_dir, ignore_errors=True) | |
| download_dir = root_dir / "_downloads" / "GenImage" | |
| cls.download_files(download_dir, url=url, force=force) | |
| cls._extract_downloaded_archives(download_dir) | |
| copied = {"train": 0, "test": 0} | |
| for destination_split in ("train", "test"): | |
| destination_split_dir = root_dir / destination_split | |
| destination_split_dir.mkdir(parents=True, exist_ok=True) | |
| for source_path in _iter_images(download_dir): | |
| source_relative_path = source_path.relative_to(download_dir) | |
| if not cls._source_split_for_path(source_relative_path, destination_split): | |
| continue | |
| label_dir = cls._label_dir_for_path(source_relative_path) | |
| if label_dir is None: | |
| continue | |
| relative_path = cls._relative_image_path(source_relative_path) | |
| destination_path = cls._deduplicated_destination( | |
| destination_split_dir / label_dir / relative_path, | |
| source_path, | |
| ) | |
| destination_path.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(source_path, destination_path) | |
| copied[destination_split] += 1 | |
| missing = [split for split, count in copied.items() if count == 0] | |
| if missing: | |
| raise RuntimeError( | |
| "GenImage download did not contain recognizable images for " | |
| f"{', '.join(missing)}. Expected train/val split folders and ai/nature class folders." | |
| ) | |
| if not keep_downloads: | |
| shutil.rmtree(root_dir / "_downloads", ignore_errors=True) | |
| def prepare_split( | |
| cls, | |
| root_dir: str | Path, | |
| split: Literal["train", "val", "test"], | |
| url: str | None = None, | |
| force: bool = False, | |
| keep_downloads: bool = True, | |
| ) -> None: | |
| """Download and normalize GenImage; val is stored as the test split.""" | |
| destination_split = cls.split_aliases[split] | |
| destination_dir = Path(root_dir) / destination_split | |
| if not force and _has_images(destination_dir): | |
| return | |
| cls.prepare_dataset(root_dir, url=url, force=force, keep_downloads=keep_downloads) | |
| def _download_data(self, force: bool = False) -> None: | |
| """Download and normalize this dataset split from Google Drive.""" | |
| self.prepare_split( | |
| root_dir=self.root_dir, | |
| split=self.requested_split, | |
| url=self.google_drive_folder_url, | |
| force=force, | |
| ) | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _iter_images(self, directory: Path): | |
| return _iter_images(directory) | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| split_dir = self.destination_dir | |
| for filepath in self._iter_images(split_dir): | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append(self.split) | |
| return images, labels, folder_names | |
| def _get_label(filepath: Path) -> int: | |
| names = [part.lower() for part in filepath.parent.parts] | |
| if "real" in names: | |
| return 0 | |
| if "fake" in names or "false" in names: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from filename: {filepath}") | |
| class CNNDetectionDataset(Dataset): | |
| google_drive_folder_url = "https://drive.google.com/drive/u/0/folders/1RwCSaraEUctIwFgoQXWMKFvW07gM80_3" | |
| archive_name_markers = { | |
| "train": ("progan_train", "train"), | |
| "val": ("progan_val", "val"), | |
| "test": ("cnn_synth_testset", "test"), | |
| } | |
| def __init__(self, root_dir: str | Path, split: Literal["train", "val", "test"], transform: AIGITransformations | None = None, prefetch: bool = False): | |
| super().__init__() | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.transform = transform | |
| self.prefetch = prefetch | |
| self.destination_dir = self.root_dir / self.split | |
| if prefetch and not _has_images(self.destination_dir): | |
| print(f"Downloading {self.split} split from Google Drive...") | |
| self._download_data() | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def download_archives( | |
| cls, | |
| archive_dir: str | Path, | |
| url: str | None = None, | |
| force: bool = False, | |
| ) -> Path: | |
| """Download the shared CNNDetection Google Drive folder into archive_dir.""" | |
| archive_dir = Path(archive_dir) | |
| if archive_dir.exists() and any(archive_dir.iterdir()) and not force: | |
| return archive_dir | |
| archive_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| import gdown | |
| except ImportError as exc: | |
| raise ImportError( | |
| "Downloading a Google Drive folder requires `gdown`. " | |
| "Install it with `pip install gdown`." | |
| ) from exc | |
| gdown.download_folder( | |
| url=url or cls.google_drive_folder_url, | |
| output=str(archive_dir), | |
| quiet=False, | |
| use_cookies=False, | |
| ) | |
| return archive_dir | |
| def _find_split_archives( | |
| cls, | |
| archive_dir: Path, | |
| split: Literal["train", "val", "test"], | |
| ) -> list[Path]: | |
| markers = cls.archive_name_markers[split] | |
| archive_suffixes = (".zip", ".7z", ".001") | |
| archives = [ | |
| p | |
| for p in archive_dir.rglob("*") | |
| if p.is_file() | |
| and p.name.lower().endswith(archive_suffixes) | |
| and any(marker in p.name.lower() for marker in markers) | |
| ] | |
| if split == "train": | |
| zip_archives = [p for p in archives if p.suffix.lower() == ".zip"] | |
| if zip_archives: | |
| return sorted(zip_archives) | |
| first_7z_parts = [p for p in archives if p.name.lower().endswith(".7z.001")] | |
| if first_7z_parts: | |
| return sorted(first_7z_parts) | |
| return sorted(archives) | |
| def _extract_7z(archive_path: Path, destination_dir: Path) -> None: | |
| seven_zip = shutil.which("7z") or shutil.which("7za") | |
| if seven_zip is None: | |
| raise RuntimeError("Extracting .7z archives requires the `7z` command. Install p7zip-full.") | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| subprocess.run( | |
| [seven_zip, "x", str(archive_path), f"-o{destination_dir}", "-y"], | |
| cwd=archive_path.parent, | |
| check=True, | |
| ) | |
| def _extract_archive(cls, archive_path: Path, destination_dir: Path) -> None: | |
| if archive_path.suffix.lower() == ".zip": | |
| _safe_extract_zip(archive_path, destination_dir) | |
| return | |
| if archive_path.name.lower().endswith(".7z") or archive_path.name.lower().endswith(".7z.001"): | |
| cls._extract_7z(archive_path, destination_dir) | |
| for zip_path in destination_dir.rglob("*.zip"): | |
| _safe_extract_zip(zip_path, destination_dir) | |
| return | |
| raise RuntimeError(f"Unsupported archive type: {archive_path}") | |
| def prepare_split( | |
| cls, | |
| root_dir: str | Path, | |
| split: Literal["train", "val", "test"], | |
| force: bool = False, | |
| keep_archives: bool = True, | |
| ) -> None: | |
| """Download and extract one CNNDetection split under root_dir/split.""" | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if not force and _has_images(destination_dir): | |
| return | |
| archive_dir = root_dir / "_archives" / "CNNDetection" | |
| cls.download_archives(archive_dir) | |
| archive_paths = cls._find_split_archives(archive_dir, split) | |
| if not archive_paths: | |
| raise RuntimeError(f"No archive found for CNNDetection {split!r} split in {archive_dir}") | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| for archive_path in archive_paths: | |
| cls._extract_archive(archive_path, destination_dir) | |
| if not keep_archives: | |
| shutil.rmtree(archive_dir, ignore_errors=True) | |
| def prepare_dataset( | |
| cls, | |
| root_dir: str | Path, | |
| splits: tuple[Literal["train", "val", "test"], ...] = ("train", "val", "test"), | |
| force: bool = False, | |
| keep_archives: bool = True, | |
| ) -> None: | |
| """Download and extract the requested CNNDetection splits.""" | |
| for split in splits: | |
| cls.prepare_split(root_dir, split, force=force, keep_archives=keep_archives) | |
| def _download_data(self, force: bool = False) -> None: | |
| """Download and extract this dataset split under root_dir/split.""" | |
| self.prepare_split(self.root_dir, self.split, force=force) | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _get_label(filepath: Path) -> int: | |
| name = filepath.parent.name.lower() | |
| if "real" in name: | |
| return 0 | |
| if "fake" in name or "false" in name: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from filename: {filepath}") | |
| def _iter_images(self, directory: Path): | |
| return _iter_images(directory) | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| split_dir = self.destination_dir | |
| for filepath in self._iter_images(split_dir): | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append(filepath.parent.parent.name if filepath.parent.parent != split_dir.parent else self.split) | |
| return images, labels, folder_names | |
| class CNNDetectionDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| train_list = list(config.DatasetConfig.train_transform_list) | |
| eval_list = list(config.DatasetConfig.eval_transform_list) | |
| self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = getattr(config.DatasetConfig, "prefetch", False) | |
| self.train_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch) | |
| self.val_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "val", self.eval_transform, prefetch=prefetch) | |
| self.test_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch) | |
| def train_dataloader(self): | |
| return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def val_dataloader(self): | |
| return DataLoader(self.val_dataset, batch_size=self.config.DatasetConfig.val_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| class GenImageDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| train_list = list(config.DatasetConfig.train_transform_list) | |
| eval_list = list(config.DatasetConfig.eval_transform_list) | |
| self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = getattr(config.DatasetConfig, "prefetch", False) | |
| source_url = config.DatasetConfig.get("source_url", GenImageDataset.google_drive_folder_url) | |
| GenImageDataset.google_drive_folder_url = source_url | |
| GenImageDataset.hf_repo_id = config.DatasetConfig.get("hf_repo_id", None) | |
| GenImageDataset.hf_subdir = config.DatasetConfig.get("hf_subdir", None) | |
| self.train_dataset = GenImageDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch) | |
| self.val_dataset = GenImageDataset(config.DatasetConfig.root_dir, "val", self.eval_transform, prefetch=prefetch) | |
| self.test_dataset = GenImageDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch) | |
| def train_dataloader(self): | |
| return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def val_dataloader(self): | |
| return DataLoader(self.val_dataset, batch_size=self.config.DatasetConfig.val_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| class AIGIBenchDataset(Dataset): | |
| def __init__( | |
| self, | |
| root_dir: str | Path, | |
| split: Literal["train", "val", "test"], | |
| setting: Literal["Setting-I", "Setting-II"], | |
| transform: AIGITransformations | None = None, | |
| prefetch: bool = False, | |
| ): | |
| super().__init__() | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.setting = setting | |
| self.transform = transform | |
| self.unique_folder_names = ["train"] | |
| self.destination_dir = self.root_dir / self.split | |
| self.huggingface_path = "https://huggingface.co/datasets/HorizonTEL/AIGIBench/tree/main" | |
| self.huggingface_repo_id = "HorizonTEL/AIGIBench" | |
| if prefetch: | |
| if _has_images(self.destination_dir): | |
| print(f"on prefetch_hf mode, {self.destination_dir} already exists") | |
| else: | |
| print(f"Downloading {self.split} split from Hugging Face...") | |
| self._download_data() | |
| if self.split != "train": | |
| self.unique_folder_names += [d.name for d in self.destination_dir.iterdir() if d.is_dir()] if self.destination_dir.exists() else [] | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| def _split_has_images(directory: Path) -> bool: | |
| return _has_images(directory) | |
| def prepare_split( | |
| cls, | |
| destination_dir: str | Path, | |
| split: Literal["train", "val", "test"], | |
| repo_id: str = "HorizonTEL/AIGIBench", | |
| force: bool = False, | |
| ) -> None: | |
| """Download all zip archives for one split from the Hub and extract under root_dir.""" | |
| from huggingface_hub import hf_hub_download, list_repo_files | |
| destination_dir = Path(destination_dir) | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| prefix = f"{split}/" | |
| files = list_repo_files(repo_id, repo_type="dataset") | |
| zips = sorted(f for f in files if f.startswith(prefix) and f.lower().endswith(".zip")) | |
| if not zips: | |
| raise RuntimeError(f"No .zip files under {prefix!r} in dataset {repo_id!r}") | |
| for rel in zips: | |
| stem = Path(rel).stem | |
| target_dir = destination_dir / stem | |
| if not force and target_dir.is_dir() and cls._split_has_images(target_dir): | |
| continue | |
| local_zip = hf_hub_download( | |
| repo_id=repo_id, | |
| filename=rel, | |
| repo_type="dataset", | |
| ) | |
| _safe_extract_zip(local_zip, destination_dir) | |
| def prepare_dataset( | |
| cls, | |
| root_dir: str | Path, | |
| splits: tuple[Literal["train", "val", "test"], ...] = ("train", "val", "test"), | |
| repo_id: str = "HorizonTEL/AIGIBench", | |
| force: bool = False, | |
| ) -> None: | |
| """Download and extract AIGIBench split archives under root_dir/train, root_dir/val, root_dir/test.""" | |
| root_dir = Path(root_dir) | |
| for split in splits: | |
| cls.prepare_split( | |
| destination_dir=root_dir / split, | |
| split=split, | |
| repo_id=repo_id, | |
| force=force, | |
| ) | |
| def _download_data(self, force: bool = False) -> None: | |
| """Download and extract this dataset split from Hugging Face (skips subsets that already contain images).""" | |
| self.prepare_split( | |
| destination_dir=self.destination_dir, | |
| split=self.split, | |
| repo_id=self.huggingface_repo_id, | |
| force=force, | |
| ) | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _get_label(filepath: Path, folder_names: list[str] | None = None) -> int: | |
| name = filepath.parent.name | |
| if "real" in name: | |
| return 0 | |
| elif "fake" in name or "false" in name: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from filename: {filepath}") | |
| def _iter_images(self, directory: Path): | |
| return _iter_images(directory) | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| split_dir = self.destination_dir | |
| for filepath in self._iter_images(split_dir): | |
| if self.split == "train" and self.setting == "Setting-I": | |
| if "sdv1.4" in str(filepath): | |
| continue | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath, self.unique_folder_names)) | |
| folder_names.append( | |
| filepath.parent.parent.name if self.split != "train" else "train" | |
| ) | |
| return images, labels, folder_names | |
| class DiTFakeDataset(Dataset): | |
| repo_id = "lioooox/DiTFake" | |
| def __init__( | |
| self, | |
| root_dir: str | Path, | |
| split: Literal["test"], | |
| transform: AIGITransformations | None = None, | |
| prefetch: bool = False, | |
| repo_id: str | None = None, | |
| ): | |
| super().__init__() | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.transform = transform | |
| self.repo_id = repo_id or self.repo_id | |
| self.destination_dir = self.root_dir / self.split | |
| self._normalize_existing_layout() | |
| if prefetch and not _has_images(self.destination_dir): | |
| print("Downloading DiTFake test split from Hugging Face...") | |
| self._download_data() | |
| self.destination_dir = self.root_dir / self.split | |
| self._normalize_existing_layout() | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def _normalize_existing_layout(self) -> None: | |
| nested_split_dir = self.root_dir / "DiTFake" / self.split | |
| if _has_images(nested_split_dir) and not _has_images(self.destination_dir): | |
| if self.destination_dir.exists(): | |
| shutil.rmtree(self.destination_dir) | |
| self.destination_dir.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.move(str(nested_split_dir), str(self.destination_dir)) | |
| nested_root = self.root_dir / "DiTFake" | |
| if nested_root.exists() and not any(nested_root.rglob("*")): | |
| shutil.rmtree(nested_root, ignore_errors=True) | |
| def prepare_split( | |
| cls, | |
| root_dir: str | Path, | |
| split: Literal["test"] = "test", | |
| repo_id: str = "lioooox/DiTFake", | |
| force: bool = False, | |
| ) -> None: | |
| """Download one DiTFake split subtree into root_dir/split.""" | |
| from huggingface_hub import snapshot_download | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if not force and _has_images(destination_dir): | |
| return | |
| root_dir.mkdir(parents=True, exist_ok=True) | |
| download_dir = root_dir / "_downloads" / "DiTFake" | |
| snapshot_download( | |
| repo_id=repo_id, | |
| repo_type="dataset", | |
| local_dir=download_dir, | |
| allow_patterns=[f"DiTFake/{split}/**"], | |
| ) | |
| downloaded_split_dir = download_dir / "DiTFake" / split | |
| if downloaded_split_dir.exists() and downloaded_split_dir != destination_dir: | |
| if destination_dir.exists() and (force or not _has_images(destination_dir)): | |
| shutil.rmtree(destination_dir) | |
| destination_dir.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.move(str(downloaded_split_dir), str(destination_dir)) | |
| shutil.rmtree(root_dir / "_downloads", ignore_errors=True) | |
| nested_root = root_dir / "DiTFake" | |
| if nested_root.exists(): | |
| shutil.rmtree(nested_root, ignore_errors=True) | |
| def prepare_dataset( | |
| cls, | |
| root_dir: str | Path, | |
| repo_id: str = "lioooox/DiTFake", | |
| force: bool = False, | |
| ) -> None: | |
| """Download DiTFake test split under root_dir.""" | |
| cls.prepare_split(root_dir=root_dir, split="test", repo_id=repo_id, force=force) | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split( | |
| root_dir=self.root_dir, | |
| split=self.split, | |
| repo_id=self.repo_id, | |
| force=force, | |
| ) | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _get_label(filepath: Path) -> int: | |
| name = filepath.parent.name.lower() | |
| if "real" in name: | |
| return 0 | |
| if "fake" in name or "false" in name: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from filename: {filepath}") | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| for filepath in _iter_images(self.destination_dir): | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append(filepath.parent.parent.name) | |
| return images, labels, folder_names | |
| class FreqNetDetectionDataset(Dataset): | |
| google_drive_folder_url = "https://drive.google.com/drive/folders/11E0Knf9J1qlv2UuTnJSOFUjIIi90czSj" | |
| def __init__( | |
| self, | |
| root_dir: str | Path, | |
| split: Literal["test"], | |
| transform: AIGITransformations | None = None, | |
| prefetch: bool = False, | |
| source_url: str | None = None, | |
| ): | |
| super().__init__() | |
| if split != "test": | |
| raise ValueError("FreqNetDetectionDataset only supports the test split.") | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.transform = transform | |
| self.prefetch = prefetch | |
| self.source_url = source_url or self.google_drive_folder_url | |
| self.destination_dir = self.root_dir / self.split | |
| if prefetch and not _has_images(self.destination_dir): | |
| print("Downloading FreqNetDetection test split from Google Drive...") | |
| self._download_data() | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def download_archives( | |
| cls, | |
| archive_dir: str | Path, | |
| url: str | None = None, | |
| force: bool = False, | |
| ) -> Path: | |
| """Download the shared FreqNetDetection Google Drive folder into archive_dir.""" | |
| archive_dir = Path(archive_dir) | |
| if archive_dir.exists() and any(archive_dir.iterdir()) and not force: | |
| return archive_dir | |
| if force: | |
| shutil.rmtree(archive_dir, ignore_errors=True) | |
| archive_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| import gdown | |
| except ImportError as exc: | |
| raise ImportError( | |
| "Downloading the FreqNetDetection Google Drive folder requires `gdown`. " | |
| "Install it with `pip install gdown`." | |
| ) from exc | |
| gdown.download_folder( | |
| url=url or cls.google_drive_folder_url, | |
| output=str(archive_dir), | |
| quiet=False, | |
| use_cookies=False, | |
| ) | |
| return archive_dir | |
| def _archive_paths(archive_dir: Path) -> list[Path]: | |
| archive_suffixes = (".tar.gz", ".tgz", ".tar") | |
| return sorted( | |
| p | |
| for p in archive_dir.rglob("*") | |
| if p.is_file() and p.name.lower().endswith(archive_suffixes) | |
| ) | |
| def prepare_split( | |
| cls, | |
| root_dir: str | Path, | |
| split: Literal["test"] = "test", | |
| source_url: str | None = None, | |
| force: bool = False, | |
| keep_archives: bool = True, | |
| ) -> None: | |
| """Download FreqNetDetection generator archives and extract them under root_dir/test.""" | |
| if split != "test": | |
| raise ValueError("FreqNetDetectionDataset only supports the test split.") | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if _has_images(destination_dir) and not force: | |
| return | |
| if force: | |
| shutil.rmtree(destination_dir, ignore_errors=True) | |
| archive_dir = root_dir / "_archives" / "FreqNetDetection" | |
| cls.download_archives(archive_dir, url=source_url, force=force) | |
| archive_paths = cls._archive_paths(archive_dir) | |
| if not archive_paths: | |
| raise RuntimeError(f"No .tar archives found for FreqNetDetection in {archive_dir}") | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| for archive_path in archive_paths: | |
| _safe_extract_tar(archive_path, destination_dir) | |
| if not _has_images(destination_dir): | |
| raise RuntimeError("FreqNetDetection archives did not contain recognizable images.") | |
| if not keep_archives: | |
| shutil.rmtree(archive_dir, ignore_errors=True) | |
| def prepare_dataset( | |
| cls, | |
| root_dir: str | Path, | |
| source_url: str | None = None, | |
| force: bool = False, | |
| keep_archives: bool = True, | |
| ) -> None: | |
| """Download FreqNetDetection as a test-only dataset under root_dir/test.""" | |
| cls.prepare_split( | |
| root_dir=root_dir, | |
| split="test", | |
| source_url=source_url, | |
| force=force, | |
| keep_archives=keep_archives, | |
| ) | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split( | |
| root_dir=self.root_dir, | |
| split=self.split, | |
| source_url=self.source_url, | |
| force=force, | |
| ) | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _get_label(filepath: Path) -> int: | |
| name = filepath.parent.name.lower() | |
| if "real" in name: | |
| return 0 | |
| if "fake" in name or "false" in name: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from filename: {filepath}") | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| for filepath in _iter_images(self.destination_dir): | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append(filepath.parent.parent.name) | |
| return images, labels, folder_names | |
| class UniversialFakeDetectionDataset(Dataset): | |
| source_url = "https://drive.google.com/file/d/1FXlGIRh_Ud3cScMgSVDbEWmPDmjcrm1t/view" | |
| def __init__( | |
| self, | |
| root_dir: str | Path, | |
| split: Literal["test"], | |
| transform: AIGITransformations | None = None, | |
| prefetch: bool = False, | |
| source_url: str | None = None, | |
| ): | |
| super().__init__() | |
| if split != "test": | |
| raise ValueError("UniversialFakeDetectionDataset only supports the test split.") | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.transform = transform | |
| self.prefetch = prefetch | |
| self.source_url = source_url or self.source_url | |
| self.destination_dir = self.root_dir / self.split | |
| if prefetch and not _has_images(self.destination_dir): | |
| print(f"Downloading {self.split} split from Google Drive...") | |
| self._download_data() | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _flatten_extracted_tree(extract_dir: Path, destination_dir: Path) -> None: | |
| source_dir = extract_dir / "diffusion_models" | |
| if not source_dir.exists(): | |
| nested = [p for p in extract_dir.rglob("diffusion_models") if p.is_dir()] | |
| source_dir = nested[0] if nested else extract_dir | |
| container_names = {"diffusion_datasets", "diffusion_dataset", "datasets", "data"} | |
| child_dirs = [p for p in source_dir.iterdir() if p.is_dir()] | |
| if len(child_dirs) == 1 and child_dirs[0].name.lower() in container_names: | |
| source_dir = child_dirs[0] | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| for child in source_dir.iterdir(): | |
| target = destination_dir / child.name | |
| if target.exists(): | |
| if target.is_dir(): | |
| shutil.rmtree(target) | |
| else: | |
| target.unlink() | |
| shutil.move(str(child), str(target)) | |
| def prepare_split( | |
| cls, | |
| root_dir: str | Path, | |
| split: Literal["test"] = "test", | |
| source_url: str | None = None, | |
| force: bool = False, | |
| keep_downloads: bool = False, | |
| ) -> None: | |
| """Download the Universal Fake Detection zip and flatten diffusion_models under root_dir/test.""" | |
| if split != "test": | |
| raise ValueError("UniversialFakeDetectionDataset only supports the test split.") | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if _has_images(destination_dir) and not force: | |
| return | |
| if force: | |
| shutil.rmtree(destination_dir, ignore_errors=True) | |
| import gdown | |
| download_dir = root_dir / "_downloads" / "UniversialFakeDetection" | |
| extract_dir = download_dir / "extracted" | |
| zip_path = download_dir / "universial_fake_detection.zip" | |
| shutil.rmtree(download_dir, ignore_errors=True) | |
| download_dir.mkdir(parents=True, exist_ok=True) | |
| download_source = source_url or cls.source_url | |
| file_id_match = re.search(r"/d/([^/]+)", download_source) or re.search(r"[?&]id=([^&]+)", download_source) | |
| download_kwargs = {"id": file_id_match.group(1)} if file_id_match else {"url": download_source} | |
| gdown.download( | |
| output=str(zip_path), | |
| quiet=False, | |
| **download_kwargs, | |
| ) | |
| _safe_extract_zip(zip_path, extract_dir) | |
| cls._flatten_extracted_tree(extract_dir, destination_dir) | |
| if not _has_images(destination_dir): | |
| raise RuntimeError( | |
| "Universal Fake Detection download did not contain images after " | |
| "flattening diffusion_models into the test split." | |
| ) | |
| if not keep_downloads: | |
| shutil.rmtree(download_dir, ignore_errors=True) | |
| def prepare_dataset( | |
| cls, | |
| root_dir: str | Path, | |
| source_url: str | None = None, | |
| force: bool = False, | |
| keep_downloads: bool = False, | |
| ) -> None: | |
| """Download Universal Fake Detection as a test-only dataset under root_dir/test.""" | |
| cls.prepare_split( | |
| root_dir=root_dir, | |
| split="test", | |
| source_url=source_url, | |
| force=force, | |
| keep_downloads=keep_downloads, | |
| ) | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| for filepath in _iter_images(self.destination_dir): | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append(filepath.parent.parent.name) | |
| return images, labels, folder_names | |
| def _get_label(self, filepath: Path) -> int: | |
| name = filepath.parent.name.lower() | |
| if "real" in name: | |
| return 0 | |
| if "fake" in name or "false" in name: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from filename: {filepath}") | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split( | |
| root_dir=self.root_dir, | |
| split=self.split, | |
| source_url=self.source_url, | |
| force=force, | |
| ) | |
| class AIGIBenchDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| train_list = list(config.DatasetConfig.train_transform_list) | |
| eval_list = list(config.DatasetConfig.eval_transform_list) | |
| self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = getattr(config.DatasetConfig, "prefetch", False) | |
| self.train_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "train", config.DatasetConfig.setting, self.train_transform, prefetch=prefetch) | |
| self.val_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "val", config.DatasetConfig.setting, self.eval_transform, prefetch=prefetch) | |
| self.test_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "test", config.DatasetConfig.setting, self.eval_transform, prefetch=prefetch) | |
| def train_dataloader(self): | |
| return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def val_dataloader(self): | |
| return DataLoader(self.val_dataset, batch_size=self.config.DatasetConfig.val_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| class DiTFakeDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| eval_list = list(config.DatasetConfig.eval_transform_list) | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = getattr(config.DatasetConfig, "prefetch", False) | |
| repo_id = config.DatasetConfig.get("repo_id", DiTFakeDataset.repo_id) | |
| self.test_dataset = DiTFakeDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch, repo_id=repo_id) | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| class FreqNetDetectionDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| eval_list = list(config.DatasetConfig.eval_transform_list) | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = getattr(config.DatasetConfig, "prefetch", False) | |
| source_url = config.DatasetConfig.get("source_url", FreqNetDetectionDataset.google_drive_folder_url) | |
| self.test_dataset = FreqNetDetectionDataset( | |
| config.DatasetConfig.root_dir, | |
| "test", | |
| self.eval_transform, | |
| prefetch=prefetch, | |
| source_url=source_url, | |
| ) | |
| def train_dataloader(self): | |
| return None | |
| def val_dataloader(self): | |
| return None | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| class UniversialFakeDetectionDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| eval_list = list(config.DatasetConfig.eval_transform_list) | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = getattr(config.DatasetConfig, "prefetch", False) | |
| source_url = config.DatasetConfig.get("source_url", UniversialFakeDetectionDataset.source_url) | |
| self.test_dataset = UniversialFakeDetectionDataset( | |
| config.DatasetConfig.root_dir, | |
| "test", | |
| self.eval_transform, | |
| prefetch=prefetch, | |
| source_url=source_url, | |
| ) | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def train_dataloader(self): | |
| return None | |
| def val_dataloader(self): | |
| return None | |
| class CoSpyDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| eval_list = list(config.DatasetConfig.eval_transform_list) | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = getattr(config.DatasetConfig, "prefetch", False) | |
| source_datasets = config.DatasetConfig.get("source_datasets", None) | |
| generator_models = config.DatasetConfig.get("generator_models", None) | |
| include_real = bool(config.DatasetConfig.get("include_real", True)) | |
| repo_id = config.DatasetConfig.get("repo_id", CoSpyDataset.repo_id) | |
| real_repo_id = config.DatasetConfig.get("real_repo_id", CoSpyDataset.real_repo_id) | |
| real_filename = config.DatasetConfig.get("real_filename", CoSpyDataset.real_filename) | |
| self.test_dataset = CoSpyDataset( | |
| config.DatasetConfig.root_dir, | |
| "test", | |
| self.eval_transform, | |
| prefetch=prefetch, | |
| source_datasets=list(source_datasets) if source_datasets else None, | |
| generator_models=list(generator_models) if generator_models else None, | |
| include_real=include_real, | |
| repo_id=repo_id, | |
| real_repo_id=real_repo_id, | |
| real_filename=real_filename, | |
| ) | |
| def train_dataloader(self): | |
| return None | |
| def val_dataloader(self): | |
| return None | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| class VideoFolderDataset(Dataset): | |
| """Generic AI-generated-video benchmark backed by a folder of video files. | |
| Expected layout under ``root_dir/<split>`` (mirrors ``CNNDetectionDataset``):: | |
| <root>/<split>/ | |
| ├── <generator_a>/ | |
| │ ├── 0_real/*.mp4 (or real/) | |
| │ └── 1_fake/*.mp4 (or fake/) | |
| ├── <generator_b>/ | |
| │ └── ... | |
| └── ... | |
| A flat ``<root>/<split>/{0_real,1_fake}`` layout (no per-generator subdir) is also | |
| supported. The label is read from the leaf directory name (``real`` -> 0, | |
| ``fake``/``false`` -> 1) and the per-generator ``folder_name`` from the grandparent | |
| directory, exactly like the image datasets, so the per-generator AP/AUROC metrics keep | |
| working. ``__getitem__`` returns ``(frames[T, 3, H, W] float in [0, 1], label, folder_id_index)``. | |
| Downloading the (large) video benchmarks is out of scope: ``prefetch`` defaults to | |
| ``False``. If ``hf_repo_id`` is configured the download stub fetches the split subtree | |
| from the Hugging Face Hub; otherwise it raises ``NotImplementedError`` with guidance. | |
| """ | |
| def __init__( | |
| self, | |
| root_dir: str | Path, | |
| split: Literal["train", "val", "test"], | |
| transform: AIGITransformations | None = None, | |
| prefetch: bool = False, | |
| num_frames: int = 16, | |
| fps: float | None = None, | |
| window_seconds: float = 2.0, | |
| frame_size: int = 224, | |
| hf_repo_id: str | None = None, | |
| window_center: bool = False, | |
| resize_interp: str = "bilinear", | |
| ): | |
| super().__init__() | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.transform = transform | |
| self.num_frames = int(num_frames) | |
| self.fps = fps if fps else None | |
| self.window_seconds = float(window_seconds) | |
| self.frame_size = int(frame_size) | |
| self.hf_repo_id = hf_repo_id | |
| self.window_center = bool(window_center) | |
| self.resize_interp = str(resize_interp) | |
| self.destination_dir = self.root_dir / self.split | |
| if prefetch and not _has_videos(self.destination_dir): | |
| print(f"Downloading {self.split} video split into {self.destination_dir}...") | |
| self._download_data() | |
| self.videos, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def prepare_split( | |
| cls, | |
| root_dir: str | Path, | |
| split: Literal["train", "val", "test"], | |
| hf_repo_id: str | None = None, | |
| force: bool = False, | |
| ) -> None: | |
| """Download one video split into ``root_dir/split`` (HF Hub only; stub otherwise).""" | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if not force and _has_videos(destination_dir): | |
| return | |
| if hf_repo_id: | |
| from huggingface_hub import snapshot_download | |
| destination_dir.mkdir(parents=True, exist_ok=True) | |
| snapshot_download( | |
| repo_id=hf_repo_id, | |
| repo_type="dataset", | |
| local_dir=str(destination_dir), | |
| allow_patterns=[f"{split}/**", f"{split}/*"], | |
| ) | |
| return | |
| raise NotImplementedError( | |
| f"No automatic downloader is configured for video split {destination_dir!s}. " | |
| "Place the benchmark videos under '<root>/<split>/<generator>/{0_real,1_fake}/' " | |
| "manually, or set 'hf_repo_id' in the dataset config to a Hugging Face dataset repo." | |
| ) | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split( | |
| root_dir=self.root_dir, | |
| split=self.split, | |
| hf_repo_id=self.hf_repo_id, | |
| force=force, | |
| ) | |
| def __len__(self): | |
| return len(self.videos) | |
| def __getitem__(self, idx): | |
| frames = sample_video_frames( | |
| self.videos[idx], | |
| num_frames=self.num_frames, | |
| fps=self.fps, | |
| window_seconds=self.window_seconds, | |
| size=self.frame_size, | |
| window_center=self.window_center, | |
| interp=self.resize_interp, | |
| ) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| frames = self.transform(frames) | |
| return frames, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _get_label(filepath: Path) -> int: | |
| name = filepath.parent.name.lower() | |
| if "real" in name: | |
| return 0 | |
| if "fake" in name or "false" in name: | |
| return 1 | |
| raise ValueError(f"Cannot determine label from path: {filepath}") | |
| def _load_data(self): | |
| videos, labels, folder_names = [], [], [] | |
| split_dir = self.destination_dir | |
| for filepath in _iter_videos(split_dir): | |
| videos.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append( | |
| filepath.parent.parent.name | |
| if filepath.parent.parent != split_dir.parent | |
| else self.split | |
| ) | |
| return videos, labels, folder_names | |
| class VideoFolderDatasetModule: | |
| def __init__(self, config): | |
| self.config = config | |
| cfg = config.DatasetConfig | |
| train_list = list(cfg.get("train_transform_list", [])) | |
| eval_list = list(cfg.get("eval_transform_list", [])) | |
| self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| prefetch = bool(cfg.get("prefetch", False)) | |
| common = dict( | |
| num_frames=cfg.get("num_frames", 16), | |
| fps=cfg.get("fps", None), | |
| window_seconds=cfg.get("window_seconds", 2.0), | |
| frame_size=cfg.get("frame_size", 224), | |
| hf_repo_id=cfg.get("hf_repo_id", None), | |
| window_center=cfg.get("window_center", False), | |
| resize_interp=cfg.get("resize_interp", "bilinear"), | |
| ) | |
| self.train_dataset = VideoFolderDataset(cfg.root_dir, "train", self.train_transform, prefetch=prefetch, **common) | |
| self.val_dataset = VideoFolderDataset(cfg.root_dir, "val", self.eval_transform, prefetch=prefetch, **common) | |
| self.test_dataset = VideoFolderDataset(cfg.root_dir, "test", self.eval_transform, prefetch=prefetch, **common) | |
| def train_dataloader(self): | |
| return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def val_dataloader(self): | |
| return DataLoader(self.val_dataset, batch_size=self.config.DatasetConfig.val_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| def test_dataloader(self): | |
| return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory) | |
| class AIGIDatasetModule: | |
| def __init__(self, config): | |
| self.delegate = SynthScopeModuleDict[config.DatasetConfig.name](config) | |
| def train_dataloader(self): | |
| return self.delegate.train_dataloader() | |
| def val_dataloader(self): | |
| return self.delegate.val_dataloader() | |
| def test_dataloader(self): | |
| return self.delegate.test_dataloader() | |
| class EvalCrafterDataset(VideoFolderDataset): | |
| """EvalCrafter T2V benchmark (Liu et al. 2024) — used by D3. | |
| All clips are AI-generated (no real videos), so every generator is placed under | |
| ``<root>/test/1_fake/<generator>/*.mp4``. ``prepare_split`` snapshots the per-generator | |
| ``*.tar.gz`` from the Hugging Face Hub and safe-extracts each into the ``1_fake`` tree. | |
| """ | |
| hf_repo_id = "RaphaelLiu/EvalCrafter_T2V_Dataset" | |
| def prepare_split(cls, root_dir, split="test", hf_repo_id=None, force=False): | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if not force and _has_videos(destination_dir): | |
| return | |
| from huggingface_hub import snapshot_download | |
| download_dir = root_dir / "_downloads" | |
| snapshot_download( | |
| repo_id=hf_repo_id or cls.hf_repo_id, repo_type="dataset", | |
| local_dir=str(download_dir), allow_patterns=["*.tar.gz"], | |
| ) | |
| fake_dir = destination_dir / "1_fake" | |
| fake_dir.mkdir(parents=True, exist_ok=True) | |
| for tar in sorted(download_dir.glob("*.tar.gz")): | |
| _safe_extract_tar(tar, fake_dir) # -> 1_fake/<generator>/*.mp4 | |
| shutil.rmtree(download_dir, ignore_errors=True) | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split(self.root_dir, self.split, self.hf_repo_id, force) | |
| class GenVidBenchDataset(VideoFolderDataset): | |
| """GenVidBench (Ni et al. 2025) — used by ReStraV. | |
| Real sources (Vript, HD-VG-130M) -> ``<root>/test/0_real/<src>/``; the 8 generators | |
| (ms, pika, t2vz, vc2, cogvideo, mora, musev, svd) -> ``<root>/test/1_fake/<gen>/``. | |
| ``prepare_split`` snapshots the per-source ``.rar``/multi-part ``.7z`` archives and | |
| extracts them (needs the ``unrar`` binary; ``7z`` for the HD-VG archive). | |
| """ | |
| hf_repo_id = "bitmind/GenVidBench" | |
| REAL_SOURCES = {"vript", "hd_vg_130m"} | |
| def prepare_split(cls, root_dir, split="test", hf_repo_id=None, force=False): | |
| root_dir = Path(root_dir) | |
| destination_dir = root_dir / split | |
| if not force and _has_videos(destination_dir): | |
| return | |
| from huggingface_hub import snapshot_download | |
| download_dir = root_dir / "_downloads" | |
| snapshot_download(repo_id=hf_repo_id or cls.hf_repo_id, repo_type="dataset", local_dir=str(download_dir)) | |
| for rar in sorted(download_dir.glob("**/*.rar")): | |
| label = "0_real" if rar.stem.lower() in cls.REAL_SOURCES else "1_fake" | |
| out = destination_dir / label | |
| out.mkdir(parents=True, exist_ok=True) | |
| cls._extract_rar(rar, out) | |
| seven = sorted(download_dir.glob("**/hd_vg_130m.7z.001")) # multi-part 7z real source | |
| if seven: | |
| out = destination_dir / "0_real" | |
| out.mkdir(parents=True, exist_ok=True) | |
| cls._extract_7z(seven[0], out) | |
| shutil.rmtree(download_dir, ignore_errors=True) | |
| def _extract_rar(archive, out): | |
| unrar = shutil.which("unrar") or "unrar" | |
| subprocess.run([unrar, "x", "-o+", str(archive), str(out) + "/"], check=True, capture_output=True) | |
| def _extract_7z(archive, out): | |
| seven = shutil.which("7z") or "7z" | |
| subprocess.run([seven, "x", "-y", f"-o{out}", str(archive)], check=True, capture_output=True) | |
| for nested in Path(out).glob("*.7z"): | |
| subprocess.run([seven, "x", "-y", f"-o{out}", str(nested)], check=True, capture_output=True) | |
| nested.unlink(missing_ok=True) | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split(self.root_dir, self.split, self.hf_repo_id, force) | |
| class _TestOnlyVideoModule: | |
| """Shared loader for the eval-only video benchmarks (test split only).""" | |
| DATASET_CLASS = VideoFolderDataset | |
| def __init__(self, config): | |
| self.config = config | |
| cfg = config.DatasetConfig | |
| eval_list = list(cfg.get("eval_transform_list", [])) | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| self.test_dataset = self.DATASET_CLASS( | |
| cfg.root_dir, "test", self.eval_transform, prefetch=bool(cfg.get("prefetch", False)), | |
| num_frames=cfg.get("num_frames", 16), fps=cfg.get("fps", None), | |
| window_seconds=cfg.get("window_seconds", 2.0), frame_size=cfg.get("frame_size", 224), | |
| hf_repo_id=cfg.get("hf_repo_id", self.DATASET_CLASS.hf_repo_id), | |
| window_center=cfg.get("window_center", False), resize_interp=cfg.get("resize_interp", "bilinear"), | |
| ) | |
| def train_dataloader(self): | |
| return None | |
| def val_dataloader(self): | |
| return None | |
| def test_dataloader(self): | |
| cfg = self.config.DatasetConfig | |
| return DataLoader(self.test_dataset, batch_size=cfg.test_batch_size, shuffle=False, | |
| num_workers=cfg.num_workers, persistent_workers=cfg.persistent_workers, pin_memory=cfg.pin_memory) | |
| class EvalCrafterDatasetModule(_TestOnlyVideoModule): | |
| DATASET_CLASS = EvalCrafterDataset | |
| class GenVidBenchDatasetModule(_TestOnlyVideoModule): | |
| DATASET_CLASS = GenVidBenchDataset | |
| # --------------------------------------------------------------------------- | |
| # Eval-only cross-generator benchmark datasets (reproduction suite). Image benchmarks share a | |
| # small test-only base + module (mirroring the video VideoFolderDataset / _TestOnlyVideoModule | |
| # pattern); each subclass only declares its download source and restructures the download into | |
| # the common ``<root>/test/<generator>/{0_real,1_fake}/`` layout used by the per-generator metrics. | |
| # --------------------------------------------------------------------------- | |
| def _extract_archives_into(download_dir, staging_dir): | |
| """Extract every .zip/.tar(.gz) under ``download_dir`` into ``staging_dir`` and return the dir | |
| that holds the extracted generator tree (``staging_dir``; or ``download_dir`` if already flat).""" | |
| download_dir, staging_dir = Path(download_dir), Path(staging_dir) | |
| archives = [p for p in download_dir.rglob("*") | |
| if p.is_file() and (p.suffix.lower() == ".zip" | |
| or p.name.lower().endswith((".tar.gz", ".tgz", ".tar")))] | |
| if not archives: | |
| return download_dir | |
| staging_dir.mkdir(parents=True, exist_ok=True) | |
| for a in archives: | |
| if a.suffix.lower() == ".zip": | |
| _safe_extract_zip(a, staging_dir) | |
| else: | |
| _safe_extract_tar(a, staging_dir) | |
| return staging_dir | |
| def _restructure_image_pergen(src_root, dest_split, cap=None, rename=None): | |
| """Lay a downloaded tree out as ``<dest_split>/<generator>/{0_real,1_fake}/``. | |
| For each immediate subdirectory of ``src_root`` (one generator), images found under any | |
| real-token dir (0_real/real/nature) become 0_real and fake-token dir (1_fake/fake/ai) become | |
| 1_fake — searched recursively so per-class source-domain sublevels (e.g. DIRE's | |
| ``<subset>/<domain>/{0_real,1_fake}``) are merged. ``cap`` limits images/class; ``rename`` maps | |
| the generator dir name (e.g. strip GenImage's ``_imagenet`` suffix).""" | |
| src_root, dest_split = Path(src_root), Path(dest_split) | |
| real_toks = ("0_real", "real", "reals", "nature", "natural") | |
| fake_toks = ("1_fake", "fake", "fakes", "ai", "generated") | |
| for gen_dir in sorted(p for p in src_root.iterdir() if p.is_dir()): | |
| name = rename(gen_dir.name) if rename else gen_dir.name | |
| for label, toks in (("0_real", real_toks), ("1_fake", fake_toks)): | |
| imgs = [] | |
| for tok in toks: | |
| for sub in gen_dir.rglob(tok): | |
| if sub.is_dir(): | |
| imgs += [p for p in sub.rglob("*") | |
| if p.is_file() and p.suffix.lower() in IMAGE_EXTENSIONS] | |
| imgs = sorted(set(imgs)) | |
| if cap: | |
| imgs = imgs[:cap] | |
| if not imgs: | |
| continue | |
| out = dest_split / name / label | |
| out.mkdir(parents=True, exist_ok=True) | |
| for i, p in enumerate(imgs): | |
| tgt = out / f"{i:06d}_{p.name}" | |
| if tgt.exists(): | |
| continue | |
| try: | |
| os.link(p, tgt) | |
| except OSError: | |
| shutil.copy2(p, tgt) | |
| def _restructure_video_pergen(src_root, dest_split, cap=None): | |
| """Video analogue of ``_restructure_image_pergen`` → ``<dest_split>/<gen>/{0_real,1_fake}/``.""" | |
| src_root, dest_split = Path(src_root), Path(dest_split) | |
| real_toks = ("0_real", "real", "reals") | |
| fake_toks = ("1_fake", "fake", "fakes") | |
| for gen_dir in sorted(p for p in src_root.iterdir() if p.is_dir()): | |
| for label, toks in (("0_real", real_toks), ("1_fake", fake_toks)): | |
| vids = [] | |
| for tok in toks: | |
| for sub in gen_dir.rglob(tok): | |
| if sub.is_dir(): | |
| vids += [p for p in sub.rglob("*") | |
| if p.is_file() and p.suffix.lower() in VIDEO_EXTENSIONS] | |
| vids = sorted(set(vids)) | |
| if cap: | |
| vids = vids[:cap] | |
| if not vids: | |
| continue | |
| out = dest_split / gen_dir.name / label | |
| out.mkdir(parents=True, exist_ok=True) | |
| for i, p in enumerate(vids): | |
| tgt = out / f"{i:06d}_{p.name}" | |
| if tgt.exists(): | |
| continue | |
| try: | |
| os.link(p, tgt) | |
| except OSError: | |
| shutil.copy2(p, tgt) | |
| def _iter_images_follow(directory): | |
| """Like ``_iter_images`` but follows symlinked directories (``os.walk`` with followlinks), | |
| so generators whose ``0_real``/``1_fake`` are directory symlinks (e.g. GenImage's SD-v1.4 slice | |
| reused from the single-pool dataset) are still discovered.""" | |
| for dirpath, _dirnames, filenames in os.walk(Path(directory), followlinks=True): | |
| for fn in filenames: | |
| if Path(fn).suffix.lower() in IMAGE_EXTENSIONS: | |
| yield Path(dirpath) / fn | |
| class _TestOnlyImageDataset(Dataset): | |
| """Base for eval-only image benchmarks laid out as ``<root>/test/<generator>/{0_real,1_fake}/``. | |
| Subclasses declare a download source and override ``prepare_split``; everything else (label | |
| from the leaf dir name, per-generator ``folder_name`` from the grandparent dir, ``__getitem__`` | |
| returning ``(image, label, folder_id)``) matches the other image datasets so the per-generator | |
| AP/AUROC metrics keep working.""" | |
| def __init__(self, root_dir, split: Literal["test"] = "test", | |
| transform: AIGITransformations | None = None, prefetch: bool = False): | |
| super().__init__() | |
| self.root_dir = Path(root_dir) | |
| self.split = split | |
| self.transform = transform | |
| self.destination_dir = self.root_dir / self.split | |
| if prefetch and not _has_images(self.destination_dir): | |
| print(f"Downloading {type(self).__name__} {split} split...") | |
| self._download_data() | |
| self.images, self.labels, self.folder_names = self._load_data() | |
| self.unique_folder_names = sorted(set(self.folder_names)) or [self.split] | |
| def prepare_split(cls, root_dir, split: str = "test", force: bool = False) -> None: | |
| raise NotImplementedError( | |
| f"No downloader configured for {cls.__name__}; place the benchmark under " | |
| f"'{Path(root_dir) / split}/<generator>/{{0_real,1_fake}}/' manually.") | |
| def _download_data(self, force: bool = False) -> None: | |
| self.prepare_split(self.root_dir, self.split, force=force) | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, idx): | |
| image = _decode_rgb(self.images[idx]) | |
| label = torch.tensor(self.labels[idx]) | |
| if self.transform: | |
| image = self.transform(image) | |
| return image, label, self.unique_folder_names.index(self.folder_names[idx]) | |
| def _get_label(filepath: Path) -> int: | |
| name = filepath.parent.name.lower() | |
| if "real" in name or name in ("nature", "natural"): | |
| return 0 | |
| if "fake" in name or "false" in name or name == "ai": | |
| return 1 | |
| raise ValueError(f"Cannot determine label from path: {filepath}") | |
| def _load_data(self): | |
| images, labels, folder_names = [], [], [] | |
| for filepath in _iter_images_follow(self.destination_dir): | |
| images.append(filepath) | |
| labels.append(self._get_label(filepath)) | |
| folder_names.append(filepath.parent.parent.name) | |
| return images, labels, folder_names | |
| class DiffusionForensicsDataset(_TestOnlyImageDataset): | |
| """DiffusionForensics / DIRE diffusion test set (Wang et al. 2023) — used by NPR (Table 3) and | |
| FatFormer's diffusion table. 8 subsets (adm, ddpm, iddpm, ldm, pndm, sdv1, sdv2, vqdiffusion), | |
| each with its own paired reals (LSUN/ImageNet/CelebA-HQ).""" | |
| gdrive_folder_id = "1tKsOU-6FDdstrrKLPYuZ7RpQwtOSHxUD" | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| import gdown | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| gdown.download_folder(id=cls.gdrive_folder_id, output=str(dl), quiet=False, use_cookies=False) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| _restructure_image_pergen(staging, dest, cap=1000) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| class Diffusion1kStepDataset(_TestOnlyImageDataset): | |
| """Diffusion-1kStep self-synthesis test set — used by NPR (Table 5). 5 subsets (DALLE, ddpm, | |
| guided-diffusion=ADM, improved-diffusion=IDDPM, midjourney).""" | |
| gdrive_folder_id = "14f0vApTLiukiPvIHukHDzLujrvJpDpRq" | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| import gdown | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| gdown.download_folder(id=cls.gdrive_folder_id, output=str(dl), quiet=False, use_cookies=False) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| _restructure_image_pergen(staging, dest, cap=1000) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| class AIGCDetectBenchmarkDataset(_TestOnlyImageDataset): | |
| """AIGCDetectBenchmark test set (16-17 GAN and diffusion generators) — used by Co-Spy (ProGAN/AIGCDetect table), | |
| AIDE and CuRe. Sourced from the ModelScope dataset ``aemilia/AIGCDetectionBenchmark`` (test_set.zip). The full | |
| test data is kept (the papers evaluate all of it, about 76k real / 76k fake).""" | |
| modelscope_url = ("https://modelscope.cn/api/v1/datasets/aemilia/AIGCDetectionBenchmark/" | |
| "repo?Revision=master&FilePath=AIGCDetectionBenchMark%2Ftest_set.zip") | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| zip_path = dl / "test_set.zip" | |
| subprocess.run(["curl", "-L", "--fail", "-C", "-", "-o", str(zip_path), cls.modelscope_url], | |
| check=True) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| inner = staging | |
| while len(subdirs := [p for p in inner.iterdir() if p.is_dir()]) == 1: # step into wrapper folders | |
| inner = subdirs[0] | |
| _restructure_image_pergen(inner, dest) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| class CommGen15Dataset(_TestOnlyImageDataset): | |
| """CommGen15 — 15 commercial-generator test set introduced by PGC (Table 3). Sourced from the | |
| Hugging Face dataset ``xiaoyuzhou68/CommGen15`` (CommGen15.zip; com_plat_v3/<gen>/{0_real,1_fake}).""" | |
| hf_repo_id = "xiaoyuzhou68/CommGen15" | |
| hf_filename = "CommGen15.zip" | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| from huggingface_hub import hf_hub_download | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| z = hf_hub_download(repo_id=cls.hf_repo_id, filename=cls.hf_filename, | |
| repo_type="dataset", local_dir=str(dl)) | |
| staging = _extract_archives_into(Path(z).parent, root_dir / "_staging") | |
| inner = next((p for p in staging.rglob("com_plat_v3") if p.is_dir()), staging) | |
| _restructure_image_pergen(inner, dest, cap=1000) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| class GenImageFullDataset(_TestOnlyImageDataset): | |
| """Full 8-generator GenImage test set (Midjourney, SD-v1.4/1.5, ADM, GLIDE, Wukong, VQDM, | |
| BigGAN) — used by AIDE/VIB-Net/SAFE/PGC/Co-Spy for the cross-generator GenImage table. (The | |
| single-pool SD-v1.4 slice is the separate ``genimage`` dataset.) Sourced from the HF mirror | |
| ``jzousz/GenImage`` (genimage_test.zip: test/<gen>_imagenet/{nature,ai}).""" | |
| hf_repo_id = "jzousz/GenImage" | |
| hf_filename = "genimage_test.zip" | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| from huggingface_hub import hf_hub_download | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| z = hf_hub_download(repo_id=cls.hf_repo_id, filename=cls.hf_filename, | |
| repo_type="dataset", local_dir=str(dl)) | |
| staging = _extract_archives_into(Path(z).parent, root_dir / "_staging") | |
| inner = next((p for p in staging.rglob("test") if p.is_dir()), staging) | |
| _restructure_image_pergen(inner, dest, cap=1500, rename=lambda n: n.replace("_imagenet", "")) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| class SynthbusterDataset(_TestOnlyImageDataset): | |
| """Synthbuster (Bammey, 2023) — 9 generators (DALL-E 2/3, Firefly, GLIDE, Midjourney-v5, SD 1.3/1.4/2/XL), | |
| 1000 fakes each. Used by SPAI (Table 1) and other detectors as a high-quality diffusion test set. | |
| Sourced from Zenodo record 10066460 (synthbuster.zip = <gen>/*.png, fakes only). Synthbuster ships | |
| NO reals (the originals are RAISE-1k, which require a separate request); this loader pairs each | |
| generator's fakes against a shared clean-real pool (GenImage nature photos) as a documented proxy — | |
| the detector keys on generative artifacts, so proxy reals give a valid per-generator AP/AUROC.""" | |
| zenodo_record = "10066460" | |
| zenodo_file = "synthbuster.zip" | |
| def prepare_split(cls, root_dir, split="test", force=False, real_pool=None, cap=1000): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| import urllib.request | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| zpath = dl / cls.zenodo_file | |
| if not zpath.exists(): | |
| url = f"https://zenodo.org/records/{cls.zenodo_record}/files/{cls.zenodo_file}?download=1" | |
| print(f"Downloading Synthbuster from {url} ...") | |
| urllib.request.urlretrieve(url, zpath) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| inner = next((p for p in staging.rglob("synthbuster") if p.is_dir()), staging) | |
| # proxy reals: a clean real-photo pool (GenImage nature by default) | |
| pool = real_pool or "/hddstore/ssalehi/SynthScope/GenImage/test/real" | |
| reals = sorted(p for p in Path(pool).glob("*") if p.suffix.lower() in IMAGE_EXTENSIONS)[:cap] | |
| for gen_dir in sorted(p for p in inner.iterdir() if p.is_dir()): | |
| fakes = sorted(p for p in gen_dir.glob("*") if p.suffix.lower() in IMAGE_EXTENSIONS)[:cap] | |
| for label, imgs in (("1_fake", fakes), ("0_real", reals)): | |
| out = dest / gen_dir.name / label; out.mkdir(parents=True, exist_ok=True) | |
| for i, p in enumerate(imgs): | |
| tgt = out / f"{i:05d}_{p.name}" | |
| if not tgt.exists(): | |
| try: os.link(p, tgt) | |
| except OSError: shutil.copy2(p, tgt) | |
| shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| def _link_images(imgs, out_dir): | |
| """Hard-link (copy as fallback) ``imgs`` into ``out_dir`` as ``<i>_<name>``, keeping the order.""" | |
| out_dir = Path(out_dir); out_dir.mkdir(parents=True, exist_ok=True) | |
| for i, p in enumerate(imgs): | |
| tgt = out_dir / f"{i:05d}_{p.name}" | |
| if not tgt.exists(): | |
| try: os.link(p, tgt) | |
| except OSError: shutil.copy2(p, tgt) | |
| def _first_int(path): | |
| """Sort key: the first integer in the file stem (``Flux1_100_person.jpg`` -> 100), then the name.""" | |
| m = re.search(r"\d+", Path(path).stem) | |
| return (int(m.group()) if m else -1, Path(path).name) | |
| class RealChainDataset(_TestOnlyImageDataset): | |
| """RealChain (REM paper; used by CuRe Table 1) — 7 generators (Flux1, SDv3.5, QwenImage, Hunyuan3, | |
| NanoBanana, Seedream4, i2i), 1000 fakes each, plus one pool of 7000 reals. Sourced from the HF dataset | |
| ``handsomerich/RealChain`` (CC BY-NC 4.0: <gen>.zip, Real.tar). The paper describes a clean (ND) and a | |
| chain-degraded (CD) version, but only one image set is released; every image in it (real and fake) is | |
| JPEG at the same quality (luma quant step 46.1, i.e. libjpeg q60). The real pool is not paired with | |
| the generators, so it is split into 7 disjoint slices in file-index order, one per generator: the mean | |
| of the per-generator balanced accuracies then equals the pooled 7000-vs-7000 balanced accuracy.""" | |
| hf_repo_id = "handsomerich/RealChain" | |
| generators = ("Flux1", "SDv3.5", "QwenImage", "Hunyuan3", "NanoBanana", "Seedream4", "i2i") | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| from huggingface_hub import snapshot_download | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| snapshot_download(repo_id=cls.hf_repo_id, repo_type="dataset", local_dir=str(dl), | |
| allow_patterns=[f"{g}.zip" for g in cls.generators] + ["Real.tar"]) | |
| staging = root_dir / "_staging" | |
| for g in cls.generators: # the zips are flat, so each gets its own folder | |
| _safe_extract_zip(dl / f"{g}.zip", staging / g) | |
| _safe_extract_tar(dl / "Real.tar", staging) | |
| reals = sorted((p for p in (staging / "Real").rglob("*") if p.suffix.lower() in IMAGE_EXTENSIONS), | |
| key=_first_int) | |
| per_gen = len(reals) // len(cls.generators) | |
| for i, g in enumerate(cls.generators): | |
| fakes = sorted((p for p in (staging / g).rglob("*") if p.suffix.lower() in IMAGE_EXTENSIONS), | |
| key=_first_int) | |
| _link_images(fakes, dest / g / "1_fake") | |
| _link_images(reals[i * per_gen:(i + 1) * per_gen], dest / g / "0_real") | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(staging, ignore_errors=True) | |
| class EvalGENDataset(_TestOnlyImageDataset): | |
| """EvalGEN (DDA, NeurIPS 2025; used by DDA and CuRe) — 5 generators (Flux, GoT, Infinity, OmniGen, | |
| NOVA) on GenEval prompts, about 11,060 fakes each, JPEG q96. Sourced from the HF dataset | |
| ``Junwei-Xi/EvalGEN`` (Apache-2.0: EvalGEN.zip = GenEval-JPEG/<gen>/*.jpg). EvalGEN ships no reals: | |
| CuRe pairs it with an unspecified "shared MSCOCO real pool", and DDA reports accuracy on the fakes | |
| only. This loader pairs every generator with the same MSCOCO val2017 pool (5000 images, downloaded | |
| from cocodataset.org), so ``fake_acc`` reproduces the DDA protocol and ``acc`` the balanced one.""" | |
| hf_repo_id = "Junwei-Xi/EvalGEN" | |
| hf_filename = "EvalGEN.zip" | |
| coco_url = "http://images.cocodataset.org/zips/val2017.zip" | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| from huggingface_hub import hf_hub_download | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| z = hf_hub_download(repo_id=cls.hf_repo_id, filename=cls.hf_filename, | |
| repo_type="dataset", local_dir=str(dl)) | |
| coco = dl / "val2017.zip" | |
| if not coco.exists(): | |
| subprocess.run(["curl", "-L", "--fail", "-C", "-", "-o", str(coco), cls.coco_url], check=True) | |
| staging = root_dir / "_staging" | |
| _safe_extract_zip(z, staging) | |
| _safe_extract_zip(coco, staging) | |
| reals = sorted(p for p in (staging / "val2017").glob("*") if p.suffix.lower() in IMAGE_EXTENSIONS) | |
| inner = next((p for p in staging.rglob("GenEval-JPEG") if p.is_dir()), staging) | |
| for gen_dir in sorted(p for p in inner.iterdir() if p.is_dir()): | |
| fakes = sorted(p for p in gen_dir.glob("*") if p.suffix.lower() in IMAGE_EXTENSIONS) | |
| _link_images(fakes, dest / gen_dir.name / "1_fake") | |
| _link_images(reals, dest / gen_dir.name / "0_real") | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(staging, ignore_errors=True) | |
| def _read_csv_url(url, headers): | |
| """Rows of the CSV file at ``url`` as dicts.""" | |
| with urllib.request.urlopen(urllib.request.Request(url, None, headers), timeout=60) as resp: | |
| return list(csv.DictReader(resp.read().decode().splitlines())) | |
| def _fetch_url_list(rows, out_dir, headers, name, attempts=3): | |
| """Download each ``row["url"]`` to ``out_dir / row["filename"]`` (datasets released as URL lists). A link that | |
| answers with an HTTP error, keeps failing, or (when the row has an ``md5``) returns different bytes, e.g. a | |
| placeholder for a deleted image, is skipped. Returns the filenames that could not be fetched.""" | |
| missing = [] | |
| for i, row in enumerate(rows): | |
| fpath = Path(out_dir) / row["filename"] | |
| fpath.parent.mkdir(parents=True, exist_ok=True) | |
| for attempt in range(attempts): | |
| if fpath.is_file(): | |
| break | |
| try: | |
| with urllib.request.urlopen(urllib.request.Request(row["url"], None, headers), timeout=30) as resp: | |
| data = resp.read() | |
| except urllib.error.HTTPError: # deleted post or page: the official scripts also skip it | |
| break | |
| except (OSError, ValueError): # network hiccup or bad URL: back off and retry | |
| time.sleep(2 ** attempt) | |
| continue | |
| if row.get("md5") and hashlib.md5(data).hexdigest() != row["md5"]: | |
| break | |
| fpath.write_bytes(data) | |
| if not fpath.is_file(): | |
| missing.append(row["filename"]) | |
| if (i + 1) % 200 == 0: | |
| print(f" {name}: {i + 1}/{len(rows)} fetched, {len(missing)} missing", flush=True) | |
| return missing | |
| class SynthWildXDataset(_TestOnlyImageDataset): | |
| """SynthWildX (Cozzolino et al., CVPRW 2024; used by DDA and CuRe) — images posted on X: 500 real and 500 each | |
| from DALL-E 3, Firefly and Midjourney v5. Only the image URLs are released (``data/synthwildx/list.csv`` in | |
| grip-unina/ClipBased-SyntheticImageDetection), so, like the official script, this loader fetches each image | |
| from X. Posts deleted since release return 404 and are skipped (listed in ``<root>/missing.txt``), so the set | |
| on disk can be smaller than the paper's. Each generator is paired with all 500 reals.""" | |
| list_url = ("https://raw.githubusercontent.com/grip-unina/ClipBased-SyntheticImageDetection/" | |
| "main/data/synthwildx/list.csv") | |
| headers = {"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7"} | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| rows = _read_csv_url(cls.list_url, cls.headers) | |
| missing = _fetch_url_list(rows, dl, cls.headers, "SynthWildX") | |
| (root_dir / "missing.txt").write_text("".join(f"{m}\n" for m in missing)) | |
| reals = sorted((dl / "real").glob("*")) | |
| for gen_dir in sorted(p for p in dl.iterdir() if p.is_dir() and p.name != "real"): | |
| _link_images(sorted(gen_dir.glob("*")), dest / gen_dir.name / "1_fake") | |
| _link_images(reals, dest / gen_dir.name / "0_real") | |
| print(f"SynthWildX: {len(rows) - len(missing)}/{len(rows)} images available ({len(missing)} deleted from X)") | |
| shutil.rmtree(dl, ignore_errors=True) | |
| class WildRFDataset(_TestOnlyImageDataset): | |
| """WildRF (Cavia et al. 2024, the LaDeDa paper; used by DDA and CuRe) — real and fake images collected from | |
| social platforms. Its test split has one folder per platform (facebook, reddit, twitter), each with real/fake; | |
| those become the "generators" here. Sourced from the authors' Google Drive zip (barcavia/ | |
| RealTime-DeepfakeDetection-in-the-RealWorld); only the test split is laid out.""" | |
| gdrive_file_id = "1A0xoL44Yg68ixd-FuIJn2VC4vdZ6M2gn" | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| import gdown | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| gdown.download(id=cls.gdrive_file_id, output=str(dl / "WildRF.zip"), quiet=False, use_cookies=False) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| inner = next(p for p in staging.rglob("test") if p.is_dir()) | |
| _restructure_image_pergen(inner, dest) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| class BFreeOnlineDataset(_TestOnlyImageDataset): | |
| """BFree-Online — the B-Free paper's viral-images set (Guillaro et al., CVPR 2025; used by DDA and CuRe): real | |
| and fake images that went viral, with several web-scraped copies of each (about 1400 in the paper). Only URLs are | |
| released (``viral_images_dataset/BFree_viral_images.csv`` in grip-unina/B-Free, 1111 rows, nonprofit use only); | |
| the loader fetches them and keeps an image only when its md5 matches the list, so dead links and placeholders are | |
| skipped (listed in ``<root>/missing.txt``). The 3 Facebook links need a browser to resolve and are skipped too. | |
| Laid out as a single group, ``test/viral/{0_real,1_fake}``.""" | |
| list_url = "https://raw.githubusercontent.com/grip-unina/B-Free/main/viral_images_dataset/BFree_viral_images.csv" | |
| headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/128.0.0.0 Safari/537.36"} | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| rows = _read_csv_url(cls.list_url, cls.headers) | |
| missing = _fetch_url_list(rows, dl, cls.headers, "BFree-Online") | |
| (root_dir / "missing.txt").write_text("".join(f"{m}\n" for m in missing)) | |
| for label, folder in (("REAL", "0_real"), ("FAKE", "1_fake")): | |
| _link_images(sorted((dl / label).rglob("*.*")), dest / "viral" / folder) | |
| print(f"BFree-Online: {len(rows) - len(missing)}/{len(rows)} images available ({len(missing)} links dead)") | |
| shutil.rmtree(dl, ignore_errors=True) | |
| class RRDataset(_TestOnlyImageDataset): | |
| """RRDataset (ICCV 2025, "Bridging the Gap Between Ideal and Real-world Evaluation"; used by CuRe) — real and | |
| AI-generated images in three scenarios: ``original``, ``transfer`` (after sharing over online platforms) and | |
| ``redigital`` (re-captured / re-digitized), about 8500 real and 8500 fake each. The scenarios become the | |
| "generators" here. Sourced from Zenodo record 14963880 (RRDataset_test.tar.gz, 20 GB, CC BY 4.0); the separate | |
| train/val archive is not used.""" | |
| zenodo_record = "14963880" | |
| zenodo_file = "RRDataset_test.tar.gz" | |
| def prepare_split(cls, root_dir, split="test", force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_images(dest): | |
| return | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| archive = dl / cls.zenodo_file | |
| if not archive.exists(): | |
| url = f"https://zenodo.org/api/records/{cls.zenodo_record}/files/{cls.zenodo_file}/content" | |
| subprocess.run(["curl", "-L", "--fail", "-C", "-", "--retry", "5", "-o", str(archive), url], check=True) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| inner = next((p for p in staging.rglob("RRDataset_final") if p.is_dir()), staging) | |
| _restructure_image_pergen(inner, dest) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| class _TestOnlyImageModule: | |
| """Shared loader for the eval-only image benchmarks (test split only).""" | |
| DATASET_CLASS = _TestOnlyImageDataset | |
| def __init__(self, config): | |
| self.config = config | |
| cfg = config.DatasetConfig | |
| eval_list = list(cfg.get("eval_transform_list", [])) | |
| self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None | |
| self.test_dataset = self.DATASET_CLASS( | |
| cfg.root_dir, "test", self.eval_transform, prefetch=bool(cfg.get("prefetch", False))) | |
| def train_dataloader(self): | |
| return None | |
| def val_dataloader(self): | |
| return None | |
| def test_dataloader(self): | |
| cfg = self.config.DatasetConfig | |
| return DataLoader(self.test_dataset, batch_size=cfg.test_batch_size, shuffle=False, | |
| num_workers=cfg.num_workers, persistent_workers=cfg.persistent_workers, | |
| pin_memory=cfg.pin_memory) | |
| class DiffusionForensicsDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = DiffusionForensicsDataset | |
| class Diffusion1kStepDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = Diffusion1kStepDataset | |
| class AIGCDetectBenchmarkDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = AIGCDetectBenchmarkDataset | |
| class CommGen15DatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = CommGen15Dataset | |
| class GenImageFullDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = GenImageFullDataset | |
| class SynthbusterDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = SynthbusterDataset | |
| class RealChainDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = RealChainDataset | |
| class EvalGENDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = EvalGENDataset | |
| class SynthWildXDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = SynthWildXDataset | |
| class WildRFDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = WildRFDataset | |
| class BFreeOnlineDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = BFreeOnlineDataset | |
| class RRDatasetModule(_TestOnlyImageModule): | |
| DATASET_CLASS = RRDataset | |
| class VideoFeedbackDataset(VideoFolderDataset): | |
| """VideoFeedback (TIGER-Lab) — used by STALL. 11 generators vs DiDeMo+Panda70M real, laid out | |
| as ``<root>/test/<generator>/{0_real,1_fake}/*.mp4``.""" | |
| hf_repo_id = "TIGER-Lab/VideoFeedback" | |
| def prepare_split(cls, root_dir, split="test", hf_repo_id=None, force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_videos(dest): | |
| return | |
| from huggingface_hub import snapshot_download | |
| dl = root_dir / "_downloads" | |
| snapshot_download(repo_id=hf_repo_id or cls.hf_repo_id, repo_type="dataset", local_dir=str(dl)) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| _restructure_video_pergen(staging, dest, cap=150) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| def _download_data(self, force=False): | |
| self.prepare_split(self.root_dir, self.split, self.hf_repo_id, force) | |
| class ComGenVidDataset(VideoFolderDataset): | |
| """ComGenVid (STALL's new benchmark) — Sora + VEO3 fakes vs MSVD real, laid out as | |
| ``<root>/test/<generator>/{0_real,1_fake}/*.mp4``. The HF repo ``OmerXYZ/comgenvid`` also ships | |
| precomputed embeddings for STALL's official embedding-based eval.""" | |
| hf_repo_id = "OmerXYZ/comgenvid" | |
| def prepare_split(cls, root_dir, split="test", hf_repo_id=None, force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_videos(dest): | |
| return | |
| from huggingface_hub import snapshot_download | |
| dl = root_dir / "_downloads" | |
| snapshot_download(repo_id=hf_repo_id or cls.hf_repo_id, repo_type="dataset", | |
| local_dir=str(dl), allow_patterns=["*.mp4", "**/*.mp4"]) | |
| staging = _extract_archives_into(dl, root_dir / "_staging") | |
| _restructure_video_pergen(staging, dest, cap=150) | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(root_dir / "_staging", ignore_errors=True) | |
| def _download_data(self, force=False): | |
| self.prepare_split(self.root_dir, self.split, self.hf_repo_id, force) | |
| GENVIDEO_VAL_URL = "https://modelscope.cn/datasets/cccnju/Gen-Video/resolve/master/GenVideo-Val.zip" | |
| def _genvideo_val_zip(dl_dir): | |
| """GenVideo-Val.zip (DeMamba's validation set, 14 GB) from ModelScope, downloaded into ``dl_dir`` unless present.""" | |
| zip_path = Path(dl_dir) / "GenVideo-Val.zip" | |
| if not zip_path.is_file(): | |
| Path(dl_dir).mkdir(parents=True, exist_ok=True) | |
| subprocess.run(["curl", "-L", "--fail", "-C", "-", "--retry", "5", "-o", str(zip_path), GENVIDEO_VAL_URL], | |
| check=True) | |
| return zip_path | |
| def _msrvtt_reals(dl_dir): | |
| """The 10,000 MSR-VTT real videos of GenVideo-Val (``Real/msrvtt_<n>.mp4``), extracted into ``dl_dir/msrvtt`` and | |
| returned in numeric order. D3 pairs every fake-only video benchmark (EvalCrafter, VideoPhy, VidProM) with them.""" | |
| out = Path(dl_dir) / "msrvtt" | |
| if not _has_videos(out): | |
| out.mkdir(parents=True, exist_ok=True) | |
| with zipfile.ZipFile(_genvideo_val_zip(dl_dir)) as zf: | |
| for m in zf.infolist(): | |
| name = Path(m.filename).name | |
| if ("/Real/" in m.filename and "__MACOSX" not in m.filename and not name.startswith("._") | |
| and name.lower().endswith(VIDEO_EXTENSIONS)): # skip macOS resource-fork stubs | |
| with zf.open(m) as src, open(out / name, "wb") as dst: | |
| shutil.copyfileobj(src, dst) | |
| return sorted((p for p in out.iterdir() if p.suffix.lower() in VIDEO_EXTENSIONS), key=_first_int) | |
| class GenVideoValDataset(VideoFolderDataset): | |
| """GenVideo-Val (DeMamba, Chen et al. 2024) — the test set of D3 (Table 1) and STALL: 10 generators (Crafter, Gen2, | |
| HotShot, Lavie, ModelScope, MoonValley, MorphStudio, Show_1, Sora, WildScrape) against 10,000 MSR-VTT real videos. | |
| Sourced from ModelScope ``cccnju/Gen-Video`` (GenVideo-Val.zip, 14 GB). Laid out as ``test/<generator>/1_fake`` with | |
| the shared real pool hard-linked into every ``test/<generator>/0_real`` (numeric order, so ``--limit 1000`` takes | |
| the first 1000 reals, as D3's official eval does with ``max_len=1000``).""" | |
| hf_repo_id = None | |
| def prepare_split(cls, root_dir, split="test", hf_repo_id=None, force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_videos(dest): | |
| return | |
| dl = root_dir / "_downloads" | |
| staging = root_dir / "_staging" | |
| _safe_extract_zip(_genvideo_val_zip(dl), staging) | |
| inner = next(p for p in staging.rglob("Fake") if p.is_dir() and "__MACOSX" not in p.parts).parent | |
| ok = lambda p: p.suffix.lower() in VIDEO_EXTENSIONS and not p.name.startswith("._") # noqa: E731 | |
| reals = sorted((p for p in (inner / "Real").iterdir() if ok(p)), key=_first_int) | |
| for gen_dir in sorted(p for p in (inner / "Fake").iterdir() if p.is_dir()): | |
| fakes = sorted((p for p in gen_dir.rglob("*") if ok(p)), key=_first_int) | |
| _link_images(fakes, dest / gen_dir.name / "1_fake") | |
| _link_images(reals, dest / gen_dir.name / "0_real") | |
| shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(staging, ignore_errors=True) | |
| def _download_data(self, force=False): | |
| self.prepare_split(self.root_dir, self.split, self.hf_repo_id, force) | |
| class VideoPhyDataset(VideoFolderDataset): | |
| """VideoPhy test set (Bansal et al. 2024; D3 Table 3) — 3360 text-to-video clips from 10 generators, released only as | |
| URLs (``videophysics/videophy_test_public`` on Hugging Face, MIT). The loader fetches each clip; links that no longer | |
| work (both CogVideoX sets were offline in October 2026) are skipped and listed in ``<root>/missing.txt``. VideoPhy has | |
| no real videos, so, as D3 does for its fake-only sets, every generator is paired with GenVideo-Val's MSR-VTT reals.""" | |
| hf_repo_id = None | |
| csv_url = "https://huggingface.co/datasets/videophysics/videophy_test_public/resolve/main/videophy_test_public.csv" | |
| headers = {"User-Agent": "Mozilla/5.0"} | |
| source_names = {"lavie": "LaVie", "vc2": "VideoCrafter2", "opensora": "OpenSora", "zeroscope": "ZeroScope", | |
| "gen2": "Gen-2", "svd": "SVD", "pika": "Pika", "Dream Machine (LumaAI)": "Dream-Machine", | |
| "CogVideoX-2B": "CogVideoX-2B", "CogVideoX-5B": "CogVideoX-5B"} | |
| def prepare_split(cls, root_dir, split="test", hf_repo_id=None, force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_videos(dest): | |
| return | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| rows = [] | |
| for i, row in enumerate(_read_csv_url(cls.csv_url, cls.headers)): | |
| url = urllib.parse.quote(row["video_url"], safe=":/?=&%") | |
| gen = cls.source_names.get(row["source"], row["source"]) | |
| rows.append({"url": url, "filename": f"{gen}/{i:05d}_{Path(urllib.parse.urlparse(url).path).name}"}) | |
| missing = _fetch_url_list(rows, dl / "videos", cls.headers, "VideoPhy") | |
| (root_dir / "missing.txt").write_text("".join(f"{m}\n" for m in missing)) | |
| reals = _msrvtt_reals(dl) | |
| for gen_dir in sorted(p for p in (dl / "videos").iterdir() if p.is_dir()): | |
| fakes = sorted(p for p in gen_dir.iterdir() if p.suffix.lower() in VIDEO_EXTENSIONS) | |
| if fakes: | |
| _link_images(fakes, dest / gen_dir.name / "1_fake") | |
| _link_images(reals, dest / gen_dir.name / "0_real") | |
| print(f"VideoPhy: {len(rows) - len(missing)}/{len(rows)} clips available ({len(missing)} links dead)") | |
| shutil.rmtree(dl, ignore_errors=True) | |
| def _download_data(self, force=False): | |
| self.prepare_split(self.root_dir, self.split, self.hf_repo_id, force) | |
| class VidProMDataset(VideoFolderDataset): | |
| """VidProM (Wang & Yang, NeurIPS 2024; D3 Table 4, ReStraV) — millions of text-to-video clips from ModelScope, Pika, | |
| Text2Video-Zero and VideoCrafter2 on Hugging Face ``WenhaoWang/VidProM`` (CC BY-NC 4.0; 30 tar parts per generator, | |
| 0.35-1 TB each). The loader streams only the first ``cap`` clips of part 1 per generator instead of downloading | |
| terabytes, so results are on a subsample (D3 does not say how it sampled). OpenSora and StreamingT2V, also in D3's | |
| table, are not in that repository. Each generator is paired with GenVideo-Val's MSR-VTT reals.""" | |
| hf_repo_id = "WenhaoWang/VidProM" | |
| generators = {"ms": "ModelScope", "pika": "Pika", "t2vz": "Text2Video-Zero", "vc2": "VideoCrafter2"} | |
| cap = 1000 | |
| def prepare_split(cls, root_dir, split="test", hf_repo_id=None, force=False): | |
| root_dir = Path(root_dir); dest = root_dir / split | |
| if not force and _has_videos(dest): | |
| return | |
| dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True) | |
| repo = hf_repo_id or cls.hf_repo_id | |
| for key, gen in cls.generators.items(): | |
| out = dl / "videos" / gen; out.mkdir(parents=True, exist_ok=True) | |
| url = f"https://huggingface.co/datasets/{repo}/resolve/main/{key}_videos/{key}_videos_1.tar" | |
| kept = len(list(out.glob("*.mp4"))) | |
| if kept < cls.cap: | |
| with urllib.request.urlopen(urllib.request.Request(url, None, {"User-Agent": "Mozilla/5.0"})) as resp, \ | |
| tarfile.open(fileobj=resp, mode="r|*") as tf: # streamed: stops after `cap` clips | |
| for m in tf: | |
| name = Path(m.name).name | |
| if not (m.isfile() and name.lower().endswith(VIDEO_EXTENSIONS)): | |
| continue | |
| with tf.extractfile(m) as src, open(out / name, "wb") as dst: | |
| shutil.copyfileobj(src, dst) | |
| kept += 1 | |
| if kept >= cls.cap: | |
| break | |
| print(f" VidProM {gen}: {kept} clips", flush=True) | |
| reals = _msrvtt_reals(dl) | |
| for gen_dir in sorted(p for p in (dl / "videos").iterdir() if p.is_dir()): | |
| _link_images(sorted(gen_dir.glob("*.mp4"))[:cls.cap], dest / gen_dir.name / "1_fake") | |
| _link_images(reals, dest / gen_dir.name / "0_real") | |
| shutil.rmtree(dl, ignore_errors=True) | |
| def _download_data(self, force=False): | |
| self.prepare_split(self.root_dir, self.split, self.hf_repo_id, force) | |
| class VideoFeedbackDatasetModule(_TestOnlyVideoModule): | |
| DATASET_CLASS = VideoFeedbackDataset | |
| class ComGenVidDatasetModule(_TestOnlyVideoModule): | |
| DATASET_CLASS = ComGenVidDataset | |
| class GenVideoValDatasetModule(_TestOnlyVideoModule): | |
| DATASET_CLASS = GenVideoValDataset | |
| class VideoPhyDatasetModule(_TestOnlyVideoModule): | |
| DATASET_CLASS = VideoPhyDataset | |
| class VidProMDatasetModule(_TestOnlyVideoModule): | |
| DATASET_CLASS = VidProMDataset | |
| SynthScopeModuleDict = { | |
| "aigibench": AIGIBenchDatasetModule, | |
| "cnndetection": CNNDetectionDatasetModule, | |
| "cospy": CoSpyDatasetModule, | |
| "ditfake": DiTFakeDatasetModule, | |
| "freqnet": FreqNetDetectionDatasetModule, | |
| "freqnetdetection": FreqNetDetectionDatasetModule, | |
| "genimage": GenImageDatasetModule, | |
| "universialfakedetection": UniversialFakeDetectionDatasetModule, | |
| # Cross-generator image benchmarks added for the reproduction suite (test-only). | |
| "genimage_full": GenImageFullDatasetModule, | |
| "synthbuster": SynthbusterDatasetModule, | |
| "diffusionforensics": DiffusionForensicsDatasetModule, | |
| "diffusion1kstep": Diffusion1kStepDatasetModule, | |
| "aigcdetect": AIGCDetectBenchmarkDatasetModule, | |
| "commgen15": CommGen15DatasetModule, | |
| "realchain": RealChainDatasetModule, | |
| "evalgen": EvalGENDatasetModule, | |
| "synthwildx": SynthWildXDatasetModule, | |
| "wildrf": WildRFDatasetModule, | |
| "bfree_online": BFreeOnlineDatasetModule, | |
| "rrdataset": RRDatasetModule, | |
| # Video benchmarks. EvalCrafter/GenVidBench/VideoFeedback/ComGenVid have dedicated classes that | |
| # download + extract their HF archives into the real/fake layout; the rest use the generic | |
| # VideoFolderDatasetModule. | |
| "genvideo": GenVideoValDatasetModule, | |
| "videophy": VideoPhyDatasetModule, | |
| "evalcrafter": EvalCrafterDatasetModule, | |
| "vidprom": VidProMDatasetModule, | |
| "genvidbench": GenVidBenchDatasetModule, | |
| "physics_iq": VideoFolderDatasetModule, | |
| "videofeedback": VideoFeedbackDatasetModule, | |
| "comgenvid": ComGenVidDatasetModule, | |
| } | |
| if __name__ == "__main__": | |
| config = compose_config() | |
| dataset_module = SynthScopeModuleDict[config.DatasetConfig.name](config) | |
| train_dataloader = dataset_module.train_dataloader() | |
| val_dataloader = dataset_module.val_dataloader() | |
| test_dataloader = dataset_module.test_dataloader() | |
| for batch in train_dataloader: | |
| print(batch) | |
| break | |
| for batch in val_dataloader: | |
| print(batch) | |
| break | |
| for batch in test_dataloader: | |
| print(batch) | |
| plt.imshow(batch[0].permute(1, 2, 0)) | |
| plt.savefig(fname='test_image.png') | |
| break | |