mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-14 05:20:46 +02:00
Detect and remove LiblibAI compact top-left pill
This commit is contained in:
16 files changed
+285
-85
No files matched your search
@@ -344,7 +344,8 @@ Visible mark support includes:
|
||||
|
||||
- Google Gemini and Nano Banana visible sparkle watermark;
|
||||
- Doubao, the Jimeng wordmark and top-left `AI生成` pill, Qwen, Kling AI,
|
||||
Yuanbao, Baidu, LiblibAI, and RunningHub labels;
|
||||
Yuanbao, Baidu, LiblibAI's bottom-center wordmark and compact top-left pill,
|
||||
and RunningHub labels;
|
||||
- one calibrated Microsoft top-right white AI-badge variant;
|
||||
- one calibrated Samsung Galaxy AI label variant.
|
||||
|
||||
|
||||
@@ -20,9 +20,8 @@ engines' recorded calibration evidence.
|
||||
The Qwen, Kling, Yuanbao, Baidu, Jimeng, LiblibAI, Microsoft, Sora, Hailuo, and Veo
|
||||
directories additionally contain real provider-marked sources. These files
|
||||
retain the source bytes and names used here are deliberately distinct from the
|
||||
generated canonical examples. The current LiblibAI output is evidence of a
|
||||
provider format change, not a positive example for the registered historical
|
||||
wordmark detector:
|
||||
generated canonical examples. The current LiblibAI output exercises the
|
||||
metadata-corroborated compact-pill path, not the historical wordmark detector:
|
||||
|
||||
| File | Source | SHA-256 | Expected detector |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -31,7 +30,7 @@ wordmark detector:
|
||||
| `yuanbao/provider-original.png` | Authenticated Tencent Yuanbao AI Image generation, downloaded 2026-09-05 | `e4fd856565d0434ef06deb78ff2088e597c4fec98640ad737213c7c0a917c82d` | Yuanbao, confidence 0.49 |
|
||||
| `baidu/provider-original.jpeg` | OpenNoMark `examples/baidu/baidu_01.jpeg`, commit `9455d2d` | `52852c528f1a7b8918b665b2fcf883737ecc5016481d9c7683ebe07e152cc9d2` | Baidu, confidence 0.66 |
|
||||
| `jimeng_pill/provider-published-example.jpg` | JPEG embedded on page 29 of Zhonglun W&D's 2026 *Bank Finance Legal Newsletter* | `1182fba74d3753785345f8a35d09a8c9b52ea8383ebe9ee999d0d027437f03fd` | Jimeng AI pill, confidence 0.28; Jimeng wordmark, confidence 0.61 |
|
||||
| `liblib/current-provider-output.png` | Authenticated LiblibAI Smart Image V2 generation, downloaded 2026-09-05 | `60398d4f06fd50e047d3321c62405dae0a1c4a7070e77ac5e0548c516257d2e3` | No `liblib` detection: current top-left `AI生成` pill differs from the registered bottom-center `LiblibAI` wordmark |
|
||||
| `liblib/current-provider-output.png` | Authenticated LiblibAI Smart Image V2 generation, downloaded 2026-09-05 | `60398d4f06fd50e047d3321c62405dae0a1c4a7070e77ac5e0548c516257d2e3` | `liblib_pill`, confidence 0.216 with LiblibAI metadata corroboration; no `liblib` wordmark |
|
||||
| `microsoft/provider-original.png` | Authenticated Microsoft Copilot generation, downloaded 2026-09-03 | `0e76fb8aa0540b18fee4d63ac4b5fd63c7eee310474e896d21d27e668403afe7` | Microsoft, confidence 0.54 |
|
||||
| `sora/provider-frame.jpg` | Sora 2 video frame from LLinked's Apache-2.0 `sora-watermark-dataset` | `bbcf70b737c30568668572a5bb23e70c52c9687808b0714ca98f9fac073f12c4` | Sora frame localization, confidence 0.69 |
|
||||
| `sora/provider-original.mp4` | LLinked's Apache-2.0 `SoraWatermarkCleaner` fixture `resources/puppies.mp4`, commit `ad35ce9` | `7dd8a9d43d43cd5b901946c7ba14e8018e18d47e75f0f774157327ba7e9181c5` | Sora, 300/300 stable frames; Sora C2PA claim |
|
||||
@@ -141,10 +140,10 @@ The LiblibAI image is the original 2048 x 2048 RGBA PNG generated with Smart
|
||||
Image V2 for 18 introductory points from the same neutral prompt. It carries a
|
||||
top-left `AI生成` pill and a TC260 AIGC field whose producer code contains
|
||||
LiblibAI's registered USCC, but it does not carry the historical bottom-center
|
||||
`LiblibAI` wordmark that the registered `liblib` engine detects. This file
|
||||
therefore records the current provider behavior without falsely closing the
|
||||
historical positive-example gap. The maintainer generated and explicitly
|
||||
cleared the output for use under the repository license.
|
||||
`LiblibAI` wordmark that the `liblib` engine detects. It is the real-provider
|
||||
regression for the metadata-corroborated `liblib_pill` path. The maintainer
|
||||
generated and explicitly cleared this output for use under the repository
|
||||
license.
|
||||
|
||||
- LiblibAI user agreement (generated-output ownership and watermark choices):
|
||||
<https://www.liblib.art/activities/468ad794ccc7408d81757fd91be003ec?hideHeader=1>
|
||||
@@ -182,6 +181,7 @@ detect on its own example, so regeneration is the fix point for drift.
|
||||
|
||||
Special cases: `gemini` composites the sparkle alpha map at the provider's
|
||||
configured position; `jimeng_pill` uses the measured 3:4 portrait geometry;
|
||||
`liblib_pill` uses the shorter compact silhouette and requires product evidence;
|
||||
`microsoft` is the opaque white pill with dark text
|
||||
holes (the discriminator its detector keys on). Video examples composite the
|
||||
detector's own synthetic template on every frame; where two marks share a
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -68,6 +68,9 @@ Known examples:
|
||||
`Contenuti generati dall'AI` text variant.
|
||||
- The Jimeng top-left pill has a weak visual detector and is intentionally
|
||||
subject to additional product and background checks.
|
||||
- The compact LiblibAI top-left pill is generic on its own. Automatic removal
|
||||
requires LiblibAI metadata or the bottom-center wordmark and a flat footprint;
|
||||
`--sensitivity strict` leaves this corroboration-only component untouched.
|
||||
- Kling AI support covers the calibrated variants rather than every Kling AI label.
|
||||
|
||||
Use `erase --region` when you can see and select an unsupported or missed mark.
|
||||
|
||||
@@ -1297,6 +1297,17 @@ be represented by the shared base:
|
||||
- [`liblib_engine.py`](../src/remove_ai_watermarks/liblib_engine.py)
|
||||
- [`microsoft_engine.py`](../src/remove_ai_watermarks/microsoft_engine.py)
|
||||
|
||||
LiblibAI has two registry entries under one product. `liblib` retains the
|
||||
historical bottom-center wordmark detector. `liblib_pill` covers the compact
|
||||
top-left `AI生成` pill and never fires at strict trust: the shape is generic, so
|
||||
automatic removal requires either LiblibAI TC260 metadata or a confident
|
||||
bottom-center sibling detection. The cleared current provider fixture scores
|
||||
`0.216367` against the reused synthetic pill silhouette, above the corroborated
|
||||
`0.20` gate but below the standalone Jimeng pill's `0.22` gate. A flat-footprint
|
||||
check remains mandatory. The pill is a separate action rather than a distant
|
||||
component unioned into the bottom mask, keeping LaMa and MI-GAN crops local to
|
||||
each mark.
|
||||
|
||||
Kling keeps two independently gated silhouettes under one registry key. The
|
||||
older measured cohort uses `可灵AI 3.0`; a direct authenticated IMAGE 3.0 export
|
||||
on 2026-09-04 uses `KlingAI 3.0`. The Latin template scored 0.429 on that 1024 x
|
||||
|
||||
@@ -19,7 +19,8 @@ The `visible` command registers these mark keys:
|
||||
| `samsung` | `✦ Contenuti generati dall'AI` | Bottom left | Calibrated for the Italian text variant. |
|
||||
| `runninghub` | `RunningHub AI生成` | Top left | Strict visual and position gates. |
|
||||
| `baidu` | `百度 AI生成` | Bottom right | Detector and extended removal footprint. |
|
||||
| `liblib` | `LiblibAI` | Bottom center | Includes a minimum image size gate. A 2026-09-05 Smart Image V2 provider original instead carries a smaller top-left `AI生成` pill and is not detected by this historical-wordmark engine. |
|
||||
| `liblib` | `LiblibAI` | Bottom center | Historical wordmark variant; includes a minimum image size gate. |
|
||||
| `liblib_pill` | Compact `AI生成` pill | Top left | Auto removal requires LiblibAI metadata or the bottom-center wordmark, plus a flat background under the pill. |
|
||||
| `microsoft` | One Microsoft white AI-badge variant | Top right | Strict uses the visual gate; auto can use Microsoft provenance for the measured [relaxed gate](module-internals.md#visible-mark-removal). Other documented icon, text, and position variants are not covered. |
|
||||
| `jimeng_pill` | `AI生成` pill | Top left | Weak detector with additional product and background gates. |
|
||||
|
||||
@@ -38,8 +39,8 @@ originals exercise vendor raster fidelity.
|
||||
|
||||
Provider formats can change independently of the registered key. In particular,
|
||||
the current LiblibAI sample in the gallery preserves a real new-format output
|
||||
and its LiblibAI TC260 producer metadata, but is deliberately not listed as a
|
||||
positive detector fixture.
|
||||
and its LiblibAI TC260 producer metadata. That metadata corroborates the compact
|
||||
pill detector; the generic pill shape does not attribute LiblibAI on its own.
|
||||
|
||||
### Visible video marks
|
||||
|
||||
@@ -258,7 +259,7 @@ not a universal clean verdict.
|
||||
| Kling AI | Kling AI image and video marks | No registered pixel decoder | TC260 AIGC |
|
||||
| Hailuo AI / MiniMax video | Hailuo AI composite video label | No registered pixel decoder | TC260 AIGC where present |
|
||||
| Baidu | Baidu mark | No registered pixel decoder | TC260 AIGC |
|
||||
| LiblibAI | LiblibAI mark | No registered pixel decoder | TC260 AIGC |
|
||||
| LiblibAI | LiblibAI wordmark and compact pill | No registered pixel decoder | TC260 AIGC |
|
||||
| RunningHub | RunningHub mark | No registered pixel decoder | TC260 AIGC |
|
||||
| Samsung Galaxy AI | One locale specific mark | No registered pixel decoder | C2PA and Samsung markers |
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ _DEFAULT_STRENGTH = 238
|
||||
_SIZE: dict[str, tuple[int, int]] = {
|
||||
"qwen": (1536, 1536),
|
||||
"liblib": (1152, 1536),
|
||||
"liblib_pill": (1152, 1536),
|
||||
"samsung": (2048, 1536),
|
||||
"jimeng_pill": (1152, 1536),
|
||||
}
|
||||
@@ -169,18 +170,42 @@ def _stamp_pill(
|
||||
*,
|
||||
size_mult: float,
|
||||
alpha_mult: float,
|
||||
width_frac: float = 0.161,
|
||||
height_frac: float | None = None,
|
||||
margin_frac: float = 0.03,
|
||||
) -> tuple[np.ndarray, tuple[int, int, int, int]] | None:
|
||||
h, w = base.shape[:2]
|
||||
at = _glyph_asset("jimeng_pill.png")
|
||||
pw = max(24, int(0.161 * w * size_mult))
|
||||
ph = max(8, int(pw * at.shape[0] / at.shape[1]))
|
||||
x, y = int(0.03 * w), int(0.03 * h)
|
||||
pw = max(24, int(width_frac * w * size_mult))
|
||||
ph = (
|
||||
max(8, int(height_frac * w * size_mult))
|
||||
if height_frac is not None
|
||||
else max(8, int(pw * at.shape[0] / at.shape[1]))
|
||||
)
|
||||
x, y = int(margin_frac * w), int(margin_frac * h)
|
||||
if x + pw > w or y + ph > h:
|
||||
return None
|
||||
alpha = cv2.resize(at, (pw, ph)) * alpha_mult
|
||||
return _composite_light(base, alpha, x, y, 232), (x, y, pw, ph)
|
||||
|
||||
|
||||
def _stamp_liblib_pill(
|
||||
base: np.ndarray,
|
||||
*,
|
||||
size_mult: float,
|
||||
alpha_mult: float,
|
||||
) -> tuple[np.ndarray, tuple[int, int, int, int]] | None:
|
||||
"""Composite LiblibAI's shorter top-left pill variant."""
|
||||
return _stamp_pill(
|
||||
base,
|
||||
size_mult=size_mult,
|
||||
alpha_mult=alpha_mult,
|
||||
width_frac=0.15,
|
||||
height_frac=0.05,
|
||||
margin_frac=0.025,
|
||||
)
|
||||
|
||||
|
||||
def stamp_image_mark(
|
||||
key: str,
|
||||
base: np.ndarray,
|
||||
@@ -193,6 +218,8 @@ def stamp_image_mark(
|
||||
return _stamp_gemini(base, size_mult=size_mult, alpha_mult=alpha_mult)
|
||||
if key == "jimeng_pill":
|
||||
return _stamp_pill(base, size_mult=size_mult, alpha_mult=alpha_mult)
|
||||
if key == "liblib_pill":
|
||||
return _stamp_liblib_pill(base, size_mult=size_mult, alpha_mult=alpha_mult)
|
||||
return _stamp_text_mark(key, base, size_mult=size_mult, alpha_mult=alpha_mult)
|
||||
|
||||
|
||||
@@ -323,7 +350,7 @@ def main() -> None:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = out_dir / "example.png"
|
||||
cv2.imwrite(str(path), img)
|
||||
det = wr.get_mark(key).detect(imread(str(path)), provenance=False)
|
||||
det = wr.get_mark(key).detect(imread(str(path)), provenance=key == "liblib_pill")
|
||||
status = "OK " if det.detected else "MISS"
|
||||
print(f"{status} {key:12s} conf={det.confidence:.3f} -> {path.relative_to(_ROOT)}")
|
||||
if not det.detected:
|
||||
|
||||
@@ -21,10 +21,12 @@ remove-ai-watermarks visible image.png --keep-metadata -o clean.png
|
||||
```
|
||||
|
||||
Image mark keys: `gemini`, `doubao`, `jimeng`, `qwen`, `kling`, `yuanbao`,
|
||||
`samsung`, `runninghub`, `baidu`, `liblib`, `microsoft`, `jimeng_pill`. Default
|
||||
`samsung`, `runninghub`, `baidu`, `liblib`, `liblib_pill`, `microsoft`,
|
||||
`jimeng_pill`. Default
|
||||
`--mark auto` removes every selected match. `microsoft` is the top-right AI
|
||||
badge, separate from the invisible InvisMark watermark on the same vendor's
|
||||
images. `jimeng_pill` is a weak detector and needs corroboration.
|
||||
images. `jimeng_pill` is a weak detector and needs corroboration. The compact
|
||||
`liblib_pill` likewise requires LiblibAI metadata or the bottom-center wordmark.
|
||||
|
||||
```bash
|
||||
remove-ai-watermarks erase image.png --region 1640,1930,400,100 -o clean.png
|
||||
|
||||
@@ -173,9 +173,9 @@ def remove_visible(
|
||||
"""Remove every detected known visible AI mark through localize then fill.
|
||||
|
||||
The registry currently covers the Gemini visible watermark; Doubao, Jimeng,
|
||||
Qwen, Kling AI, Yuanbao, Samsung, RunningHub, Baidu, and LiblibAI text marks;
|
||||
one Microsoft top-right AI-badge variant; and the Jimeng pill. Returns
|
||||
``(result_bgr, [labels removed])``.
|
||||
Qwen, Kling AI, Yuanbao, Samsung, RunningHub, and Baidu text marks; the
|
||||
LiblibAI wordmark and compact pill; one Microsoft top-right AI-badge variant;
|
||||
and the Jimeng pill. Returns ``(result_bgr, [labels removed])``.
|
||||
|
||||
``source`` is a file path OR a BGR ndarray. For a PATH, metadata provenance is read
|
||||
automatically (so ``sensitivity="auto"`` recovers a moved/faint mark whenever the
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
LiblibAI (哩布哩布AI, USCC 91110105MACJ6K1C8A) stamps its generations with a
|
||||
white triangle logo + "LiblibAI" Latin wordmark at **bottom-center** (not a
|
||||
corner -- the locate box is horizontally centered). Detection matches the
|
||||
corner -- the locate box is horizontally centered). Current outputs can instead
|
||||
carry a compact top-left ``AI生成`` pill. The wordmark detector matches the
|
||||
bundled font-rendered "LiblibAI" silhouette (the triangle logo is NOT rendered
|
||||
-- logos vary, the wordmark discriminates); removal is the shared **localize ->
|
||||
fill** (the glyph blob covers logo + wordmark, both bright).
|
||||
|
||||
This module supplies only LiblibAI's tuned :class:`TextMarkConfig`
|
||||
This module supplies LiblibAI's tuned :class:`TextMarkConfig`
|
||||
(``assets/liblib_alpha.png`` from ``scripts/render_vendor_silhouettes.py``,
|
||||
never cut from an upload).
|
||||
never cut from an upload) and the corroboration-only compact-pill adapter.
|
||||
|
||||
The detector uses an Arial-class synthetic silhouette, width-based geometry, a
|
||||
strict confidence gate, and a minimum image size. The footprint includes both
|
||||
@@ -32,6 +33,7 @@ from remove_ai_watermarks._text_mark_engine import (
|
||||
TextMarkLocation,
|
||||
TextMarkScan,
|
||||
)
|
||||
from remove_ai_watermarks.pill_engine import PillDetection, PillEngine
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy.typing import NDArray
|
||||
@@ -63,6 +65,16 @@ _ALPHA_HEIGHT_FRAC = 0.026
|
||||
# Tight ladder: the NCC comb is sharp in size (see runninghub_engine).
|
||||
_LADDER = (0.9, 1.0, 1.1)
|
||||
|
||||
# LiblibAI's compact top-left ``AI生成`` pill is a separate current-output
|
||||
# variant. Its shape scores just below the standalone Jimeng pill gate because
|
||||
# the Liblib render is shorter. LiblibAI metadata or the reliable wordmark is the
|
||||
# product anchor here, so this lower component gate never acts on its own.
|
||||
_TOP_LEFT_PILL_MIN_CONFIDENCE = 0.20
|
||||
_TOP_LEFT_PILL_X_FRAC = 0.015
|
||||
_TOP_LEFT_PILL_Y_FRAC = 0.015
|
||||
_TOP_LEFT_PILL_WIDTH_FRAC = 0.17
|
||||
_TOP_LEFT_PILL_HEIGHT_FRAC = 0.08
|
||||
|
||||
_CONFIG = TextMarkConfig(
|
||||
name="LiblibAI",
|
||||
asset_name="liblib_alpha.png",
|
||||
@@ -151,3 +163,60 @@ class LibLibEngine(TextMarkEngine):
|
||||
min(w, bx + gx1 + 1 + pad),
|
||||
min(h, by + gy1 + 1 + pad),
|
||||
)
|
||||
|
||||
|
||||
class LibLibPillEngine:
|
||||
"""Detect the compact top-left pill only when LiblibAI is corroborated."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._pill = PillEngine()
|
||||
|
||||
def _raw(self, image: NDArray[Any]) -> PillDetection:
|
||||
return self._pill.detect(image)
|
||||
|
||||
def _relaxed_detected(self, image: NDArray[Any], confidence: float) -> bool:
|
||||
return confidence >= _TOP_LEFT_PILL_MIN_CONFIDENCE and self._pill.footprint_is_flat(image)
|
||||
|
||||
def detect(self, image: NDArray[Any], *, provenance: bool = False) -> PillDetection:
|
||||
"""Return a verdict; the pill is actionable only with LiblibAI evidence."""
|
||||
raw = self._raw(image)
|
||||
detected = provenance and self._relaxed_detected(image, raw.confidence)
|
||||
return PillDetection(detected, raw.confidence, raw.region)
|
||||
|
||||
def detect_both(self, image: NDArray[Any]) -> tuple[PillDetection, PillDetection]:
|
||||
"""Return strict and LiblibAI-corroborated verdicts from one raw match."""
|
||||
raw = self._raw(image)
|
||||
strict = PillDetection(False, raw.confidence, raw.region)
|
||||
relaxed = PillDetection(self._relaxed_detected(image, raw.confidence), raw.confidence, raw.region)
|
||||
return strict, relaxed
|
||||
|
||||
def footprint_mask(
|
||||
self,
|
||||
image: NDArray[Any],
|
||||
*,
|
||||
force: bool = False,
|
||||
detection: PillDetection | None = None,
|
||||
) -> NDArray[Any] | None:
|
||||
"""Mask the compact pill's fixed top-left footprint."""
|
||||
if not image.size:
|
||||
return None
|
||||
if detection is None and not force:
|
||||
raw = self._raw(image)
|
||||
if not raw.detected:
|
||||
return None
|
||||
|
||||
frame_h, frame_w = int(image.shape[0]), int(image.shape[1])
|
||||
x = int(_TOP_LEFT_PILL_X_FRAC * frame_w)
|
||||
y = int(_TOP_LEFT_PILL_Y_FRAC * frame_h)
|
||||
width = int(_TOP_LEFT_PILL_WIDTH_FRAC * frame_w)
|
||||
height = int(_TOP_LEFT_PILL_HEIGHT_FRAC * frame_w)
|
||||
if width <= 0 or height <= 0:
|
||||
return None
|
||||
|
||||
from remove_ai_watermarks import region_eraser
|
||||
|
||||
return region_eraser.boxes_to_mask(
|
||||
(frame_h, frame_w),
|
||||
[(x, y, width, height)],
|
||||
dilate=0,
|
||||
)
|
||||
@@ -29,6 +29,7 @@ Entries:
|
||||
- ``runninghub`` -- RunningHub "RunningHub AI生成" text, top-left (gray front-end).
|
||||
- ``baidu`` -- Baidu "百度 AI生成" text + white tag, bottom-right.
|
||||
- ``liblib`` -- LiblibAI "LiblibAI" wordmark, bottom-center.
|
||||
- ``liblib_pill`` -- LiblibAI compact "AI生成" pill, top-left.
|
||||
- ``microsoft`` -- one measured Microsoft white AI-badge variant, top-right.
|
||||
"""
|
||||
|
||||
@@ -85,31 +86,6 @@ Trust = Literal["strict", "confirmed"]
|
||||
# ``_PRODUCT_OF`` is derived from it right after ``_REGISTRY`` is built.
|
||||
|
||||
|
||||
# Marks whose own detection is too weak to serve as EVIDENCE for a sibling of the
|
||||
# same product, even though they share one. Sibling corroboration grants ``confirmed``
|
||||
# trust, which bypasses the sibling's false-positive gate outright -- so a detector
|
||||
# that false-fires often must not be able to hand that bypass to anyone.
|
||||
#
|
||||
# The pill detector has a meaningful raw false-fire rate. Letting it corroborate
|
||||
# produced a closed loop
|
||||
# on the DEFAULT auto path, no user flag involved:
|
||||
# pill false-fires on a clean non-ByteDance image
|
||||
# -> _PRODUCT_OF maps it to "jimeng", so jimeng resolves to `confirmed`
|
||||
# -> jimeng's NCC gate drops 0.45 -> 0.3825 and it false-fires too
|
||||
# -> _keep_pill now sees "jimeng" in keys and takes the WORDMARK arm, which
|
||||
# removes the pill unrestricted -- skipping the flatness guard that exists
|
||||
# precisely to stop the fill smearing a textured corner.
|
||||
# Calibration reproduced the full loop, including a textured footprint. Cutting the
|
||||
# pill out of corroboration removed the loop without reducing Jimeng detections, so
|
||||
# this is a defect fix, not a recall trade.
|
||||
#
|
||||
# `_keep_pill` already encodes the same distrust for the pill's own ACTION; this
|
||||
# closes the gap that its TESTIMONY was never gated.
|
||||
# Regression: tests/test_watermark_registry.py::TestArbiter::
|
||||
# test_weak_pill_detection_does_not_confirm_the_jimeng_wordmark
|
||||
_CANNOT_CORROBORATE: frozenset[str] = frozenset({"jimeng_pill"})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MarkDetection:
|
||||
"""Uniform detection result for a known mark (across heterogeneous engines)."""
|
||||
@@ -317,6 +293,9 @@ class KnownMark:
|
||||
# Optional single-pass dual verdict for the arbiter's perception stage (see
|
||||
# `detect_both`). None = fall back to two `_detect` calls.
|
||||
_detect_both: Callable[..., tuple[MarkDetection, MarkDetection]] | None = None
|
||||
# False for a weak detector that may be acted on after corroboration but must not
|
||||
# itself grant a sibling the relaxed trust level.
|
||||
can_corroborate: bool = True
|
||||
|
||||
def features(self, image: NDArray[Any]) -> dict[str, float]:
|
||||
"""Physical features the mark reports for the arbiter's gate (empty if none)."""
|
||||
@@ -467,6 +446,7 @@ _ENGINE_CLASS: dict[str, tuple[str, str]] = {
|
||||
"runninghub": ("runninghub_engine", "RunningHubEngine"),
|
||||
"baidu": ("baidu_engine", "BaiduEngine"),
|
||||
"liblib": ("liblib_engine", "LibLibEngine"),
|
||||
"liblib_pill": ("liblib_engine", "LibLibPillEngine"),
|
||||
"microsoft": ("microsoft_engine", "MicrosoftEngine"),
|
||||
}
|
||||
|
||||
@@ -573,13 +553,9 @@ def _gemini_mask(
|
||||
return _engine("gemini").footprint_mask(image, force=force, region=region)
|
||||
|
||||
|
||||
# The text-mark engines share the TextMarkEngine interface, so one parameterized
|
||||
# adapter pair drives all of them -- a new
|
||||
# text mark is one `_text_mark(...)` row below, not another copy-paste of these
|
||||
# bodies. Detection matches the glyph silhouette; the default mask is the
|
||||
# template-free glyph-bbox footprint (see TextMarkEngine.footprint_mask), while an
|
||||
# engine may override it when the mark has a measured safer footprint.
|
||||
def _text_mark_detect(key: str, label: str, location: str) -> Callable[..., MarkDetection]:
|
||||
# Engines with the standard detect/detect_both/footprint_mask interface share
|
||||
# parameterized adapters. Each engine still owns its footprint implementation.
|
||||
def _engine_mark_detect(key: str, label: str, location: str) -> Callable[..., MarkDetection]:
|
||||
def detect(image: NDArray[Any], *, provenance: bool = False) -> MarkDetection:
|
||||
d = _engine(key).detect(image, provenance=provenance)
|
||||
return MarkDetection(key, label, location, d.detected, d.confidence, d.region, engine_detection=d)
|
||||
@@ -587,7 +563,7 @@ def _text_mark_detect(key: str, label: str, location: str) -> Callable[..., Mark
|
||||
return detect
|
||||
|
||||
|
||||
def _text_mark_detect_both(key: str, label: str, location: str) -> Callable[..., tuple[MarkDetection, MarkDetection]]:
|
||||
def _engine_mark_detect_both(key: str, label: str, location: str) -> Callable[..., tuple[MarkDetection, MarkDetection]]:
|
||||
def detect_both(image: NDArray[Any]) -> tuple[MarkDetection, MarkDetection]:
|
||||
strict, relaxed = _engine(key).detect_both(image)
|
||||
return (
|
||||
@@ -602,15 +578,13 @@ def _text_mark_detect_both(key: str, label: str, location: str) -> Callable[...,
|
||||
return detect_both
|
||||
|
||||
|
||||
def _text_mark_mask(key: str) -> Callable[..., NDArray[Any] | None]:
|
||||
def _engine_mark_mask(key: str) -> Callable[..., NDArray[Any] | None]:
|
||||
def mask(
|
||||
image: NDArray[Any], *, force: bool = False, detection: MarkDetection | None = None
|
||||
) -> NDArray[Any] | None:
|
||||
# Thread the engine's OWN detection into the mask builder: the footprint is
|
||||
# bounded by the ladder sweep the detector already ran, so re-detecting here
|
||||
# repeated locate + extract_mask + an identical sweep. footprint_mask still
|
||||
# re-detects when nothing is threaded (a direct or --no-detect caller) and when
|
||||
# the threaded detection was taken at a relaxed trust level.
|
||||
# Thread the engine's OWN accepted detection into the mask builder. This avoids
|
||||
# repeating its scan and preserves the trust verdict selected by the arbiter.
|
||||
# A direct or --no-detect caller still reaches the engine's fallback path.
|
||||
return _engine(key).footprint_mask(
|
||||
image, force=force, detection=detection.engine_detection if detection is not None else None
|
||||
)
|
||||
@@ -645,12 +619,12 @@ def _text_mark(
|
||||
product or key,
|
||||
label_regime,
|
||||
platform,
|
||||
_text_mark_detect(key, label, location),
|
||||
_text_mark_mask(key),
|
||||
_engine_mark_detect(key, label, location),
|
||||
_engine_mark_mask(key),
|
||||
provenance_signals=provenance_signals,
|
||||
tc260_producer_codes=tc260_producer_codes,
|
||||
provenance_platform_tokens=provenance_platform_tokens,
|
||||
_detect_both=_text_mark_detect_both(key, label, location),
|
||||
_detect_both=_engine_mark_detect_both(key, label, location),
|
||||
)
|
||||
|
||||
|
||||
@@ -768,6 +742,22 @@ _REGISTRY: tuple[KnownMark, ...] = (
|
||||
platform="LiblibAI (visible LiblibAI mark detected)",
|
||||
tc260_producer_codes=("91110105MACJ6K1C8A",),
|
||||
),
|
||||
KnownMark(
|
||||
"liblib_pill",
|
||||
"LiblibAI AI生成 pill",
|
||||
"top-left",
|
||||
True,
|
||||
"liblib",
|
||||
"tc260",
|
||||
# The compact pill is generic on its own; the bottom wordmark or LiblibAI
|
||||
# metadata is what makes the relaxed verdict actionable.
|
||||
None,
|
||||
_engine_mark_detect("liblib_pill", "LiblibAI AI生成 pill", "top-left"),
|
||||
_engine_mark_mask("liblib_pill"),
|
||||
provenance_signals=("aigc",),
|
||||
_detect_both=_engine_mark_detect_both("liblib_pill", "LiblibAI AI生成 pill", "top-left"),
|
||||
can_corroborate=False,
|
||||
),
|
||||
# One measured Microsoft visible-mark variant: a white top-right pill with
|
||||
# dark internal shapes. Microsoft's documented feature also permits other
|
||||
# icon, text, and placement variants, which this detector does not cover.
|
||||
@@ -795,6 +785,7 @@ _REGISTRY: tuple[KnownMark, ...] = (
|
||||
_pill_mask,
|
||||
_pill_features,
|
||||
_detect_both=_pill_detect_both,
|
||||
can_corroborate=False,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -802,6 +793,17 @@ _REGISTRY: tuple[KnownMark, ...] = (
|
||||
# edit. See KnownMark.product for why Doubao and Jimeng must not cross-relax.
|
||||
_PRODUCT_OF: dict[str, str] = {m.key: m.product for m in _REGISTRY}
|
||||
|
||||
# Weak pill detectors cannot vouch for sibling marks. Letting the Jimeng pill do so
|
||||
# produced a closed false-positive loop: a clean-image pill fire relaxed the wordmark,
|
||||
# and that wordmark then bypassed the pill's flatness guard. Keep this policy on each
|
||||
# registry row so adding another weak companion cannot silently omit the safeguard.
|
||||
_CANNOT_CORROBORATE: frozenset[str] = frozenset(m.key for m in _REGISTRY if not m.can_corroborate)
|
||||
|
||||
|
||||
def _provenance_confirms_product(product: str, provenance: frozenset[str]) -> bool:
|
||||
"""Whether external provenance names any registered mark of ``product``."""
|
||||
return any(_PRODUCT_OF.get(key) == product for key in provenance)
|
||||
|
||||
|
||||
def known_marks() -> tuple[KnownMark, ...]:
|
||||
"""All registered known visible watermarks."""
|
||||
@@ -832,8 +834,13 @@ def detect_marks(
|
||||
Returns one MarkDetection per scanned mark (``detected`` flags which fired).
|
||||
``include_explicit=False`` scans only the ``in_auto`` marks -- the set used
|
||||
by ``--mark auto``. ``provenance`` names the vendor keys that external metadata
|
||||
already confirms, so each named mark's detector may relax its trust gate."""
|
||||
return [m.detect(image, provenance=m.key in provenance) for m in _REGISTRY if include_explicit or m.in_auto]
|
||||
already confirms, so every mark belonging to that product may relax its trust
|
||||
gate."""
|
||||
return [
|
||||
m.detect(image, provenance=_provenance_confirms_product(m.product, provenance))
|
||||
for m in _REGISTRY
|
||||
if include_explicit or m.in_auto
|
||||
]
|
||||
|
||||
|
||||
def resolve_trust(
|
||||
@@ -847,15 +854,15 @@ def resolve_trust(
|
||||
|
||||
The single place that turns the ``sensitivity`` policy + evidence into a per-mark
|
||||
level (which the engines consume as ``provenance = level != "strict"``). ``strict``
|
||||
never relaxes. A mark is ``confirmed`` only on same-product evidence -- the vendor
|
||||
confirmed by metadata (``key in provenance``) or a confidently strict-detected
|
||||
sibling of the same product (``_PRODUCT_OF``, minus the marks too weak to vouch,
|
||||
never relaxes. A mark is ``confirmed`` only on same-product evidence -- metadata
|
||||
naming any mark of that product, or a confidently strict-detected sibling
|
||||
(``_PRODUCT_OF``, minus the marks too weak to vouch,
|
||||
:data:`_CANNOT_CORROBORATE`). Without that evidence a mark stays ``strict``: there is
|
||||
no path that relaxes a gate on anything less than same-product evidence."""
|
||||
if sensitivity == "strict":
|
||||
return "strict"
|
||||
product = _PRODUCT_OF[key]
|
||||
confirmed = key in provenance or any(
|
||||
confirmed = _provenance_confirms_product(product, provenance) or any(
|
||||
_PRODUCT_OF[k] == product for k in strict_keys if k != key and k not in _CANNOT_CORROBORATE
|
||||
)
|
||||
return "confirmed" if confirmed else "strict"
|
||||
|
||||
+3
-3
@@ -175,10 +175,10 @@ class TestVisibleProvenance:
|
||||
keys = set(mark_keys())
|
||||
assert set(tc260_producer_vendors().values()) <= keys
|
||||
assert keys >= TC260_FALLBACK_VENDORS
|
||||
# Every TC260 mark should name its producer, or it silently falls back to the
|
||||
# ByteDance pair on an image carrying that mark.
|
||||
# Primary TC260 marks name their producer. Companion pills inherit the primary
|
||||
# mark's product-level provenance rather than duplicating an external identity.
|
||||
unmapped = {m.key for m in known_marks() if m.label_regime == "tc260" and not m.tc260_producer_codes}
|
||||
assert unmapped == {"jimeng_pill"}, f"TC260 marks with no producer code: {unmapped}"
|
||||
assert unmapped == {"jimeng_pill", "liblib_pill"}, f"TC260 marks with no producer code: {unmapped}"
|
||||
|
||||
@pytest.mark.skipif(not CHATGPT.exists(), reason="chatgpt sample not present")
|
||||
def test_openai_image_has_no_visible_vendor(self):
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""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).
|
||||
The historical wordmark constants were measured on the 15-frame vendor cohort
|
||||
(2026-07-22); these tests pin the bottom-CENTER anchor, strict-only gate, and
|
||||
match-box footprint. They also cover the newer corroboration-only compact pill
|
||||
against its cleared provider output and a synthetic wordmark-plus-pill pair.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -16,11 +18,17 @@ from remove_ai_watermarks import watermark_registry as registry
|
||||
from remove_ai_watermarks.liblib_engine import (
|
||||
_ALPHA_HEIGHT_FRAC,
|
||||
_ALPHA_WIDTH_FRAC,
|
||||
_TOP_LEFT_PILL_MIN_CONFIDENCE,
|
||||
LibLibEngine,
|
||||
_alpha_template,
|
||||
)
|
||||
from remove_ai_watermarks.pill_engine import PillEngine
|
||||
from scripts.render_visible_examples import stamp_image_mark
|
||||
|
||||
_MARK_FRAC = 0.10 # measured wordmark width, fraction of the frame WIDTH
|
||||
_CURRENT_PROVIDER_OUTPUT = (
|
||||
Path(__file__).resolve().parents[1] / "data" / "fixtures" / "visible" / "liblib" / "current-provider-output.png"
|
||||
)
|
||||
|
||||
|
||||
def _compose(w: int, h: int, bg: float = 100.0):
|
||||
@@ -42,6 +50,14 @@ def _compose(w: int, h: int, bg: float = 100.0):
|
||||
return wm, (ax, ay, gw, gh, lx0)
|
||||
|
||||
|
||||
def _compose_with_compact_pill(w: int = 1200, h: int = 1200) -> np.ndarray:
|
||||
"""Add the compact top-left AI-generated pill used beside the LiblibAI mark."""
|
||||
image, _ = _compose(w, h, bg=180.0)
|
||||
stamped = stamp_image_mark("liblib_pill", image, alpha_mult=0.5)
|
||||
assert stamped is not None
|
||||
return stamped[0]
|
||||
|
||||
|
||||
class TestLocate:
|
||||
def test_box_horizontally_centered(self):
|
||||
eng = LibLibEngine()
|
||||
@@ -79,6 +95,9 @@ class TestConfig:
|
||||
mark = registry.get_mark("liblib")
|
||||
assert mark.location == "bottom-center"
|
||||
assert mark.in_auto
|
||||
pill = registry.get_mark("liblib_pill")
|
||||
assert pill.location == "top-left"
|
||||
assert pill.product == mark.product
|
||||
|
||||
|
||||
class TestDetectAndMask:
|
||||
@@ -106,6 +125,38 @@ class TestDetectAndMask:
|
||||
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_mask_also_covers_a_compact_top_left_pill(self):
|
||||
image = _compose_with_compact_pill()
|
||||
pill = PillEngine().detect(image)
|
||||
assert not pill.detected, "the fixture must reproduce the strict pill-detector miss"
|
||||
assert pill.confidence >= _TOP_LEFT_PILL_MIN_CONFIDENCE
|
||||
|
||||
report = registry.remove_auto_marks_detailed(image, backend="cv2")
|
||||
|
||||
assert [mark.key for mark in report.marks] == ["liblib", "liblib_pill"]
|
||||
bottom, top = report.marks
|
||||
assert bottom.mask_bbox[1] > image.shape[0] // 2
|
||||
assert top.mask_bbox[1] < image.shape[0] // 4
|
||||
assert top.mask_bbox[3] < image.shape[0] // 4
|
||||
strict_report = registry.remove_auto_marks_detailed(image, sensitivity="strict", backend="cv2")
|
||||
assert [mark.key for mark in strict_report.marks] == ["liblib"]
|
||||
|
||||
def test_current_provider_pill_is_removed_with_liblib_metadata(self):
|
||||
from remove_ai_watermarks import api, image_io
|
||||
|
||||
image = image_io.imread(_CURRENT_PROVIDER_OUTPUT, cv2.IMREAD_UNCHANGED)
|
||||
provenance = api.visible_provenance(_CURRENT_PROVIDER_OUTPUT)
|
||||
strict = registry.get_mark("liblib_pill").detect(image)
|
||||
corroborated = registry.get_mark("liblib_pill").detect(image, provenance=True)
|
||||
|
||||
assert provenance == frozenset({"liblib"})
|
||||
assert not strict.detected
|
||||
assert corroborated.detected
|
||||
detected = {d.key for d in registry.detect_marks(image, provenance=provenance) if d.detected}
|
||||
assert detected == {"liblib_pill"}
|
||||
report = registry.remove_auto_marks_detailed(image, provenance=provenance, backend="cv2")
|
||||
assert [(mark.key, mark.status) for mark in report.marks] == [("liblib_pill", "cleaned")]
|
||||
|
||||
def test_no_mask_on_clean_frame(self):
|
||||
eng = LibLibEngine()
|
||||
img = np.full((2400, 1792, 3), 100, np.uint8)
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestGallery:
|
||||
def test_engine_detects_its_own_example(self, key: str) -> None:
|
||||
img = imread(str(_GALLERY / key / "example.png"))
|
||||
assert img is not None, key
|
||||
det = wr.get_mark(key).detect(img, provenance=False)
|
||||
det = wr.get_mark(key).detect(img, provenance=key == "liblib_pill")
|
||||
assert det.detected, f"{key}: confidence {det.confidence:.3f} on its own example"
|
||||
|
||||
def test_gallery_has_no_stray_directories(self) -> None:
|
||||
|
||||
@@ -27,6 +27,7 @@ _ALTERNATE_SIZE = {
|
||||
"runninghub": (1280, 960),
|
||||
"baidu": (1280, 960),
|
||||
"liblib": (960, 1280),
|
||||
"liblib_pill": (960, 1280),
|
||||
"microsoft": (1280, 960),
|
||||
"jimeng_pill": (960, 1280),
|
||||
}
|
||||
@@ -102,7 +103,8 @@ def test_cv2_mask_quality_smoke(key: str, case: str) -> None:
|
||||
assert np.any(changed), key
|
||||
|
||||
mark = wr.get_mark(key)
|
||||
before = mark.detect(marked, provenance=False)
|
||||
corroborated = key == "liblib_pill"
|
||||
before = mark.detect(marked, provenance=corroborated)
|
||||
assert before.detected, f"{key}/{case}: confidence {before.confidence:.3f}"
|
||||
localization = mark.localize(marked, force=False, detection=before)
|
||||
assert localization.mask is not None, f"{key}/{case}: no mask"
|
||||
@@ -114,7 +116,7 @@ def test_cv2_mask_quality_smoke(key: str, case: str) -> None:
|
||||
assert region is not None, key
|
||||
assert np.array_equal(filled[localization.mask == 0], marked[localization.mask == 0]), key
|
||||
|
||||
after = mark.detect(filled, provenance=False)
|
||||
after = mark.detect(filled, provenance=corroborated)
|
||||
assert not after.detected, f"{key}/{case}: residual confidence {after.confidence:.3f}"
|
||||
filled_psnr, filled_ssim = _score_box(clean, filled, changed)
|
||||
assert filled_psnr >= _MIN_FILLED_PSNR, f"{key}/{case}: PSNR {filled_psnr:.2f} dB"
|
||||
|
||||
@@ -25,6 +25,7 @@ class TestCatalog:
|
||||
"runninghub",
|
||||
"baidu",
|
||||
"liblib",
|
||||
"liblib_pill",
|
||||
"microsoft",
|
||||
"jimeng_pill",
|
||||
]
|
||||
@@ -46,6 +47,7 @@ class TestCatalog:
|
||||
assert by_key["jimeng"].location == "bottom-right"
|
||||
assert by_key["yuanbao"].location == "bottom-right"
|
||||
assert by_key["samsung"].location == "bottom-left"
|
||||
assert by_key["liblib_pill"].location == "top-left"
|
||||
assert by_key["jimeng_pill"].location == "top-left"
|
||||
|
||||
def test_get_mark_unknown_raises(self):
|
||||
@@ -114,6 +116,7 @@ class TestScan:
|
||||
"runninghub",
|
||||
"baidu",
|
||||
"liblib",
|
||||
"liblib_pill",
|
||||
"microsoft",
|
||||
"jimeng_pill",
|
||||
}
|
||||
@@ -406,6 +409,18 @@ class TestSensitivity:
|
||||
== "confirmed"
|
||||
)
|
||||
|
||||
def test_auto_relaxes_liblib_pill_via_wordmark(self):
|
||||
assert (
|
||||
reg.resolve_trust("liblib_pill", sensitivity="auto", provenance=frozenset(), strict_keys={"liblib"})
|
||||
== "confirmed"
|
||||
)
|
||||
|
||||
def test_auto_relaxes_liblib_pill_via_product_metadata(self):
|
||||
assert (
|
||||
reg.resolve_trust("liblib_pill", sensitivity="auto", provenance=frozenset({"liblib"}), strict_keys=set())
|
||||
== "confirmed"
|
||||
)
|
||||
|
||||
def test_auto_no_cross_mark_across_products(self):
|
||||
# a detected Jimeng wordmark must NOT relax Doubao (distinct products, same corner)
|
||||
assert (
|
||||
@@ -563,6 +578,12 @@ class TestArbiter:
|
||||
# removal of a textured footprint on content nothing confirmed.
|
||||
assert "jimeng_pill" not in fired
|
||||
|
||||
def test_weak_liblib_pill_does_not_confirm_the_wordmark(self):
|
||||
assert (
|
||||
reg.resolve_trust("liblib", sensitivity="auto", provenance=frozenset(), strict_keys={"liblib_pill"})
|
||||
== "strict"
|
||||
)
|
||||
|
||||
def test_real_jimeng_wordmark_still_corroborates_the_pill(self):
|
||||
"""The fix removes only the pill's TESTIMONY, not the wordmark's."""
|
||||
cands = [
|
||||
@@ -655,10 +676,14 @@ class TestMarkKnowledgeIsOnTheRow:
|
||||
|
||||
assert {m.key: m.platform for m in reg.known_marks() if m.platform is not None} == _VISIBLE_MARK_PLATFORM
|
||||
|
||||
def test_the_platformless_marks_are_the_two_with_their_own_paths(self):
|
||||
def test_platformless_marks_do_not_attribute_a_product_on_their_own(self):
|
||||
"""Gemini has the higher-confidence sparkle path; the pill is too weak to
|
||||
attribute. Everything else must name a platform or `identify` reports none."""
|
||||
assert {m.key for m in reg.known_marks() if m.platform is None} == {"gemini", "jimeng_pill"}
|
||||
assert {m.key for m in reg.known_marks() if m.platform is None} == {
|
||||
"gemini",
|
||||
"jimeng_pill",
|
||||
"liblib_pill",
|
||||
}
|
||||
|
||||
def test_platform_scan_order_follows_the_registry(self):
|
||||
"""`identify` takes the FIRST platform match, so the order is load-bearing and
|
||||
@@ -717,6 +742,7 @@ class TestPillSuppressors:
|
||||
def test_product_map_is_derived_from_the_rows(self):
|
||||
assert {m.key: m.product for m in reg.known_marks()} == reg._PRODUCT_OF
|
||||
assert reg._PRODUCT_OF["jimeng_pill"] == "jimeng" # the one shared product
|
||||
assert {m.key for m in reg.known_marks() if not m.can_corroborate} == reg._CANNOT_CORROBORATE
|
||||
|
||||
|
||||
class TestProvenanceMaskThreading:
|
||||
|
||||
Reference in new issue
Block a user