| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881 |
- """
- Gemini Watermark Removal Tool
- Removes visible Gemini watermarks using reverse alpha blending.
- Based on the mathematical formula:
- watermarked = α × logo + (1 - α) × original
-
- Solving for original:
- original = (watermarked - α × logo) / (1 - α)
- Python port with embedded alpha maps.
- Usage:
- python GWMRTool.py image.jpg
- python GWMRTool.py -i input.jpg -o output.jpg
- python GWMRTool.py -i ./input_folder/ -o ./output_folder/
- """
- import numpy as np
- from PIL import Image
- import argparse
- from pathlib import Path
- import sys
- import os
- from typing import Tuple, Optional
- # ============================================================================
- # EMBEDDED ALPHA MAPS
- # ============================================================================
- # Default paths for alpha maps (relative to script location)
- SCRIPT_DIR = Path(__file__).parent.resolve()
- DEFAULT_ALPHA_MAP_SMALL_PATH = SCRIPT_DIR / "bg_48.png"
- DEFAULT_ALPHA_MAP_LARGE_PATH = SCRIPT_DIR / "bg_96.png"
- class AlphaMapManager:
- """
- Manages loading and caching of alpha maps.
- """
-
- _cache = {}
-
- @classmethod
- def load_alpha_map(cls, path: Path, expected_size: int) -> np.ndarray:
- """
- Load alpha map from file with caching.
-
- Args:
- path: Path to alpha map image
- expected_size: Expected size (48 or 96)
-
- Returns:
- Alpha map as float32 array [0, 1]
- """
- cache_key = str(path)
-
- if cache_key in cls._cache:
- return cls._cache[cache_key]
-
- if not path.exists():
- raise FileNotFoundError(f"Alpha map not found: {path}")
-
- # Load image
- img = Image.open(path)
- img_array = np.array(img)
-
- # Calculate alpha map using max of RGB channels
- if len(img_array.shape) == 3:
- if img_array.shape[2] >= 3:
- # Use max of RGB channels for brightness
- gray = np.max(img_array[:, :, :3], axis=2)
- else:
- gray = img_array[:, :, 0]
- else:
- gray = img_array
-
- # Normalize to [0, 1]
- alpha_map = gray.astype(np.float32) / 255.0
-
- # Validate size
- if alpha_map.shape[0] != expected_size or alpha_map.shape[1] != expected_size:
- print(f"Warning: Alpha map size {alpha_map.shape} doesn't match expected {expected_size}x{expected_size}")
- # Resize if needed
- img_resized = Image.fromarray(gray).resize(
- (expected_size, expected_size),
- Image.Resampling.LANCZOS
- )
- alpha_map = np.array(img_resized).astype(np.float32) / 255.0
-
- # Cache and return
- cls._cache[cache_key] = alpha_map
- return alpha_map
-
- @classmethod
- def clear_cache(cls):
- """Clear the alpha map cache."""
- cls._cache.clear()
- # ============================================================================
- # BLEND MODES (Translated from C++ blend_modes.cpp)
- # ============================================================================
- class BlendModes:
- """
- Alpha blending operations for watermark removal/addition.
- Direct translation from C++ blend_modes.cpp
- """
-
- # Constants from C++ code
- ALPHA_THRESHOLD = 0.002 # Ignore very small alpha (noise)
- MAX_ALPHA = 0.99 # Avoid division by near-zero
-
- @staticmethod
- def calculate_alpha_map(bg_capture: np.ndarray) -> np.ndarray:
- """
- Calculate alpha map from background capture.
- Uses max of RGB channels for brightness (handles colored logos).
-
- Args:
- bg_capture: Background capture image (grayscale or RGB/RGBA)
-
- Returns:
- Alpha map as float32 array normalized to [0, 1]
- """
- if bg_capture is None or bg_capture.size == 0:
- raise ValueError("Empty background capture")
-
- # Handle different channel configurations
- if len(bg_capture.shape) == 3:
- if bg_capture.shape[2] >= 3:
- # RGB/RGBA: Use max of RGB channels for brightness
- gray = np.max(bg_capture[:, :, :3], axis=2)
- else:
- gray = bg_capture[:, :, 0]
- else:
- # Already grayscale
- gray = bg_capture.copy()
-
- # Convert to float and normalize to [0, 1]
- alpha_map = gray.astype(np.float32) / 255.0
-
- return alpha_map
-
- @staticmethod
- def remove_watermark_alpha_blend(
- image: np.ndarray,
- alpha_map: np.ndarray,
- position: Tuple[int, int],
- logo_value: float = 255.0
- ) -> np.ndarray:
- """
- Remove watermark using reverse alpha blending (vectorized).
-
- Gemini applies: watermarked = alpha * logo + (1 - alpha) * original
- We reverse: original = (watermarked - alpha * logo) / (1 - alpha)
-
- Args:
- image: Input image (RGB, uint8)
- alpha_map: Alpha map (float32, normalized to [0, 1])
- position: (x, y) position of watermark top-left corner
- logo_value: Logo pixel value (default 255 for white logo)
-
- Returns:
- Image with watermark removed
- """
- if image is None or image.size == 0:
- raise ValueError("Empty image")
- if alpha_map is None or alpha_map.size == 0:
- raise ValueError("Empty alpha map")
- if len(image.shape) != 3 or image.shape[2] != 3:
- raise ValueError("Image must be RGB (3 channels)")
- if alpha_map.dtype != np.float32:
- alpha_map = alpha_map.astype(np.float32)
-
- # Make a copy to avoid modifying original
- result = image.copy()
-
- # Calculate region of interest
- x, y = position
- h, w = alpha_map.shape[:2]
-
- # Clip to image bounds
- x1 = max(0, x)
- y1 = max(0, y)
- x2 = min(image.shape[1], x + w)
- y2 = min(image.shape[0], y + h)
-
- if x1 >= x2 or y1 >= y2:
- return result
-
- # Calculate ROI offsets
- alpha_x1 = x1 - x
- alpha_y1 = y1 - y
- alpha_x2 = alpha_x1 + (x2 - x1)
- alpha_y2 = alpha_y1 + (y2 - y1)
-
- # Get ROIs
- alpha_region = alpha_map[alpha_y1:alpha_y2, alpha_x1:alpha_x2]
- image_region = result[y1:y2, x1:x2]
-
- # Convert image region to float
- image_f = image_region.astype(np.float32)
-
- # Create mask for pixels to process (alpha >= threshold)
- process_mask = alpha_region >= BlendModes.ALPHA_THRESHOLD
-
- # Clamp alpha values
- alpha_clamped = np.minimum(alpha_region, BlendModes.MAX_ALPHA)
- one_minus_alpha = 1.0 - alpha_clamped
-
- # Avoid division by zero
- one_minus_alpha = np.maximum(one_minus_alpha, 1e-6)
-
- # Expand alpha to 3 channels for broadcasting
- alpha_3c = alpha_clamped[:, :, np.newaxis]
- one_minus_alpha_3c = one_minus_alpha[:, :, np.newaxis]
- process_mask_3c = process_mask[:, :, np.newaxis]
-
- # Reverse alpha blending formula:
- # original = (watermarked - alpha * logo) / (1 - alpha)
- original = (image_f - alpha_3c * logo_value) / one_minus_alpha_3c
- original = np.clip(original, 0.0, 255.0)
-
- # Only apply to pixels that need processing
- image_f = np.where(process_mask_3c, original, image_f)
-
- # Convert back to 8-bit
- result[y1:y2, x1:x2] = image_f.astype(np.uint8)
-
- return result
-
- @staticmethod
- def add_watermark_alpha_blend(
- image: np.ndarray,
- alpha_map: np.ndarray,
- position: Tuple[int, int],
- logo_value: float = 255.0
- ) -> np.ndarray:
- """
- Add watermark using alpha blending (same as Gemini).
-
- Formula: result = alpha * logo + (1 - alpha) * original
-
- Args:
- image: Input image (RGB, uint8)
- alpha_map: Alpha map (float32, normalized to [0, 1])
- position: (x, y) position of watermark top-left corner
- logo_value: Logo pixel value (default 255 for white logo)
-
- Returns:
- Image with watermark added
- """
- if image is None or image.size == 0:
- raise ValueError("Empty image")
- if alpha_map is None or alpha_map.size == 0:
- raise ValueError("Empty alpha map")
- if len(image.shape) != 3 or image.shape[2] != 3:
- raise ValueError("Image must be RGB (3 channels)")
- if alpha_map.dtype != np.float32:
- alpha_map = alpha_map.astype(np.float32)
-
- # Make a copy to avoid modifying original
- result = image.copy()
-
- # Calculate region of interest
- x, y = position
- h, w = alpha_map.shape[:2]
-
- # Clip to image bounds
- x1 = max(0, x)
- y1 = max(0, y)
- x2 = min(image.shape[1], x + w)
- y2 = min(image.shape[0], y + h)
-
- if x1 >= x2 or y1 >= y2:
- return result
-
- # Calculate ROI offsets
- alpha_x1 = x1 - x
- alpha_y1 = y1 - y
- alpha_x2 = alpha_x1 + (x2 - x1)
- alpha_y2 = alpha_y1 + (y2 - y1)
-
- # Get ROIs
- alpha_region = alpha_map[alpha_y1:alpha_y2, alpha_x1:alpha_x2]
- image_region = result[y1:y2, x1:x2]
-
- # Convert image region to float
- image_f = image_region.astype(np.float32)
-
- # Create mask for pixels to process
- process_mask = alpha_region >= BlendModes.ALPHA_THRESHOLD
-
- # Expand alpha to 3 channels for broadcasting
- alpha_3c = alpha_region[:, :, np.newaxis]
- one_minus_alpha_3c = (1.0 - alpha_region)[:, :, np.newaxis]
- process_mask_3c = process_mask[:, :, np.newaxis]
-
- # Alpha blending formula:
- # result = alpha * logo + (1 - alpha) * original
- blended = alpha_3c * logo_value + one_minus_alpha_3c * image_f
- blended = np.clip(blended, 0.0, 255.0)
-
- # Only apply to pixels that need processing
- image_f = np.where(process_mask_3c, blended, image_f)
-
- # Convert back to 8-bit
- result[y1:y2, x1:x2] = image_f.astype(np.uint8)
-
- return result
- # ============================================================================
- # WATERMARK REMOVER
- # ============================================================================
- class GeminiWatermarkRemover:
- """
- Removes Gemini watermarks using reverse alpha blending.
- Uses bg_48.png and bg_96.png alpha maps by default.
- """
-
- # Watermark size configurations
- SMALL_SIZE = 48
- LARGE_SIZE = 96
- SMALL_MARGIN = 32
- LARGE_MARGIN = 64
- SIZE_THRESHOLD = 1024
-
- # Default logo value (white)
- DEFAULT_LOGO_VALUE = 255.0
-
- def __init__(self,
- alpha_map_small_path: Path = None,
- alpha_map_large_path: Path = None,
- logo_value: float = DEFAULT_LOGO_VALUE):
- """
- Initialize the watermark remover.
-
- Args:
- alpha_map_small_path: Path to 48x48 alpha map (default: bg_48.png)
- alpha_map_large_path: Path to 96x96 alpha map (default: bg_96.png)
- logo_value: Logo pixel value (default 255 for white)
- """
- # Use default paths if not specified
- self.alpha_map_small_path = alpha_map_small_path or DEFAULT_ALPHA_MAP_SMALL_PATH
- self.alpha_map_large_path = alpha_map_large_path or DEFAULT_ALPHA_MAP_LARGE_PATH
- self.logo_value = logo_value
-
- # Lazy-loaded alpha maps
- self._alpha_map_small = None
- self._alpha_map_large = None
-
- @property
- def alpha_map_small(self) -> np.ndarray:
- """Lazy-load small alpha map."""
- if self._alpha_map_small is None:
- self._alpha_map_small = AlphaMapManager.load_alpha_map(
- Path(self.alpha_map_small_path),
- self.SMALL_SIZE
- )
- return self._alpha_map_small
-
- @property
- def alpha_map_large(self) -> np.ndarray:
- """Lazy-load large alpha map."""
- if self._alpha_map_large is None:
- self._alpha_map_large = AlphaMapManager.load_alpha_map(
- Path(self.alpha_map_large_path),
- self.LARGE_SIZE
- )
- return self._alpha_map_large
-
- def detect_watermark_size(self, image: np.ndarray) -> Tuple[int, int]:
- """
- Auto-detect watermark size based on image dimensions.
-
- Args:
- image: Input image as numpy array
-
- Returns:
- Tuple of (size, margin) for watermark
- """
- height, width = image.shape[:2]
-
- if width <= self.SIZE_THRESHOLD or height <= self.SIZE_THRESHOLD:
- return self.SMALL_SIZE, self.SMALL_MARGIN
- else:
- return self.LARGE_SIZE, self.LARGE_MARGIN
-
- def get_watermark_position(self, image: np.ndarray,
- size: int, margin: int) -> Tuple[int, int]:
- """
- Get the (x, y) position of the watermark top-left corner.
- Watermark is in bottom-right corner.
-
- Args:
- image: Input image
- size: Watermark size (48 or 96)
- margin: Margin from edge
-
- Returns:
- Tuple of (x, y) position
- """
- height, width = image.shape[:2]
-
- x = width - margin - size
- y = height - margin - size
-
- return x, y
-
- def remove_watermark(self, image: np.ndarray,
- force_size: str = None,
- verbose: bool = False) -> np.ndarray:
- """
- Remove watermark from image.
-
- Args:
- image: Input image as numpy array (RGB)
- force_size: Force 'small' (48x48) or 'large' (96x96)
- verbose: Print debug information
-
- Returns:
- Image with watermark removed
- """
- # Ensure RGB
- if len(image.shape) == 2:
- # Grayscale - convert to RGB
- image = np.stack([image] * 3, axis=-1)
- elif len(image.shape) == 3 and image.shape[2] == 4:
- # RGBA - drop alpha
- image = image[:, :, :3]
- elif len(image.shape) != 3 or image.shape[2] != 3:
- raise ValueError("Image must be RGB or RGBA")
-
- # Detect or force watermark size
- if force_size == 'small':
- size, margin = self.SMALL_SIZE, self.SMALL_MARGIN
- elif force_size == 'large':
- size, margin = self.LARGE_SIZE, self.LARGE_MARGIN
- else:
- size, margin = self.detect_watermark_size(image)
-
- if verbose:
- print(f" Watermark size: {size}x{size}, margin: {margin}px")
-
- # Get watermark position
- x, y = self.get_watermark_position(image, size, margin)
-
- if verbose:
- print(f" Watermark position: ({x}, {y})")
-
- # Validate position
- if x < 0 or y < 0:
- if verbose:
- print(f" Warning: Image too small for {size}x{size} watermark")
- return image.copy()
-
- # Get appropriate alpha map
- if size == self.SMALL_SIZE:
- alpha_map = self.alpha_map_small
- else:
- alpha_map = self.alpha_map_large
-
- # Remove watermark
- result = BlendModes.remove_watermark_alpha_blend(
- image=image,
- alpha_map=alpha_map,
- position=(x, y),
- logo_value=self.logo_value
- )
-
- return result
-
- def add_watermark(self, image: np.ndarray,
- force_size: str = None,
- verbose: bool = False) -> np.ndarray:
- """
- Add watermark to image (for testing purposes).
-
- Args:
- image: Input image as numpy array (RGB)
- force_size: Force 'small' (48x48) or 'large' (96x96)
- verbose: Print debug information
-
- Returns:
- Image with watermark added
- """
- # Ensure RGB
- if len(image.shape) == 2:
- image = np.stack([image] * 3, axis=-1)
- elif len(image.shape) == 3 and image.shape[2] == 4:
- image = image[:, :, :3]
- elif len(image.shape) != 3 or image.shape[2] != 3:
- raise ValueError("Image must be RGB or RGBA")
-
- # Detect or force watermark size
- if force_size == 'small':
- size, margin = self.SMALL_SIZE, self.SMALL_MARGIN
- elif force_size == 'large':
- size, margin = self.LARGE_SIZE, self.LARGE_MARGIN
- else:
- size, margin = self.detect_watermark_size(image)
-
- if verbose:
- print(f" Watermark size: {size}x{size}, margin: {margin}px")
-
- # Get watermark position
- x, y = self.get_watermark_position(image, size, margin)
-
- # Validate position
- if x < 0 or y < 0:
- if verbose:
- print(f" Warning: Image too small for {size}x{size} watermark")
- return image.copy()
-
- # Get appropriate alpha map
- if size == self.SMALL_SIZE:
- alpha_map = self.alpha_map_small
- else:
- alpha_map = self.alpha_map_large
-
- # Add watermark
- result = BlendModes.add_watermark_alpha_blend(
- image=image,
- alpha_map=alpha_map,
- position=(x, y),
- logo_value=self.logo_value
- )
-
- return result
- # ============================================================================
- # FILE OPERATIONS
- # ============================================================================
- def load_image(path: str) -> np.ndarray:
- """Load image as RGB numpy array."""
- img = Image.open(path).convert('RGB')
- return np.array(img)
- def save_image(image: np.ndarray, path: str, quality: int = 95):
- """Save numpy array as image."""
- img = Image.fromarray(image)
-
- # Determine format from extension
- ext = Path(path).suffix.lower()
-
- if ext in ['.jpg', '.jpeg']:
- img.save(path, 'JPEG', quality=quality)
- elif ext == '.png':
- img.save(path, 'PNG')
- elif ext == '.webp':
- img.save(path, 'WEBP', quality=quality)
- elif ext == '.bmp':
- img.save(path, 'BMP')
- else:
- img.save(path)
- def process_single_file(input_path: str,
- output_path: str = None,
- remover: GeminiWatermarkRemover = None,
- force_size: str = None,
- verbose: bool = False,
- add_mode: bool = False):
- """
- Process a single image file.
-
- Args:
- input_path: Path to input image
- output_path: Path for output (None = overwrite input)
- remover: Watermark remover instance
- force_size: Force 'small' or 'large'
- verbose: Print verbose output
- add_mode: Add watermark instead of removing
- """
- if output_path is None:
- output_path = input_path
-
- if verbose:
- print(f"Processing: {input_path}")
-
- # Load image
- image = load_image(input_path)
-
- if verbose:
- print(f" Image size: {image.shape[1]}x{image.shape[0]}")
-
- # Process
- if add_mode:
- result = remover.add_watermark(image, force_size, verbose)
- action = "Added watermark"
- else:
- result = remover.remove_watermark(image, force_size, verbose)
- action = "Removed watermark"
-
- # Save result
- save_image(result, output_path)
-
- if verbose:
- print(f" {action}, saved to: {output_path}")
- def process_directory(input_dir: str,
- output_dir: str,
- remover: GeminiWatermarkRemover = None,
- force_size: str = None,
- verbose: bool = False,
- add_mode: bool = False):
- """
- Process all images in a directory.
-
- Args:
- input_dir: Input directory path
- output_dir: Output directory path
- remover: Watermark remover instance
- force_size: Force 'small' or 'large'
- verbose: Print verbose output
- add_mode: Add watermark instead of removing
- """
- input_path = Path(input_dir)
- output_path = Path(output_dir)
-
- # Create output directory
- output_path.mkdir(parents=True, exist_ok=True)
-
- # Supported formats
- formats = {'.jpg', '.jpeg', '.png', '.webp', '.bmp'}
-
- # Find all images
- images = [f for f in input_path.iterdir()
- if f.is_file() and f.suffix.lower() in formats]
-
- total = len(images)
- if verbose:
- print(f"Found {total} images to process")
-
- processed = 0
- errors = 0
-
- for idx, img_path in enumerate(images, 1):
- out_file = output_path / img_path.name
- try:
- if verbose:
- print(f"\n[{idx}/{total}] ", end="")
- process_single_file(
- str(img_path), str(out_file),
- remover, force_size, verbose, add_mode
- )
- processed += 1
- except Exception as e:
- print(f"Error processing {img_path.name}: {e}")
- errors += 1
-
- print(f"\nCompleted: {processed} processed, {errors} errors")
- # ============================================================================
- # CLI
- # ============================================================================
- def print_banner():
- """Print ASCII banner."""
- banner = r"""
- ╔═══════════════════════════════════════════════════════════════════╗
- ║ ║
- ║ ██████╗ ███████╗███╗ ███╗██╗███╗ ██╗██╗ ║
- ║ ██╔════╝ ██╔════╝████╗ ████║██║████╗ ██║██║ ║
- ║ ██║ ███╗█████╗ ██╔████╔██║██║██╔██╗ ██║██║ ║
- ║ ██║ ██║██╔══╝ ██║╚██╔╝██║██║██║╚██╗██║██║ ║
- ║ ╚██████╔╝███████╗██║ ╚═╝ ██║██║██║ ╚████║██║ ║
- ║ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ║
- ║ ║
- ║ Watermark Removal Tool (Python Edition) ║
- ║ Reverse Alpha Blending Technology ║
- ║ ║
- ╚═══════════════════════════════════════════════════════════════════╝
- """
- print(banner)
- def check_alpha_maps():
- """Check if alpha map files exist and print status."""
- small_exists = DEFAULT_ALPHA_MAP_SMALL_PATH.exists()
- large_exists = DEFAULT_ALPHA_MAP_LARGE_PATH.exists()
-
- print("Alpha Map Status:")
- print(f" bg_48.png (48x48): {'✓ Found' if small_exists else '✗ Not found'} - {DEFAULT_ALPHA_MAP_SMALL_PATH}")
- print(f" bg_96.png (96x96): {'✓ Found' if large_exists else '✗ Not found'} - {DEFAULT_ALPHA_MAP_LARGE_PATH}")
- print()
-
- if not small_exists or not large_exists:
- print("WARNING: Missing alpha map files!")
- print("Please ensure bg_48.png and bg_96.png are in the same directory as this script.")
- print()
- return False
-
- return True
- def main():
- """Main entry point."""
- parser = argparse.ArgumentParser(
- description='Remove Gemini watermarks using reverse alpha blending',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog='''
- Examples:
- # Simple mode - edit in place
- python GWMRTool.py image.jpg
-
- # Specify output file
- python GWMRTool.py -i input.jpg -o output.jpg
-
- # Batch processing
- python GWMRTool.py -i ./input_folder/ -o ./output_folder/
-
- # Force specific watermark size
- python GWMRTool.py -i image.jpg -o output.jpg --force-large
-
- # Add watermark (for testing)
- python GWMRTool.py -i clean.jpg -o watermarked.jpg --add
- Required Files:
- bg_48.png - 48x48 alpha map (for images <= 1024px)
- bg_96.png - 96x96 alpha map (for images > 1024px)
-
- Place these files in the same directory as this script.
- Mathematical Formula:
- Watermark Application: watermarked = α × logo + (1 - α) × original
- Reverse (Removal): original = (watermarked - α × logo) / (1 - α)
- '''
- )
-
- parser.add_argument('simple_input', nargs='?',
- help='Image file (simple mode - edits in place)')
- parser.add_argument('-i', '--input',
- help='Input image file or directory')
- parser.add_argument('-o', '--output',
- help='Output image file or directory')
- parser.add_argument('--force-small', action='store_true',
- help='Force 48x48 watermark size')
- parser.add_argument('--force-large', action='store_true',
- help='Force 96x96 watermark size')
- parser.add_argument('-v', '--verbose', action='store_true',
- help='Enable verbose output')
- parser.add_argument('-q', '--quiet', action='store_true',
- help='Suppress all output except errors')
- parser.add_argument('-b', '--banner', action='store_true',
- help='Show ASCII banner')
- parser.add_argument('--add', action='store_true',
- help='Add watermark instead of removing (for testing)')
- parser.add_argument('--alpha-small',
- help='Path to custom 48x48 alpha map')
- parser.add_argument('--alpha-large',
- help='Path to custom 96x96 alpha map')
- parser.add_argument('--logo-value', type=float, default=255.0,
- help='Logo pixel value (default: 255 for white)')
- parser.add_argument('--check', action='store_true',
- help='Check alpha map files and exit')
- parser.add_argument('--version', action='version',
- version='Gemini Watermark Remover v1.0.0 (Python)')
-
- args = parser.parse_args()
-
- # Show banner
- if args.banner and not args.quiet:
- print_banner()
-
- # Check mode
- if args.check:
- check_alpha_maps()
- sys.exit(0)
-
- # Verbose mode (unless quiet)
- verbose = args.verbose and not args.quiet
-
- # Determine force size
- force_size = None
- if args.force_small:
- force_size = 'small'
- elif args.force_large:
- force_size = 'large'
-
- # Custom alpha map paths
- alpha_small_path = Path(args.alpha_small) if args.alpha_small else None
- alpha_large_path = Path(args.alpha_large) if args.alpha_large else None
-
- # Check default alpha maps exist (if not using custom)
- if alpha_small_path is None and not DEFAULT_ALPHA_MAP_SMALL_PATH.exists():
- print(f"Error: Alpha map not found: {DEFAULT_ALPHA_MAP_SMALL_PATH}")
- print("Please ensure bg_48.png is in the same directory as this script.")
- print("Or specify a custom path with --alpha-small")
- sys.exit(1)
-
- if alpha_large_path is None and not DEFAULT_ALPHA_MAP_LARGE_PATH.exists():
- print(f"Error: Alpha map not found: {DEFAULT_ALPHA_MAP_LARGE_PATH}")
- print("Please ensure bg_96.png is in the same directory as this script.")
- print("Or specify a custom path with --alpha-large")
- sys.exit(1)
-
- # Initialize remover
- try:
- remover = GeminiWatermarkRemover(
- alpha_map_small_path=alpha_small_path,
- alpha_map_large_path=alpha_large_path,
- logo_value=args.logo_value
- )
- except Exception as e:
- print(f"Error initializing: {e}")
- sys.exit(1)
-
- # Determine input/output
- if args.simple_input:
- # Simple mode - edit in place
- if not Path(args.simple_input).exists():
- print(f"Error: File not found: {args.simple_input}")
- sys.exit(1)
-
- try:
- process_single_file(
- args.simple_input, None, remover,
- force_size, verbose, args.add
- )
- except Exception as e:
- print(f"Error: {e}")
- sys.exit(1)
-
- elif args.input:
- input_path = Path(args.input)
-
- if not input_path.exists():
- print(f"Error: Input not found: {args.input}")
- sys.exit(1)
-
- if input_path.is_dir():
- # Directory mode
- if not args.output:
- print("Error: Output directory required for batch processing (-o)")
- sys.exit(1)
- try:
- process_directory(
- args.input, args.output, remover,
- force_size, verbose, args.add
- )
- except Exception as e:
- print(f"Error: {e}")
- sys.exit(1)
- else:
- # Single file mode
- output = args.output if args.output else args.input
- try:
- process_single_file(
- args.input, output, remover,
- force_size, verbose, args.add
- )
- except Exception as e:
- print(f"Error: {e}")
- sys.exit(1)
- else:
- # No input provided - show help
- parser.print_help()
- print("\n" + "="*60)
- check_alpha_maps()
- sys.exit(0)
-
- if not args.quiet:
- print("Done!")
- # ============================================================================
- # ENTRY POINT
- # ============================================================================
- if __name__ == '__main__':
- main()
|