GWMRTool.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. """
  2. Gemini Watermark Removal Tool
  3. Removes visible Gemini watermarks using reverse alpha blending.
  4. Based on the mathematical formula:
  5. watermarked = α × logo + (1 - α) × original
  6. Solving for original:
  7. original = (watermarked - α × logo) / (1 - α)
  8. Python port with embedded alpha maps.
  9. Usage:
  10. python GWMRTool.py image.jpg
  11. python GWMRTool.py -i input.jpg -o output.jpg
  12. python GWMRTool.py -i ./input_folder/ -o ./output_folder/
  13. """
  14. import numpy as np
  15. from PIL import Image
  16. import argparse
  17. from pathlib import Path
  18. import sys
  19. import os
  20. from typing import Tuple, Optional
  21. # ============================================================================
  22. # EMBEDDED ALPHA MAPS
  23. # ============================================================================
  24. # Default paths for alpha maps (relative to script location)
  25. SCRIPT_DIR = Path(__file__).parent.resolve()
  26. DEFAULT_ALPHA_MAP_SMALL_PATH = SCRIPT_DIR / "bg_48.png"
  27. DEFAULT_ALPHA_MAP_LARGE_PATH = SCRIPT_DIR / "bg_96.png"
  28. class AlphaMapManager:
  29. """
  30. Manages loading and caching of alpha maps.
  31. """
  32. _cache = {}
  33. @classmethod
  34. def load_alpha_map(cls, path: Path, expected_size: int) -> np.ndarray:
  35. """
  36. Load alpha map from file with caching.
  37. Args:
  38. path: Path to alpha map image
  39. expected_size: Expected size (48 or 96)
  40. Returns:
  41. Alpha map as float32 array [0, 1]
  42. """
  43. cache_key = str(path)
  44. if cache_key in cls._cache:
  45. return cls._cache[cache_key]
  46. if not path.exists():
  47. raise FileNotFoundError(f"Alpha map not found: {path}")
  48. # Load image
  49. img = Image.open(path)
  50. img_array = np.array(img)
  51. # Calculate alpha map using max of RGB channels
  52. if len(img_array.shape) == 3:
  53. if img_array.shape[2] >= 3:
  54. # Use max of RGB channels for brightness
  55. gray = np.max(img_array[:, :, :3], axis=2)
  56. else:
  57. gray = img_array[:, :, 0]
  58. else:
  59. gray = img_array
  60. # Normalize to [0, 1]
  61. alpha_map = gray.astype(np.float32) / 255.0
  62. # Validate size
  63. if alpha_map.shape[0] != expected_size or alpha_map.shape[1] != expected_size:
  64. print(f"Warning: Alpha map size {alpha_map.shape} doesn't match expected {expected_size}x{expected_size}")
  65. # Resize if needed
  66. img_resized = Image.fromarray(gray).resize(
  67. (expected_size, expected_size),
  68. Image.Resampling.LANCZOS
  69. )
  70. alpha_map = np.array(img_resized).astype(np.float32) / 255.0
  71. # Cache and return
  72. cls._cache[cache_key] = alpha_map
  73. return alpha_map
  74. @classmethod
  75. def clear_cache(cls):
  76. """Clear the alpha map cache."""
  77. cls._cache.clear()
  78. # ============================================================================
  79. # BLEND MODES (Translated from C++ blend_modes.cpp)
  80. # ============================================================================
  81. class BlendModes:
  82. """
  83. Alpha blending operations for watermark removal/addition.
  84. Direct translation from C++ blend_modes.cpp
  85. """
  86. # Constants from C++ code
  87. ALPHA_THRESHOLD = 0.002 # Ignore very small alpha (noise)
  88. MAX_ALPHA = 0.99 # Avoid division by near-zero
  89. @staticmethod
  90. def calculate_alpha_map(bg_capture: np.ndarray) -> np.ndarray:
  91. """
  92. Calculate alpha map from background capture.
  93. Uses max of RGB channels for brightness (handles colored logos).
  94. Args:
  95. bg_capture: Background capture image (grayscale or RGB/RGBA)
  96. Returns:
  97. Alpha map as float32 array normalized to [0, 1]
  98. """
  99. if bg_capture is None or bg_capture.size == 0:
  100. raise ValueError("Empty background capture")
  101. # Handle different channel configurations
  102. if len(bg_capture.shape) == 3:
  103. if bg_capture.shape[2] >= 3:
  104. # RGB/RGBA: Use max of RGB channels for brightness
  105. gray = np.max(bg_capture[:, :, :3], axis=2)
  106. else:
  107. gray = bg_capture[:, :, 0]
  108. else:
  109. # Already grayscale
  110. gray = bg_capture.copy()
  111. # Convert to float and normalize to [0, 1]
  112. alpha_map = gray.astype(np.float32) / 255.0
  113. return alpha_map
  114. @staticmethod
  115. def remove_watermark_alpha_blend(
  116. image: np.ndarray,
  117. alpha_map: np.ndarray,
  118. position: Tuple[int, int],
  119. logo_value: float = 255.0
  120. ) -> np.ndarray:
  121. """
  122. Remove watermark using reverse alpha blending (vectorized).
  123. Gemini applies: watermarked = alpha * logo + (1 - alpha) * original
  124. We reverse: original = (watermarked - alpha * logo) / (1 - alpha)
  125. Args:
  126. image: Input image (RGB, uint8)
  127. alpha_map: Alpha map (float32, normalized to [0, 1])
  128. position: (x, y) position of watermark top-left corner
  129. logo_value: Logo pixel value (default 255 for white logo)
  130. Returns:
  131. Image with watermark removed
  132. """
  133. if image is None or image.size == 0:
  134. raise ValueError("Empty image")
  135. if alpha_map is None or alpha_map.size == 0:
  136. raise ValueError("Empty alpha map")
  137. if len(image.shape) != 3 or image.shape[2] != 3:
  138. raise ValueError("Image must be RGB (3 channels)")
  139. if alpha_map.dtype != np.float32:
  140. alpha_map = alpha_map.astype(np.float32)
  141. # Make a copy to avoid modifying original
  142. result = image.copy()
  143. # Calculate region of interest
  144. x, y = position
  145. h, w = alpha_map.shape[:2]
  146. # Clip to image bounds
  147. x1 = max(0, x)
  148. y1 = max(0, y)
  149. x2 = min(image.shape[1], x + w)
  150. y2 = min(image.shape[0], y + h)
  151. if x1 >= x2 or y1 >= y2:
  152. return result
  153. # Calculate ROI offsets
  154. alpha_x1 = x1 - x
  155. alpha_y1 = y1 - y
  156. alpha_x2 = alpha_x1 + (x2 - x1)
  157. alpha_y2 = alpha_y1 + (y2 - y1)
  158. # Get ROIs
  159. alpha_region = alpha_map[alpha_y1:alpha_y2, alpha_x1:alpha_x2]
  160. image_region = result[y1:y2, x1:x2]
  161. # Convert image region to float
  162. image_f = image_region.astype(np.float32)
  163. # Create mask for pixels to process (alpha >= threshold)
  164. process_mask = alpha_region >= BlendModes.ALPHA_THRESHOLD
  165. # Clamp alpha values
  166. alpha_clamped = np.minimum(alpha_region, BlendModes.MAX_ALPHA)
  167. one_minus_alpha = 1.0 - alpha_clamped
  168. # Avoid division by zero
  169. one_minus_alpha = np.maximum(one_minus_alpha, 1e-6)
  170. # Expand alpha to 3 channels for broadcasting
  171. alpha_3c = alpha_clamped[:, :, np.newaxis]
  172. one_minus_alpha_3c = one_minus_alpha[:, :, np.newaxis]
  173. process_mask_3c = process_mask[:, :, np.newaxis]
  174. # Reverse alpha blending formula:
  175. # original = (watermarked - alpha * logo) / (1 - alpha)
  176. original = (image_f - alpha_3c * logo_value) / one_minus_alpha_3c
  177. original = np.clip(original, 0.0, 255.0)
  178. # Only apply to pixels that need processing
  179. image_f = np.where(process_mask_3c, original, image_f)
  180. # Convert back to 8-bit
  181. result[y1:y2, x1:x2] = image_f.astype(np.uint8)
  182. return result
  183. @staticmethod
  184. def add_watermark_alpha_blend(
  185. image: np.ndarray,
  186. alpha_map: np.ndarray,
  187. position: Tuple[int, int],
  188. logo_value: float = 255.0
  189. ) -> np.ndarray:
  190. """
  191. Add watermark using alpha blending (same as Gemini).
  192. Formula: result = alpha * logo + (1 - alpha) * original
  193. Args:
  194. image: Input image (RGB, uint8)
  195. alpha_map: Alpha map (float32, normalized to [0, 1])
  196. position: (x, y) position of watermark top-left corner
  197. logo_value: Logo pixel value (default 255 for white logo)
  198. Returns:
  199. Image with watermark added
  200. """
  201. if image is None or image.size == 0:
  202. raise ValueError("Empty image")
  203. if alpha_map is None or alpha_map.size == 0:
  204. raise ValueError("Empty alpha map")
  205. if len(image.shape) != 3 or image.shape[2] != 3:
  206. raise ValueError("Image must be RGB (3 channels)")
  207. if alpha_map.dtype != np.float32:
  208. alpha_map = alpha_map.astype(np.float32)
  209. # Make a copy to avoid modifying original
  210. result = image.copy()
  211. # Calculate region of interest
  212. x, y = position
  213. h, w = alpha_map.shape[:2]
  214. # Clip to image bounds
  215. x1 = max(0, x)
  216. y1 = max(0, y)
  217. x2 = min(image.shape[1], x + w)
  218. y2 = min(image.shape[0], y + h)
  219. if x1 >= x2 or y1 >= y2:
  220. return result
  221. # Calculate ROI offsets
  222. alpha_x1 = x1 - x
  223. alpha_y1 = y1 - y
  224. alpha_x2 = alpha_x1 + (x2 - x1)
  225. alpha_y2 = alpha_y1 + (y2 - y1)
  226. # Get ROIs
  227. alpha_region = alpha_map[alpha_y1:alpha_y2, alpha_x1:alpha_x2]
  228. image_region = result[y1:y2, x1:x2]
  229. # Convert image region to float
  230. image_f = image_region.astype(np.float32)
  231. # Create mask for pixels to process
  232. process_mask = alpha_region >= BlendModes.ALPHA_THRESHOLD
  233. # Expand alpha to 3 channels for broadcasting
  234. alpha_3c = alpha_region[:, :, np.newaxis]
  235. one_minus_alpha_3c = (1.0 - alpha_region)[:, :, np.newaxis]
  236. process_mask_3c = process_mask[:, :, np.newaxis]
  237. # Alpha blending formula:
  238. # result = alpha * logo + (1 - alpha) * original
  239. blended = alpha_3c * logo_value + one_minus_alpha_3c * image_f
  240. blended = np.clip(blended, 0.0, 255.0)
  241. # Only apply to pixels that need processing
  242. image_f = np.where(process_mask_3c, blended, image_f)
  243. # Convert back to 8-bit
  244. result[y1:y2, x1:x2] = image_f.astype(np.uint8)
  245. return result
  246. # ============================================================================
  247. # WATERMARK REMOVER
  248. # ============================================================================
  249. class GeminiWatermarkRemover:
  250. """
  251. Removes Gemini watermarks using reverse alpha blending.
  252. Uses bg_48.png and bg_96.png alpha maps by default.
  253. """
  254. # Watermark size configurations
  255. SMALL_SIZE = 48
  256. LARGE_SIZE = 96
  257. SMALL_MARGIN = 32
  258. LARGE_MARGIN = 64
  259. SIZE_THRESHOLD = 1024
  260. # Default logo value (white)
  261. DEFAULT_LOGO_VALUE = 255.0
  262. def __init__(self,
  263. alpha_map_small_path: Path = None,
  264. alpha_map_large_path: Path = None,
  265. logo_value: float = DEFAULT_LOGO_VALUE):
  266. """
  267. Initialize the watermark remover.
  268. Args:
  269. alpha_map_small_path: Path to 48x48 alpha map (default: bg_48.png)
  270. alpha_map_large_path: Path to 96x96 alpha map (default: bg_96.png)
  271. logo_value: Logo pixel value (default 255 for white)
  272. """
  273. # Use default paths if not specified
  274. self.alpha_map_small_path = alpha_map_small_path or DEFAULT_ALPHA_MAP_SMALL_PATH
  275. self.alpha_map_large_path = alpha_map_large_path or DEFAULT_ALPHA_MAP_LARGE_PATH
  276. self.logo_value = logo_value
  277. # Lazy-loaded alpha maps
  278. self._alpha_map_small = None
  279. self._alpha_map_large = None
  280. @property
  281. def alpha_map_small(self) -> np.ndarray:
  282. """Lazy-load small alpha map."""
  283. if self._alpha_map_small is None:
  284. self._alpha_map_small = AlphaMapManager.load_alpha_map(
  285. Path(self.alpha_map_small_path),
  286. self.SMALL_SIZE
  287. )
  288. return self._alpha_map_small
  289. @property
  290. def alpha_map_large(self) -> np.ndarray:
  291. """Lazy-load large alpha map."""
  292. if self._alpha_map_large is None:
  293. self._alpha_map_large = AlphaMapManager.load_alpha_map(
  294. Path(self.alpha_map_large_path),
  295. self.LARGE_SIZE
  296. )
  297. return self._alpha_map_large
  298. def detect_watermark_size(self, image: np.ndarray) -> Tuple[int, int]:
  299. """
  300. Auto-detect watermark size based on image dimensions.
  301. Args:
  302. image: Input image as numpy array
  303. Returns:
  304. Tuple of (size, margin) for watermark
  305. """
  306. height, width = image.shape[:2]
  307. if width <= self.SIZE_THRESHOLD or height <= self.SIZE_THRESHOLD:
  308. return self.SMALL_SIZE, self.SMALL_MARGIN
  309. else:
  310. return self.LARGE_SIZE, self.LARGE_MARGIN
  311. def get_watermark_position(self, image: np.ndarray,
  312. size: int, margin: int) -> Tuple[int, int]:
  313. """
  314. Get the (x, y) position of the watermark top-left corner.
  315. Watermark is in bottom-right corner.
  316. Args:
  317. image: Input image
  318. size: Watermark size (48 or 96)
  319. margin: Margin from edge
  320. Returns:
  321. Tuple of (x, y) position
  322. """
  323. height, width = image.shape[:2]
  324. x = width - margin - size
  325. y = height - margin - size
  326. return x, y
  327. def remove_watermark(self, image: np.ndarray,
  328. force_size: str = None,
  329. verbose: bool = False) -> np.ndarray:
  330. """
  331. Remove watermark from image.
  332. Args:
  333. image: Input image as numpy array (RGB)
  334. force_size: Force 'small' (48x48) or 'large' (96x96)
  335. verbose: Print debug information
  336. Returns:
  337. Image with watermark removed
  338. """
  339. # Ensure RGB
  340. if len(image.shape) == 2:
  341. # Grayscale - convert to RGB
  342. image = np.stack([image] * 3, axis=-1)
  343. elif len(image.shape) == 3 and image.shape[2] == 4:
  344. # RGBA - drop alpha
  345. image = image[:, :, :3]
  346. elif len(image.shape) != 3 or image.shape[2] != 3:
  347. raise ValueError("Image must be RGB or RGBA")
  348. # Detect or force watermark size
  349. if force_size == 'small':
  350. size, margin = self.SMALL_SIZE, self.SMALL_MARGIN
  351. elif force_size == 'large':
  352. size, margin = self.LARGE_SIZE, self.LARGE_MARGIN
  353. else:
  354. size, margin = self.detect_watermark_size(image)
  355. if verbose:
  356. print(f" Watermark size: {size}x{size}, margin: {margin}px")
  357. # Get watermark position
  358. x, y = self.get_watermark_position(image, size, margin)
  359. if verbose:
  360. print(f" Watermark position: ({x}, {y})")
  361. # Validate position
  362. if x < 0 or y < 0:
  363. if verbose:
  364. print(f" Warning: Image too small for {size}x{size} watermark")
  365. return image.copy()
  366. # Get appropriate alpha map
  367. if size == self.SMALL_SIZE:
  368. alpha_map = self.alpha_map_small
  369. else:
  370. alpha_map = self.alpha_map_large
  371. # Remove watermark
  372. result = BlendModes.remove_watermark_alpha_blend(
  373. image=image,
  374. alpha_map=alpha_map,
  375. position=(x, y),
  376. logo_value=self.logo_value
  377. )
  378. return result
  379. def add_watermark(self, image: np.ndarray,
  380. force_size: str = None,
  381. verbose: bool = False) -> np.ndarray:
  382. """
  383. Add watermark to image (for testing purposes).
  384. Args:
  385. image: Input image as numpy array (RGB)
  386. force_size: Force 'small' (48x48) or 'large' (96x96)
  387. verbose: Print debug information
  388. Returns:
  389. Image with watermark added
  390. """
  391. # Ensure RGB
  392. if len(image.shape) == 2:
  393. image = np.stack([image] * 3, axis=-1)
  394. elif len(image.shape) == 3 and image.shape[2] == 4:
  395. image = image[:, :, :3]
  396. elif len(image.shape) != 3 or image.shape[2] != 3:
  397. raise ValueError("Image must be RGB or RGBA")
  398. # Detect or force watermark size
  399. if force_size == 'small':
  400. size, margin = self.SMALL_SIZE, self.SMALL_MARGIN
  401. elif force_size == 'large':
  402. size, margin = self.LARGE_SIZE, self.LARGE_MARGIN
  403. else:
  404. size, margin = self.detect_watermark_size(image)
  405. if verbose:
  406. print(f" Watermark size: {size}x{size}, margin: {margin}px")
  407. # Get watermark position
  408. x, y = self.get_watermark_position(image, size, margin)
  409. # Validate position
  410. if x < 0 or y < 0:
  411. if verbose:
  412. print(f" Warning: Image too small for {size}x{size} watermark")
  413. return image.copy()
  414. # Get appropriate alpha map
  415. if size == self.SMALL_SIZE:
  416. alpha_map = self.alpha_map_small
  417. else:
  418. alpha_map = self.alpha_map_large
  419. # Add watermark
  420. result = BlendModes.add_watermark_alpha_blend(
  421. image=image,
  422. alpha_map=alpha_map,
  423. position=(x, y),
  424. logo_value=self.logo_value
  425. )
  426. return result
  427. # ============================================================================
  428. # FILE OPERATIONS
  429. # ============================================================================
  430. def load_image(path: str) -> np.ndarray:
  431. """Load image as RGB numpy array."""
  432. img = Image.open(path).convert('RGB')
  433. return np.array(img)
  434. def save_image(image: np.ndarray, path: str, quality: int = 95):
  435. """Save numpy array as image."""
  436. img = Image.fromarray(image)
  437. # Determine format from extension
  438. ext = Path(path).suffix.lower()
  439. if ext in ['.jpg', '.jpeg']:
  440. img.save(path, 'JPEG', quality=quality)
  441. elif ext == '.png':
  442. img.save(path, 'PNG')
  443. elif ext == '.webp':
  444. img.save(path, 'WEBP', quality=quality)
  445. elif ext == '.bmp':
  446. img.save(path, 'BMP')
  447. else:
  448. img.save(path)
  449. def process_single_file(input_path: str,
  450. output_path: str = None,
  451. remover: GeminiWatermarkRemover = None,
  452. force_size: str = None,
  453. verbose: bool = False,
  454. add_mode: bool = False):
  455. """
  456. Process a single image file.
  457. Args:
  458. input_path: Path to input image
  459. output_path: Path for output (None = overwrite input)
  460. remover: Watermark remover instance
  461. force_size: Force 'small' or 'large'
  462. verbose: Print verbose output
  463. add_mode: Add watermark instead of removing
  464. """
  465. if output_path is None:
  466. output_path = input_path
  467. if verbose:
  468. print(f"Processing: {input_path}")
  469. # Load image
  470. image = load_image(input_path)
  471. if verbose:
  472. print(f" Image size: {image.shape[1]}x{image.shape[0]}")
  473. # Process
  474. if add_mode:
  475. result = remover.add_watermark(image, force_size, verbose)
  476. action = "Added watermark"
  477. else:
  478. result = remover.remove_watermark(image, force_size, verbose)
  479. action = "Removed watermark"
  480. # Save result
  481. save_image(result, output_path)
  482. if verbose:
  483. print(f" {action}, saved to: {output_path}")
  484. def process_directory(input_dir: str,
  485. output_dir: str,
  486. remover: GeminiWatermarkRemover = None,
  487. force_size: str = None,
  488. verbose: bool = False,
  489. add_mode: bool = False):
  490. """
  491. Process all images in a directory.
  492. Args:
  493. input_dir: Input directory path
  494. output_dir: Output directory path
  495. remover: Watermark remover instance
  496. force_size: Force 'small' or 'large'
  497. verbose: Print verbose output
  498. add_mode: Add watermark instead of removing
  499. """
  500. input_path = Path(input_dir)
  501. output_path = Path(output_dir)
  502. # Create output directory
  503. output_path.mkdir(parents=True, exist_ok=True)
  504. # Supported formats
  505. formats = {'.jpg', '.jpeg', '.png', '.webp', '.bmp'}
  506. # Find all images
  507. images = [f for f in input_path.iterdir()
  508. if f.is_file() and f.suffix.lower() in formats]
  509. total = len(images)
  510. if verbose:
  511. print(f"Found {total} images to process")
  512. processed = 0
  513. errors = 0
  514. for idx, img_path in enumerate(images, 1):
  515. out_file = output_path / img_path.name
  516. try:
  517. if verbose:
  518. print(f"\n[{idx}/{total}] ", end="")
  519. process_single_file(
  520. str(img_path), str(out_file),
  521. remover, force_size, verbose, add_mode
  522. )
  523. processed += 1
  524. except Exception as e:
  525. print(f"Error processing {img_path.name}: {e}")
  526. errors += 1
  527. print(f"\nCompleted: {processed} processed, {errors} errors")
  528. # ============================================================================
  529. # CLI
  530. # ============================================================================
  531. def print_banner():
  532. """Print ASCII banner."""
  533. banner = r"""
  534. ╔═══════════════════════════════════════════════════════════════════╗
  535. ║ ║
  536. ║ ██████╗ ███████╗███╗ ███╗██╗███╗ ██╗██╗ ║
  537. ║ ██╔════╝ ██╔════╝████╗ ████║██║████╗ ██║██║ ║
  538. ║ ██║ ███╗█████╗ ██╔████╔██║██║██╔██╗ ██║██║ ║
  539. ║ ██║ ██║██╔══╝ ██║╚██╔╝██║██║██║╚██╗██║██║ ║
  540. ║ ╚██████╔╝███████╗██║ ╚═╝ ██║██║██║ ╚████║██║ ║
  541. ║ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ║
  542. ║ ║
  543. ║ Watermark Removal Tool (Python Edition) ║
  544. ║ Reverse Alpha Blending Technology ║
  545. ║ ║
  546. ╚═══════════════════════════════════════════════════════════════════╝
  547. """
  548. print(banner)
  549. def check_alpha_maps():
  550. """Check if alpha map files exist and print status."""
  551. small_exists = DEFAULT_ALPHA_MAP_SMALL_PATH.exists()
  552. large_exists = DEFAULT_ALPHA_MAP_LARGE_PATH.exists()
  553. print("Alpha Map Status:")
  554. print(f" bg_48.png (48x48): {'✓ Found' if small_exists else '✗ Not found'} - {DEFAULT_ALPHA_MAP_SMALL_PATH}")
  555. print(f" bg_96.png (96x96): {'✓ Found' if large_exists else '✗ Not found'} - {DEFAULT_ALPHA_MAP_LARGE_PATH}")
  556. print()
  557. if not small_exists or not large_exists:
  558. print("WARNING: Missing alpha map files!")
  559. print("Please ensure bg_48.png and bg_96.png are in the same directory as this script.")
  560. print()
  561. return False
  562. return True
  563. def main():
  564. """Main entry point."""
  565. parser = argparse.ArgumentParser(
  566. description='Remove Gemini watermarks using reverse alpha blending',
  567. formatter_class=argparse.RawDescriptionHelpFormatter,
  568. epilog='''
  569. Examples:
  570. # Simple mode - edit in place
  571. python GWMRTool.py image.jpg
  572. # Specify output file
  573. python GWMRTool.py -i input.jpg -o output.jpg
  574. # Batch processing
  575. python GWMRTool.py -i ./input_folder/ -o ./output_folder/
  576. # Force specific watermark size
  577. python GWMRTool.py -i image.jpg -o output.jpg --force-large
  578. # Add watermark (for testing)
  579. python GWMRTool.py -i clean.jpg -o watermarked.jpg --add
  580. Required Files:
  581. bg_48.png - 48x48 alpha map (for images <= 1024px)
  582. bg_96.png - 96x96 alpha map (for images > 1024px)
  583. Place these files in the same directory as this script.
  584. Mathematical Formula:
  585. Watermark Application: watermarked = α × logo + (1 - α) × original
  586. Reverse (Removal): original = (watermarked - α × logo) / (1 - α)
  587. '''
  588. )
  589. parser.add_argument('simple_input', nargs='?',
  590. help='Image file (simple mode - edits in place)')
  591. parser.add_argument('-i', '--input',
  592. help='Input image file or directory')
  593. parser.add_argument('-o', '--output',
  594. help='Output image file or directory')
  595. parser.add_argument('--force-small', action='store_true',
  596. help='Force 48x48 watermark size')
  597. parser.add_argument('--force-large', action='store_true',
  598. help='Force 96x96 watermark size')
  599. parser.add_argument('-v', '--verbose', action='store_true',
  600. help='Enable verbose output')
  601. parser.add_argument('-q', '--quiet', action='store_true',
  602. help='Suppress all output except errors')
  603. parser.add_argument('-b', '--banner', action='store_true',
  604. help='Show ASCII banner')
  605. parser.add_argument('--add', action='store_true',
  606. help='Add watermark instead of removing (for testing)')
  607. parser.add_argument('--alpha-small',
  608. help='Path to custom 48x48 alpha map')
  609. parser.add_argument('--alpha-large',
  610. help='Path to custom 96x96 alpha map')
  611. parser.add_argument('--logo-value', type=float, default=255.0,
  612. help='Logo pixel value (default: 255 for white)')
  613. parser.add_argument('--check', action='store_true',
  614. help='Check alpha map files and exit')
  615. parser.add_argument('--version', action='version',
  616. version='Gemini Watermark Remover v1.0.0 (Python)')
  617. args = parser.parse_args()
  618. # Show banner
  619. if args.banner and not args.quiet:
  620. print_banner()
  621. # Check mode
  622. if args.check:
  623. check_alpha_maps()
  624. sys.exit(0)
  625. # Verbose mode (unless quiet)
  626. verbose = args.verbose and not args.quiet
  627. # Determine force size
  628. force_size = None
  629. if args.force_small:
  630. force_size = 'small'
  631. elif args.force_large:
  632. force_size = 'large'
  633. # Custom alpha map paths
  634. alpha_small_path = Path(args.alpha_small) if args.alpha_small else None
  635. alpha_large_path = Path(args.alpha_large) if args.alpha_large else None
  636. # Check default alpha maps exist (if not using custom)
  637. if alpha_small_path is None and not DEFAULT_ALPHA_MAP_SMALL_PATH.exists():
  638. print(f"Error: Alpha map not found: {DEFAULT_ALPHA_MAP_SMALL_PATH}")
  639. print("Please ensure bg_48.png is in the same directory as this script.")
  640. print("Or specify a custom path with --alpha-small")
  641. sys.exit(1)
  642. if alpha_large_path is None and not DEFAULT_ALPHA_MAP_LARGE_PATH.exists():
  643. print(f"Error: Alpha map not found: {DEFAULT_ALPHA_MAP_LARGE_PATH}")
  644. print("Please ensure bg_96.png is in the same directory as this script.")
  645. print("Or specify a custom path with --alpha-large")
  646. sys.exit(1)
  647. # Initialize remover
  648. try:
  649. remover = GeminiWatermarkRemover(
  650. alpha_map_small_path=alpha_small_path,
  651. alpha_map_large_path=alpha_large_path,
  652. logo_value=args.logo_value
  653. )
  654. except Exception as e:
  655. print(f"Error initializing: {e}")
  656. sys.exit(1)
  657. # Determine input/output
  658. if args.simple_input:
  659. # Simple mode - edit in place
  660. if not Path(args.simple_input).exists():
  661. print(f"Error: File not found: {args.simple_input}")
  662. sys.exit(1)
  663. try:
  664. process_single_file(
  665. args.simple_input, None, remover,
  666. force_size, verbose, args.add
  667. )
  668. except Exception as e:
  669. print(f"Error: {e}")
  670. sys.exit(1)
  671. elif args.input:
  672. input_path = Path(args.input)
  673. if not input_path.exists():
  674. print(f"Error: Input not found: {args.input}")
  675. sys.exit(1)
  676. if input_path.is_dir():
  677. # Directory mode
  678. if not args.output:
  679. print("Error: Output directory required for batch processing (-o)")
  680. sys.exit(1)
  681. try:
  682. process_directory(
  683. args.input, args.output, remover,
  684. force_size, verbose, args.add
  685. )
  686. except Exception as e:
  687. print(f"Error: {e}")
  688. sys.exit(1)
  689. else:
  690. # Single file mode
  691. output = args.output if args.output else args.input
  692. try:
  693. process_single_file(
  694. args.input, output, remover,
  695. force_size, verbose, args.add
  696. )
  697. except Exception as e:
  698. print(f"Error: {e}")
  699. sys.exit(1)
  700. else:
  701. # No input provided - show help
  702. parser.print_help()
  703. print("\n" + "="*60)
  704. check_alpha_maps()
  705. sys.exit(0)
  706. if not args.quiet:
  707. print("Done!")
  708. # ============================================================================
  709. # ENTRY POINT
  710. # ============================================================================
  711. if __name__ == '__main__':
  712. main()