mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-07 06:28:36 +02:00
fix(identify): kill three visible-detector false positives
Bright-background photos/renders and a tiny app icon were flagged as AI-generated by the visible detectors. Two failure modes: - Gemini sparkle on a bright background (snow+sky photo, white product render) scored ~0.51. The FP gate only demoted on a low core-ring brightness margin, which a bright background makes high. Add a gradient floor (_SPARKLE_FP_GRAD 0.55): a real sparkle is a crisp star (grad ~0.97-1.0), a smooth luminance blob that NCC-matches the diamond is not (the two FPs measured grad 0.105 / 0.463). The OR is a strict superset of the old margin-only demotion, so it cannot regress dark/mid (kept by margin) or white-bg (kept by confidence) real sparkles. - A 48x48 geometric icon matched the Doubao/Jimeng CJK silhouette at 0.41/0.47 NCC. Purely a small-size artifact (the same icon at >=256px collapses to ~0.06-0.10). Guard text-mark detection below a 200px short side (_MIN_DETECT_SHORT_SIDE); real marks ship on full-resolution renders (smallest captured sample 1086px). Corpus re-sweep flips only OpenAI content and already-cleaned outputs, all sub-0.5, so no provenance verdict changes. Add synthetic regression fixtures for both modes; docs/module-internals.md updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f269b75ded
commit
0f54c6b54d
@@ -59,6 +59,19 @@ _OVERSUB_BODY_ALPHA_FLOOR = 0.15 # alpha above which a block pixel counts as gl
|
||||
_OVERSUB_INPAINT_DILATE = 9
|
||||
_OVERSUB_INPAINT_RADIUS = 4
|
||||
|
||||
# Minimum image short side (px) for text-mark DETECTION. Below this the glyph
|
||||
# template degrades to the ``min_gw`` floor (~8 px) and TM_CCOEFF_NORMED on a few
|
||||
# pixels is noise, so an unrelated small geometric shape can spuriously correlate
|
||||
# with the CJK silhouette (2026-06-26 FP: a 48x48 app icon -- a blue chevron --
|
||||
# scored Doubao 0.41 / Jimeng 0.47, both above their thresholds). The FP is purely
|
||||
# a small-size artifact: the same icon upscaled collapses to ~0.06-0.10 NCC at 256
|
||||
# px and above. A real AI-generation text label is stamped on a full-resolution
|
||||
# render (the captured samples are 1086-2048 px wide), so 200 px sits far below any
|
||||
# genuine mark while killing the icon/thumbnail noise band (<=96 px). Detection is
|
||||
# skipped (verdict stays "unknown", the safe default) rather than risk a false
|
||||
# positive; removal is gated on detection, so it is suppressed too.
|
||||
_MIN_DETECT_SHORT_SIDE = 200
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TextMarkConfig:
|
||||
@@ -256,6 +269,17 @@ class TextMarkEngine:
|
||||
det = TextMarkDetection()
|
||||
if image is None or image.size == 0:
|
||||
return det
|
||||
# Guard against the small-image NCC-noise false positive (see
|
||||
# _MIN_DETECT_SHORT_SIDE): an icon/thumbnail is too small to carry a real
|
||||
# text label, and the degraded few-pixel template spuriously correlates.
|
||||
if min(image.shape[:2]) < _MIN_DETECT_SHORT_SIDE:
|
||||
logger.debug(
|
||||
"%s detect: image short side %d < %d; too small to carry the mark, skipping.",
|
||||
c.name,
|
||||
min(image.shape[:2]),
|
||||
_MIN_DETECT_SHORT_SIDE,
|
||||
)
|
||||
return det
|
||||
loc = self.locate(image)
|
||||
box = self.extract_mask(image, loc) # box-sized mask (== old full-frame cropped to bbox)
|
||||
_x, _y, bw, bh = loc.bbox
|
||||
|
||||
@@ -185,6 +185,21 @@ class GeminiEngine:
|
||||
# fail to demote.
|
||||
_SPARKLE_FP_CONF = 0.65
|
||||
_SPARKLE_FP_MARGIN = 5.0
|
||||
# Bright-background content false positives (2026-06-26 landing-page FPs: a snow+sky
|
||||
# photo and a white-background product render both scored ~0.51). The margin gate
|
||||
# above cannot catch them -- a bright background gives the "core" a HIGH core-ring
|
||||
# margin (it is genuinely brighter than its surroundings), so the brightness check
|
||||
# reads it as a real overlay. The discriminating signature is the GRADIENT NCC: a
|
||||
# real white sparkle is a crisp star silhouette (grad ~0.97-1.0 on the synthetic
|
||||
# composites, ~0.96 on the real #36 corner sparkle), while a smooth luminance blob
|
||||
# that shape-NCC-matches the rough outline has low gradient fidelity (the two FPs
|
||||
# measured 0.105 and 0.463). So ALSO demote a low-confidence match whose gradient
|
||||
# NCC is below this floor, regardless of margin -- 0.55 sits well above the worst FP
|
||||
# (0.463) and far below every real sparkle (>=0.8). This only ADDS demotions on
|
||||
# bright backgrounds (a real bright-bg sparkle keeps grad ~0.97), so it cannot
|
||||
# regress a dark/mid sparkle (already kept by margin) or a white-bg one (kept by
|
||||
# confidence >= 0.65, above the gate).
|
||||
_SPARKLE_FP_GRAD = 0.55
|
||||
|
||||
# Self-verify fallback. The gain estimate corrects most under-subtractions, but
|
||||
# on the spaces corpus a tail of strong sparkles still survived reverse-alpha
|
||||
@@ -399,16 +414,24 @@ class GeminiEngine:
|
||||
# best_fused is the selected candidate's spatial*0.5 + grad*0.3 + var*0.2.
|
||||
confidence = best_fused
|
||||
|
||||
# False-positive gate: a low-confidence shape match whose core is NOT brighter
|
||||
# than its surroundings is a content false positive, not a white sparkle overlay.
|
||||
# False-positive gate: a low-confidence match that shows NEITHER real-sparkle
|
||||
# signature is a content false positive, not a white sparkle overlay. A real
|
||||
# sparkle proves itself by a bright core (high core-ring margin, on dark/mid
|
||||
# backgrounds) OR a crisp star silhouette (high gradient NCC, on any background
|
||||
# incl. bright). Demote when both are weak -- this catches the dark/mid no-core
|
||||
# FP (low margin) AND the bright-background smooth-blob FP (high margin but low
|
||||
# gradient), which the margin check alone misses. See _SPARKLE_FP_GRAD.
|
||||
if confidence < self._SPARKLE_FP_CONF:
|
||||
margin = self._core_ring_margin(image, self.get_interpolated_alpha(best_scale), (pos_x, pos_y))
|
||||
if margin is not None and margin < self._SPARKLE_FP_MARGIN:
|
||||
low_margin = margin is not None and margin < self._SPARKLE_FP_MARGIN
|
||||
low_grad = grad_score < self._SPARKLE_FP_GRAD
|
||||
if low_margin or low_grad:
|
||||
logger.debug(
|
||||
"Sparkle FP gate: conf=%.3f, core-ring margin=%.1f < %.1f; demoting.",
|
||||
"Sparkle FP gate: conf=%.3f, core-ring margin=%s, grad=%.3f < %.2f; demoting.",
|
||||
confidence,
|
||||
margin,
|
||||
self._SPARKLE_FP_MARGIN,
|
||||
f"{margin:.1f}" if margin is not None else "n/a",
|
||||
grad_score,
|
||||
self._SPARKLE_FP_GRAD,
|
||||
)
|
||||
confidence = min(confidence, 0.30)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user