From eb1f65ae45501c12abf8cdd6e6112c3a125017f4 Mon Sep 17 00:00:00 2001 From: test-user Date: Sun, 26 Apr 2026 12:17:25 -0700 Subject: [PATCH] fix(gemini): no-op remove_watermark when nothing detected Reverse alpha blending applied at the assumed default position painted a visible inverse-sparkle artifact onto clean or edited images. The function now returns an unmodified copy when detection fails, instead of falling back to the hardcoded Gemini corner. Bump to 0.3.5. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- src/remove_ai_watermarks/gemini_engine.py | 46 +++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a5e15aa..95f93b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "remove-ai-watermarks" -version = "0.3.4" +version = "0.3.5" description = "Remove visible and invisible AI watermarks from images (Gemini / Nano Banana, ChatGPT, Stable Diffusion)" readme = "README.md" requires-python = ">=3.10" diff --git a/src/remove_ai_watermarks/gemini_engine.py b/src/remove_ai_watermarks/gemini_engine.py index e85878c..fafb9ce 100644 --- a/src/remove_ai_watermarks/gemini_engine.py +++ b/src/remove_ai_watermarks/gemini_engine.py @@ -309,12 +309,19 @@ class GeminiEngine: ) -> NDArray: """Remove Gemini visible watermark from an image using reverse alpha blending. + No-op when the detector does not find a watermark: returns an unmodified + copy. Reverse alpha blending applied where no sparkle exists creates a + visible inverse artifact, so we refuse to touch pixels without a positive + detection. To bypass detection (e.g. you know the exact region), use + ``remove_watermark_custom``. + Args: image: BGR image as numpy array (will NOT be modified in-place). force_size: Force a specific watermark size (auto-detect if None). Returns: - Cleaned BGR image as numpy array. + Cleaned BGR image as numpy array, or an unmodified copy when no + watermark is detected. """ result = image.copy() @@ -324,35 +331,28 @@ class GeminiEngine: elif result.shape[2] == 1: result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR) - h, w = result.shape[:2] - size = force_size or get_watermark_size(w, h) + size = force_size or get_watermark_size(result.shape[1], result.shape[0]) # Detect dynamic position & size detection = self.detect_watermark(image, force_size=size) - # If the confidence is really high (>0.15), use the dynamically detected position and size. - if detection.confidence > 0.15: - pos = (detection.region[0], detection.region[1]) - alpha_map = self.get_interpolated_alpha(detection.region[2]) + if not detection.detected: logger.debug( - "Using dynamic watermark position (%d, %d) at size %dx%d [conf=%.3f]", - pos[0], - pos[1], - detection.region[2], - detection.region[3], + "No watermark detected (conf=%.3f); returning image unchanged.", detection.confidence, ) - else: - config = get_watermark_config(w, h) - pos = config.get_position(w, h) - alpha_map = self.get_alpha_map(size) - logger.debug( - "Dynamic search failed. Using fallback default position (%d, %d) with %dx%d alpha map.", - pos[0], - pos[1], - alpha_map.shape[1], - alpha_map.shape[0], - ) + return result + + pos = (detection.region[0], detection.region[1]) + alpha_map = self.get_interpolated_alpha(detection.region[2]) + logger.debug( + "Removing watermark at (%d, %d) size %dx%d [conf=%.3f]", + pos[0], + pos[1], + detection.region[2], + detection.region[3], + detection.confidence, + ) self._reverse_alpha_blend(result, alpha_map, pos) return result