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) <noreply@anthropic.com>
This commit is contained in:
test-user
2026-04-26 12:17:25 -07:00
parent 51105db3b1
commit eb1f65ae45
2 changed files with 24 additions and 24 deletions
+1 -1
View File
@@ -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"
+23 -23
View File
@@ -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