mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-07-11 18:46:32 +02:00
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:
@@ -27,7 +27,7 @@ It does **not** target watermarks that protect someone else's paid or copyrighte
|
||||
|
||||
## Features
|
||||
|
||||
- **Visible watermark removal** — a registry of known marks in their usual places: the Gemini / Nano Banana sparkle, the Doubao "豆包AI生成" text strip, the Jimeng "★ 即梦AI" wordmark, and the Samsung Galaxy AI "✦ Contenuti generati dall'AI" strip (bottom-left, locale-specific). Each mark is **localized to a footprint mask, then filled**: the engine finds the mark, builds a binary mask over its footprint, and one shared, swappable fill inpaints that region. Choose the fill with `--backend`: `cv2` (classical inpaint, no deps, the floor), `migan` (MI-GAN, light, the preferred default when the `migan` extra is installed), or `lama` (big-LaMa, best quality, heavier, explicit opt-in); the default `auto` uses MI-GAN if installed, else cv2. The localizer is cheap CPU (cv2/numpy), so it runs anywhere; the heavier MI-GAN/LaMa fill is opt-in. Detection keys on each mark's own shape (NCC against a captured silhouette; the alpha captures rebuilt by `scripts/visible_alpha_solve.py` are used to detect and to shape the mask, not for pixel recovery). The visual detector needs no metadata, but a borderline (faint or moved) mark is only trusted with corroboration: `--sensitivity` (default `auto`) relaxes a mark's gate when local metadata confirms the vendor or a same-product sibling mark is found; `--sensitivity assume-ai` relaxes every mark on the assertion that the image is AI, recovering the moved or re-rendered marks the conservative gate skips on a metadata-stripped screenshot (`strict` never relaxes). `visible --mark auto` finds and removes every detected mark in one pass. Fast, offline, no GPU. (For arbitrary logos/objects, see `erase`.)
|
||||
- **Visible watermark removal** — a registry of known marks in their usual places: the Gemini / Nano Banana sparkle, the Doubao "豆包AI生成" text strip, the Jimeng "★ 即梦AI" wordmark, and the Samsung Galaxy AI "✦ Contenuti generati dall'AI" strip (bottom-left, locale-specific). Each mark is **localized to a footprint mask, then filled**: the engine finds the mark, builds a binary mask over its footprint, and one shared, swappable fill inpaints that region. Choose the fill with `--backend`: `cv2` (classical inpaint, no deps, the floor), `migan` (MI-GAN, light, the memory-tight pick where LaMa will not fit), or `lama` (big-LaMa, best quality, heavier, auto-preferred when a learned backend is available); the default `auto` uses LaMa > MI-GAN > cv2, best available. The localizer is cheap CPU (cv2/numpy), so it runs anywhere; the heavier MI-GAN/LaMa fill is opt-in. Detection keys on each mark's own shape (NCC against a captured silhouette; the alpha captures rebuilt by `scripts/visible_alpha_solve.py` are used to detect and to shape the mask, not for pixel recovery). The visual detector needs no metadata, but a borderline (faint or moved) mark is only trusted with corroboration: `--sensitivity` (default `auto`) relaxes a mark's gate when local metadata confirms the vendor or a same-product sibling mark is found; `--sensitivity assume-ai` relaxes every mark on the assertion that the image is AI, recovering the moved or re-rendered marks the conservative gate skips on a metadata-stripped screenshot (`strict` never relaxes). `visible --mark auto` finds and removes every detected mark in one pass. Fast, offline, no GPU. (For arbitrary logos/objects, see `erase`.)
|
||||
- **Universal region eraser (`erase`)** — remove any logo / watermark / object inside boxes you specify, regardless of position or color. Default cv2 inpainting (CPU, instant); optional big-LaMa via onnxruntime (`lama` extra) for higher quality
|
||||
- **Invisible watermark removal** — SynthID, StableSignature, TreeRing via diffusion-based regeneration (needs a local GPU, or run it with no setup on [raiw.cc](https://raiw.cc))
|
||||
- **AI metadata stripping** — EXIF, PNG text chunks, C2PA provenance manifests (PNG / JPEG / AVIF / HEIF / JPEG-XL, **MP4 / MOV / M4V / M4A** at the container level, and **WebM / MP3 / WAV / FLAC / OGG** losslessly via ffmpeg), XMP DigitalSourceType
|
||||
@@ -334,7 +334,7 @@ remove-ai-watermarks identify image.png
|
||||
# Samsung Galaxy AI "Contenuti generati dall'AI"); force one with
|
||||
# --mark gemini / doubao / jimeng / samsung. Removal localizes each mark to a
|
||||
# footprint mask and inpaints it with a shared fill; --backend auto|cv2|migan|lama
|
||||
# (default auto) picks the fill (auto = MI-GAN if installed, else cv2).
|
||||
# (default auto) picks the fill (auto = LaMa > MI-GAN > cv2, best available).
|
||||
# --sensitivity auto|strict|assume-ai (default auto) sets how hard a borderline
|
||||
# mark is trusted; use assume-ai on a metadata-stripped screenshot to recover a
|
||||
# moved or faint mark the conservative gate would skip.
|
||||
|
||||
@@ -61,7 +61,7 @@ module.
|
||||
|
||||
`watermark_registry.py` — **single catalog of known visible watermarks**, the unified "find known marks in their usual places, recognize, remove" entry.
|
||||
|
||||
**Localize -> fill by policy (replaced reverse-alpha):** each mark is localized to a binary full-frame footprint mask (a `Localization`), and one shared, swappable fill inpaints that mask via `fill(image, mask, backend=...)` (delegates to `region_eraser.erase`). This replaced the old reverse-alpha removal (invert a captured alpha map, `original = (wm - a*logo)/(1-a)`, plus a thin residual inpaint) for ALL marks — gemini, doubao, jimeng, samsung, and jimeng_pill. **Why it changed:** reverse-alpha depended on a fixed captured alpha map at a fixed position, so it broke whenever a vendor moved or re-rendered its mark; and it was not color-lossless even with the right map (it amplifies 8-bit quantization and JPEG-chroma error by `1/(1-a)`), which showed up as "the color just changed, not removed" reports. Localize -> fill has a benign failure mode: a slightly-off localization just inpaints a small region near-losslessly instead of leaving a color-shifted smear. The captured alpha maps are still used to DETECT the marks and to shape the mask (gemini's footprint), but NOT for pixel recovery. Fill backends: `cv2` (classical inpaint, no deps, the floor), `migan` (MI-GAN ONNX, light, the preferred default when the `migan` extra is installed), `lama` (big-LaMa ONNX, best quality, heavier, explicit opt-in); `auto` = MI-GAN if installed, else cv2. Each `KnownMark` ties a key to {usual `location`, `in_auto` flag, a `_detect` callable → uniform `MarkDetection`, a `_mask` callable → full-frame footprint mask}; `KnownMark.remove(image, *, backend="auto", provenance=False, force=False)`. Entries today: `gemini` (bottom-right sparkle), `doubao` (bottom-right "豆包AI生成"), `jimeng` (bottom-right "★ 即梦AI"), `samsung` (bottom-**LEFT** "✦ Contenuti generati dall'AI", Samsung Galaxy AI, Italian locale), and the capture-less `jimeng_pill` (top-left "AI生成"). `detect_marks(image, *, provenance=frozenset())` scans all (strict, for the identify verdict); `remove_auto_marks(image, *, sensitivity="auto", provenance=frozenset(), backend="auto")` removes every detected mark in one pass. **Sensitivity (`auto`/`strict`/`assume_ai`)** decides how hard a borderline mark is trusted: the visual detectors are pixel-based (no metadata needed) and the recall gain comes from relaxing the false-positive gate, not from metadata. `resolve_relax` turns the policy + evidence into the per-mark relaxation boolean — `strict` never relaxes, `assume_ai` always relaxes (the caller asserts AI, e.g. a metadata-stripped screenshot; ~46% -> ~92% Gemini recall, at the cost of a small near-lossless fill on some clean corners), `auto` relaxes only on same-product evidence (metadata provenance for that vendor, or a confidently strict-detected sibling of the same `_PRODUCT_OF` — Doubao and Jimeng are both bottom-right ByteDance but distinct products, so they do NOT cross-relax). **Perception / decision / action are separated three ways** (the removal path only; `identify` keeps calling `KnownMark.detect` directly, so its verdict is untouched): `_build_candidates(image)` is PERCEPTION — it runs each detector at both trust levels and packages raw verdicts + the pill's flatness feature into `Candidate`s, no policy; `decide(candidates, Context(sensitivity, provenance)) -> [Decision]` is the pure DECISION arbiter — all keep/drop policy (`resolve_relax` cross-mark corroboration + the pill gate) in one image-free, unit-testable function (`tests/test_watermark_registry.py::TestArbiter`); then `remove_auto_marks` does the ACTION, localizing -> filling each winner. The Gemini FP gate deliberately stays inside `gemini_engine` (not the arbiter) because `identify` reads that same gated confidence — pulling it out would drift the provenance verdict. Behavior is byte-identical to the pre-arbiter two-pass (corpus-verified: strict/auto 46%, assume_ai 92% Gemini recall unchanged).
|
||||
**Localize -> fill by policy (replaced reverse-alpha):** each mark is localized to a binary full-frame footprint mask (a `Localization`), and one shared, swappable fill inpaints that mask via `fill(image, mask, backend=...)` (delegates to `region_eraser.erase`). This replaced the old reverse-alpha removal (invert a captured alpha map, `original = (wm - a*logo)/(1-a)`, plus a thin residual inpaint) for ALL marks — gemini, doubao, jimeng, samsung, and jimeng_pill. **Why it changed:** reverse-alpha depended on a fixed captured alpha map at a fixed position, so it broke whenever a vendor moved or re-rendered its mark; and it was not color-lossless even with the right map (it amplifies 8-bit quantization and JPEG-chroma error by `1/(1-a)`), which showed up as "the color just changed, not removed" reports. Localize -> fill has a benign failure mode: a slightly-off localization just inpaints a small region near-losslessly instead of leaving a color-shifted smear. The captured alpha maps are still used to DETECT the marks and to shape the mask (gemini's footprint), but NOT for pixel recovery. Fill backends: `cv2` (classical inpaint, no deps, the floor), `migan` (MI-GAN ONNX, light, the memory-tight pick where LaMa will not fit), `lama` (big-LaMa ONNX, best quality, heavier, auto-preferred when a learned backend is available); `auto` = LaMa > MI-GAN > cv2, best available. Each `KnownMark` ties a key to {usual `location`, `in_auto` flag, a `_detect` callable → uniform `MarkDetection`, a `_mask` callable → full-frame footprint mask}; `KnownMark.remove(image, *, backend="auto", provenance=False, force=False)`. Entries today: `gemini` (bottom-right sparkle), `doubao` (bottom-right "豆包AI生成"), `jimeng` (bottom-right "★ 即梦AI"), `samsung` (bottom-**LEFT** "✦ Contenuti generati dall'AI", Samsung Galaxy AI, Italian locale), and the capture-less `jimeng_pill` (top-left "AI生成"). `detect_marks(image, *, provenance=frozenset())` scans all (strict, for the identify verdict); `remove_auto_marks(image, *, sensitivity="auto", provenance=frozenset(), backend="auto")` removes every detected mark in one pass. **Sensitivity (`auto`/`strict`/`assume_ai`)** decides how hard a borderline mark is trusted: the visual detectors are pixel-based (no metadata needed) and the recall gain comes from relaxing the false-positive gate, not from metadata. `resolve_relax` turns the policy + evidence into the per-mark relaxation boolean — `strict` never relaxes, `assume_ai` always relaxes (the caller asserts AI, e.g. a metadata-stripped screenshot; ~46% -> ~92% Gemini recall, at the cost of a small near-lossless fill on some clean corners), `auto` relaxes only on same-product evidence (metadata provenance for that vendor, or a confidently strict-detected sibling of the same `_PRODUCT_OF` — Doubao and Jimeng are both bottom-right ByteDance but distinct products, so they do NOT cross-relax). **Perception / decision / action are separated three ways** (the removal path only; `identify` keeps calling `KnownMark.detect` directly, so its verdict is untouched): `_build_candidates(image)` is PERCEPTION — it runs each detector at both trust levels and packages raw verdicts + the pill's flatness feature into `Candidate`s, no policy; `decide(candidates, Context(sensitivity, provenance)) -> [Decision]` is the pure DECISION arbiter — all keep/drop policy (`resolve_relax` cross-mark corroboration + the pill gate) in one image-free, unit-testable function (`tests/test_watermark_registry.py::TestArbiter`); then `remove_auto_marks` does the ACTION, localizing -> filling each winner. The Gemini FP gate deliberately stays inside `gemini_engine` (not the arbiter) because `identify` reads that same gated confidence — pulling it out would drift the provenance verdict. Behavior is byte-identical to the pre-arbiter two-pass (corpus-verified: strict/auto 46%, assume_ai 92% Gemini recall unchanged).
|
||||
|
||||
**Provenance prior:** when local metadata already confirms the vendor, the mark's detection trust gate is relaxed (a confirmed vendor means the mark is present with high prior, so a mark the conservative detector would demote as a content false positive is trusted). `detect_marks` / `remove_auto_marks` take a `provenance` frozenset and `KnownMark.remove` a `provenance` flag. Mapping: a Google/Gemini C2PA issuer relaxes gemini (skips its false-positive gate and lowers the trust threshold from 0.5 to 0.35); a China-AIGC (TC260) label relaxes doubao/jimeng; `samsung_genai` relaxes samsung. Corpus finding: on Google-C2PA images, Gemini sparkle recall rose from ~46% (plain detector) to ~90% with the provenance prior (recovering marks the vendor moved or re-rendered). The localizer is cheap CPU (cv2/numpy), so a memory-tight caller runs it anywhere; the heavy MI-GAN/LaMa fill is opt-in and chosen by the caller.
|
||||
|
||||
@@ -228,7 +228,7 @@ Diffusion SynthID removal. The `--tile/--no-tile` knob is the *lossless* alterna
|
||||
|
||||
### `visible`
|
||||
|
||||
Known-visible-mark removal by **localize -> fill**: each detected mark is localized to a binary full-frame footprint mask, then one shared, swappable fill inpaints that mask. `--backend auto|cv2|migan|lama` (default `auto`) picks the fill: `cv2` (classical inpaint, no deps, the floor), `migan` (MI-GAN ONNX, the preferred default when the `migan` extra is installed), `lama` (big-LaMa ONNX, best quality, heavier, explicit opt-in); `auto` = MI-GAN if installed, else cv2 (big-LaMa is NOT auto-selected). `--sensitivity auto|strict|assume-ai` (default `auto`) controls how hard a borderline mark is trusted (see the registry section: the visual detectors are metadata-independent; `auto` relaxes a mark only on same-product evidence, `assume-ai` relaxes every mark on the caller's AI assertion — the only path to high recall on a metadata-stripped screenshot). `--backend` and `--sensitivity` are shared across `visible`/`all`/`batch`. Detection keys on each mark's own shape, and under `auto` the trust gate is relaxed when local metadata confirms the vendor (a Google/Gemini C2PA issuer relaxes gemini, a China-AIGC label relaxes doubao/jimeng, `samsung_genai` relaxes samsung), so a moved or re-rendered mark is still caught. `--mark auto` (default) removes EVERY detected mark in one pass (`registry.remove_auto_marks`, not the single strongest -- a Jimeng-basic image carries both the top-left pill and the bottom-right wordmark) from: the Gemini sparkle, the Doubao "豆包AI生成" text strip, the Jimeng "★ 即梦AI" wordmark, the Samsung Galaxy AI "✦ Contenuti generati dall'AI" strip (bottom-LEFT, Italian-locale detection), and the capture-less Jimeng "AI生成" pill (top-left, `pill_engine`). The pill's weak edge-NCC detector is gated in `remove_auto_marks` via `_keep_pill` (32k real-upload corpus validation 2026-07): never on Doubao, and two confirmation arms since metadata confirms the platform, not pill presence. (1) The bottom-right wordmark fired — ~94% precise and survives metadata-STRIPPED uploads (screenshots / re-saves) — removes the pill unrestricted. (2) TC260 metadata confirms Jimeng (`"jimeng" in provenance`, from `cli._visible_provenance`) OR the caller asserts AI (`sensitivity == "assume_ai"`), no wordmark — ~27% precise, its false fires are textured ceilings/walls that the fill visibly SMEARS — removes the pill ONLY when the top-left footprint is flat enough for an invisible fill (`pill_engine.footprint_is_flat`, median-Sobel ≤ `_FLAT_TEXTURE_MAX`; the flatness guard holds even under `assume_ai`). No confirmation → never removed. `--mark gemini|doubao|jimeng|samsung|jimeng_pill` forces one (choices come from the registry). Corpus validation: doubao and jimeng localize + remove at ~100% with clean footprints (the filled region blends into its surroundings within a few LAB levels, no color shift, no dark pit); clean images with no vendor signature had 0% false removal. For arbitrary logos/objects use `erase`. **When `--mark auto` finds no known mark (the common case — ~74% of real uploads carry no registered visible mark), the command does NOT silently re-serve the input as a finished result.** It runs a cheap metadata-only `identify`, prints actionable guidance (if the image carries an invisible/metadata mark, e.g. an OpenAI/Gemini C2PA image, it points to `all`; otherwise it does NOT imply the image is clean -- it warns that an invisible pixel watermark like SynthID cannot be detected once the metadata proxy is gone and routes to both `all` and `erase --region`), writes NO output file, and exits **`EXIT_NO_VISIBLE_MARK` (2)** — distinct from success (0) and a hard error (1) so a wrapping service (raiw.cc) can surface the message instead of treating the unchanged image as done (the production "it didn't work" / score-0 trap). Same handling for an explicit `--mark <name>` that is not detected. Helper `cli._no_visible_mark_exit`; regression-guarded by `tests/test_cli.py::TestVisibleCommand::test_visible_auto_no_mark_exits_two_with_eraser_hint` and `test_visible_auto_no_mark_routes_to_all_when_metadata`. `--no-detect` still forces the gemini fallback and proceeds (exit 0).
|
||||
Known-visible-mark removal by **localize -> fill**: each detected mark is localized to a binary full-frame footprint mask, then one shared, swappable fill inpaints that mask. `--backend auto|cv2|migan|lama` (default `auto`) picks the fill: `cv2` (classical inpaint, no deps, the floor), `migan` (MI-GAN ONNX, the memory-tight pick where LaMa will not fit), `lama` (big-LaMa ONNX, best quality, heavier, auto-preferred when a learned backend is available); `auto` = LaMa > MI-GAN > cv2, best available (LaMa is auto-preferred when a learned backend is present; a memory-tight deploy pins migan). `--sensitivity auto|strict|assume-ai` (default `auto`) controls how hard a borderline mark is trusted (see the registry section: the visual detectors are metadata-independent; `auto` relaxes a mark only on same-product evidence, `assume-ai` relaxes every mark on the caller's AI assertion — the only path to high recall on a metadata-stripped screenshot). `--backend` and `--sensitivity` are shared across `visible`/`all`/`batch`. Detection keys on each mark's own shape, and under `auto` the trust gate is relaxed when local metadata confirms the vendor (a Google/Gemini C2PA issuer relaxes gemini, a China-AIGC label relaxes doubao/jimeng, `samsung_genai` relaxes samsung), so a moved or re-rendered mark is still caught. `--mark auto` (default) removes EVERY detected mark in one pass (`registry.remove_auto_marks`, not the single strongest -- a Jimeng-basic image carries both the top-left pill and the bottom-right wordmark) from: the Gemini sparkle, the Doubao "豆包AI生成" text strip, the Jimeng "★ 即梦AI" wordmark, the Samsung Galaxy AI "✦ Contenuti generati dall'AI" strip (bottom-LEFT, Italian-locale detection), and the capture-less Jimeng "AI生成" pill (top-left, `pill_engine`). The pill's weak edge-NCC detector is gated in `remove_auto_marks` via `_keep_pill` (32k real-upload corpus validation 2026-07): never on Doubao, and two confirmation arms since metadata confirms the platform, not pill presence. (1) The bottom-right wordmark fired — ~94% precise and survives metadata-STRIPPED uploads (screenshots / re-saves) — removes the pill unrestricted. (2) TC260 metadata confirms Jimeng (`"jimeng" in provenance`, from `cli._visible_provenance`) OR the caller asserts AI (`sensitivity == "assume_ai"`), no wordmark — ~27% precise, its false fires are textured ceilings/walls that the fill visibly SMEARS — removes the pill ONLY when the top-left footprint is flat enough for an invisible fill (`pill_engine.footprint_is_flat`, median-Sobel ≤ `_FLAT_TEXTURE_MAX`; the flatness guard holds even under `assume_ai`). No confirmation → never removed. `--mark gemini|doubao|jimeng|samsung|jimeng_pill` forces one (choices come from the registry). Corpus validation: doubao and jimeng localize + remove at ~100% with clean footprints (the filled region blends into its surroundings within a few LAB levels, no color shift, no dark pit); clean images with no vendor signature had 0% false removal. For arbitrary logos/objects use `erase`. **When `--mark auto` finds no known mark (the common case — ~74% of real uploads carry no registered visible mark), the command does NOT silently re-serve the input as a finished result.** It runs a cheap metadata-only `identify`, prints actionable guidance (if the image carries an invisible/metadata mark, e.g. an OpenAI/Gemini C2PA image, it points to `all`; otherwise it does NOT imply the image is clean -- it warns that an invisible pixel watermark like SynthID cannot be detected once the metadata proxy is gone and routes to both `all` and `erase --region`), writes NO output file, and exits **`EXIT_NO_VISIBLE_MARK` (2)** — distinct from success (0) and a hard error (1) so a wrapping service (raiw.cc) can surface the message instead of treating the unchanged image as done (the production "it didn't work" / score-0 trap). Same handling for an explicit `--mark <name>` that is not detected. Helper `cli._no_visible_mark_exit`; regression-guarded by `tests/test_cli.py::TestVisibleCommand::test_visible_auto_no_mark_exits_two_with_eraser_hint` and `test_visible_auto_no_mark_routes_to_all_when_metadata`. `--no-detect` still forces the gemini fallback and proceeds (exit 0).
|
||||
|
||||
### `batch`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -40,9 +40,9 @@ def _compose_textmark(engine, bg: float = 120.0, w: int = 1024, h: int = 1024):
|
||||
|
||||
|
||||
class TestResolveBackend:
|
||||
def test_auto_resolves_to_installed_backend(self) -> None:
|
||||
# auto picks the preferred installed model (MI-GAN) or cv2; either is fine.
|
||||
assert registry.resolve_backend("auto") in {"cv2", "migan"}
|
||||
def test_auto_resolves_to_available_backend(self) -> None:
|
||||
# auto picks the best available model (LaMa > MI-GAN) or cv2; any is fine.
|
||||
assert registry.resolve_backend("auto") in {"cv2", "migan", "lama"}
|
||||
|
||||
def test_cv2_passthrough(self) -> None:
|
||||
assert registry.resolve_backend("cv2") == "cv2"
|
||||
@@ -103,19 +103,29 @@ class TestFillDispatch:
|
||||
|
||||
|
||||
class TestBackendSelection:
|
||||
"""MI-GAN is the preferred inpaint backend (light, droplet-friendly); big-LaMa
|
||||
is NOT auto-selected. cv2 is the floor when no ONNX model is present."""
|
||||
"""auto resolves to the best available inpaint backend: LaMa > MI-GAN > cv2.
|
||||
cv2 is the floor when no learned ONNX model is present (and warns once)."""
|
||||
|
||||
def test_prefers_migan_when_available(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_prefers_lama_when_available(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from remove_ai_watermarks import region_eraser
|
||||
|
||||
monkeypatch.setattr(region_eraser, "lama_available", lambda: True)
|
||||
monkeypatch.setattr(region_eraser, "migan_available", lambda: True)
|
||||
assert registry.preferred_inpaint_backend() == "lama"
|
||||
|
||||
def test_migan_when_only_migan(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from remove_ai_watermarks import region_eraser
|
||||
|
||||
monkeypatch.setattr(region_eraser, "lama_available", lambda: False)
|
||||
monkeypatch.setattr(region_eraser, "migan_available", lambda: True)
|
||||
assert registry.preferred_inpaint_backend() == "migan"
|
||||
|
||||
def test_cv2_when_no_model(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from remove_ai_watermarks import region_eraser
|
||||
|
||||
monkeypatch.setattr(region_eraser, "lama_available", lambda: False)
|
||||
monkeypatch.setattr(region_eraser, "migan_available", lambda: False)
|
||||
monkeypatch.setattr(registry, "_warned_cv2_fallback", True)
|
||||
assert registry.preferred_inpaint_backend() == "cv2"
|
||||
|
||||
def test_inpaint_model_available_reflects_either(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
@@ -51,13 +51,26 @@ class TestScan:
|
||||
|
||||
|
||||
class TestBackendResolution:
|
||||
def test_auto_resolves_to_installed_backend(self):
|
||||
assert reg.resolve_backend("auto") in ("cv2", "migan")
|
||||
def test_auto_resolves_to_available_backend(self):
|
||||
assert reg.resolve_backend("auto") in ("cv2", "migan", "lama")
|
||||
|
||||
def test_explicit_backend_passes_through(self):
|
||||
assert reg.resolve_backend("cv2") == "cv2"
|
||||
assert reg.resolve_backend("lama") == "lama"
|
||||
|
||||
def test_cv2_fallback_warns_once(self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture):
|
||||
import logging
|
||||
|
||||
from remove_ai_watermarks import region_eraser
|
||||
|
||||
monkeypatch.setattr(region_eraser, "lama_available", lambda: False)
|
||||
monkeypatch.setattr(region_eraser, "migan_available", lambda: False)
|
||||
monkeypatch.setattr(reg, "_warned_cv2_fallback", False)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert reg.preferred_inpaint_backend() == "cv2"
|
||||
assert reg.preferred_inpaint_backend() == "cv2"
|
||||
assert sum("cv2 classical inpaint" in r.message for r in caplog.records) == 1
|
||||
|
||||
|
||||
class TestFill:
|
||||
def test_fill_erases_masked_region(self):
|
||||
|
||||
Reference in New Issue
Block a user