mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-06 22:18:36 +02:00
Register RunningHub, Baidu, and LibLibAI visible marks; park Qingyan and MiniMax (measured)
New engines, each calibrated on its TC260 USCC cohort and validated by a full-corpus sweep (42009 files): - runninghub: top-left corner (new corner="tl"), faint mid-gray text via the new raw-grayscale "gray" detection front-end, anchor-position gate - baidu: text-run-only template (pill is a bright-blob magnet), load-bearing Doubao+Qwen rival margins, corner-extended footprint for the white tag - liblib: bottom-center (new corner="bc"), Arial silhouette (font is the discriminative lever against latin UI text), logo-extended footprint Qingyan parked (no clean-arm separation at any render/box), MiniMax/Hailuo parked (1 visible frame, the xinghui rule); silhouettes kept as starting points.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Tests for the Baidu ("百度 AI生成") visible-watermark engine.
|
||||
|
||||
Every tuned constant in ``baidu_engine`` was measured on the 16-frame vendor
|
||||
cohort (2026-07-22); these tests pin the load-bearing ones: detection keys on
|
||||
the 百度 text run ONLY (the text+pill template was a measured bright-blob
|
||||
magnet), the load-bearing Doubao rival margin, the strict-only gate, and the
|
||||
corner-extended footprint (the tag's flat white interior gives no top-hat
|
||||
response, so a blob-bbox mask leaves the tag as a ghost).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from remove_ai_watermarks import watermark_registry as registry
|
||||
from remove_ai_watermarks.baidu_engine import (
|
||||
_ALPHA_HEIGHT_FRAC,
|
||||
_ALPHA_WIDTH_FRAC,
|
||||
BaiduEngine,
|
||||
_alpha_template,
|
||||
)
|
||||
|
||||
_TEXT_FRAC = 0.090 # measured 百度 text-run width, fraction of the short side
|
||||
_TEXT_RIGHT = 0.099 # measured right margin of the text run (the tag is right of it)
|
||||
_TAG_FRAC = 0.075 # the white tag's width, approx (text-right to corner)
|
||||
|
||||
|
||||
def _compose(w: int, h: int, bg: float = 100.0):
|
||||
"""Composite the 百度 text run + a solid white tag at the measured layout."""
|
||||
img = np.full((h, w, 3), bg, np.float32)
|
||||
at = _alpha_template()
|
||||
short = min(w, h)
|
||||
gw = int(_TEXT_FRAC * short)
|
||||
gh = max(4, int(_TEXT_FRAC * (_ALPHA_HEIGHT_FRAC / _ALPHA_WIDTH_FRAC) * short))
|
||||
margin_b = int(0.006 * short)
|
||||
ax = w - int(_TEXT_RIGHT * short) - gw
|
||||
ay = h - margin_b - gh
|
||||
amap = np.zeros((h, w), np.float32)
|
||||
amap[ay : ay + gh, ax : ax + gw] = cv2.resize(at, (gw, gh))
|
||||
# the white rounded tag between the text and the corner
|
||||
tx0 = w - int(0.015 * short) - int(_TAG_FRAC * short)
|
||||
amap[ay - gh // 8 : ay + gh + gh // 8, tx0 : w - int(0.015 * short)] = 1.0
|
||||
a3 = amap[:, :, None]
|
||||
wm = (a3 * 255.0 + (1 - a3) * img).clip(0, 255).astype(np.uint8)
|
||||
return wm, (ax, ay, gw, gh, tx0)
|
||||
|
||||
|
||||
class TestLocate:
|
||||
def test_box_anchored_bottom_right(self):
|
||||
eng = BaiduEngine()
|
||||
img = np.zeros((2048, 2048, 3), np.uint8)
|
||||
loc = eng.locate(img)
|
||||
assert 2048 - (loc.x + loc.w) < 40
|
||||
assert 2048 - (loc.y + loc.h) < 40
|
||||
|
||||
def test_box_scales_with_short_side(self):
|
||||
eng = BaiduEngine()
|
||||
landscape = eng.locate(np.zeros((640, 1280, 3), np.uint8))
|
||||
wider = eng.locate(np.zeros((640, 2560, 3), np.uint8))
|
||||
assert wider.w == landscape.w
|
||||
|
||||
|
||||
class TestConfig:
|
||||
def test_tophat_frontend(self):
|
||||
assert BaiduEngine().config.detect_frontend == "tophat"
|
||||
|
||||
def test_doubao_rival_margin(self):
|
||||
# 百度 vs 豆包 share a glyph and a corner: the candidate fires on 45.8% of
|
||||
# Doubao-marked frames at the gate, and the 0.10 margin suppresses ALL of
|
||||
# it at zero genuine-detection cost (crossfire, 2026-07-22).
|
||||
assert "doubao_alpha.png" in BaiduEngine().config.rivals
|
||||
|
||||
def test_strict_only_no_provenance_relaxation(self):
|
||||
assert BaiduEngine().config.provenance_ncc_factor == 1.0
|
||||
|
||||
def test_gate_above_clean_arm_max(self):
|
||||
# Clean arm scored p99 0.314 / max 0.352 on 278 hand-labelled frames;
|
||||
# the 741-frame eval set surfaced cross-fires up to 0.426 (a 抖音
|
||||
# AI创作 mark no rival can suppress), and the full-corpus sweep put the
|
||||
# outside-cohort false arm at 0.47 max vs true carriers at 0.50-0.66,
|
||||
# so the gate sits at 0.48.
|
||||
assert BaiduEngine().config.detect_ncc_threshold >= 0.48
|
||||
|
||||
def test_qwen_is_a_rival(self):
|
||||
# 百度 vs 千问 are near-identical after binarization: 12 of 14 full-corpus
|
||||
# cross-fires at the 0.37 gate were Qwen marks (Qwen's template beats
|
||||
# Baidu's there by 0.17-0.35, so the margin suppresses them).
|
||||
assert "qwen_alpha.png" in BaiduEngine().config.rivals
|
||||
|
||||
def test_registry_row(self):
|
||||
mark = registry.get_mark("baidu")
|
||||
assert mark.location == "bottom-right"
|
||||
assert mark.in_auto
|
||||
|
||||
|
||||
class TestDetectAndMask:
|
||||
def test_detects_composed_mark(self):
|
||||
eng = BaiduEngine()
|
||||
wm, _ = _compose(1024, 1024)
|
||||
det = eng.detect(wm)
|
||||
assert det.detected, f"composed mark missed (conf={det.confidence:.3f})"
|
||||
|
||||
def test_clean_frame_stays_quiet(self):
|
||||
eng = BaiduEngine()
|
||||
img = np.full((1024, 1024, 3), 100, np.uint8)
|
||||
assert not eng.detect(img).detected
|
||||
|
||||
def test_mask_extends_to_the_corner_tag(self):
|
||||
"""Regression (2026-07-22): the tag's flat white interior gives no top-hat
|
||||
response, so a blob-bbox mask ended at the text run and the fill left the
|
||||
tag as a ghost. The footprint must extend right to the corner."""
|
||||
eng = BaiduEngine()
|
||||
wm, (ax, _ay, gw, _gh, tx0) = _compose(1024, 1024)
|
||||
mask = eng.footprint_mask(wm)
|
||||
assert mask is not None
|
||||
_ys, xs = np.where(mask > 0)
|
||||
assert xs.min() <= ax + int(0.1 * gw) # covers the text run's left edge
|
||||
assert xs.max() >= tx0 + 10 # covers the white tag right of the text
|
||||
|
||||
def test_no_mask_on_clean_frame(self):
|
||||
eng = BaiduEngine()
|
||||
img = np.full((1024, 1024, 3), 100, np.uint8)
|
||||
assert eng.footprint_mask(img) is None
|
||||
|
||||
|
||||
class TestPillInteraction:
|
||||
def test_confident_baidu_detection_suppresses_the_jimeng_pill(self):
|
||||
# A Baidu image is TC260 too but is not Jimeng-basic: like Doubao/Qwen/
|
||||
# Kling, a confident Baidu detection must veto the pill (``_keep_pill``).
|
||||
from remove_ai_watermarks.watermark_registry import _keep_pill
|
||||
|
||||
assert not _keep_pill({"baidu"}, provenance=frozenset({"jimeng"}), footprint_flat=1.0)
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Tests for the LibLibAI ("LibLibAI" wordmark) visible-watermark engine.
|
||||
|
||||
Every tuned constant in ``liblib_engine`` was measured on the 15-frame vendor
|
||||
cohort (2026-07-22); these tests pin the load-bearing ones: the bottom-CENTER
|
||||
anchor, the strict-only gate, and the match-box footprint (the blob bbox both
|
||||
bled into background structure and did not own the triangle logo).
|
||||
"""
|
||||
|
||||
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.liblib_engine import (
|
||||
_ALPHA_HEIGHT_FRAC,
|
||||
_ALPHA_WIDTH_FRAC,
|
||||
LibLibEngine,
|
||||
_alpha_template,
|
||||
)
|
||||
|
||||
_MARK_FRAC = 0.10 # measured wordmark width, fraction of the frame WIDTH
|
||||
|
||||
|
||||
def _compose(w: int, h: int, bg: float = 100.0):
|
||||
"""Composite a triangle logo + the LibLibAI wordmark, bottom-center."""
|
||||
img = np.full((h, w, 3), bg, np.float32)
|
||||
at = _alpha_template()
|
||||
gw = int(_MARK_FRAC * w)
|
||||
gh = max(4, int(_MARK_FRAC * (_ALPHA_HEIGHT_FRAC / _ALPHA_WIDTH_FRAC) * w))
|
||||
ax = (w - gw) // 2
|
||||
ay = int(0.94 * h) - gh
|
||||
amap = np.zeros((h, w), np.float32)
|
||||
amap[ay : ay + gh, ax : ax + gw] = cv2.resize(at, (gw, gh))
|
||||
# the triangle logo, its own height to the LEFT of the wordmark
|
||||
lx1 = ax - int(0.3 * gh)
|
||||
lx0 = lx1 - gh
|
||||
cv2.fillPoly(amap, [np.array([(lx0, ay + gh), (lx1, ay + gh), (lx1, ay)])], 1.0)
|
||||
a3 = amap[:, :, None]
|
||||
wm = (a3 * 255.0 + (1 - a3) * img).clip(0, 255).astype(np.uint8)
|
||||
return wm, (ax, ay, gw, gh, lx0)
|
||||
|
||||
|
||||
class TestLocate:
|
||||
def test_box_horizontally_centered(self):
|
||||
eng = LibLibEngine()
|
||||
img = np.zeros((2048, 1536, 3), np.uint8)
|
||||
loc = eng.locate(img)
|
||||
assert (1536 - loc.w) // 2 == pytest.approx(loc.x, abs=2) # corner="bc"
|
||||
assert 2048 - (loc.y + loc.h) > 0 # bottom-anchored
|
||||
|
||||
def test_box_scales_with_width(self):
|
||||
eng = LibLibEngine()
|
||||
narrow = eng.locate(np.zeros((2048, 1024, 3), np.uint8))
|
||||
wide = eng.locate(np.zeros((2048, 2048, 3), np.uint8))
|
||||
assert wide.w == pytest.approx(narrow.w * 2, rel=0.05)
|
||||
|
||||
|
||||
class TestConfig:
|
||||
def test_tophat_frontend(self):
|
||||
assert LibLibEngine().config.detect_frontend == "tophat"
|
||||
|
||||
def test_strict_only_no_provenance_relaxation(self):
|
||||
assert LibLibEngine().config.provenance_ncc_factor == 1.0
|
||||
|
||||
def test_gate_above_clean_arm_max(self):
|
||||
# With the Arial silhouette the full-corpus false arm (latin UI text)
|
||||
# tops out at 0.398 while the cohort sits at 0.43-0.59; gate 0.42.
|
||||
assert LibLibEngine().config.detect_ncc_threshold >= 0.42
|
||||
|
||||
def test_small_image_size_floor(self):
|
||||
# The one full-corpus false fire with the final template was a 200x200
|
||||
# icon on a 20px template; the engine refuses small images outright.
|
||||
eng = LibLibEngine()
|
||||
assert not eng.detect(np.full((200, 200, 3), 100, np.uint8)).detected
|
||||
wm, _ = _compose(200, 200)
|
||||
assert not eng.detect(wm).detected # even a composed mark under the floor
|
||||
|
||||
def test_registry_row(self):
|
||||
mark = registry.get_mark("liblib")
|
||||
assert mark.location == "bottom-center"
|
||||
assert mark.in_auto
|
||||
|
||||
|
||||
class TestDetectAndMask:
|
||||
def test_detects_composed_mark(self):
|
||||
eng = LibLibEngine()
|
||||
wm, _ = _compose(1792, 2400)
|
||||
det = eng.detect(wm)
|
||||
assert det.detected, f"composed mark missed (conf={det.confidence:.3f})"
|
||||
|
||||
def test_clean_frame_stays_quiet(self):
|
||||
eng = LibLibEngine()
|
||||
img = np.full((2400, 1792, 3), 100, np.uint8)
|
||||
assert not eng.detect(img).detected
|
||||
|
||||
def test_mask_covers_logo_and_wordmark(self):
|
||||
"""The footprint must cover the triangle logo LEFT of the wordmark while
|
||||
staying bounded by the match box vertically (the blob bbox bled into
|
||||
background structure and ate real content, 2026-07-22)."""
|
||||
eng = LibLibEngine()
|
||||
wm, (ax, ay, gw, gh, lx0) = _compose(1792, 2400)
|
||||
mask = eng.footprint_mask(wm)
|
||||
assert mask is not None
|
||||
ys, xs = np.where(mask > 0)
|
||||
assert xs.min() <= lx0 + gh // 2 # covers the logo
|
||||
assert xs.max() >= ax + gw - int(0.05 * gw) # covers the wordmark's right edge
|
||||
assert ys.min() >= ay - gh # does not bleed far above the mark
|
||||
|
||||
def test_no_mask_on_clean_frame(self):
|
||||
eng = LibLibEngine()
|
||||
img = np.full((2400, 1792, 3), 100, np.uint8)
|
||||
assert eng.footprint_mask(img) is None
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Tests for the RunningHub ("RunningHub AI生成") visible-watermark engine.
|
||||
|
||||
Every tuned constant in ``runninghub_engine`` was measured on the 73-frame
|
||||
vendor cohort (2026-07-22, ``scripts/vendor_cohort_harvest.py`` +
|
||||
``scripts/vendor_mark_calibrate.py``); these tests pin the load-bearing ones:
|
||||
the top-left corner, the gray front-end, the exact-size tight ladder, the
|
||||
strict-only gate, and the mask/coverage parity regression (the partial-blob
|
||||
"Runni" miss).
|
||||
"""
|
||||
|
||||
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.runninghub_engine import (
|
||||
_ALPHA_HEIGHT_FRAC,
|
||||
_ALPHA_WIDTH_FRAC,
|
||||
RunningHubEngine,
|
||||
_alpha_template,
|
||||
)
|
||||
|
||||
_MARK_FRAC = 0.32 # measured mark width, fraction of the frame WIDTH
|
||||
|
||||
|
||||
def _compose(w: int, h: int, mode: float = _MARK_FRAC, bg: float = 100.0):
|
||||
"""Composite the RunningHub silhouette at the measured size, top-left."""
|
||||
img = np.full((h, w, 3), bg, np.float32)
|
||||
at = _alpha_template()
|
||||
gw = int(mode * w)
|
||||
gh = max(4, int(mode * (_ALPHA_HEIGHT_FRAC / _ALPHA_WIDTH_FRAC) * w))
|
||||
ax, ay = int(0.008 * w), int(0.006 * h)
|
||||
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, (ax, ay, gw, gh)
|
||||
|
||||
|
||||
class TestLocate:
|
||||
def test_box_anchored_top_left(self):
|
||||
eng = RunningHubEngine()
|
||||
img = np.zeros((2048, 1536, 3), np.uint8)
|
||||
loc = eng.locate(img)
|
||||
assert loc.x < 40 # hugs the left edge
|
||||
assert loc.y < 40 # hugs the top edge (corner="tl")
|
||||
|
||||
def test_box_scales_with_width(self):
|
||||
# scale_basis="width" (measured: mark width is 0.32 of the frame width).
|
||||
eng = RunningHubEngine()
|
||||
narrow = eng.locate(np.zeros((2048, 1024, 3), np.uint8))
|
||||
wide = eng.locate(np.zeros((2048, 2048, 3), np.uint8))
|
||||
assert wide.w == pytest.approx(narrow.w * 2, rel=0.05)
|
||||
|
||||
|
||||
class TestConfig:
|
||||
def test_gray_frontend(self):
|
||||
# The mark is a faint mid-gray the top-hat suppresses to clean-arm levels;
|
||||
# the raw-grayscale front-end is what separates (measured 2026-07-22).
|
||||
assert RunningHubEngine().config.detect_frontend == "gray"
|
||||
|
||||
def test_tight_ladder(self):
|
||||
# The NCC comb is razor-sharp in size (0.537 on-size, 0.223 at +5.6%), so
|
||||
# the nominal sits exactly on the measured 0.32 with +-5% rungs.
|
||||
assert RunningHubEngine().config.ladder == (0.95, 1.0, 1.05)
|
||||
assert RunningHubEngine().config.alpha_width_frac == pytest.approx(0.32)
|
||||
|
||||
def test_strict_only_no_provenance_relaxation(self):
|
||||
assert RunningHubEngine().config.provenance_ncc_factor == 1.0
|
||||
|
||||
def test_gate_above_clean_arm_max(self):
|
||||
# Clean arm scored p99 0.273 / max 0.295 on 286 hand-labelled frames.
|
||||
assert RunningHubEngine().config.detect_ncc_threshold > 0.295
|
||||
|
||||
def test_registry_row(self):
|
||||
mark = registry.get_mark("runninghub")
|
||||
assert mark.location == "top-left"
|
||||
assert mark.in_auto
|
||||
|
||||
|
||||
class TestDetectAndMask:
|
||||
def test_detects_composed_mark(self):
|
||||
eng = RunningHubEngine()
|
||||
wm, _ = _compose(1080, 1620)
|
||||
det = eng.detect(wm)
|
||||
assert det.detected, f"composed mark missed (conf={det.confidence:.3f})"
|
||||
|
||||
def test_clean_frame_stays_quiet(self):
|
||||
eng = RunningHubEngine()
|
||||
img = np.full((1620, 1080, 3), 100, np.uint8)
|
||||
assert not eng.detect(img).detected
|
||||
|
||||
def test_mask_covers_the_whole_mark(self):
|
||||
"""Regression (2026-07-22): the binary blob under-segments the faint head
|
||||
glyphs, so a blob-bbox mask left "Runni" unremoved. The gray front-end's
|
||||
mask must come from the detector's own match box and cover the mark."""
|
||||
eng = RunningHubEngine()
|
||||
wm, (ax, ay, gw, gh) = _compose(1080, 1620)
|
||||
mask = eng.footprint_mask(wm)
|
||||
assert mask is not None
|
||||
ys, xs = np.where(mask > 0)
|
||||
assert xs.min() <= ax + int(0.05 * gw) # covers the LEFT edge of the mark
|
||||
assert xs.max() >= ax + gw - int(0.05 * gw)
|
||||
assert ys.min() <= ay + gh // 2 <= ys.max()
|
||||
|
||||
def test_no_mask_on_clean_frame(self):
|
||||
eng = RunningHubEngine()
|
||||
img = np.full((1620, 1080, 3), 100, np.uint8)
|
||||
assert eng.footprint_mask(img) is None
|
||||
|
||||
def test_anchor_window_rejects_off_corner_match(self):
|
||||
"""The raw-gray front-end false-fires on text-like structure ANYWHERE in
|
||||
the box (37/42009 outside-cohort frames in the 2026-07-22 sweep); the
|
||||
anchor window is what keeps it about THIS mark. A composed mark placed
|
||||
off the measured corner anchor must NOT be detected."""
|
||||
eng = RunningHubEngine()
|
||||
wm, _ = _compose(1080, 1620)
|
||||
det = eng.detect(wm)
|
||||
assert det.detected # on-anchor control
|
||||
# the same mark shifted right/down, off the anchor window
|
||||
shifted = np.full((1620, 1080, 3), 100, np.uint8)
|
||||
region = wm[10:60, 12:360]
|
||||
shifted[100 : 100 + region.shape[0], 200 : 200 + region.shape[1]] = region
|
||||
assert not eng.detect(shifted).detected
|
||||
|
||||
|
||||
class TestPillInteraction:
|
||||
def test_confident_runninghub_detection_suppresses_the_jimeng_pill(self):
|
||||
# A RunningHub frame names its own product; its detection must veto the
|
||||
# Jimeng pill the same way Doubao/Qwen/Kling do (``_keep_pill``).
|
||||
from remove_ai_watermarks.watermark_registry import _keep_pill
|
||||
|
||||
assert not _keep_pill({"runninghub"}, provenance=frozenset({"jimeng"}), footprint_flat=1.0)
|
||||
@@ -14,7 +14,18 @@ DOUBAO_SAMPLE = Path(__file__).resolve().parents[1] / "data" / "samples" / "doub
|
||||
|
||||
class TestCatalog:
|
||||
def test_keys(self):
|
||||
assert reg.mark_keys() == ["gemini", "doubao", "jimeng", "qwen", "kling", "samsung", "jimeng_pill"]
|
||||
assert reg.mark_keys() == [
|
||||
"gemini",
|
||||
"doubao",
|
||||
"jimeng",
|
||||
"qwen",
|
||||
"kling",
|
||||
"samsung",
|
||||
"runninghub",
|
||||
"baidu",
|
||||
"liblib",
|
||||
"jimeng_pill",
|
||||
]
|
||||
|
||||
def test_all_in_auto(self):
|
||||
assert all(m.in_auto for m in reg.known_marks())
|
||||
@@ -43,7 +54,18 @@ class TestScan:
|
||||
def test_detect_marks_scans_all(self):
|
||||
img = np.zeros((256, 256, 3), np.uint8)
|
||||
keys = {d.key for d in reg.detect_marks(img)}
|
||||
assert keys == {"gemini", "doubao", "jimeng", "qwen", "kling", "samsung", "jimeng_pill"}
|
||||
assert keys == {
|
||||
"gemini",
|
||||
"doubao",
|
||||
"jimeng",
|
||||
"qwen",
|
||||
"kling",
|
||||
"samsung",
|
||||
"runninghub",
|
||||
"baidu",
|
||||
"liblib",
|
||||
"jimeng_pill",
|
||||
}
|
||||
|
||||
def test_blank_image_no_auto_mark(self):
|
||||
dets = reg.detect_marks(np.zeros((256, 256, 3), np.uint8), include_explicit=False)
|
||||
|
||||
Reference in New Issue
Block a user