235 lines
6.7 KiB
Python
235 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PPT Master - Gemini Watermark Remover
|
|
|
|
Removes the watermark logo from the bottom-right corner of Gemini-generated images.
|
|
Uses a reverse blending algorithm to restore original pixels.
|
|
|
|
Usage:
|
|
python3 scripts/gemini_watermark_remover.py <image_path>
|
|
python3 scripts/gemini_watermark_remover.py <image_path> -o output_path.png
|
|
|
|
Examples:
|
|
python3 scripts/gemini_watermark_remover.py projects/demo/images/bg_01.png
|
|
python3 scripts/gemini_watermark_remover.py image.jpg -o image_clean.jpg
|
|
|
|
Dependencies:
|
|
pip install Pillow numpy
|
|
|
|
Notes:
|
|
- Supports PNG, JPG, JPEG formats
|
|
- Automatically detects watermark size (48px or 96px)
|
|
- Output file defaults to adding an _unwatermarked suffix
|
|
"""
|
|
|
|
import sys
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
# Import modules from the same directory
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
from console_encoding import configure_utf8_stdio
|
|
|
|
configure_utf8_stdio()
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
# Algorithm parameters
|
|
ALPHA_THRESHOLD = 0.002 # Alpha threshold; values below this are not processed
|
|
MAX_ALPHA = 0.99 # Maximum alpha value to prevent division by zero
|
|
LOGO_VALUE = 255 # Logo pixel value (white)
|
|
LARGE_IMAGE_THRESHOLD = 1024
|
|
LARGE_LOGO_SIZE = 96
|
|
SMALL_LOGO_SIZE = 48
|
|
LARGE_MARGIN = 64
|
|
SMALL_MARGIN = 32
|
|
|
|
# Watermark background image paths
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
BG_48_PATH = SCRIPT_DIR / "assets" / "bg_48.png"
|
|
BG_96_PATH = SCRIPT_DIR / "assets" / "bg_96.png"
|
|
|
|
|
|
def detect_watermark_config(width: int, height: int) -> dict[str, int]:
|
|
"""
|
|
Detect watermark configuration based on image dimensions
|
|
|
|
Args:
|
|
width: Image width
|
|
height: Image height
|
|
|
|
Returns:
|
|
Configuration dict containing logo_size, margin_right, margin_bottom
|
|
"""
|
|
if width > LARGE_IMAGE_THRESHOLD and height > LARGE_IMAGE_THRESHOLD:
|
|
return {
|
|
"logo_size": LARGE_LOGO_SIZE,
|
|
"margin_right": LARGE_MARGIN,
|
|
"margin_bottom": LARGE_MARGIN,
|
|
}
|
|
return {
|
|
"logo_size": SMALL_LOGO_SIZE,
|
|
"margin_right": SMALL_MARGIN,
|
|
"margin_bottom": SMALL_MARGIN,
|
|
}
|
|
|
|
|
|
def calculate_watermark_position(width: int, height: int, config: dict[str, int]) -> dict[str, int]:
|
|
"""
|
|
Calculate watermark position
|
|
|
|
Args:
|
|
width: Image width
|
|
height: Image height
|
|
config: Watermark configuration
|
|
|
|
Returns:
|
|
Position dict containing x, y, width, height
|
|
"""
|
|
logo_size = config["logo_size"]
|
|
return {
|
|
"x": width - config["margin_right"] - logo_size,
|
|
"y": height - config["margin_bottom"] - logo_size,
|
|
"width": logo_size,
|
|
"height": logo_size,
|
|
}
|
|
|
|
|
|
def calculate_alpha_map(bg_image: Image.Image) -> np.ndarray:
|
|
"""
|
|
Calculate the alpha channel map from the watermark background image
|
|
|
|
Args:
|
|
bg_image: Watermark background PNG image
|
|
|
|
Returns:
|
|
Alpha map array (range 0-1)
|
|
"""
|
|
bg_array = np.array(bg_image.convert("RGB"), dtype=np.float32)
|
|
max_channel = np.max(bg_array, axis=2)
|
|
return max_channel / 255.0
|
|
|
|
|
|
def remove_watermark(image: Image.Image, alpha_map: np.ndarray, position: dict) -> Image.Image:
|
|
"""
|
|
Remove watermark using a reverse blending algorithm
|
|
|
|
Args:
|
|
image: Original image
|
|
alpha_map: Alpha map array
|
|
position: Watermark position
|
|
|
|
Returns:
|
|
Image with watermark removed
|
|
"""
|
|
img_array = np.array(image.convert("RGBA"), dtype=np.float32)
|
|
x, y, w, h = position["x"], position["y"], position["width"], position["height"]
|
|
|
|
for row in range(h):
|
|
for col in range(w):
|
|
alpha = alpha_map[row, col]
|
|
if alpha < ALPHA_THRESHOLD:
|
|
continue
|
|
alpha = min(alpha, MAX_ALPHA)
|
|
one_minus_alpha = 1.0 - alpha
|
|
|
|
img_y, img_x = y + row, x + col
|
|
for c in range(3):
|
|
watermarked = img_array[img_y, img_x, c]
|
|
original = (watermarked - alpha * LOGO_VALUE) / one_minus_alpha
|
|
img_array[img_y, img_x, c] = np.clip(original, 0, 255)
|
|
|
|
return Image.fromarray(img_array.astype(np.uint8))
|
|
|
|
|
|
def process_image(input_path: Path, output_path: Path | None = None, verbose: bool = True) -> Path:
|
|
"""
|
|
Process a single image to remove its watermark
|
|
|
|
Args:
|
|
input_path: Input image path
|
|
output_path: Output image path (optional)
|
|
verbose: Whether to output detailed information
|
|
|
|
Returns:
|
|
Output file path
|
|
"""
|
|
image = Image.open(input_path)
|
|
width, height = image.size
|
|
|
|
config = detect_watermark_config(width, height)
|
|
position = calculate_watermark_position(width, height, config)
|
|
|
|
if verbose:
|
|
print(f" Image size: {width} x {height}")
|
|
print(f" Watermark size: {config['logo_size']} x {config['logo_size']}")
|
|
print(f" Watermark position: ({position['x']}, {position['y']})")
|
|
|
|
bg_path = BG_96_PATH if config["logo_size"] == 96 else BG_48_PATH
|
|
|
|
if not bg_path.exists():
|
|
print(f"Error: Watermark background image not found: {bg_path}")
|
|
sys.exit(1)
|
|
|
|
bg_image = Image.open(bg_path)
|
|
alpha_map = calculate_alpha_map(bg_image)
|
|
|
|
result = remove_watermark(image, alpha_map, position)
|
|
|
|
if output_path is None:
|
|
stem = input_path.stem
|
|
suffix = input_path.suffix or ".png"
|
|
output_path = input_path.parent / f"{stem}_unwatermarked{suffix}"
|
|
|
|
if output_path.suffix.lower() in (".jpg", ".jpeg"):
|
|
result = result.convert("RGB")
|
|
|
|
result.save(output_path)
|
|
return output_path
|
|
|
|
|
|
def main() -> None:
|
|
"""Run the CLI entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description='PPT Master - Gemini Watermark Remover',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog='''
|
|
Examples:
|
|
%(prog)s projects/demo/images/bg_01.png
|
|
%(prog)s image.jpg -o image_clean.jpg
|
|
|
|
Notes:
|
|
- Automatically detects watermark size (96px for large images, 48px for small)
|
|
- Supports PNG, JPG, JPEG formats
|
|
- Output file defaults to adding an _unwatermarked suffix
|
|
'''
|
|
)
|
|
|
|
parser.add_argument('input', type=Path, help='Input image path')
|
|
parser.add_argument('-o', '--output', type=Path, default=None, help='Output image path')
|
|
parser.add_argument('-q', '--quiet', action='store_true', help='Quiet mode')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.input.exists():
|
|
print(f"Error: File not found: {args.input}")
|
|
sys.exit(1)
|
|
|
|
verbose = not args.quiet
|
|
if verbose:
|
|
print("PPT Master - Gemini Watermark Remover")
|
|
print("=" * 40)
|
|
print(f" Input file: {args.input}")
|
|
|
|
output = process_image(args.input, args.output, verbose=verbose)
|
|
|
|
if verbose:
|
|
print()
|
|
print(f"[Done] Saved to: {output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|