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:
Victor Kuznetsov
2026-06-26 09:44:24 -07:00
co-authored by Claude Opus 4.8
parent f269b75ded
commit 0f54c6b54d
7 changed files with 125 additions and 8 deletions
@@ -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
+29 -6
View File
@@ -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)