mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-06-05 02:28:00 +02:00
feat(auto): adaptive detail-targeting polish + --adaptive-polish flag
The fixed mild auto polish (unsharp 0.5 / grain 2.0) under-corrected soft photo/face output (gemini_3 stayed at lap-var 84 vs its 592 original) and its grain speckled small text. Replace it with humanizer.adaptive_polish: target the input's Laplacian variance with a capped unsharp scaled to the deficit + edge- masked grain (smooth regions only), calibrated by a short sigma search. Self- limiting on text/graphics -- already high-frequency, so almost no polish lands and text edges are masked out. Validated on the spaces corpus (gemini_3 84 -> 334 end-to-end; openai_1 text near-untouched). Interface: every --auto decision is now independently overridable -- add --adaptive-polish/--no-adaptive-polish (matching --restore-faces; works without --auto too) so the polish can be disabled or used manually. _apply_auto overrides exactly the three content-adaptive modes (pipeline, restore-faces, adaptive- polish); --unsharp/--humanize stay independent fixed filters. cv2-only, no new deps. Threaded through invisible/all (not batch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,8 @@ class TestPlan:
|
||||
assert cfg is not None
|
||||
assert cfg.pipeline == "default" # structure-less -> plain SDXL
|
||||
assert cfg.restore_faces is False
|
||||
assert cfg.unsharp == 0.0 # no smoothing pass -> no polish
|
||||
assert cfg.adaptive_polish is False # no smoothing pass -> no polish
|
||||
assert cfg.unsharp == 0.0
|
||||
assert cfg.humanize == 0.0
|
||||
assert cfg.min_resolution == 1024
|
||||
|
||||
@@ -65,8 +66,9 @@ class TestPlan:
|
||||
assert cfg.has_face
|
||||
assert cfg.restore_faces
|
||||
assert cfg.pipeline == "controlnet"
|
||||
assert cfg.unsharp == 0.5 # smoothing pass ran -> polish on
|
||||
assert cfg.humanize == 2.0
|
||||
assert cfg.adaptive_polish # smoothing pass ran -> adaptive polish on
|
||||
assert cfg.unsharp == 0.0 # fixed knobs off; the adaptive polish replaces them
|
||||
assert cfg.humanize == 0.0
|
||||
|
||||
def test_text_signal_forces_controlnet_on_flat(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(auto_config, "detect_text", lambda _img: True)
|
||||
@@ -82,8 +84,9 @@ class TestReason:
|
||||
cfg = auto_config.AutoConfig(
|
||||
pipeline="controlnet",
|
||||
restore_faces=True,
|
||||
unsharp=0.5,
|
||||
humanize=2.0,
|
||||
adaptive_polish=True,
|
||||
unsharp=0.0,
|
||||
humanize=0.0,
|
||||
min_resolution=1024,
|
||||
has_face=True,
|
||||
has_text=False,
|
||||
@@ -95,4 +98,4 @@ class TestReason:
|
||||
assert "controlnet" in r
|
||||
assert "face" in r
|
||||
assert "face-restore on" in r
|
||||
assert "unsharp 0.5" in r
|
||||
assert "adaptive polish" in r
|
||||
|
||||
@@ -102,3 +102,48 @@ def test_unsharp_flat_image_is_a_noop():
|
||||
img = np.full((30, 30, 3), 128, dtype=np.uint8)
|
||||
result = unsharp_mask(img, amount=0.8, sigma=1.0)
|
||||
assert np.array_equal(result, img)
|
||||
|
||||
|
||||
class TestAdaptivePolish:
|
||||
"""Adaptive polish: target the reference's detail level, sparing text/edges."""
|
||||
|
||||
def test_noop_when_already_sharp(self):
|
||||
from remove_ai_watermarks.humanizer import adaptive_polish
|
||||
|
||||
rng = np.random.default_rng(1)
|
||||
sharp = rng.integers(0, 256, (120, 120, 3), dtype=np.uint8) # high detail
|
||||
soft_ref = np.full((120, 120, 3), 128, dtype=np.uint8) # flat -> low target
|
||||
out = adaptive_polish(sharp, soft_ref)
|
||||
assert np.array_equal(out, sharp) # current >= target -> unchanged copy
|
||||
|
||||
def test_sharpens_a_soft_image_toward_reference(self):
|
||||
import cv2
|
||||
|
||||
from remove_ai_watermarks.humanizer import _laplacian_variance, adaptive_polish
|
||||
|
||||
rng = np.random.default_rng(2)
|
||||
reference = rng.integers(0, 256, (160, 160, 3), dtype=np.uint8) # very high detail
|
||||
soft = cv2.GaussianBlur(reference, (0, 0), sigmaX=4.0) # blurred -> low detail
|
||||
out = adaptive_polish(soft, reference, seed=0)
|
||||
assert _laplacian_variance(out) > _laplacian_variance(soft) # moved toward the target
|
||||
|
||||
def test_mask_spares_edges(self):
|
||||
from remove_ai_watermarks.humanizer import _smooth_grain_mask
|
||||
|
||||
img = np.full((100, 100, 3), 128, dtype=np.uint8)
|
||||
img[:, 50:] = 30 # a hard vertical edge down the middle
|
||||
mask = _smooth_grain_mask(img)
|
||||
# Flat far-left region keeps grain; the column at the edge is suppressed.
|
||||
assert mask[:, :15].mean() > mask[:, 45:55].mean()
|
||||
|
||||
def test_deterministic_with_seed(self):
|
||||
import cv2
|
||||
|
||||
from remove_ai_watermarks.humanizer import adaptive_polish
|
||||
|
||||
rng = np.random.default_rng(3)
|
||||
reference = rng.integers(0, 256, (140, 140, 3), dtype=np.uint8)
|
||||
soft = cv2.GaussianBlur(reference, (0, 0), sigmaX=3.0)
|
||||
a = adaptive_polish(soft, reference, seed=7)
|
||||
b = adaptive_polish(soft, reference, seed=7)
|
||||
assert np.array_equal(a, b)
|
||||
|
||||
Reference in New Issue
Block a user