Files
remove-ai-watermarks/tests/test_generic_ai_label_engine.py
Victor KuznetsovandClaude Sonnet 5 83be3a8926 Add generic bare "AI生成" text-mark detector (brand-less TC260 fallback)
Every existing text-mark engine (Doubao, Jimeng, Qwen, Baidu, Kling,
Yuanbao) is tuned to one vendor's exact wordmark silhouette, so none of
them match a bare "AI生成" strip with no brand name in front of it.
Confirmed on raiw-app production: a free `visible` run left a bottom-right
"AI生成" watermark completely unremoved because no detector fired.

Research found the wording is not tied to one dominant generator: vivo's
own support documentation confirms its Gallery stamps a bare "AI生成"
label after AI消除/AI扩图/大片模式 editing, and independent reporting
documents the same watermark on Xiaomi Gallery's AI-edit output. Since
the label is a GB 45438-2025/TC260 compliance stamp rather than one
vendor's brand mark, this adds one generic fallback (`generic_ai_label`)
instead of another per-OEM tuned config.

The detection silhouette is font-rendered (no logo element to capture)
via the shared scripts/render_vendor_silhouettes.py pipeline already
used for Qwen/Baidu/Kling/Yuanbao's own text-only marks. Calibration is
synthetic-only (no real captured generator screenshot was available);
the module docstring and DETECT_NCC_THRESHOLD comment carry that caveat
explicitly and this should be recalibrated once real captures exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:01:10 -07:00

202 lines
8.2 KiB
Python

