| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- """
- 水印合成脚本:在clean图片上叠加斜向浅色文字水印,输出带水印图 + 精确mask。
- 用法:
- python watermark_synthesis.py # 默认参数演示
- python watermark_synthesis.py --input ./test_images/clean/ # 指定输入目录
- python watermark_synthesis.py --text "SAMPLE" --opacity 0.15 --angle 45
- """
- from __future__ import annotations
- import argparse
- import math
- from pathlib import Path
- from typing import Optional
- import cv2
- import numpy as np
- from loguru import logger
- from PIL import Image, ImageDraw, ImageFont
- def _find_font() -> str:
- """查找可用中文字体,找不到返回默认字体。"""
- candidates = [
- "/System/Library/Fonts/PingFang.ttc",
- "/System/Library/Fonts/STHeiti Light.ttc",
- "/System/Library/Fonts/Hiragino Sans GB.ttc",
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
- "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
- ]
- for fp in candidates:
- if Path(fp).exists():
- return fp
- logger.warning("未找到中文字体,使用PIL默认字体")
- return ""
- def _text_size_to_font_size(text_height_px: int) -> int:
- """根据目标文字像素高度估算 font_size。"""
- return int(text_height_px * 1.15)
- def _render_watermark_tile(
- pil_img: Image.Image,
- text: str,
- font_path: str,
- font_size: int,
- opacity: float,
- angle_deg: float,
- spacing_x: int,
- spacing_y: int,
- ) -> tuple[np.ndarray, np.ndarray]:
- """
- 在图上平铺斜向水印文字,返回 (watermarked_np, mask_np)。
- mask_np: H×W bool, True=水印像素位置。
- """
- w, h = pil_img.size
- text_height = int(font_size / 1.15)
- gray_value = int(255 * (1 - opacity))
- # 创建水印文字mask(稍大画布以覆盖旋转后区域)
- diag = int(math.sqrt(w * w + h * h)) + text_height * 4
- tile_w = diag
- tile_h = diag
- tile = Image.new("L", (tile_w, tile_h), 0)
- draw = ImageDraw.Draw(tile)
- font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
- # 步长取spacing + 文字大小,确保均匀分布
- step_x = text_height + spacing_x
- step_y = text_height + spacing_y
- for y in range(0, tile_h, step_y):
- for x in range(0, tile_w, step_x):
- draw.text((x, y), text, fill=255, font=font)
- # 旋转
- tile_rot = tile.rotate(angle_deg, expand=False, fillcolor=0)
- # 裁剪到原图大小(中心对齐)
- cx, cy = tile_rot.size[0] // 2, tile_rot.size[1] // 2
- left = cx - w // 2
- top = cy - h // 2
- watermark_tile = tile_rot.crop((left, top, left + w, top + h))
- mask_np = np.array(watermark_tile) > 0
- # 叠加到原图
- base = np.array(pil_img.convert("RGB"))
- alpha = opacity
- result = base.copy()
- result[mask_np] = (
- base[mask_np].astype(np.float32) * (1 - alpha)
- + np.array([gray_value, gray_value, gray_value], dtype=np.float32) * alpha
- ).astype(np.uint8)
- return result, mask_np
- def synthesize_watermark(
- input_path: Path,
- output_dir: Path,
- *,
- text: str = "SAMPLE",
- font_path: str = "",
- text_height_px: int = 36,
- opacity: float = 0.12,
- angle_deg: float = 45.0,
- spacing_x: int = 180,
- spacing_y: int = 180,
- save_mask: bool = True,
- ) -> Path:
- """
- 在输入图片上合成水印,输出到 output_dir。
- Returns:
- 合成后的图片路径
- """
- output_dir.mkdir(parents=True, exist_ok=True)
- pil_img = Image.open(str(input_path)).convert("RGB")
- fp = font_path or _find_font()
- font_size = _text_size_to_font_size(text_height_px)
- logger.info(
- f"合成水印: {input_path.name} | "
- f"text='{text}' font_size={font_size} opacity={opacity} angle={angle_deg}"
- )
- result_np, mask_np = _render_watermark_tile(
- pil_img, text, fp, font_size, opacity, angle_deg, spacing_x, spacing_y
- )
- out_name = f"{input_path.stem}_watermarked{input_path.suffix}"
- out_path = output_dir / out_name
- Image.fromarray(result_np).save(str(out_path))
- logger.info(f" 水印图: {out_path}")
- if save_mask:
- mask_path = output_dir / f"{input_path.stem}_mask.png"
- cv2.imwrite(str(mask_path), (mask_np.astype(np.uint8) * 255))
- logger.info(f" mask: {mask_path}")
- return out_path
- def main():
- parser = argparse.ArgumentParser(description="水印合成工具")
- parser.add_argument("--input", type=Path, default=None,
- help="输入图片或目录(默认: test_images/clean/)")
- parser.add_argument("--output", type=Path, default=None,
- help="输出目录(默认: test_images/synthetic/)")
- parser.add_argument("--text", type=str, default="行内内部使用",
- help="水印文字内容")
- parser.add_argument("--text-height", type=int, default=48,
- help="文字像素高度(默认48)")
- parser.add_argument("--opacity", type=float, default=0.10,
- help="水印透明度 0~1(默认0.10)")
- parser.add_argument("--angle", type=float, default=45.0,
- help="水印倾斜角度(默认45°)")
- parser.add_argument("--spacing-x", type=int, default=250,
- help="水印文字水平间距(默认250px)")
- parser.add_argument("--spacing-y", type=int, default=250,
- help="水印文字垂直间距(默认250px)")
- parser.add_argument("--font", type=str, default="",
- help="字体文件路径")
- parser.add_argument("--no-mask", action="store_true",
- help="不保存mask")
- parser.add_argument("--demo", action="store_true",
- help="使用input目录下第一张测试图生成演示图")
- args = parser.parse_args()
- root = Path(__file__).parent
- input_dir = args.input or (root / "test_images" / "clean")
- output_dir = args.output or (root / "test_images" / "synthetic")
- if args.demo:
- # 无clean图时,直接用input目录的水印图再加一层合成水印做演示
- img_files = sorted(root.glob("test_images/input/*"))
- if not img_files:
- logger.error("test_images/input/ 下没有测试图片,请放入图片后重试")
- return
- input_dir = root / "test_images" / "input"
- output_dir = root / "test_images" / "synthetic"
- input_dir = Path(input_dir)
- output_dir = Path(output_dir)
- output_dir.mkdir(parents=True, exist_ok=True)
- if input_dir.is_dir():
- img_files = sorted([
- f for f in input_dir.iterdir()
- if f.suffix.lower() in {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"}
- ])
- elif input_dir.is_file():
- img_files = [input_dir]
- else:
- logger.error(f"输入路径不存在: {input_dir}")
- return
- if not img_files:
- logger.warning(f"{input_dir} 下没有图片文件")
- return
- for f in img_files:
- synthesize_watermark(
- f, output_dir,
- text=args.text,
- font_path=args.font,
- text_height_px=args.text_height,
- opacity=args.opacity,
- angle_deg=args.angle,
- spacing_x=args.spacing_x,
- spacing_y=args.spacing_y,
- save_mask=not args.no_mask,
- )
- if __name__ == "__main__":
- main()
|