mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-06 22:18:36 +02:00
Mask faint text marks with the detector's match box, not a response threshold
The faint-mask fallback added for the tophat front-end thresholded the max-normalized uint8 response at 0.5 -- which selects every non-zero pixel, not "half the peak" as its comment claimed -- and filled ~120% of the corner box on textured frames. Measured on 14 real faint-path frames (cv2 fill, detector re-run after): the detector's own best-match box fills a 58.7%-median corner box vs 120.9% for the threshold, both 100% detector-clean. Detection and the mask now read one method, _tophat_best, whose score gates detection and whose argmax box bounds the fill, so the two cannot drift by construction -- which is how the mismatch arose. The 0.5 constant is deleted. Parity could not catch this (a mask that fills everything is trivially detector-clean) and the regression test could not either: its flat fixture gives every threshold the same box, so mutating the constant to 99.0 stayed green. The fixture now carries texture and asserts the mask area is bounded, not merely non-empty; it reproduces the corpus number (127% pre-fix). Also lands the Tier B2 verification harnesses that found and bounded this: - detector_response.py: response curves (detected AND maskable per cell); found the size response is a comb, contrast is near-irrelevant, no unmaskable cells. - ladder_headroom.py: measured that a denser scale ladder recovers 7.6% of misses for a 2.52%->3.05% false-fire rise, and the one landscape rung that helps is a geometry shift that helps and hurts equally (1.7:1) -- do not add. - cjk_tail_probe.py: a generic shared-tail (AI生成) template does not separate uncovered vendors from clean corners (0.407 vs clean p99 0.298). Records the visible-parity re-run confirming the earlier front-end fix (doubao 91.8% -> 99.3%), and dedups the thrice-written stamp forward model into one fill_quality.composite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7d00debdca
commit
52eb40c2ca
@@ -19,13 +19,20 @@ import pytest
|
||||
from remove_ai_watermarks.doubao_engine import DoubaoEngine
|
||||
|
||||
|
||||
def _faint_mark_image(w: int = 900, h: int = 1200, alpha: float = 0.06) -> np.ndarray:
|
||||
"""A mid-gray frame carrying the REAL Doubao glyph shape at very low opacity.
|
||||
def _faint_mark_image(w: int = 900, h: int = 1200, alpha: float = 0.06, textured: bool = False) -> np.ndarray:
|
||||
"""A frame carrying the REAL Doubao glyph shape at very low opacity.
|
||||
|
||||
The shape has to be genuine or the NCC detector will not fire and the test would be
|
||||
exercising nothing; the low ``alpha`` is what keeps the binarizing path from finding
|
||||
a blob. Composited with the same forward model the marks use:
|
||||
``stamped = (1-a)*bg + a*white``.
|
||||
|
||||
``textured`` adds fine luminance noise to the background. It is not decoration: on a
|
||||
FLAT frame the top-hat response is non-zero only on the glyph, so every threshold
|
||||
yields the same bounding box and a test built on a flat fixture cannot see a wrong
|
||||
threshold at all -- mutating the constant to an absurd value left the flat tests green.
|
||||
Texture puts response outside the glyph, which is the condition under which the mask's
|
||||
sizing actually matters, and is what real corner backgrounds look like.
|
||||
"""
|
||||
from remove_ai_watermarks._text_mark_engine import load_alpha_template
|
||||
|
||||
@@ -33,6 +40,11 @@ def _faint_mark_image(w: int = 900, h: int = 1200, alpha: float = 0.06) -> np.nd
|
||||
if tmpl is None:
|
||||
pytest.skip("doubao alpha asset unavailable")
|
||||
img = np.full((h, w, 3), 120, np.uint8)
|
||||
if textured:
|
||||
rng = np.random.default_rng(7)
|
||||
noise = rng.normal(0, 14, (h, w, 1)).repeat(3, axis=2)
|
||||
img = np.clip(img.astype(np.float32) + noise, 0, 255).astype(np.uint8)
|
||||
img = cv2.GaussianBlur(img, (0, 0), sigmaX=1.2)
|
||||
eng = DoubaoEngine()
|
||||
loc = eng.locate(img)
|
||||
base = eng.scale_base(img)
|
||||
@@ -82,3 +94,48 @@ class TestFaintMarkIsMaskable:
|
||||
mask = eng.footprint_mask(img, force=False)
|
||||
assert mask is not None
|
||||
assert int((mask > 0).sum()) > 0
|
||||
|
||||
|
||||
class TestFaintMaskStaysTight:
|
||||
"""The faint fallback must cover the mark WITHOUT filling the whole corner.
|
||||
|
||||
Corpus-found 2026-07-20: the first version of the fallback thresholded the continuous
|
||||
response at 0.5, but `tophat_response` returns uint8 0..255 -- so it selected every
|
||||
non-zero pixel, not "half the peak" as its comment claimed. On 12 of 12 real frames
|
||||
taking this path the resulting box covered 100% of the corner ROI, so removal inpainted
|
||||
the entire corner instead of the glyph. Parity could not see it: parity asks whether
|
||||
the detector is clean afterwards, and a mask that fills everything passes trivially.
|
||||
The cost, not the outcome, was the defect.
|
||||
"""
|
||||
|
||||
def test_mask_does_not_swallow_the_whole_corner_on_a_textured_frame(self):
|
||||
eng = DoubaoEngine()
|
||||
img = _faint_mark_image(alpha=0.10, textured=True)
|
||||
# Asserted, not skipped: a skip here would silently stop guarding the moment the
|
||||
# detector changed, which is exactly when this needs to be guarding.
|
||||
assert eng.detect(img).detected, "fixture must reach the faint path to test it"
|
||||
mask = eng.footprint_mask(img, force=False)
|
||||
assert mask is not None, "a detected faint mark must still produce a mask"
|
||||
area = int((mask > 0).sum())
|
||||
loc = eng.locate(img)
|
||||
roi = loc.w * loc.h
|
||||
# The mark's own glyph box is ~40% of the corner ROI and the mask pads it, so a
|
||||
# correct mask lands near 60%. The pre-fix behaviour measured 120.9% (the whole
|
||||
# ROI plus padding), which this bound excludes.
|
||||
assert area < 0.85 * roi, f"mask covers {100 * area / roi:.0f}% of the corner box"
|
||||
|
||||
def test_mask_still_covers_the_stamped_glyph(self):
|
||||
"""Tightness is only a virtue if the mark is still inside. Guards the other way."""
|
||||
eng = DoubaoEngine()
|
||||
img = _faint_mark_image(alpha=0.10, textured=True)
|
||||
assert eng.detect(img).detected, "fixture must reach the faint path to test it"
|
||||
mask = eng.footprint_mask(img, force=False)
|
||||
assert mask is not None
|
||||
loc = eng.locate(img)
|
||||
base = eng.scale_base(img)
|
||||
gw = max(eng.config.min_gw, int(eng.config.alpha_width_frac * base))
|
||||
gh = max(4, int(eng.config.alpha_height_frac * base))
|
||||
x = loc.x + (loc.w - gw) // 2
|
||||
y = loc.y + (loc.h - gh) // 2
|
||||
covered = int(np.count_nonzero(mask[y : y + gh, x : x + gw])) / max(1, gw * gh)
|
||||
assert covered > 0.6, f"mask covers only {100 * covered:.0f}% of the stamped glyph"
|
||||
|
||||
Reference in New Issue
Block a user