mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-07 06:28:36 +02:00
fix(visible): safe-inpaint pill gate, cut metadata-only false fires
Verified 0.13.0 pill removal on a 32k real-upload corpus. The metadata-OR-wordmark gate was only ~1/3 precise: TC260 metadata confirms Jimeng-class provenance, not pill presence, so the weak edge-NCC detector's false fires (textured ceilings/walls, where inpaint visibly smears) were admitted whenever metadata was present. Split into two arms (_keep_pill): the reliable bottom-right wordmark (~94% precise, survives metadata stripping) removes the pill unrestricted; the metadata-only arm removes it ONLY when the top-left footprint is flat enough for an invisible inpaint (PillEngine.footprint_is_flat, median-Sobel <= _FLAT_TEXTURE_MAX). Keeps real flat-scene pills and harmless flat false fires; leaves the damaging textured false fires untouched. Corpus: 270 -> 118 removals, ~90 true preserved, damaging FP -> ~0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0e5a4cbc54
commit
a8fd02a8f7
@@ -340,10 +340,12 @@ def _warn_if_esrgan_unavailable(upscaler: str) -> None:
|
||||
|
||||
|
||||
def _aigc_metadata_present(path: Path) -> bool:
|
||||
"""True when the file carries a China-AIGC (TC260) metadata label. Used to gate
|
||||
the weak-detector 'AI生成' pill: metadata confirms Jimeng-class provenance. NB
|
||||
this is only ONE of two confirmations -- ``remove_auto_marks`` also accepts the
|
||||
bottom-right wordmark, so a metadata-STRIPPED upload can still be handled."""
|
||||
"""True when the file carries a China-AIGC (TC260) metadata label. Feeds the
|
||||
weak-detector 'AI生成' pill gate (``remove_auto_marks`` → ``_keep_pill``): metadata
|
||||
confirms Jimeng-class provenance but not pill presence, so the metadata-only arm
|
||||
removes the pill ONLY on a flat, safe-to-inpaint footprint. The reliable
|
||||
bottom-right wordmark is the other, unrestricted confirmation arm (and it survives
|
||||
a metadata-STRIPPED upload)."""
|
||||
with contextlib.suppress(Exception):
|
||||
from remove_ai_watermarks import metadata
|
||||
|
||||
|
||||
@@ -51,6 +51,19 @@ _DETECT_THRESHOLD = 0.22 # edge-NCC gate, corpus-calibrated
|
||||
_MASK_X0, _MASK_Y0 = 0.012, 0.006 # x0 of W, y0 of H
|
||||
_MASK_W, _MASK_H = 0.205, 0.115 # width of W, height of W
|
||||
|
||||
# Background-flatness gate for the metadata-only pill arm (see remove_auto_marks).
|
||||
# The pill detector is weak (~7% raw false-fire); metadata confirms the platform,
|
||||
# not pill presence, so its false fires are real Jimeng-class content WITHOUT a pill.
|
||||
# Those false fires cluster on TEXTURED top-left corners (ceiling fixtures, structure)
|
||||
# where inpaint visibly SMEARS, while real pills and harmless false fires sit on FLAT
|
||||
# corners (sky / wall / solid) where inpaint is invisible. So the metadata-only arm
|
||||
# removes the pill only when the footprint background is flat enough for a safe,
|
||||
# invisible inpaint. Threshold = median Sobel magnitude over the footprint box at a
|
||||
# normalized width; corpus-validated on 32k real uploads 2026-07 (real pills median
|
||||
# ~3.2, textured-ceiling false fires median ~8+). The reliable bottom-right wordmark
|
||||
# arm is NOT texture-gated -- a wordmark-confirmed pill is removed regardless.
|
||||
_FLAT_TEXTURE_MAX = 6.0
|
||||
|
||||
_silhouette: NDArray[Any] | None = None
|
||||
|
||||
|
||||
@@ -104,6 +117,38 @@ class PillEngine:
|
||||
score, box = m
|
||||
return PillDetection(score >= _DETECT_THRESHOLD, score, box)
|
||||
|
||||
def _footprint_box(self, image: NDArray[Any]) -> tuple[int, int, int, int] | None:
|
||||
h, w = image.shape[:2]
|
||||
x0, y0 = int(_MASK_X0 * w), int(_MASK_Y0 * h)
|
||||
x1, y1 = min(w, x0 + int(_MASK_W * w)), min(h, y0 + int(_MASK_H * w))
|
||||
if x1 <= x0 or y1 <= y0:
|
||||
return None
|
||||
return x0, y0, x1, y1
|
||||
|
||||
def footprint_texture(self, image: NDArray[Any]) -> float:
|
||||
"""Median gradient magnitude over the fixed top-left footprint box at a
|
||||
normalized width. A robust flatness proxy: low = flat (sky / wall / solid,
|
||||
inpaint invisible), high = textured (ceiling fixtures / structure, inpaint
|
||||
smears). Median (not mean) so the pill's own edges -- a minority of the box --
|
||||
do not inflate it. Backs the metadata-only arm's safe-inpaint gate."""
|
||||
if image is None or image.size == 0:
|
||||
return 0.0
|
||||
box = self._footprint_box(image)
|
||||
if box is None:
|
||||
return 0.0
|
||||
x0, y0, x1, y1 = box
|
||||
crop = image[y0:y1, x0:x1]
|
||||
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) if crop.ndim == 3 else crop
|
||||
tw = 220
|
||||
gray = cv2.resize(gray, (tw, max(1, int(gray.shape[0] * tw / gray.shape[1])))).astype(np.float32)
|
||||
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
|
||||
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
||||
return float(np.median(cv2.magnitude(gx, gy)))
|
||||
|
||||
def footprint_is_flat(self, image: NDArray[Any], *, thresh: float = _FLAT_TEXTURE_MAX) -> bool:
|
||||
"""True when the top-left footprint is flat enough for an invisible inpaint."""
|
||||
return self.footprint_texture(image) <= thresh
|
||||
|
||||
def footprint_mask(self, image: NDArray[Any], *, force: bool = False) -> NDArray[Any] | None:
|
||||
"""Full-frame uint8 mask (255 = pill) over the pill's known top-left region.
|
||||
|
||||
@@ -116,11 +161,11 @@ class PillEngine:
|
||||
fixed regardless)."""
|
||||
if image is None or image.size == 0:
|
||||
return None
|
||||
h, w = image.shape[:2]
|
||||
x0, y0 = int(_MASK_X0 * w), int(_MASK_Y0 * h)
|
||||
x1, y1 = min(w, x0 + int(_MASK_W * w)), min(h, y0 + int(_MASK_H * w))
|
||||
if x1 <= x0 or y1 <= y0:
|
||||
box = self._footprint_box(image)
|
||||
if box is None:
|
||||
return None
|
||||
x0, y0, x1, y1 = box
|
||||
h, w = image.shape[:2]
|
||||
mask = np.zeros((h, w), np.uint8)
|
||||
mask[y0:y1, x0:x1] = 255
|
||||
return mask
|
||||
|
||||
@@ -344,6 +344,31 @@ def best_auto_mark(image: NDArray[Any]) -> MarkDetection | None:
|
||||
return max(fired, key=lambda d: d.confidence) if fired else None
|
||||
|
||||
|
||||
def _keep_pill(image: NDArray[Any], keys: set[str], *, pill_metadata: bool) -> bool:
|
||||
"""Whether to auto-remove the capture-less 'AI生成' pill given the fired marks.
|
||||
|
||||
The pill detector is weak (~7% raw false-fire) and metadata confirms the platform,
|
||||
not pill presence, so a naive metadata-OR-wordmark gate over-fires: on a 32k
|
||||
real-upload corpus (2026-07) the metadata-only arm was only ~27% precise and its
|
||||
false fires were textured ceilings/walls that inpaint visibly SMEARS. Two arms:
|
||||
* bottom-right "★ 即梦AI" wordmark fired -> ~94% precise, and it survives
|
||||
metadata-STRIPPED uploads: remove the pill unrestricted;
|
||||
* metadata only (TC260 AIGC, no wordmark) -> remove ONLY when the top-left
|
||||
footprint is flat enough for an invisible inpaint (``footprint_is_flat``),
|
||||
so real flat-scene pills (and harmless flat false fires) are cleaned while
|
||||
the damaging textured false fires are left untouched.
|
||||
A Doubao image is TC260 too but is not Jimeng-basic, so the pill never rides on a
|
||||
Doubao detection. No confirmation at all -> never remove (blocks false fires on
|
||||
non-Jimeng content)."""
|
||||
if "doubao" in keys:
|
||||
return False
|
||||
if "jimeng" in keys:
|
||||
return True
|
||||
if pill_metadata:
|
||||
return bool(_engine("jimeng_pill").footprint_is_flat(image))
|
||||
return False
|
||||
|
||||
|
||||
def remove_auto_marks(
|
||||
image: NDArray[Any],
|
||||
*,
|
||||
@@ -362,16 +387,12 @@ def remove_auto_marks(
|
||||
its own corner on the progressively-cleaned image, so order does not matter.
|
||||
|
||||
The capture-less ``jimeng_pill`` has a weak edge-NCC detector (~7% raw
|
||||
false-fire), so it is kept only when the image is CONFIRMED Jimeng and NOT
|
||||
Doubao. Confirmation is ``pill_metadata`` (the China-AIGC / TC260 metadata
|
||||
signal, supplied by the caller) OR the reliable bottom-right wordmark firing --
|
||||
the wordmark keeps recall on metadata-STRIPPED uploads (screenshots / re-saved
|
||||
files) that the metadata gate alone would miss. Returns ``(result, [labels
|
||||
removed])``; an empty list means nothing fired."""
|
||||
false-fire), so its removal is gated by ``_keep_pill`` (Jimeng-class confirmation
|
||||
+ a safe-inpaint check on the metadata-only arm; see that helper). Returns
|
||||
``(result, [labels removed])``; an empty list means nothing fired."""
|
||||
fired = [d for d in detect_marks(image, include_explicit=False) if d.detected]
|
||||
keys = {d.key for d in fired}
|
||||
jimeng_confirmed = pill_metadata or "jimeng" in keys
|
||||
if "jimeng_pill" in keys and (not jimeng_confirmed or "doubao" in keys):
|
||||
if "jimeng_pill" in keys and not _keep_pill(image, keys, pill_metadata=pill_metadata):
|
||||
fired = [d for d in fired if d.key != "jimeng_pill"]
|
||||
result = image
|
||||
for det in fired:
|
||||
|
||||
Reference in New Issue
Block a user