mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-06 22:18:36 +02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
192 lines
8.8 KiB
Python
192 lines
8.8 KiB
Python
"""Policy-level tests for the shared text-mark engine config.
|
|
|
|
These assert TUNING that was set by corpus measurement, not algorithm behaviour --
|
|
they exist so a future edit cannot silently revert a calibrated constant back to a
|
|
value that was measured to be wrong. The measurements themselves live in
|
|
`docs/module-internals.md` and in the comment at
|
|
`_text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR`.
|
|
"""
|
|
|
|
|
|
class TestRivalMargin:
|
|
"""Detection among same-corner marks is COMPETITIVE, not just absolute.
|
|
|
|
Doubao "豆包AI生成" and Jimeng "★ 即梦AI" both sit bottom-right in near-white CJK
|
|
and survive binarization as similar blobs, so an absolute NCC gate cannot tell
|
|
them apart -- 33 of jimeng's 68 false additions were Doubao marks. Measured
|
|
separability scoring both templates on the SAME blob (n=40 jimeng / 75 doubao):
|
|
absolute ncc_jimeng 0.96, ncc_jimeng MINUS ncc_doubao 0.99. Corpus effect of the
|
|
margin gate: jimeng precision 38% -> 63% with genuine detections unchanged at 40
|
|
(false fires 65 -> 23).
|
|
"""
|
|
|
|
def test_jimeng_competes_against_doubao(self):
|
|
from remove_ai_watermarks import jimeng_engine
|
|
|
|
assert "doubao_alpha.png" in jimeng_engine._CONFIG.rivals
|
|
|
|
def test_doubao_has_no_rival_margin(self):
|
|
"""Asymmetric by measurement, not oversight: the symmetric gate cost Doubao 7
|
|
genuine detections to prevent 5 false ones (1.4:1 against), while Jimeng gained
|
|
25pp for free. Doubao's absolute detector is already 86% precise."""
|
|
from remove_ai_watermarks import doubao_engine
|
|
|
|
assert doubao_engine._CONFIG.rivals == ()
|
|
|
|
def test_a_doubao_shaped_blob_loses_the_jimeng_margin(self):
|
|
"""The decisive case: a blob matching Doubao BETTER than Jimeng must not be
|
|
booked as a Jimeng wordmark, however high its absolute Jimeng score."""
|
|
import numpy as np
|
|
|
|
from remove_ai_watermarks import jimeng_engine
|
|
from remove_ai_watermarks._text_mark_engine import glyph_silhouette
|
|
|
|
eng = jimeng_engine.JimengEngine()
|
|
doubao_blob = glyph_silhouette("doubao_alpha.png")
|
|
assert doubao_blob is not None
|
|
canvas = np.zeros((doubao_blob.shape[0] + 20, doubao_blob.shape[1] + 20), np.uint8)
|
|
canvas[10 : 10 + doubao_blob.shape[0], 10 : 10 + doubao_blob.shape[1]] = doubao_blob
|
|
width = int(doubao_blob.shape[1] / jimeng_engine._CONFIG.alpha_width_frac)
|
|
jimeng_score = eng._template_match_score(canvas, width)
|
|
assert not eng._rival_margin_ok(jimeng_score, canvas, width)
|
|
|
|
|
|
class TestPerMarkProvenanceRelaxation:
|
|
"""The provenance NCC relaxation is PER MARK, not one shared multiplier.
|
|
|
|
Corpus-measured 2026-07-18 on the default `auto` path (4417 unique TC260
|
|
carriers, blind hand-label, two-sided control): the single shared 0.7 ran at
|
|
76% precision on doubao but 17% on jimeng, because jimeng's relaxed silhouette
|
|
keys on "text in the bottom-right corner" rather than the wordmark -- 33 of its
|
|
68 false additions were DOUBAO marks. Full table at
|
|
`_text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR`.
|
|
"""
|
|
|
|
|
|
class TestScaleBasis:
|
|
"""Mark geometry scales with a PER-MARK image dimension, measured not assumed.
|
|
|
|
Every tuned fraction was calibrated on PORTRAIT captures, where width and short
|
|
side coincide, so the basis was never exercised until landscape inputs were
|
|
measured. Corpus-measured 2026-07-18 (2572 unique TC260 carriers): doubao
|
|
detection was portrait 60% / square 41% / **landscape 0% of 435** -- a width-scaled
|
|
box is inflated by the aspect ratio on a wide image and the glyph never lands in
|
|
it. A short-side basis recovered 56% of the previously-undetected landscape set.
|
|
The same switch broke JIMENG (labelled landscape positives 13/13 -> 0/13), whose
|
|
wordmark tracks the width -- hence per-mark, not a house rule.
|
|
"""
|
|
|
|
def test_doubao_scales_with_the_short_side(self):
|
|
from remove_ai_watermarks import doubao_engine
|
|
|
|
assert doubao_engine._CONFIG.scale_basis == "short"
|
|
|
|
def test_jimeng_scales_with_width(self):
|
|
"""Measured, not an oversight: the short-side basis took jimeng's labelled
|
|
landscape positives from 13/13 to 0/13."""
|
|
from remove_ai_watermarks import jimeng_engine
|
|
|
|
assert jimeng_engine._CONFIG.scale_basis == "width"
|
|
|
|
def test_samsung_keeps_width_because_it_is_unmeasured(self):
|
|
"""1 addition corpus-wide, so there is no evidence either way; an unmeasured
|
|
change is not an improvement."""
|
|
from remove_ai_watermarks import samsung_engine
|
|
|
|
assert samsung_engine._CONFIG.scale_basis == "width"
|
|
|
|
def test_basis_only_differs_on_non_square_images(self):
|
|
"""The basis is a no-op wherever width IS the short side, which is why the bug
|
|
survived: every calibration capture was portrait."""
|
|
import numpy as np
|
|
|
|
from remove_ai_watermarks import doubao_engine, jimeng_engine
|
|
|
|
portrait = np.zeros((1600, 900, 3), np.uint8)
|
|
landscape = np.zeros((900, 1600, 3), np.uint8)
|
|
d, j = doubao_engine.DoubaoEngine(), jimeng_engine.JimengEngine()
|
|
assert d.scale_base(portrait) == j.scale_base(portrait) == 900
|
|
assert d.scale_base(landscape) == 900
|
|
assert j.scale_base(landscape) == 1600
|
|
|
|
def test_landscape_box_stays_inside_the_frame(self):
|
|
"""The concrete failure: a width-scaled box on a wide image overshoots the
|
|
mark's real footprint. The short-side box must be proportionally smaller."""
|
|
import numpy as np
|
|
|
|
from remove_ai_watermarks import doubao_engine
|
|
|
|
landscape = np.zeros((900, 2400, 3), np.uint8)
|
|
loc = doubao_engine.DoubaoEngine().locate(landscape)
|
|
assert loc.w < int(2400 * doubao_engine._CONFIG.width_frac)
|
|
assert loc.x + loc.w <= 2400
|
|
assert loc.y + loc.h <= 900
|
|
|
|
|
|
class TestTophatFrontend:
|
|
"""Detection can correlate the CONTINUOUS top-hat instead of a binarized blob.
|
|
|
|
`extract_mask` thresholds the top-hat into a 0/255 glyph blob, which is fine for a
|
|
mark stamped bold and opaque and destructive for a faint one -- a thin translucent
|
|
overlay shatters into specks and no template can match a blob that is not there
|
|
(measured: 千问 scored 0.170 mean vs doubao's 0.723 through the binary path, 0% over
|
|
the gate). The `tophat` front-end never binarizes: the saturation/luma gates become
|
|
weights, and the response is max-normalized so the score is contrast-invariant.
|
|
|
|
Corpus effect on the 240-image unbiased recall sample: doubao recall 89% -> 92% at
|
|
an unchanged 99% precision.
|
|
"""
|
|
|
|
def test_doubao_uses_the_continuous_frontend(self):
|
|
from remove_ai_watermarks import doubao_engine
|
|
|
|
assert doubao_engine._CONFIG.detect_frontend == "tophat"
|
|
|
|
def test_other_marks_stay_binary_until_measured(self):
|
|
"""A front-end switch must be measured per mark before it ships; jimeng and
|
|
samsung have no such measurement yet."""
|
|
from remove_ai_watermarks import jimeng_engine, samsung_engine
|
|
|
|
assert jimeng_engine._CONFIG.detect_frontend == "binary"
|
|
assert samsung_engine._CONFIG.detect_frontend == "binary"
|
|
|
|
def test_response_is_contrast_invariant(self):
|
|
"""The whole point: a faint mark and a bold one produce the same response, so a
|
|
single threshold works for both. Binarizing is what loses the faint one."""
|
|
import numpy as np
|
|
|
|
from remove_ai_watermarks import doubao_engine
|
|
|
|
eng = doubao_engine.DoubaoEngine()
|
|
h, w = 400, 900
|
|
out = []
|
|
for amplitude in (12, 90): # a barely-there overlay and a bold one
|
|
img = np.full((h, w, 3), 100, np.uint8)
|
|
loc = eng.locate(img)
|
|
x, y, bw, bh = loc.bbox
|
|
img[y + bh // 3 : y + 2 * bh // 3, x + bw // 4 : x + 3 * bw // 4] = 100 + amplitude
|
|
resp = eng.tophat_response(img, loc)
|
|
assert resp is not None
|
|
out.append(resp)
|
|
# max-normalized, so the two responses agree despite a 7.5x contrast difference
|
|
assert abs(int(out[0].max()) - int(out[1].max())) <= 1
|
|
|
|
def test_flat_input_yields_no_response(self):
|
|
"""A blank corner has no top-hat at all; the engine must return None rather
|
|
than divide by a zero peak."""
|
|
import numpy as np
|
|
|
|
from remove_ai_watermarks import doubao_engine
|
|
|
|
eng = doubao_engine.DoubaoEngine()
|
|
img = np.full((400, 900, 3), 128, np.uint8)
|
|
assert eng.tophat_response(img, eng.locate(img)) is None
|
|
|
|
def test_threshold_is_frontend_specific(self):
|
|
"""The continuous front-end scores higher overall (0.809 vs 0.723 mean on the
|
|
same positives), so it needs its own gate; the binary-era 0.40 left the
|
|
provenance-relaxed gate low enough to admit 8 false fires where 0.50 admits 1."""
|
|
from remove_ai_watermarks import doubao_engine
|
|
|
|
assert doubao_engine._CONFIG.detect_ncc_threshold == 0.50
|