scanner / scripts /dataset /simulate_shared_copy.py
github-actions[bot]
Deploy from GitHub 1af737237f5d10dfa9cf12849d98f47c3b30ce37 (filtered: code + Dockerfile + README + NOTICES only)
a2f55b2
Raw History Blame Contribute Delete
4.42 kB
"""
Produce "shared copy" variants of eval images: what survives being passed around.
Why
---
Every P0 measurement so far is on *untouched downloads*, where a C2PA manifest
decides the verdict and detection is 100%. That is not how images reach a
visitor. A messaging app, a social upload or a re-save downscales the image,
recompresses it as JPEG, and discards the metadata — including the signature.
At that point the model is alone.
This script isolates exactly that: same pixels, minus the manifest, plus the
recompression an ordinary sharing path applies. Comparing the two conditions
gives the cost of losing provenance, measured rather than assumed.
What this is NOT
----------------
**Not the screenshot arm.** A screenshot adds display rescaling, a second
round of resampling, possible UI chrome and colour-profile conversion. This is
the milder, cleaner case: metadata stripped and one recompression. Treat the
numbers as an OPTIMISTIC bound on the sharing path — real screenshots should be
no better and are likely worse. Owner-collected screenshots remain the thing
that settles it; see the P0 spec.
Output is written to `ai/<tool>/reencoded_sim/`, deliberately suffixed `_sim`
so no reader mistakes a generated condition for collected evidence.
Usage
-----
python scripts/dataset/simulate_shared_copy.py --root data/closed_eval
python scripts/dataset/simulate_shared_copy.py --root data/closed_eval --force
"""
from __future__ import annotations
import argparse
from pathlib import Path
from PIL import Image
# Chosen to mimic a mainstream messaging/social pipeline rather than a worst
# case: most resize the long edge to roughly this and recompress at high-ish
# quality. A harsher setting would flatter the argument for P1 by making the
# baseline look worse than reality.
LONG_EDGE = 1080
JPEG_QUALITY = 85
def shared_copy(src: Path, dst: Path) -> None:
"""Downscale, recompress as JPEG, and drop every metadata block."""
with Image.open(src) as im:
im = im.convert("RGB")
w, h = im.size
scale = LONG_EDGE / max(w, h)
if scale < 1.0:
im = im.resize((round(w * scale), round(h * scale)), Image.LANCZOS)
# A fresh image object carries no info dict, so the C2PA chunk, EXIF and
# ICC profile are all left behind — the same way a share pipeline does it.
out = Image.new("RGB", im.size)
out.putdata(list(im.getdata()))
out.save(dst, "JPEG", quality=JPEG_QUALITY, optimize=True)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--root", type=Path, default=Path("data/closed_eval"))
ap.add_argument("--force", action="store_true",
help="Regenerate even if the output folder already has files.")
args = ap.parse_args()
ai_root = args.root / "ai"
if not ai_root.is_dir():
raise SystemExit(f"{ai_root} does not exist.")
markers = (b"caBX", b"jumbf", b"c2pa")
total = 0
for tool_dir in sorted(p for p in ai_root.iterdir() if p.is_dir()):
src_dir = tool_dir / "original"
if not src_dir.is_dir():
continue
dst_dir = tool_dir / "reencoded_sim"
dst_dir.mkdir(parents=True, exist_ok=True)
existing = list(dst_dir.glob("*.jpg"))
if existing and not args.force:
print(f"{tool_dir.name}: {len(existing)} already present, skipping "
f"(use --force to regenerate)")
continue
made = leaked = 0
for src in sorted(src_dir.glob("*.png"), key=lambda p: p.stem):
dst = dst_dir / f"{src.stem}.jpg"
shared_copy(src, dst)
# Verify the point of the exercise actually happened. If a manifest
# survived, the condition is not what it claims to be.
if any(m in dst.read_bytes() for m in markers):
leaked += 1
made += 1
total += made
status = "all manifests stripped" if not leaked else f"{leaked} STILL CARRY C2PA"
print(f"{tool_dir.name}: wrote {made} shared copies -> {dst_dir} ({status})")
print(f"\n{total} images written. Re-run the eval to compare conditions:")
print(" python scripts/eval_closed_generators.py --dir data/closed_eval")
if __name__ == "__main__":
main()