mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-14 13:30:55 +02:00
- A decoded manifest no longer takes its generator from a raw claim_generator CBOR value or a soft binding from raw bytes. Device tokens (NIKON, Pixel Camera, sony.sig) stay a byte scan, since the decoded strings do not carry them, but read the manifest store: an EXIF Make beside an unrelated AI manifest is not a verified capture. The source-kind scan keeps the whole buffer because it also reads IPTC digitalSourceType in XMP. - The byte fallback's c2pa.ai-disclosure kind now uses the structured rule: human_validated beside prompt_guided is a generation, not an enhancement. - Wan and Yuanbao share BottomRightAnchoredEngine instead of two copies of the corner gate. The Yuanbao anchor test shifted the mark outside the locate window, so it passed with or without the gate; it now shifts it inside. Verdicts over 1,379 C2PA files, 468 fixture and capture files, and Wan and Yuanbao detections over 285 images are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.0 KiB
Python
104 lines
4.0 KiB
Python
"""Tests for the Tencent Yuanbao (元宝 / AI生成) visible-watermark engine."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from remove_ai_watermarks import watermark_registry as registry
|
|
from remove_ai_watermarks.yuanbao_engine import (
|
|
_ALPHA_HEIGHT_FRAC,
|
|
_ALPHA_WIDTH_FRAC,
|
|
YuanbaoEngine,
|
|
_alpha_template,
|
|
)
|
|
|
|
_MARK_WIDTH_FRAC = 0.08
|
|
_RIGHT_MARGIN_FRAC = 0.028
|
|
_BOTTOM_MARGIN_FRAC = 0.031
|
|
|
|
|
|
def _compose(w: int, h: int, *, bg: float, foreground: float):
|
|
"""Composite the synthetic Yuanbao silhouette at its measured geometry."""
|
|
image = np.full((h, w, 3), bg, np.float32)
|
|
alpha = _alpha_template()
|
|
assert alpha is not None
|
|
short = min(w, h)
|
|
gw = int(_MARK_WIDTH_FRAC * short)
|
|
gh = max(6, int((_ALPHA_HEIGHT_FRAC / _ALPHA_WIDTH_FRAC) * gw))
|
|
ax = w - int(_RIGHT_MARGIN_FRAC * short) - gw
|
|
ay = h - int(_BOTTOM_MARGIN_FRAC * short) - gh
|
|
mark_alpha = np.zeros((h, w), np.float32)
|
|
mark_alpha[ay : ay + gh, ax : ax + gw] = cv2.resize(alpha, (gw, gh))
|
|
a3 = mark_alpha[:, :, None]
|
|
composed = (a3 * foreground + (1 - a3) * image).clip(0, 255).astype(np.uint8)
|
|
return composed, (ax, ay, gw, gh)
|
|
|
|
|
|
class TestConfig:
|
|
def test_uses_two_polarity_contrast_frontend(self):
|
|
assert YuanbaoEngine().config.detect_frontend == "contrast"
|
|
|
|
def test_strict_only(self):
|
|
assert YuanbaoEngine().config.provenance_ncc_factor == 1.0
|
|
|
|
def test_registry_row(self):
|
|
mark = registry.get_mark("yuanbao")
|
|
assert mark.location == "bottom-right"
|
|
assert "元宝" in mark.label
|
|
assert mark.in_auto
|
|
|
|
|
|
class TestDetectAndMask:
|
|
def test_detects_light_mark_on_dark_background(self):
|
|
watermark, _ = _compose(1024, 1024, bg=60, foreground=230)
|
|
detection = YuanbaoEngine().detect(watermark)
|
|
assert detection.detected
|
|
assert detection.confidence >= 0.80
|
|
|
|
def test_detects_dark_mark_on_light_background(self):
|
|
"""Yuanbao switches mark polarity with the background.
|
|
|
|
A white top-hat alone misses the dark-gray stamp used on pale scenes.
|
|
"""
|
|
watermark, _ = _compose(1024, 1024, bg=230, foreground=110)
|
|
detection = YuanbaoEngine().detect(watermark)
|
|
assert detection.detected
|
|
assert detection.confidence >= 0.80
|
|
|
|
def test_clean_gradient_stays_quiet(self):
|
|
ramp = np.tile(np.linspace(40, 220, 1024, dtype=np.uint8), (1024, 1))
|
|
image = cv2.cvtColor(ramp, cv2.COLOR_GRAY2BGR)
|
|
assert not YuanbaoEngine().detect(image).detected
|
|
|
|
def test_match_must_hug_bottom_right_anchor(self):
|
|
# Shift the mark 60 px in from the corner: still inside the locate window, so
|
|
# the sweep finds it, but beyond the 0.04 anchor margin, so only the anchor
|
|
# gate can demote it. A far shift would fall outside the window and pass
|
|
# with or without the gate.
|
|
class Ungated(YuanbaoEngine):
|
|
def _post_gate(self, det, scan):
|
|
return det
|
|
|
|
watermark, (ax, ay, gw, gh) = _compose(1024, 1024, bg=60, foreground=230)
|
|
assert YuanbaoEngine().detect(watermark).detected
|
|
shifted = np.full_like(watermark, 60)
|
|
shifted[ay - 60 : ay - 60 + gh, ax - 60 : ax - 60 + gw] = watermark[ay : ay + gh, ax : ax + gw]
|
|
assert Ungated().detect(shifted).detected
|
|
assert not YuanbaoEngine().detect(shifted).detected
|
|
|
|
def test_mask_uses_detector_box_for_dark_mark(self):
|
|
watermark, (ax, ay, gw, gh) = _compose(1024, 1024, bg=230, foreground=110)
|
|
mask = YuanbaoEngine().footprint_mask(watermark)
|
|
assert mask is not None
|
|
ys, xs = np.where(mask > 0)
|
|
assert xs.min() <= ax + int(0.05 * gw)
|
|
assert xs.max() >= ax + gw - int(0.05 * gw)
|
|
assert ys.min() <= ay + gh // 2 <= ys.max()
|
|
|
|
def test_remove_clears_detector(self):
|
|
watermark, _ = _compose(1024, 1024, bg=60, foreground=230)
|
|
output, region = registry.get_mark("yuanbao").remove(watermark, backend="cv2")
|
|
assert region is not None
|
|
assert not YuanbaoEngine().detect(output).detected
|