mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-08 23:16:02 +02:00
fix(invisible): disable protect-text/protect-faces by default; add docs/synthid.md
Both text and face protection were shielding SynthID from removal. The text-protection high-res re-scrub regenerates pixels at an upscaled resolution where the per-region pass may not be strong enough to re-destroy the SynthID payload, allowing it to survive in text areas. Face protection has an even more direct mechanism: it pastes back the original (pre-diffusion, watermarked) face pixels after the global pass, guaranteeing SynthID survives in face regions regardless of strength. Both --protect-text and --protect-faces are now off by default and opt-in. Rename from --no-protect-text / --no-protect-faces to --protect-text / --protect-faces. Extract shared click.option decorators to module-level constants (_protect_text_option, _protect_faces_option) to eliminate copy-paste between cmd_invisible and cmd_all. Add docs/synthid.md: primary-source-cited technical reference for SynthID-Image covering mechanism (post-hoc encoder/decoder, 136-bit payload, pixel-space, no model-weight modification), robustness numbers (arXiv:2510.09263: ~99.98% TPR at 0.1% FPR across 30 transforms), removal attacks and forensic detectability (arXiv:2605.09203: all 6 attacks detectable >98% TPR@1%FPR), detectability limits, oracle scope, adoption landscape, and practical implications including the protect-text/faces SynthID-preservation finding. Verified June 2026 on gpt-image 1600x1600 via openai.com/verify: with --protect-text SynthID detected; without, SynthID removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
72812de03c
commit
4b0b370ac0
@@ -136,6 +136,25 @@ def _validate_image(path: Path) -> Path:
|
||||
|
||||
_ALPHA_FORMATS = {".png", ".webp"}
|
||||
|
||||
# Shared option decorators for commands that run the invisible-watermark pipeline.
|
||||
# Both cmd_invisible and cmd_all expose these flags; defining them once avoids
|
||||
# copy-paste drift.
|
||||
_protect_text_option = click.option(
|
||||
"--protect-text",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help=(
|
||||
"Enable text region protection (experimental: re-scrubs text blocks at high resolution). "
|
||||
"May prevent SynthID removal in text areas -- verify with oracle before relying on it."
|
||||
),
|
||||
)
|
||||
_protect_faces_option = click.option(
|
||||
"--protect-faces",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Enable face protection (experimental: YOLO detect + blend original faces back).",
|
||||
)
|
||||
|
||||
|
||||
def _watermark_region(det: DetectionResult, width: int, height: int) -> tuple[int, int, int, int]:
|
||||
"""Pick a watermark bbox: detector's region if confident, else the default config slot."""
|
||||
@@ -459,18 +478,8 @@ def cmd_erase(
|
||||
default=0,
|
||||
help="Cap long side (px) before diffusion; 0 = native (best quality, like raiw.cc). Raise only on GPU/MPS OOM.",
|
||||
)
|
||||
@click.option(
|
||||
"--no-protect-text",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Disable automatic text protection (text/CJK is preserved by default on the SDXL pipeline).",
|
||||
)
|
||||
@click.option(
|
||||
"--no-protect-faces",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Disable face protection (skips the YOLO face detector; use when the image has no people).",
|
||||
)
|
||||
@_protect_text_option
|
||||
@_protect_faces_option
|
||||
@click.pass_context
|
||||
def cmd_invisible(
|
||||
ctx: click.Context,
|
||||
@@ -484,8 +493,8 @@ def cmd_invisible(
|
||||
hf_token: str | None,
|
||||
humanize: float,
|
||||
max_resolution: int,
|
||||
no_protect_text: bool,
|
||||
no_protect_faces: bool,
|
||||
protect_text: bool,
|
||||
protect_faces: bool,
|
||||
) -> None:
|
||||
"""Remove invisible AI watermarks (SynthID, StableSignature, TreeRing).
|
||||
|
||||
@@ -531,8 +540,8 @@ def cmd_invisible(
|
||||
guidance_scale=None,
|
||||
seed=seed,
|
||||
humanize=humanize,
|
||||
protect_text=not no_protect_text,
|
||||
protect_faces=not no_protect_faces,
|
||||
protect_text=protect_text,
|
||||
protect_faces=protect_faces,
|
||||
max_resolution=max_resolution,
|
||||
)
|
||||
elapsed = time.monotonic() - t0
|
||||
@@ -707,18 +716,8 @@ def cmd_identify(ctx: click.Context, source: Path, no_visible: bool, as_json: bo
|
||||
default=0,
|
||||
help="Cap long side (px) before diffusion; 0 = native (best quality, like raiw.cc). Raise only on GPU/MPS OOM.",
|
||||
)
|
||||
@click.option(
|
||||
"--no-protect-text",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Disable automatic text protection (text/CJK is preserved by default on the SDXL pipeline).",
|
||||
)
|
||||
@click.option(
|
||||
"--no-protect-faces",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Disable face protection (skips the YOLO face detector; use when the image has no people).",
|
||||
)
|
||||
@_protect_text_option
|
||||
@_protect_faces_option
|
||||
@click.pass_context
|
||||
def cmd_all(
|
||||
ctx: click.Context,
|
||||
@@ -735,8 +734,8 @@ def cmd_all(
|
||||
hf_token: str | None,
|
||||
humanize: float,
|
||||
max_resolution: int,
|
||||
no_protect_text: bool,
|
||||
no_protect_faces: bool,
|
||||
protect_text: bool,
|
||||
protect_faces: bool,
|
||||
) -> None:
|
||||
"""Remove ALL watermarks: visible + invisible + metadata.
|
||||
|
||||
@@ -827,8 +826,8 @@ def cmd_all(
|
||||
num_inference_steps=steps,
|
||||
seed=seed,
|
||||
humanize=humanize,
|
||||
protect_text=not no_protect_text,
|
||||
protect_faces=not no_protect_faces,
|
||||
protect_text=protect_text,
|
||||
protect_faces=protect_faces,
|
||||
max_resolution=max_resolution,
|
||||
)
|
||||
console.print(" Invisible watermark removed")
|
||||
|
||||
@@ -125,8 +125,8 @@ class InvisibleEngine:
|
||||
guidance_scale: float | None = None,
|
||||
seed: int | None = None,
|
||||
humanize: float = 0.0,
|
||||
protect_faces: bool = True,
|
||||
protect_text: bool = True,
|
||||
protect_faces: bool = False,
|
||||
protect_text: bool = False,
|
||||
max_resolution: int = 0,
|
||||
) -> Path:
|
||||
"""Remove invisible watermark from an image.
|
||||
|
||||
Reference in New Issue
Block a user