"""Tests for the generic bare "AI生成" text-mark engine (brand-less TC260 fallback).
No real sample of this mark is committed -- the confirmed production bug this engine
was built for (a bottom-right "AI生成" strip with no vendor wordmark, left completely
unremoved because no existing tuned detector matched it) carries no customer image
bytes in any repository. Detection/removal is exercised against a watermark
synthesized from the engine's own font-rendered alpha asset, mirroring every other
text-mark engine's test pattern (see test_doubao_engine.py / test_jimeng_engine.py).
"""
from __future__ import annotations
import cv2
import numpy as np
import pytest
from remove_ai_watermarks import watermark_registry as registry
from remove_ai_watermarks.generic_ai_label_engine import (
_ALPHA_HEIGHT_FRAC,
_ALPHA_NATIVE_WIDTH,
_ALPHA_WIDTH_FRAC,
DETECT_NCC_THRESHOLD,
GenericAiLabelEngine,
_alpha_template,
_glyph_silhouette,
_template_match_score,
)
def _compose(w: int, h: int, bg: float = 100.0):
"""Composite the rendered alpha (scaled to width ``w``) onto a flat bg.
Returns ``(watermarked_uint8, mark_bool_mask)``."""
img = np.full((h, w, 3), bg, np.float32)
at = _alpha_template()
gw, gh = int(_ALPHA_WIDTH_FRAC * w), int(_ALPHA_HEIGHT_FRAC * w)
margin = int(0.015 * w)
ax = w - margin - gw
ay = h - margin - gh
amap = np.zeros((h, w), np.float32)
amap[ay : ay + gh, ax : ax + gw] = cv2.resize(at, (gw, gh))
a3 = amap[:, :, None]
wm = (a3 * 255.0 + (1 - a3) * img).clip(0, 255).astype(np.uint8)
return wm, amap > 0.2
class TestLocate:
def test_box_anchored_bottom_right(self):
eng = GenericAiLabelEngine()
img = np.zeros((2048, 2048, 3), np.uint8)
loc = eng.locate(img)
assert 2048 - (loc.x + loc.w) < int(2048 * 0.05)
assert 2048 - (loc.y + loc.h) < int(2048 * 0.05)
def test_box_scales_with_width(self):
eng = GenericAiLabelEngine()
small = eng.locate(np.zeros((1024, 1024, 3), np.uint8))
large = eng.locate(np.zeros((2048, 2048, 3), np.uint8))
assert large.w == pytest.approx(small.w * 2, rel=0.1)
class TestDetect:
def test_clean_gradient_not_detected(self):
eng = GenericAiLabelEngine()
ramp = np.tile(np.linspace(0, 255, 1024, dtype=np.uint8), (1024, 1))
img = cv2.cvtColor(ramp, cv2.COLOR_GRAY2BGR)
assert not eng.detect(img).detected
def test_solid_blob_corner_not_detected(self):
"""A bright blob is not the glyph shape -> low correlation, not detected."""
eng = GenericAiLabelEngine()
img = np.zeros((1024, 1024, 3), np.uint8)
x, y, bw, bh = eng.locate(img).bbox
img[y + bh // 4 : y + bh * 3 // 4, x : x + bw // 2] = 200
assert not eng.detect(img).detected
def test_silhouette_loads(self):
sil = _glyph_silhouette()
assert sil is not None
assert set(np.unique(sil)).issubset({0, 255})
def test_match_score_shape_sensitive(self):
"""The rendered glyph silhouette correlates with itself, not with a filled block."""
sil = _glyph_silhouette()
h, w = sil.shape
box = np.zeros((h + 8, int(w / _ALPHA_WIDTH_FRAC * 0.2) + w), np.uint8)
box[4 : 4 + h, 4 : 4 + w] = sil
assert _template_match_score(box, _ALPHA_NATIVE_WIDTH) >= DETECT_NCC_THRESHOLD
solid = np.full_like(box, 255)
assert _template_match_score(solid, _ALPHA_NATIVE_WIDTH) < DETECT_NCC_THRESHOLD
def test_detects_synthetic_mark(self):
wm, _mark = _compose(_ALPHA_NATIVE_WIDTH, _ALPHA_NATIVE_WIDTH)
eng = GenericAiLabelEngine()
det = eng.detect(wm)
assert det.detected
assert det.confidence >= DETECT_NCC_THRESHOLD
def test_small_image_guarded_from_false_positive(self):
"""Below the shared minimum short side, detection is skipped outright (the
same small-image NCC-noise guard every text mark inherits from
TextMarkEngine._scan)."""
wm, _mark = _compose(_ALPHA_NATIVE_WIDTH, _ALPHA_NATIVE_WIDTH)
eng = GenericAiLabelEngine()
assert eng.detect(wm).detected # native: real mark detected
assert not eng.detect(cv2.resize(wm, (150, 150))).detected # below guard: suppressed
class TestRivalMarks:
"""The generic template must not double-fire on a mark already attributed to its
own tuned brand engine -- every rival below contains the literal substring
"AI生成" in its own glyphs, which is exactly the collision the ``rivals`` guard
exists to prevent. See generic_ai_label_engine's DETECT_NCC_THRESHOLD comment for
the calibration this pins."""
@pytest.mark.parametrize("key", ["doubao", "qwen", "baidu", "kling", "yuanbao"])
def test_does_not_fire_on_rival_brand_mark(self, key: str):
from remove_ai_watermarks import watermark_registry as wr
marked = wr.get_mark(key)
# Build the rival's own canonical composite via the shared example generator,
# so this test does not need its own copy of each mark's geometry.
from scripts.render_visible_examples import base_photo, stamp_image_mark
base = base_photo(1536, 1152, seed=13)
out = stamp_image_mark(key, base)
assert out is not None, key
img, _box = out
assert marked.detect(img).detected, f"{key}: sanity check -- its own engine should still fire"
assert not GenericAiLabelEngine().detect(img).detected, f"generic engine double-fired on {key}"
class TestAlphaAsset:
def test_alpha_asset_loads(self):
at = _alpha_template()
assert at is not None
assert at.dtype.kind == "f"
assert float(at.min()) >= 0.0
assert float(at.max()) <= 1.0
class TestFootprintMaskAndRemoval:
def test_footprint_mask_in_bottom_right(self):
wm, _mark = _compose(_ALPHA_NATIVE_WIDTH, _ALPHA_NATIVE_WIDTH)
mask = GenericAiLabelEngine().footprint_mask(wm)
assert mask is not None
assert mask.shape == wm.shape[:2]
ys, xs = np.where(mask > 0)
assert ys.mean() > wm.shape[0] / 2
assert xs.mean() > wm.shape[1] / 2
def test_removes_synthetic_mark(self):
wm, _mark = _compose(_ALPHA_NATIVE_WIDTH, _ALPHA_NATIVE_WIDTH)
assert GenericAiLabelEngine().detect(wm).detected
out, region = registry.get_mark("generic_ai_label").remove(wm, backend="cv2")
assert region is not None
assert not GenericAiLabelEngine().detect(out).detected
def test_far_region_untouched(self):
wm, _mark = _compose(_ALPHA_NATIVE_WIDTH, _ALPHA_NATIVE_WIDTH)
out, _ = registry.get_mark("generic_ai_label").remove(wm, backend="cv2")
h, w = wm.shape[:2]
assert np.array_equal(wm[: h // 2, : w // 2], out[: h // 2, : w // 2])
class TestDegenerateAndChannelInputs:
"""footprint_mask must not crash on degenerate sizes or non-3-channel inputs."""
@pytest.mark.parametrize(("w", "h"), [(2048, 1), (1, 2048), (2048, 8)])
def test_wide_short_does_not_raise(self, w, h):
eng = GenericAiLabelEngine()
img = np.zeros((h, w, 3), np.uint8)
mask = eng.footprint_mask(img, force=True)
assert mask is None or mask.shape == (h, w)
def test_grayscale_2d_does_not_raise(self):
eng = GenericAiLabelEngine()
gray = np.zeros((2048, 2048), np.uint8)
mask = eng.footprint_mask(gray, force=True)
assert mask is None or mask.shape == (2048, 2048)
def test_bgra_4channel_does_not_raise(self):
eng = GenericAiLabelEngine()
bgra = np.zeros((2048, 2048, 4), np.uint8)
mask = eng.footprint_mask(bgra, force=True)
assert mask is None or mask.shape == (2048, 2048)
class TestRegistryWiring:
"""The mark must actually be reachable through the shared registry, the same way
identify/remove_auto_marks reach every other mark."""
def test_registered_with_generic_key(self):
mark = registry.get_mark("generic_ai_label")
assert mark.key == "generic_ai_label"
assert mark.in_auto
def test_participates_in_detect_marks(self):
wm, _mark = _compose(_ALPHA_NATIVE_WIDTH, _ALPHA_NATIVE_WIDTH)
results = registry.detect_marks(wm)
hit = next(r for r in results if r.key == "generic_ai_label")
assert hit.detected