mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-18 15:31:06 +02:00
Improve Kling visible watermark masking
This commit is contained in:
6 files changed
+131
-33
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user