Raise default SynthID-removal strength 0.05 → 0.10 (current Google SynthID) (#32)

* Raise default SynthID-removal strength 0.05 -> 0.10 (current Google SynthID)

The old default (0.04/0.05) no longer removes the CURRENT Google SynthID (Nano
Banana / Gemini 3): verified 2026-05-30 via the Gemini 'Verify with SynthID'
oracle on a real image -- 0.05 still detected, 0.10 not detected (OpenAI's was
already cleared at 0.05). Add DEFAULT_STRENGTH=0.10 in watermark_profiles, route
the engine + CLI defaults to it. At 0.10 small text deforms more, which is why
text protection (_run_region_hires) runs by default. CLAUDE.md SynthID note
corrected. CAVEAT: n=1 Google + n=1 OpenAI; broad corpus oracle validation
pending (task tracked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Drop unused LOW/MEDIUM/HIGH strength profiles; CLI --strength defaults to DEFAULT_STRENGTH

The fixed strength presets (and get_recommended_strength) were dead -- nothing in
the pipeline used them, only tests. One knob now: DEFAULT_STRENGTH (0.10),
overridable per-call via the CLI --strength flag, which now defaults to that
constant (single source of truth). Removed the WatermarkRemover.LOW/MEDIUM/HIGH
class attrs and the get_recommended_strength tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-05-30 13:15:58 -07:00
committed by GitHub
parent e4f558dccf
commit 29da3c52b6
6 changed files with 27 additions and 66 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -2
View File
@@ -21,6 +21,7 @@ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeEl
from rich.table import Table
from remove_ai_watermarks import __version__, watermark_registry
from remove_ai_watermarks.noai.watermark_profiles import DEFAULT_STRENGTH
if TYPE_CHECKING:
from numpy.typing import NDArray
@@ -362,7 +363,7 @@ def cmd_erase(
@click.option(
"-o", "--output", type=click.Path(path_type=Path), default=None, help="Output path (default: <source>_clean.<ext>)."
)
@click.option("--strength", type=float, default=0.05, help="Denoising strength (0.0-1.0). Default: 0.05.")
@click.option("--strength", type=float, default=DEFAULT_STRENGTH, help="Denoising strength (0.0-1.0). Default: 0.10.")
@click.option("--steps", type=int, default=50, help="Number of denoising steps. Default: 50.")
@click.option(
"--pipeline",
@@ -593,7 +594,9 @@ def cmd_identify(ctx: click.Context, source: Path, no_visible: bool, as_json: bo
@click.option(
"--inpaint-method", type=click.Choice(["ns", "telea", "gaussian"]), default="ns", help="Inpainting method."
)
@click.option("--strength", type=float, default=0.05, help="Invisible watermark denoising strength (0.0-1.0).")
@click.option(
"--strength", type=float, default=DEFAULT_STRENGTH, help="Invisible watermark denoising strength. Default: 0.10."
)
@click.option("--steps", type=int, default=50, help="Number of denoising steps for invisible removal.")
@click.option(
"--pipeline",
+4 -2
View File
@@ -19,6 +19,8 @@ import warnings
from pathlib import Path
from typing import TYPE_CHECKING
from remove_ai_watermarks.noai.watermark_profiles import DEFAULT_STRENGTH
if TYPE_CHECKING:
from collections.abc import Callable
@@ -133,7 +135,7 @@ class InvisibleEngine:
Args:
image_path: Path to the watermarked image.
output_path: Output path (None = overwrite source).
strength: Denoising strength (0.0-1.0). Default 0.04.
strength: Denoising strength (0.0-1.0). None -> DEFAULT_STRENGTH (0.10).
steps: Number of denoising steps.
guidance_scale: Classifier-free guidance scale.
seed: Random seed for reproducibility.
@@ -277,7 +279,7 @@ class InvisibleEngine:
self,
input_dir: Path,
output_dir: Path,
strength: float = 0.04,
strength: float = DEFAULT_STRENGTH,
steps: int = 50,
) -> list[Path]:
"""Remove invisible watermarks from all images in a directory."""
@@ -1,4 +1,4 @@
"""Watermark removal model profiles, strength presets, and profile detection.
"""Watermark removal model profiles, the default strength, and profile detection.
Pure configuration and lookup functions with no ML dependencies.
"""
@@ -8,12 +8,18 @@ from __future__ import annotations
DEFAULT_MODEL_ID = "stabilityai/stable-diffusion-xl-base-1.0"
CTRLREGEN_MODEL_ID = "yepengliu/ctrlregen"
LOW_STRENGTH = 0.04
MEDIUM_STRENGTH = 0.35
HIGH_STRENGTH = 0.7
_HIGH_PERTURBATION = ("stegasamp", "stegastamp", "treering", "ringid")
_LOW_PERTURBATION = ("stablesignature", "dwtectsvd", "rivagan", "ssl", "hidden")
# Single default denoising strength for the SDXL img2img scrub, overridable from
# the CLI (`--strength`). Raised from the old 0.04/0.05 because that no longer
# removes the CURRENT Google SynthID (Nano Banana / Gemini 3): verified 2026-05-30
# via the Gemini "Verify with SynthID" oracle on a real image -- 0.05 still
# detected, 0.10 not detected (OpenAI's SynthID was already cleared at 0.05). 0.10
# keeps the visible change modest while removing both. CAVEAT: confirmed on n=1
# Google + n=1 OpenAI image; broad oracle validation across the corpus is pending
# (different images may need a different strength). At this higher strength small
# text deforms more -- which is exactly why text protection (`_run_region_hires`)
# runs by default. (Fixed LOW/MEDIUM/HIGH presets were removed -- unused; the one
# knob is this default plus the per-call `--strength` override.)
DEFAULT_STRENGTH = 0.10
def get_model_id_for_profile(profile: str) -> str:
@@ -31,21 +37,3 @@ def detect_model_profile(model_id: str) -> str:
if "ctrlregen" in model_id.lower():
return "ctrlregen"
return "default"
def get_recommended_strength(watermark_type: str) -> float:
"""Get recommended strength for different watermark types.
Args:
watermark_type: Type of watermark. One of: 'low', 'medium', 'high',
or specific names like 'stegastamp', 'treering', etc.
Returns:
Recommended strength value.
"""
wt = watermark_type.lower()
if any(name in wt for name in _HIGH_PERTURBATION):
return HIGH_STRENGTH
if any(name in wt for name in _LOW_PERTURBATION):
return LOW_STRENGTH
return MEDIUM_STRENGTH
@@ -31,9 +31,7 @@ from PIL import Image
from remove_ai_watermarks.noai.watermark_profiles import (
CTRLREGEN_MODEL_ID,
DEFAULT_MODEL_ID,
HIGH_STRENGTH,
LOW_STRENGTH,
MEDIUM_STRENGTH,
DEFAULT_STRENGTH,
detect_model_profile,
)
@@ -277,9 +275,7 @@ class WatermarkRemover:
DEFAULT_MODEL_ID = DEFAULT_MODEL_ID
CTRLREGEN_MODEL_ID = CTRLREGEN_MODEL_ID
LOW_STRENGTH = LOW_STRENGTH
MEDIUM_STRENGTH = MEDIUM_STRENGTH
HIGH_STRENGTH = HIGH_STRENGTH
DEFAULT_STRENGTH = DEFAULT_STRENGTH
def __init__(
self,
@@ -446,7 +442,7 @@ class WatermarkRemover:
if output_path is None:
output_path = image_path
strength = strength or self.LOW_STRENGTH
strength = strength or self.DEFAULT_STRENGTH
if not 0.0 <= strength <= 1.0:
raise ValueError(f"Strength must be between 0.0 and 1.0, got {strength}")
@@ -853,7 +849,7 @@ class WatermarkRemover:
def remove_watermark(
image_path: Path,
output_path: Path | None = None,
strength: float = 0.04,
strength: float = DEFAULT_STRENGTH,
model_id: str | None = None,
device: str | None = None,
hf_token: str | None = None,
-28
View File
@@ -15,7 +15,6 @@ from remove_ai_watermarks.noai.utils import get_image_format, is_supported_forma
from remove_ai_watermarks.noai.watermark_profiles import (
detect_model_profile,
get_model_id_for_profile,
get_recommended_strength,
)
from remove_ai_watermarks.noai.watermark_remover import get_device, is_watermark_removal_available
@@ -121,33 +120,6 @@ class TestModelProfiles:
def test_detect_ctrlregen(self):
assert detect_model_profile("yepengliu/ctrlregen") == "ctrlregen"
def test_recommended_strength_high(self):
assert get_recommended_strength("treering") == 0.7
def test_recommended_strength_low(self):
assert get_recommended_strength("stablesignature") == 0.04
def test_recommended_strength_medium(self):
assert get_recommended_strength("unknown_type") == 0.35
@pytest.mark.parametrize("wm_type", ["stegastamp", "stegasamp", "treering", "ringid"])
def test_high_perturbation_watermark_types(self, wm_type):
"""Robust spatial watermarks need aggressive (0.7) regeneration."""
assert get_recommended_strength(wm_type) == 0.7
@pytest.mark.parametrize("wm_type", ["stablesignature", "dwtectsvd", "rivagan", "ssl", "hidden"])
def test_low_perturbation_watermark_types(self, wm_type):
"""Fragile frequency/latent watermarks break at low (0.04) strength."""
assert get_recommended_strength(wm_type) == 0.04
def test_strength_match_is_case_insensitive(self):
assert get_recommended_strength("TreeRing") == 0.7
assert get_recommended_strength("StableSignature") == 0.04
def test_strength_matches_substring_in_descriptive_name(self):
# e.g. a CLI passing "treering_v2" or "synthid-stegastamp" still maps.
assert get_recommended_strength("treering_v2") == 0.7
# ── Format utilities ────────────────────────────────────────────────