mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-06-05 02:28:00 +02:00
fix(invisible): ctrlregen defaults to clean-noise strength, not the SDXL 0.10
The ctrlregen profile inherited the SDXL img2img --strength default (0.10), a near-identity pass that loaded ControlNet + DINOv2-giant and barely changed the image -- a no-op for removal. resolve_strength() now resolves an unset strength per profile: 0.10 for the SDXL default, CTRLREGEN_DEFAULT_STRENGTH (1.0, clean-noise) for ctrlregen. It checks `is None` rather than falsiness, so an explicit 0.0 is respected (the old `strength or DEFAULT` swallowed it). Research basis: CtrlRegen (ICLR 2025, arXiv:2410.05470) removes robust watermarks by regenerating from clean Gaussian noise; partial-noise img2img retains watermark info that diffuses back, so a high (clean-noise) strength is the lever, not a knob on the light SDXL pass. CLI wiring (--strength default None) lands with the cli refactor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,6 @@ 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
|
||||
|
||||
@@ -135,7 +133,8 @@ class InvisibleEngine:
|
||||
Args:
|
||||
image_path: Path to the watermarked image.
|
||||
output_path: Output path (None = overwrite source).
|
||||
strength: Denoising strength (0.0-1.0). None -> DEFAULT_STRENGTH (0.10).
|
||||
strength: Denoising strength (0.0-1.0). None -> profile default
|
||||
(0.10 for SDXL, 1.0 clean-noise for ctrlregen).
|
||||
steps: Number of denoising steps.
|
||||
guidance_scale: Classifier-free guidance scale.
|
||||
seed: Random seed for reproducibility.
|
||||
@@ -281,7 +280,7 @@ class InvisibleEngine:
|
||||
self,
|
||||
input_dir: Path,
|
||||
output_dir: Path,
|
||||
strength: float = DEFAULT_STRENGTH,
|
||||
strength: float | None = None,
|
||||
steps: int = 50,
|
||||
) -> list[Path]:
|
||||
"""Remove invisible watermarks from all images in a directory."""
|
||||
|
||||
@@ -21,6 +21,33 @@ CTRLREGEN_MODEL_ID = "yepengliu/ctrlregen"
|
||||
# knob is this default plus the per-call `--strength` override.)
|
||||
DEFAULT_STRENGTH = 0.10
|
||||
|
||||
# CtrlRegen removes watermarks by regenerating from (near) clean Gaussian noise,
|
||||
# NOT by the light-touch partial-noise img2img the SDXL default uses. The research
|
||||
# is explicit (CtrlRegen, ICLR 2025, arXiv:2410.05470): partial-noise regeneration
|
||||
# "struggles with high-perturbation watermarks" because a small noise step "retains"
|
||||
# watermark information that diffuses back into the output; the fix is to start from
|
||||
# clean noise. With the StableDiffusionControlNetImg2ImgPipeline that maps to a high
|
||||
# strength (~1.0 = full noise at the first timestep, structure held by the canny
|
||||
# ControlNet + DINOv2 IP-Adapter, not by the watermarked latent). So the ctrlregen
|
||||
# profile must NOT inherit the SDXL 0.10 default -- at 0.10 it loads ControlNet +
|
||||
# DINOv2-giant and then barely changes the image (a no-op for removal). Tunable via
|
||||
# `--strength`; lower it to trade removal strength for fidelity (the CtrlRegen+ regime).
|
||||
CTRLREGEN_DEFAULT_STRENGTH = 1.0
|
||||
|
||||
|
||||
def resolve_strength(strength: float | None, profile: str) -> float:
|
||||
"""Resolve the denoising strength, applying the profile-specific default when unset.
|
||||
|
||||
``None`` means "the user did not pass ``--strength``": the SDXL default profile
|
||||
resolves to ``DEFAULT_STRENGTH`` (a light SynthID-tuned touch), while ``ctrlregen``
|
||||
resolves to ``CTRLREGEN_DEFAULT_STRENGTH`` (clean-noise regeneration). An explicit
|
||||
value always wins. Shared by the CLI (for display) and the engine (for execution)
|
||||
so the two never disagree.
|
||||
"""
|
||||
if strength is not None:
|
||||
return strength
|
||||
return CTRLREGEN_DEFAULT_STRENGTH if profile == "ctrlregen" else DEFAULT_STRENGTH
|
||||
|
||||
|
||||
def get_model_id_for_profile(profile: str) -> str:
|
||||
"""Map CLI model profile names to concrete Hugging Face model IDs."""
|
||||
|
||||
@@ -33,6 +33,7 @@ from remove_ai_watermarks.noai.watermark_profiles import (
|
||||
DEFAULT_MODEL_ID,
|
||||
DEFAULT_STRENGTH,
|
||||
detect_model_profile,
|
||||
resolve_strength,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -474,7 +475,7 @@ class WatermarkRemover:
|
||||
if output_path is None:
|
||||
output_path = image_path
|
||||
|
||||
strength = strength or self.DEFAULT_STRENGTH
|
||||
strength = resolve_strength(strength, self.model_profile)
|
||||
|
||||
if not 0.0 <= strength <= 1.0:
|
||||
raise ValueError(f"Strength must be between 0.0 and 1.0, got {strength}")
|
||||
@@ -894,12 +895,16 @@ class WatermarkRemover:
|
||||
def remove_watermark(
|
||||
image_path: Path,
|
||||
output_path: Path | None = None,
|
||||
strength: float = DEFAULT_STRENGTH,
|
||||
strength: float | None = None,
|
||||
model_id: str | None = None,
|
||||
device: str | None = None,
|
||||
hf_token: str | None = None,
|
||||
) -> Path:
|
||||
"""Convenience function to remove watermark from an image."""
|
||||
"""Convenience function to remove watermark from an image.
|
||||
|
||||
``strength=None`` lets the profile pick its default (0.10 for SDXL, clean-noise
|
||||
1.0 for ctrlregen); pass a value to override.
|
||||
"""
|
||||
remover = WatermarkRemover(model_id=model_id, device=device, hf_token=hf_token)
|
||||
return remover.remove_watermark(
|
||||
image_path=image_path,
|
||||
|
||||
Reference in New Issue
Block a user