Spaces:
Sleeping
Sleeping
| """Generate demo images that exercise each EXIF anomaly rule. | |
| Each output image carries crafted EXIF that fires exactly one of the | |
| rules in ``Aperture.metadata.anomalies``. Pixel content is pulled from | |
| the public CIFAKE HuggingFace mirror so the images look like real | |
| photos rather than synthetic gradients. | |
| Outputs (under ``examples/``): | |
| photoshopped_photo.jpg β editing_software (medium, "Adobe Photoshop") | |
| date_drift_photo.jpg β modified_after_capture (medium) | |
| low_quality_jpeg.jpg β low_jpeg_quality (low, q=30) | |
| ai_generated_metadata.jpg β editing_software (high, "Midjourney") | |
| + camera_unknown (low) β covers HIGH severity | |
| Run from the repo root: | |
| python scripts/create_metadata_examples.py | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import urllib.request | |
| from pathlib import Path | |
| import piexif | |
| import pyarrow.parquet as pq | |
| from PIL import Image | |
| OUT = Path(__file__).resolve().parent.parent / "examples" | |
| PARQUET_URL = ( | |
| "https://huggingface.co/datasets/dragonintelligence/CIFAKE-image-dataset/" | |
| "resolve/main/data/test-00000-of-00001.parquet" | |
| ) | |
| TARGET_SIZE = (256, 256) | |
| def _load_real_bases(n: int) -> list[Image.Image]: | |
| """Pull `n` CIFAKE REAL (label=1) images and upscale to 256x256.""" | |
| req = urllib.request.Request(PARQUET_URL, headers={"User-Agent": "aperture-meta"}) | |
| with urllib.request.urlopen(req, timeout=120) as r: | |
| data = r.read() | |
| table = pq.read_table(io.BytesIO(data)) | |
| labels = table.column("label").to_pylist() | |
| images = table.column("image").to_pylist() | |
| # Stride through the REAL half so chosen images are visually distinct. | |
| real_indices = [i for i, lbl in enumerate(labels) if lbl == 1] | |
| step = max(1, len(real_indices) // (n + 1)) | |
| bases: list[Image.Image] = [] | |
| for i in range(n): | |
| idx = real_indices[(i + 1) * step] | |
| img = Image.open(io.BytesIO(images[idx]["bytes"])).convert("RGB") | |
| bases.append(img.resize(TARGET_SIZE, Image.BICUBIC)) | |
| return bases | |
| def _make_exif( | |
| *, | |
| software: str | None = None, | |
| make: str | None = None, | |
| model: str | None = None, | |
| datetime_original: str | None = None, | |
| datetime_modified: str | None = None, | |
| ) -> bytes: | |
| """Build an EXIF byte-string carrying the tags we set.""" | |
| zeroth: dict = {} | |
| exif: dict = {} | |
| if software is not None: | |
| zeroth[piexif.ImageIFD.Software] = software.encode("ascii") | |
| if make is not None: | |
| zeroth[piexif.ImageIFD.Make] = make.encode("ascii") | |
| if model is not None: | |
| zeroth[piexif.ImageIFD.Model] = model.encode("ascii") | |
| if datetime_modified is not None: | |
| zeroth[piexif.ImageIFD.DateTime] = datetime_modified.encode("ascii") | |
| if datetime_original is not None: | |
| exif[piexif.ExifIFD.DateTimeOriginal] = datetime_original.encode("ascii") | |
| exif[piexif.ExifIFD.DateTimeDigitized] = datetime_original.encode("ascii") | |
| return piexif.dump({"0th": zeroth, "Exif": exif, "GPS": {}, "1st": {}, "thumbnail": None}) | |
| def _save(path: Path, img: Image.Image, *, quality: int, exif: bytes) -> None: | |
| img.save(path, format="JPEG", quality=quality, exif=exif) | |
| print(f" wrote {path}") | |
| def main() -> None: | |
| OUT.mkdir(parents=True, exist_ok=True) | |
| bases = _load_real_bases(4) | |
| print("Generating metadata example variety set...") | |
| # 1. Photoshop signature -> MEDIUM editing_software | |
| _save( | |
| OUT / "photoshopped_photo.jpg", | |
| bases[0], | |
| quality=92, | |
| exif=_make_exif( | |
| software="Adobe Photoshop 2024", | |
| make="Canon", | |
| model="EOS 5D Mark IV", | |
| datetime_original="2023:07:15 14:30:00", | |
| datetime_modified="2023:07:15 14:30:00", # equal => no date-drift flag | |
| ), | |
| ) | |
| # 2. Capture-vs-modify drift -> MEDIUM modified_after_capture | |
| _save( | |
| OUT / "date_drift_photo.jpg", | |
| bases[1], | |
| quality=92, | |
| exif=_make_exif( | |
| software="Camera Firmware 1.0", # not in editor signatures | |
| make="Sony", | |
| model="ILCE-7RM5", | |
| datetime_original="2023:07:15 14:30:00", | |
| datetime_modified="2024:03:01 10:00:00", # ~8 months later | |
| ), | |
| ) | |
| # 3. Heavy recompression -> LOW low_jpeg_quality | |
| _save( | |
| OUT / "low_quality_jpeg.jpg", | |
| bases[2], | |
| quality=30, # libjpeg estimate will read < 70 | |
| exif=_make_exif( | |
| software="Camera Firmware 1.0", | |
| make="Nikon", | |
| model="NIKON Z 9", | |
| datetime_original="2023:07:15 14:30:00", | |
| datetime_modified="2023:07:15 14:30:00", | |
| ), | |
| ) | |
| # 4. AI-tool signature -> HIGH editing_software (+ camera_unknown LOW, | |
| # since AI generators don't carry camera Make/Model) | |
| _save( | |
| OUT / "ai_generated_metadata.jpg", | |
| bases[3], | |
| quality=92, | |
| exif=_make_exif( | |
| software="Midjourney v6.1", # 'midjourney' substring triggers HIGH | |
| ), | |
| ) | |
| print("Done.") | |
| if __name__ == "__main__": | |
| main() | |