mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-07 14:38:35 +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:
co-authored by
Claude Opus 4.8
parent
9bd2c17cc4
commit
b686dbdd79
@@ -82,3 +82,87 @@ def unsharp_mask(image: NDArray, amount: float = 0.5, sigma: float = 1.0) -> NDA
|
||||
blurred = cv2.GaussianBlur(img_f, (0, 0), sigmaX=sigma, sigmaY=sigma)
|
||||
sharpened = cv2.addWeighted(img_f, 1.0 + amount, blurred, -amount, 0.0)
|
||||
return np.clip(sharpened, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
# ── Adaptive polish (target the input's detail level; spare text) ──────────────
|
||||
# A capped unsharp scaled to the sharpness deficit, then edge-masked grain to close
|
||||
# the rest -- tunable constants. Validated 2026-06-03 on the spaces corpus: a soft
|
||||
# gemini_3 face/photo (lap-var 84 vs the 592 of its original) is pulled up to ~327
|
||||
# with full polish, while a sharp openai_1 text card (1175 vs 1644) gets near-zero
|
||||
# (the deficit is tiny) so text is left alone -- the polish self-limits on text.
|
||||
_ADAPTIVE_MAX_UNSHARP = 1.0
|
||||
_ADAPTIVE_UNSHARP_GAIN = 0.4 # unsharp amount per unit of (deficit - 1), before the cap
|
||||
_ADAPTIVE_MAX_GRAIN = 8.0
|
||||
_MASK_EDGE_PERCENTILE = 85.0 # local-energy percentile above which a pixel is an "edge/text"
|
||||
_MASK_EDGE_DILATE = 5 # grow the edge mask so grain is suppressed in a margin around text
|
||||
_MASK_GAMMA = 2.0 # push the smooth weight toward 0 except in genuinely flat areas
|
||||
|
||||
|
||||
def _to_gray(image: NDArray) -> NDArray:
|
||||
"""Single-channel grayscale; passes a 2D (already-gray) input through unchanged."""
|
||||
return image if image.ndim == 2 else cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
|
||||
def _laplacian_variance(image: NDArray) -> float:
|
||||
"""Variance of the Laplacian -- a cheap proxy for high-frequency detail/sharpness."""
|
||||
return float(cv2.Laplacian(_to_gray(image), cv2.CV_64F).var())
|
||||
|
||||
|
||||
def _smooth_grain_mask(image: NDArray) -> NDArray:
|
||||
"""Per-pixel weight ~1 in flat/smooth regions, ~0 over text and hard edges.
|
||||
|
||||
Grain in smooth ("AI-plastic") regions reads as natural sensor noise; grain over
|
||||
text/edges just speckles them, so this masks grain to the smooth regions only.
|
||||
"""
|
||||
energy = cv2.GaussianBlur(np.abs(cv2.Laplacian(_to_gray(image).astype(np.float32), cv2.CV_32F)), (0, 0), sigmaX=2.0)
|
||||
thr = float(np.percentile(energy, _MASK_EDGE_PERCENTILE))
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (_MASK_EDGE_DILATE, _MASK_EDGE_DILATE))
|
||||
edges = cv2.dilate((energy > thr).astype(np.uint8), kernel)
|
||||
mask = np.clip(1.0 - energy / (thr + 1e-6), 0.0, 1.0) ** _MASK_GAMMA
|
||||
mask[edges > 0] = 0.0
|
||||
return cv2.GaussianBlur(mask, (0, 0), sigmaX=1.5)
|
||||
|
||||
|
||||
def adaptive_polish(image: NDArray, reference: NDArray, seed: int | None = None) -> NDArray:
|
||||
"""Restore the detail level of ``reference`` in a softened ``image``, sparing text.
|
||||
|
||||
Diffusion + face restoration leave an over-smoothed "AI-plastic" look, worst on
|
||||
photo/face regions. This targets the reference's Laplacian variance (the input's
|
||||
detail level): a capped unsharp scaled to the deficit, then edge-masked grain
|
||||
(smooth regions only) calibrated to close the remaining gap. **Self-limiting on
|
||||
text/graphics** -- they are already high-frequency, so the deficit is small and
|
||||
almost no polish is applied (text legibility is a generation-side concern, not a
|
||||
filter one). No-op when the image already meets the reference's detail level.
|
||||
|
||||
Args:
|
||||
image: the cleaned BGR output (uint8).
|
||||
reference: the original input BGR at the same resolution (the detail target).
|
||||
seed: optional RNG seed for reproducible grain.
|
||||
|
||||
Returns:
|
||||
Polished BGR image (uint8).
|
||||
"""
|
||||
target = _laplacian_variance(reference)
|
||||
current = _laplacian_variance(image)
|
||||
if target <= 0.0 or current >= target:
|
||||
return image.copy()
|
||||
|
||||
deficit = target / max(current, 1.0)
|
||||
amount = min(_ADAPTIVE_MAX_UNSHARP, _ADAPTIVE_UNSHARP_GAIN * (deficit - 1.0))
|
||||
work = unsharp_mask(image, amount=amount, sigma=1.2) if amount > 0.0 else image.copy()
|
||||
if _laplacian_variance(work) >= target:
|
||||
return work
|
||||
|
||||
# Calibrate the grain sigma by a short search: its lap-var contribution depends on
|
||||
# the per-pixel mask (no closed form), so step it up until the target is met. A few
|
||||
# full-image Laplacians here are negligible against the diffusion pass that precedes.
|
||||
mask = _smooth_grain_mask(work)
|
||||
noise = np.random.default_rng(seed).normal(0.0, 1.0, work.shape[:2]).astype(np.float32) * mask
|
||||
best = work
|
||||
sigma = 2.0
|
||||
while sigma <= _ADAPTIVE_MAX_GRAIN:
|
||||
best = np.clip(work.astype(np.float32) + (noise * sigma)[:, :, np.newaxis], 0.0, 255.0).astype(np.uint8)
|
||||
if _laplacian_variance(best) >= target:
|
||||
break
|
||||
sigma += 1.0
|
||||
return best
|
||||
|
||||
Reference in New Issue
Block a user