feat(visible): auto fill prefers LaMa > MI-GAN > cv2, warn on cv2 fallback

The auto backend now resolves best-first: LaMa (highest quality, recovers the
textured/structured backgrounds the classical fill smears) > MI-GAN > cv2. Both
learned backends share the same onnxruntime availability check, so auto cannot
tell them apart and always prefers the better one; a memory-tight deployment
that cannot afford LaMa's ~4.7 GB peak pins MI-GAN explicitly via
`--backend migan` / `backend="migan"` (the deployment's call, not the library's).
cv2 stays the no-deps floor and now emits a one-time quality warning when auto
falls back to it, since it smears texture/structure.

Motivated by a v0.12.1 reverse-alpha vs 0.14 localize->fill head-to-head:
reverse-alpha recovered structured backgrounds more cleanly than any inpaint;
LaMa closes most of that gap, MI-GAN can ghost/hallucinate, cv2 is weakest.
doubao/jimeng removal is identical between versions; gemini strict coverage is
4pp lower (all recovered via assume_ai) with cleaner clearance and no
outside-box damage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-07-09 18:24:20 +03:00
co-authored by Claude Opus 4.8
parent 9756189eaf
commit c858006e93
7 changed files with 78 additions and 31 deletions
+6 -6
View File
@@ -296,10 +296,10 @@ _visible_backend_option = click.option(
"backend",
type=click.Choice(["auto", "cv2", "migan", "lama"]),
default="auto",
help="Fill backend for visible-mark removal (localize -> fill). auto: MI-GAN when "
"the 'migan' extra is installed, else cv2. cv2: classical inpaint (no deps). "
"migan: MI-GAN ONNX (light, needs 'migan'). lama: big-LaMa ONNX (best quality, "
"needs 'lama').",
help="Fill backend for visible-mark removal (localize -> fill). auto: best available, "
"LaMa > MI-GAN > cv2 (a learned backend needs the 'lama' or 'migan' extra; else cv2, "
"with a warning). cv2: classical inpaint (no deps, smears texture). migan: MI-GAN ONNX "
"(light, ~1 GB, the memory-tight pick). lama: big-LaMa ONNX (best quality, ~4.7 GB).",
)
@@ -556,8 +556,8 @@ def cmd_visible(
Finds a known mark in its usual place (Gemini sparkle / Doubao-Jimeng-Samsung
text) via the watermark registry and removes it by LOCALIZING the mark to a mask
and filling that mask with the chosen ``--backend`` (auto: MI-GAN if the 'migan'
extra is installed, else cv2). ``--mark auto`` removes every detected mark in one
and filling that mask with the chosen ``--backend`` (auto: best available, LaMa >
MI-GAN > cv2). ``--mark auto`` removes every detected mark in one
pass. For arbitrary logos/objects, use ``erase``.
"""
from remove_ai_watermarks import watermark_registry as registry
+35 -11
View File
@@ -26,6 +26,7 @@ Entries:
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Literal
@@ -34,11 +35,13 @@ if TYPE_CHECKING:
from numpy.typing import NDArray
logger = logging.getLogger(__name__)
Region = tuple[int, int, int, int]
# Fill backend for the shared removal path. ``auto`` resolves to the preferred
# installed ONNX model (MI-GAN) or cv2 (see ``resolve_backend``); the others force
# a specific backend (mirrors the ``erase`` command's ``--backend``).
# Fill backend for the shared removal path. ``auto`` resolves best-first to the highest
# quality installed model -- LaMa, else MI-GAN, else cv2 (see ``resolve_backend``); the
# others force a specific backend (mirrors the ``erase`` command's ``--backend``).
Backend = Literal["auto", "cv2", "migan", "lama"]
# Detection sensitivity for the removal path -- how much to trust a borderline mark.
@@ -193,7 +196,7 @@ class KnownMark:
"""Remove this mark by localize -> fill; returns ``(result, region)`` where
``region`` is the removed mark's bbox, or None if nothing was removed.
``backend`` picks the fill (``auto`` = MI-GAN if installed else cv2; or force
``backend`` picks the fill (``auto`` = LaMa > MI-GAN > cv2, best available; or force
``cv2``/``migan``/``lama``). ``provenance`` relaxes the detector's trust gate
when metadata already confirms the vendor. ``force`` removes at the mark's
usual footprint even without a positive detection (the ``--no-detect`` path).
@@ -276,15 +279,36 @@ def inpaint_model_available() -> bool:
return region_eraser.migan_available() or region_eraser.lama_available()
def preferred_inpaint_backend() -> Literal["migan", "cv2"]:
"""Backend used by the ``auto`` fill: MI-GAN (light, droplet-friendly, the
default) when its ONNX runtime is available, else cv2. big-LaMa is NOT auto-
selected -- it is a heavier explicit opt-in via ``--backend lama`` (both models
run on onnxruntime, so availability alone cannot express the user's intent; the
light model is the safe default)."""
_warned_cv2_fallback = False
def preferred_inpaint_backend() -> Literal["lama", "migan", "cv2"]:
"""Backend the ``auto`` fill resolves to, best-first: LaMa > MI-GAN > cv2.
LaMa is the highest-quality inpaint (it recovers the textured/structured backgrounds
the classical fill smears), so ``auto`` prefers it whenever a learned backend can run
(onnxruntime present). MI-GAN is the lighter learned model; both currently share the
SAME onnxruntime availability check, so ``auto`` cannot tell them apart and always
prefers the better one -- a memory-tight deployment that cannot afford LaMa's ~4.7 GB
peak pins MI-GAN explicitly via ``--backend migan`` / ``backend="migan"`` (that is the
deployment's call, not the library's). cv2 is the classical no-deps floor and the last
resort: it smears textured/structured backgrounds, so a one-time quality warning fires
when ``auto`` falls back to it."""
from remove_ai_watermarks import region_eraser
return "migan" if region_eraser.migan_available() else "cv2"
if region_eraser.lama_available():
return "lama"
if region_eraser.migan_available():
return "migan"
global _warned_cv2_fallback
if not _warned_cv2_fallback:
_warned_cv2_fallback = True
logger.warning(
"No learned-inpaint backend available (onnxruntime not installed); falling back "
"to the cv2 classical inpaint, which can smear textured or structured backgrounds. "
"Install the 'lama' (best) or 'migan' (lighter) extra for higher-quality fills."
)
return "cv2"
def resolve_backend(backend: Backend) -> Literal["cv2", "migan", "lama"]: