mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-09-04 11:26:32 +02:00
fix(gemini): demote sparkle false positives with a core-brightness gate
detect_watermark's shape-only NCC (spatial/gradient/var fusion) fires on ornate or flat content (text strips, banners, hatching) that coincidentally matches the diamond shape. The NCC is contrast-invariant, so it cannot see the defining property of a real Gemini sparkle: a bright WHITE overlay whose core sits above the local background. The fusion now demotes (caps confidence to 0.30) a match that is BOTH low-confidence (< _SPARKLE_FP_CONF 0.65) AND has a low core-ring brightness margin (_core_ring_margin < _SPARKLE_FP_MARGIN 5). Real sparkles escape via EITHER high confidence (white-bg sparkles score >=0.79 despite a low margin) OR high margin (dark/mid backgrounds, incl. the #36 faint-corner case), so both must fail to demote. The gate is monotonic -- it only removes detections, never adds -- so it cannot regress the verified-negative corpus (already 0 FPs). On the spaces corpus it demoted 16/495 flagged sparkles (13 no AI metadata = content FPs; the 3 AI-meta ones were visually FPs / a near-invisible white-on-white sparkle whose AI verdict is held by metadata), and dropped the removal-audit failures 20 -> 15. - _core_and_bg shared helper (core 75th-pct brightness vs background-ring median); _estimate_alpha_gain refactored onto it, new _core_ring_margin wrapper. - TestSparkleFalsePositiveGate: margin high/low, strong-sparkle kept (incl. on white via high conf), blurred no-core blob demoted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b686dbdd79
commit
32a0779e1d
@@ -346,6 +346,85 @@ class TestUnderSubtractionGain:
|
||||
assert abs(float(footprint.mean()) - 80.0) < 20.0
|
||||
|
||||
|
||||
class TestSparkleFalsePositiveGate:
|
||||
"""False-positive gate: a low-confidence shape match whose core is NOT brighter
|
||||
than its surroundings (ornate/flat content, not a white sparkle overlay) is
|
||||
demoted below the detection threshold. Real sparkles escape via high confidence
|
||||
or a bright core-ring margin.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup_engine(self):
|
||||
self.engine = GeminiEngine()
|
||||
|
||||
def _composite_sparkle(self, bg_value: int, alpha_scale: float, size: int = 1400):
|
||||
img = np.full((size, size, 3), bg_value, dtype=np.float32)
|
||||
config = get_watermark_config(size, size)
|
||||
x, y = config.get_position(size, size)
|
||||
alpha = self.engine.get_alpha_map(WatermarkSize.LARGE)
|
||||
ah, aw = alpha.shape[:2]
|
||||
a = np.clip(alpha * alpha_scale, 0.0, 1.0)[:, :, None]
|
||||
roi = img[y : y + ah, x : x + aw]
|
||||
img[y : y + ah, x : x + aw] = a * 255.0 + (1.0 - a) * roi
|
||||
return np.clip(img, 0, 255).astype(np.uint8), (x, y, aw, ah)
|
||||
|
||||
def test_bright_core_has_high_margin(self):
|
||||
image, (x, y, w, _h) = self._composite_sparkle(bg_value=60, alpha_scale=1.0)
|
||||
margin = self.engine._core_ring_margin(image, self.engine.get_interpolated_alpha(w), (x, y))
|
||||
assert margin is not None
|
||||
assert margin > self.engine._SPARKLE_FP_MARGIN
|
||||
|
||||
def test_flat_region_has_low_margin(self):
|
||||
"""A uniform region (no white sparkle) has ~zero core-ring margin."""
|
||||
flat = np.full((1400, 1400, 3), 128, dtype=np.uint8)
|
||||
config = get_watermark_config(1400, 1400)
|
||||
pos = config.get_position(1400, 1400)
|
||||
alpha = self.engine.get_interpolated_alpha(96)
|
||||
margin = self.engine._core_ring_margin(flat, alpha, pos)
|
||||
assert margin is not None
|
||||
assert abs(margin) < self.engine._SPARKLE_FP_MARGIN
|
||||
|
||||
def test_strong_sparkle_not_demoted(self):
|
||||
image, _ = self._composite_sparkle(bg_value=60, alpha_scale=1.0)
|
||||
det = self.engine.detect_watermark(image)
|
||||
assert det.detected
|
||||
assert det.confidence >= 0.5
|
||||
|
||||
def test_strong_sparkle_on_white_kept(self):
|
||||
"""A real sparkle on a near-white background has a LOW core-ring margin (the
|
||||
white overlay barely lifts white) but a HIGH NCC confidence, so the gate must
|
||||
NOT demote it -- high confidence is the escape hatch."""
|
||||
image, (x, y, w, _h) = self._composite_sparkle(bg_value=251, alpha_scale=1.0)
|
||||
margin = self.engine._core_ring_margin(image, self.engine.get_interpolated_alpha(w), (x, y))
|
||||
assert margin is not None
|
||||
assert margin < self.engine._SPARKLE_FP_MARGIN # low margin
|
||||
det = self.engine.detect_watermark(image)
|
||||
assert det.detected
|
||||
assert det.confidence >= 0.65 # but kept via high confidence
|
||||
|
||||
def test_low_margin_blurred_blob_is_demoted(self):
|
||||
"""A heavily-blurred faint near-white blob NCC-matches the sparkle shape (the
|
||||
stage scores fuse above the 0.5 promote bar) but has no bright core (low
|
||||
margin), so the gate demotes the returned confidence below it -- the content
|
||||
false-positive case."""
|
||||
size = 1400
|
||||
config = get_watermark_config(size, size)
|
||||
x, y = config.get_position(size, size)
|
||||
alpha = self.engine.get_alpha_map(WatermarkSize.LARGE)
|
||||
ah, aw = alpha.shape[:2]
|
||||
img = np.full((size, size, 3), 247, dtype=np.float32)
|
||||
a = np.clip(alpha * 0.5, 0.0, 1.0)[:, :, None]
|
||||
img[y : y + ah, x : x + aw] = a * 255.0 + (1.0 - a) * img[y : y + ah, x : x + aw]
|
||||
img = cv2.GaussianBlur(np.clip(img, 0, 255).astype(np.uint8), (31, 31), 0)
|
||||
det = self.engine.detect_watermark(img)
|
||||
# The raw stage scores fuse above 0.5 (would be promoted)...
|
||||
pre = det.spatial_score * 0.5 + det.gradient_score * 0.3 + det.variance_score * 0.2
|
||||
assert pre > 0.5
|
||||
# ...but the no-bright-core gate caps the returned confidence below the bar.
|
||||
assert det.confidence < 0.5
|
||||
assert not det.detected
|
||||
|
||||
|
||||
class TestCornerPromotion:
|
||||
"""Issue #36: a small sparkle in the corner must not be lost to a larger decoy.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user