mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-06-05 02:28:00 +02:00
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:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user