Improve Kling visible watermark masking

This commit is contained in:
Victor Kuznetsov committed 2026-09-04 20:05:07 -07:00
1 parent d391cc42d1
commit 11026ec0c6
6 files changed
+131 -33

No files matched your search

+16 -4
View File
@@ -1279,15 +1279,15 @@ belongs in the `_post_gate` hook, never in a `detect` override: an override is
invisible to the single-pass path, and the RunningHub and Yuanbao anchor gates
were briefly skipped there for exactly that reason.
A mark whose removable footprint differs from what the detector localizes
A mark whose removable rectangular footprint differs from what the detector localizes
overrides `_footprint_rect` (which policy) and `_extend_match_box` (how far the
box grows), not the whole `footprint_mask`. Baidu extends right to the corner tag
and LiblibAI extends left to the triangle logo; both inherit every guard around
that arithmetic.
Doubao is the deliberate exception because its captured alpha supplies more
information than a rectangle. Its continuous top-hat response locates the mark,
then `DoubaoEngine.footprint_mask` resizes the alpha to that same winning
Doubao and Kling are deliberate sparse-mask exceptions because their alpha assets
supply more information than a rectangle. Doubao's continuous top-hat response
locates the mark, then `DoubaoEngine.footprint_mask` resizes the alpha to that same winning
`match_box` and masks only the glyphs. The canonical 2048-pixel fixture has bright
branch texture behind the bottom-edge wordmark; bounding the thresholded response,
padding it, and dilating it produced a solid 404-by-134 mask clamped to the bottom
@@ -1296,6 +1296,18 @@ with large triangular wedges. The aligned sparse mask keeps both frame edges
untouched and still clears the detector. `force` cannot align a missing detection,
so it retains the shared geometry-box fallback.
Kling keeps the pixel-derived footprint for release-specific strokes, then unions
it with the synthetic alpha aligned to the detector's winning match box. This is
necessary because its faint canonical overlay produced a blob covering only the
left half of `可灵AI 3.0`, leaving `3.0` after fill. On 60 constructed pairs from 20
tracked source images crossed with the three detector scales, the hybrid covered
100% of the known stamped footprint and cleared 60/60, versus 29/60 for the blob
alone. In the same paired region the hybrid improved median PSNR by 6.59 dB and
median SSIM by 0.062; both moved in the favorable direction on 52 pairs, tied on
seven, and regressed on one. The union is deliberate: replacing the old footprint
with the synthetic core alone would overfit the font render and could discard a
real variant stroke the pixel mask already found.
Yuanbao uses the polarity-independent `contrast` front end because its standard
two-line mark can be light on dark scenes or dark on light scenes. Its detector
and footprint both use the same best-match box. The separate one-line overlay
+1 -1
View File
@@ -54,7 +54,7 @@ CORPUS = REPO / ".local-eval" / "originals"
OUT = REPO / ".local-eval" / "fill-quality.jsonl"
# Text marks: a bundled alpha PNG plus the engine's own corner geometry.
STAMPABLE = ("doubao", "jimeng", "samsung")
STAMPABLE = ("doubao", "jimeng", "kling", "samsung")
# Marks with no bundled alpha asset. Both expose a default footprint via
# `footprint_mask(force=True)`, so they are stamped by fitting their own alpha source
# into that slot: Gemini's alpha is derived from its background captures, the pill's
@@ -793,6 +793,44 @@ class TextMarkEngine:
return None
return self._extend_match_box(det.match_box, loc, image.shape[:2])
def _aligned_alpha_mask(
self,
image: NDArray[Any],
detection: TextMarkDetection,
alpha: NDArray[Any],
*,
alpha_floor: float,
dilate: int,
) -> NDArray[Any] | None:
"""Place an alpha footprint on the detector's winning template box.
Continuous front ends locate faint glyphs that the thresholded pixel response
can only partly recover. The winning match already fixes the template's position
and scale, so engines with a trustworthy alpha asset can reuse that alignment
for their removal mask instead of running another search.
"""
if not detection.detected or detection.match_box is None:
return None
source = image_io.to_bgr(image)
loc = self.locate(source)
x0, y0, x1, y1 = detection.match_box # inclusive ROI-local coordinates
x0, y0 = max(0, x0), max(0, y0)
x1, y1 = min(loc.w - 1, x1), min(loc.h - 1, y1)
if x1 < x0 or y1 < y0:
return None
glyph_w, glyph_h = x1 - x0 + 1, y1 - y0 + 1
aligned = cv2.resize(alpha, (glyph_w, glyph_h), interpolation=cv2.INTER_LINEAR)
local_mask = np.zeros((loc.h, loc.w), np.uint8)
local_mask[y0 : y0 + glyph_h, x0 : x0 + glyph_w] = (aligned > alpha_floor).astype(np.uint8) * 255
if dilate > 0:
kernel = np.ones((2 * dilate + 1, 2 * dilate + 1), np.uint8)
local_mask = cv2.dilate(local_mask, kernel)
mask = np.zeros(source.shape[:2], np.uint8)
mask[loc.y : loc.y + loc.h, loc.x : loc.x + loc.w] = local_mask
return mask
def footprint_mask(
self,
image: NDArray[Any] | None,
+7 -20
View File
@@ -24,9 +24,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
import cv2
import numpy as np
from remove_ai_watermarks import _text_mark_engine, image_io
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection, TextMarkEngine
@@ -159,21 +156,11 @@ class DoubaoEngine(TextMarkEngine):
if alpha is None:
return super().footprint_mask(image, force=False, dilate=dilate, detection=det)
loc = self.locate(image)
x0, y0, x1, y1 = det.match_box # inclusive ROI-local coordinates
x0, y0 = max(0, x0), max(0, y0)
x1, y1 = min(loc.w - 1, x1), min(loc.h - 1, y1)
if x1 < x0 or y1 < y0:
return None
glyph_w, glyph_h = x1 - x0 + 1, y1 - y0 + 1
aligned = cv2.resize(alpha, (glyph_w, glyph_h), interpolation=cv2.INTER_LINEAR)
local_mask = np.zeros((loc.h, loc.w), np.uint8)
local_mask[y0 : y0 + glyph_h, x0 : x0 + glyph_w] = (aligned > _FOOTPRINT_ALPHA_FLOOR).astype(np.uint8) * 255
radius = _FOOTPRINT_DILATE if dilate is None else max(0, dilate)
if radius > 0:
kernel = np.ones((2 * radius + 1, 2 * radius + 1), np.uint8)
local_mask = cv2.dilate(local_mask, kernel)
mask = np.zeros(image.shape[:2], np.uint8)
mask[loc.y : loc.y + loc.h, loc.x : loc.x + loc.w] = local_mask
return mask
return self._aligned_alpha_mask(
image,
det,
alpha,
alpha_floor=_FOOTPRINT_ALPHA_FLOOR,
dilate=radius,
)
+53 -8
View File
@@ -7,13 +7,13 @@ Known variants: an "Omni" suffix release, a latin "KlingAI 3.0" release, and a
version-less "可灵AI" -- the silhouette targets the common "可灵AI 3.0" core, so the
suffix variants are only caught when the core run is bold enough (measured below).
Detection matches the bundled glyph silhouette against the corner; removal is the
shared **localize -> fill** (the glyph-bbox :meth:`footprint_mask` feeds
``region_eraser``), NOT reverse-alpha. This module supplies only Kling AI's tuned
:class:`TextMarkConfig` (``assets/kling_alpha.png`` -- a font-rendered synthetic
silhouette from ``scripts/render_vendor_silhouettes.py``, never cut from an
upload). It also feeds ``identify`` as the medium-confidence ``visible_kling``
signal via the registry.
Detection matches the bundled glyph silhouette against the corner. Removal unions
the pixel-derived footprint with that silhouette aligned to the winning match box,
then sends the hybrid mask through the shared **localize -> fill** path in
``region_eraser``; it is not reverse-alpha. The asset
(``assets/kling_alpha.png``) is a font-rendered synthetic silhouette from
``scripts/render_vendor_silhouettes.py``, never cut from an upload. The detector also
feeds ``identify`` as the medium-confidence ``visible_kling`` signal via the registry.
EVERY tuned number below was measured on the vendor cohort (30 TC260 carriers whose
producer USCC 91110108335469089C names the entity, 2026-07-21; harness
@@ -51,8 +51,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
import cv2
from remove_ai_watermarks import _text_mark_engine
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkEngine
from remove_ai_watermarks._text_mark_engine import TextMarkConfig, TextMarkDetection, TextMarkEngine
if TYPE_CHECKING:
from numpy.typing import NDArray
@@ -86,6 +88,13 @@ DETECT_NCC_THRESHOLD = 0.35
_ALPHA_WIDTH_FRAC = 0.12
_ALPHA_HEIGHT_FRAC = 0.0287
# A faint Kling overlay can yield a partial thresholded blob even though the
# continuous detector aligned the complete text core. Keep that pixel-derived mask
# for release-specific strokes, then add the aligned synthetic core with a one-pixel
# halo so every glyph that justified the detection is removed on the first fill.
_FOOTPRINT_ALPHA_FLOOR = 0.05
_FOOTPRINT_DILATE = 1
_CONFIG = TextMarkConfig(
name="Kling AI",
asset_name="kling_alpha.png",
@@ -128,3 +137,39 @@ class KlingEngine(TextMarkEngine):
def __init__(self) -> None:
super().__init__(_CONFIG)
def footprint_mask(
self,
image: NDArray[Any] | None,
*,
force: bool = False,
dilate: int | None = None,
detection: TextMarkDetection | None = None,
) -> NDArray[Any] | None:
"""Add the detector-aligned Kling core to the pixel-derived footprint.
The legacy mask remains in the union because it can cover variant-specific
strokes outside the synthetic core. The aligned alpha closes holes caused by
a faint top-hat blob without replacing that evidence or triggering another
detector pass. Explicit ``force`` has no trustworthy alignment and retains the
shared geometry-box behavior.
"""
if force or image is None or image.size == 0:
return super().footprint_mask(image, force=force, dilate=dilate, detection=detection)
det = detection if detection is not None else self.detect(image)
legacy = super().footprint_mask(image, force=False, dilate=dilate, detection=det)
alpha = _alpha_template()
if alpha is None:
return legacy
radius = _FOOTPRINT_DILATE if dilate is None else max(0, dilate)
core = self._aligned_alpha_mask(
image,
det,
alpha,
alpha_floor=_FOOTPRINT_ALPHA_FLOOR,
dilate=radius,
)
if core is None:
return legacy
return core if legacy is None else cv2.bitwise_or(legacy, core)
+16
View File
@@ -8,6 +8,8 @@ geometry or relax the measured strict-only gate.
from __future__ import annotations
from pathlib import Path
import cv2
import numpy as np
import pytest
@@ -136,6 +138,20 @@ class TestFootprintMaskAndRemoval:
h, w = wm.shape[:2]
assert np.array_equal(out[: h // 2, : w // 2], wm[: h // 2, : w // 2]) # far region exact
def test_removes_complete_faint_canonical_mark(self):
"""A partial top-hat blob must not truncate the detector's full match."""
path = Path(__file__).parents[1] / "data" / "fixtures" / "visible" / "kling" / "example.png"
image = cv2.imread(str(path), cv2.IMREAD_COLOR)
assert image is not None
mark = registry.get_mark("kling")
before = mark.detect(image)
assert before.detected
output, region = mark.remove(image, backend="cv2", detection=before)
assert region is not None
assert not KlingEngine().detect(output).detected
def test_footprint_mask_in_bottom_right(self):
wm, _ = _compose(853, 640)
mask = KlingEngine().footprint_mask(wm)