mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-07-28 10:18:49 +02:00
fix(io): Unicode-safe cv2 image IO + un-eat the [gpu] install hint (v0.6.6)
Two CLI/IO robustness bugs surfaced by issues #17 and #19. #17 -- non-ASCII image paths (Chinese/Cyrillic/accented) failed on Windows: cv2.imread/imwrite use the platform ANSI code-page API, so the decode came back empty with a "can't open/read file" warning. New image_io.imread/imwrite route through np.fromfile+cv2.imdecode / cv2.imencode+tofile (Unicode-safe, byte- identical output, cv2.imread None-semantics preserved); all 8 cv2 read/write call sites now go through it. Behavior-neutral on macOS/Linux (already accept UTF-8 paths), so the fix is correct-by-construction for the Windows-only bug. #19 (incidental) -- rich parsed the "[gpu]" in the GPU-extra install hint as a style tag and dropped it, so the printed command was the un-installable "pip install 'remove-ai-watermarks'". Escaped as \[gpu] at both call sites. Tests: test_image_io.py (non-ASCII round-trip, alpha, missing/empty/garbage semantics); test_cli.py::TestGpuHintMarkup (install hint keeps the extra). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d847b39292
commit
7b47fa9f6a
@@ -1,3 +1,3 @@
|
||||
"""Remove-AI-Watermarks: Unified tool for removing visible and invisible AI watermarks."""
|
||||
|
||||
__version__ = "0.6.5"
|
||||
__version__ = "0.6.6"
|
||||
|
||||
@@ -85,7 +85,9 @@ def _read_bgr_and_alpha(path: Path) -> tuple[np.ndarray | None, np.ndarray | Non
|
||||
"""
|
||||
import cv2
|
||||
|
||||
image = cv2.imread(str(path), cv2.IMREAD_UNCHANGED)
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
image = image_io.imread(path, cv2.IMREAD_UNCHANGED)
|
||||
if image is None:
|
||||
return None, None
|
||||
if image.ndim == 2:
|
||||
@@ -109,11 +111,12 @@ def _write_bgr_with_alpha(
|
||||
forced to 0 inside that bbox (expanded by ``pad`` px) so the watermark area
|
||||
becomes fully transparent in the saved file.
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
if alpha is None or path.suffix.lower() not in _ALPHA_FORMATS:
|
||||
cv2.imwrite(str(path), bgr)
|
||||
image_io.imwrite(path, bgr)
|
||||
return
|
||||
|
||||
alpha_out = alpha
|
||||
@@ -127,7 +130,7 @@ def _write_bgr_with_alpha(
|
||||
alpha_out[y0:y1, x0:x1] = 0
|
||||
|
||||
bgra = np.dstack([bgr, alpha_out])
|
||||
cv2.imwrite(str(path), bgra)
|
||||
image_io.imwrite(path, bgra)
|
||||
|
||||
|
||||
def _run_doubao_if_selected(
|
||||
@@ -481,7 +484,7 @@ def cmd_invisible(
|
||||
if not invisible_available():
|
||||
console.print(
|
||||
"[red]Error:[/] GPU dependencies not installed.\n"
|
||||
" Install them with: [bold]pip install 'remove-ai-watermarks[gpu]'[/]"
|
||||
" Install them with: [bold]pip install 'remove-ai-watermarks\\[gpu]'[/]"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
@@ -744,7 +747,7 @@ def cmd_all(
|
||||
if not invisible_available():
|
||||
console.print(
|
||||
" [yellow]⚠[/] Skipped — GPU dependencies not installed.\n"
|
||||
" Install them with: [bold]pip install 'remove-ai-watermarks[gpu]'[/]"
|
||||
" Install them with: [bold]pip install 'remove-ai-watermarks\\[gpu]'[/]"
|
||||
)
|
||||
else:
|
||||
from remove_ai_watermarks.invisible_engine import InvisibleEngine
|
||||
|
||||
@@ -239,7 +239,9 @@ class DoubaoEngine:
|
||||
|
||||
def load_image_bgr(path: str | Path) -> NDArray:
|
||||
"""Read an image as BGR ndarray (helper for scripts/tests)."""
|
||||
img = cv2.imread(str(path), cv2.IMREAD_COLOR)
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
img = image_io.imread(path, cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"Failed to read image: {path}")
|
||||
return img
|
||||
|
||||
@@ -556,7 +556,9 @@ def detect_sparkle_confidence(image_path: Path) -> float | None:
|
||||
(cv2 returns None for unsupported containers such as HEIC). Kept here so the
|
||||
cv2 dependency stays in this module; callers apply their own threshold.
|
||||
"""
|
||||
img = cv2.imread(str(image_path))
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
img = image_io.imread(image_path)
|
||||
if img is None:
|
||||
return None
|
||||
return float(GeminiEngine().detect_watermark(img).confidence)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Unicode-safe cv2 image IO (issue #17).
|
||||
|
||||
``cv2.imread`` / ``cv2.imwrite`` pass the path to the platform C runtime, which
|
||||
on Windows uses the narrow (ANSI) code-page API and therefore fails on paths
|
||||
containing non-ASCII characters (Chinese, Cyrillic, ...). The symptom is a
|
||||
``can't open/read file`` warning and a ``None`` decode even though the file
|
||||
exists.
|
||||
|
||||
These wrappers route through numpy buffers instead: ``np.fromfile`` /
|
||||
``ndarray.tofile`` open the path in Python (full Unicode), and
|
||||
``cv2.imdecode`` / ``cv2.imencode`` do the codec work. The decoded/encoded
|
||||
bytes are byte-for-byte identical to ``imread`` / ``imwrite``. On macOS/Linux
|
||||
cv2 already accepts UTF-8 paths, so the wrappers are behavior-neutral there.
|
||||
|
||||
cv2/numpy are imported lazily inside the functions so importing this module
|
||||
stays cheap in a bare environment (matching the rest of the package).
|
||||
"""
|
||||
|
||||
# cv2 ships no type stubs; mirror the pragma used by the other cv2-using modules.
|
||||
# pyright: reportMissingTypeStubs=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
def imread(path: str | Path, flags: int | None = None) -> NDArray[Any] | None:
|
||||
"""Unicode-safe ``cv2.imread``.
|
||||
|
||||
``flags`` defaults to ``cv2.IMREAD_COLOR`` (same as ``cv2.imread``). Returns
|
||||
``None`` when the file is missing or cannot be decoded, matching
|
||||
``cv2.imread`` semantics so existing ``if img is None`` checks keep working.
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
if flags is None:
|
||||
flags = cv2.IMREAD_COLOR
|
||||
try:
|
||||
data = np.fromfile(str(path), dtype=np.uint8)
|
||||
except OSError:
|
||||
return None
|
||||
if data.size == 0:
|
||||
return None
|
||||
return cv2.imdecode(data, flags)
|
||||
|
||||
|
||||
def imwrite(path: str | Path, img: NDArray[Any]) -> bool:
|
||||
"""Unicode-safe ``cv2.imwrite``.
|
||||
|
||||
The output format is taken from the path extension (e.g. ``.png``), exactly
|
||||
like ``cv2.imwrite``. Returns ``True`` on success, ``False`` if the codec
|
||||
rejects the image.
|
||||
"""
|
||||
import cv2
|
||||
|
||||
ext = Path(path).suffix or ".png"
|
||||
ok, buf = cv2.imencode(ext, img)
|
||||
if not ok:
|
||||
return False
|
||||
buf.tofile(str(path))
|
||||
return True
|
||||
@@ -217,7 +217,9 @@ class InvisibleEngine:
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
out_cv = cv2.imread(str(out_path), cv2.IMREAD_COLOR)
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
out_cv = image_io.imread(out_path, cv2.IMREAD_COLOR)
|
||||
|
||||
if protect_faces and original_faces:
|
||||
if self._progress_callback:
|
||||
@@ -243,20 +245,22 @@ class InvisibleEngine:
|
||||
# Using INTER_LANCZOS4 for high-quality upscaling back to original
|
||||
out_cv = cv2.resize(out_cv, orig_size, interpolation=cv2.INTER_LANCZOS4)
|
||||
|
||||
cv2.imwrite(str(out_path), out_cv)
|
||||
image_io.imwrite(out_path, out_cv)
|
||||
|
||||
else:
|
||||
# Even if no protect_faces or humanize, we must restore original size if needed
|
||||
import cv2
|
||||
|
||||
out_cv = cv2.imread(str(out_path), cv2.IMREAD_COLOR)
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
out_cv = image_io.imread(out_path, cv2.IMREAD_COLOR)
|
||||
if out_cv is not None and (out_cv.shape[1], out_cv.shape[0]) != orig_size:
|
||||
if self._progress_callback:
|
||||
self._progress_callback(
|
||||
f"Upscaling result back to original resolution {orig_size[0]}x{orig_size[1]}..."
|
||||
)
|
||||
out_cv = cv2.resize(out_cv, orig_size, interpolation=cv2.INTER_LANCZOS4)
|
||||
cv2.imwrite(str(out_path), out_cv)
|
||||
image_io.imwrite(out_path, out_cv)
|
||||
|
||||
return out_path
|
||||
finally:
|
||||
|
||||
@@ -78,10 +78,11 @@ def detect_invisible_watermark(image_path: Path) -> str | None:
|
||||
"""
|
||||
if not is_available():
|
||||
return None
|
||||
import cv2
|
||||
from imwatermark import WatermarkDecoder
|
||||
|
||||
img = cv2.imread(str(image_path))
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
img = image_io.imread(image_path)
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user