能否添加水印?后处理脚本集成部署案例
1. 项目背景:人像卡通化工具的实用进化
你有没有遇到过这样的场景:刚用AI生成一张惊艳的人像卡通图,想发到社交平台,却担心被随意转载?或者给客户交付批量处理结果时,需要悄悄打上品牌标识?又或者团队内部使用时,希望自动标注处理时间与版本信息?
这正是今天要聊的——在已有的UNet人像卡通化WebUI工具中,无缝集成水印功能。这个工具由科哥基于阿里达摩院ModelScope的cv_unet_person-image-cartoon模型构建,开箱即用,界面友好,但原生并不支持水印。本文不讲大道理,不堆技术术语,就带你从零开始,亲手给它“加个印章”:一个轻量、稳定、可配置的后处理水印脚本,并完成完整部署。
整个过程不需要修改模型代码,不重写前端,不碰核心推理逻辑。我们只在图片生成后的“最后一公里”动手——就像洗完照片后,在相纸上轻轻盖个章。它兼容单图/批量两种模式,支持文字+Logo双水印,位置、透明度、字体大小全可调,且不影响原有功能和响应速度。
如果你已经部署过这个卡通化工具,那么接下来30分钟,就能让它多一项“自带版权意识”的能力。
2. 水印为什么不能直接加在模型里?
先说个常见误区:很多人第一反应是“改模型输出层,把水印画进去”。这看似直接,实则问题重重:
- 破坏模型纯度:DCT-Net本身是端到端的图像风格迁移网络,强行注入绘图逻辑会干扰梯度回传(即使推理时不用),增加调试复杂度;
- 耦合度过高:水印需求千变万化(公司名/日期/二维码/半透明角标),硬编码进模型意味着每次换文案都要重导出ONNX、重打包镜像;
- 格式受限:模型输出是Tensor,而水印涉及字体渲染、Alpha混合、坐标计算——这些是典型的CPU侧图像处理任务,GPU做反而低效;
- 批量处理失效:WebUI的批量模式是异步队列+多进程,若在模型层加水印,需同步等待所有图生成完毕再统一处理,反而拖慢整体吞吐。
所以,最佳实践是“后处理”:让模型专注做好一件事——生成高质量卡通图;把水印这件事,交给独立、轻量、可热更新的Python脚本,在图片保存前一刻完成叠加。这正是Unix哲学的体现:每个程序只做一件事,并做好。
3. 后处理水印脚本设计与实现
我们不造轮子,用最稳妥的方案:在outputs/目录生成图片后,由一个独立脚本监听新文件,自动添加水印并覆盖保存。整个流程如下:
模型推理 → 保存原始图(outputs/xxx.png) ↓ 水印脚本检测到新文件 → 读取 → 叠加水印 → 覆盖保存(同名)3.1 脚本核心功能清单
- 支持PNG/JPG/WEBP三种输出格式自动识别
- 文字水印:自定义内容、字体、大小、颜色、透明度、旋转角度、位置(9宫格锚点)
- Logo水印:支持透明PNG图标,可缩放、平铺或居中放置
- 智能避让:文字水印自动避开人物脸部区域(基于OpenCV简单人脸检测)
- 批量兼容:单图即时处理,批量模式下逐张处理,不阻塞主流程
- 静默运行:无界面、无日志轰炸,只在出错时打印错误信息
3.2 完整可运行脚本(watermark_postproc.py)
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 人像卡通化后处理水印脚本 支持文字+Logo双水印,智能避让人脸,静默运行 """ import os import time import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageOps import argparse from pathlib import Path # ------------------- 配置区(按需修改)------------------- WATERMARK_TEXT = "科哥AI工坊 · 2026" # 文字水印内容 TEXT_FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" # 字体路径(Linux默认) TEXT_SIZE = 48 TEXT_COLOR = (255, 255, 255, 64) # RGBA,最后一位为透明度(0-255) TEXT_ANGLE = -22 TEXT_POSITION = "bottom-right" # top-left, center, bottom-right 等9种 LOGO_PATH = "/root/watermark/logo.png" # Logo路径,留空则不启用 LOGO_SCALE = 0.15 # 相对于图像短边的比例 LOGO_ALPHA = 0.3 # Logo透明度(0.0-1.0) OUTPUT_DIR = "/root/unet-cartoon/outputs" # 与WebUI输出目录一致 CHECK_INTERVAL = 1.0 # 检测间隔(秒) # --------------------------------------------------------- def add_text_watermark(img_pil, text, font_path, size, color, angle, position): """在PIL图像上添加旋转文字水印""" if not os.path.exists(font_path): print(f"[WARN] 字体未找到 {font_path},使用默认字体") font = ImageFont.load_default() else: try: font = ImageFont.truetype(font_path, size) except: print(f"[WARN] 字体加载失败,回退到默认字体") font = ImageFont.load_default() # 创建透明文字图层 txt = Image.new('RGBA', img_pil.size, (255, 255, 255, 0)) draw = ImageDraw.Draw(txt) # 计算文字尺寸 try: bbox = draw.textbbox((0, 0), text, font=font) w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] except: w, h = 300, 50 # 根据position确定锚点坐标 iw, ih = img_pil.size positions = { "top-left": (20, 20), "top-center": (iw//2 - w//2, 20), "top-right": (iw - w - 20, 20), "center": (iw//2 - w//2, ih//2 - h//2), "bottom-left": (20, ih - h - 20), "bottom-center": (iw//2 - w//2, ih - h - 20), "bottom-right": (iw - w - 20, ih - h - 20), } x, y = positions.get(position, positions["bottom-right"]) # 绘制文字 draw.text((x, y), text, fill=color, font=font) # 旋转 txt_rotated = txt.rotate(angle, expand=1, resample=Image.BICUBIC) # 裁剪回原图尺寸并合成 rx, ry = txt_rotated.size sx = (rx - iw) // 2 sy = (ry - ih) // 2 txt_cropped = txt_rotated.crop((sx, sy, sx+iw, sy+ih)) return Image.alpha_composite(img_pil.convert('RGBA'), txt_cropped) def add_logo_watermark(img_pil, logo_path, scale, alpha): """添加Logo水印(居中缩放)""" if not os.path.exists(logo_path): return img_pil try: logo = Image.open(logo_path).convert('RGBA') except Exception as e: print(f"[ERROR] Logo加载失败 {logo_path}: {e}") return img_pil iw, ih = img_pil.size short_side = min(iw, ih) logo_w = int(short_side * scale) logo_h = int(logo_w * logo.height / logo.width) logo_resized = logo.resize((logo_w, logo_h), Image.LANCZOS) # 调整Logo透明度 alpha_array = np.array(logo_resized)[:, :, 3] alpha_array = (alpha_array * alpha).astype(np.uint8) logo_alpha = Image.fromarray(alpha_array, mode='L') logo_resized.putalpha(logo_alpha) # 居中粘贴 x = (iw - logo_w) // 2 y = (ih - logo_h) // 2 result = img_pil.convert('RGBA') result.paste(logo_resized, (x, y), logo_resized) return result def detect_face_region(img_cv): """简易人脸检测(仅用于避让,不依赖深度模型)""" gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') faces = face_cascade.detectMultiScale(gray, 1.1, 4) if len(faces) == 0: return None # 返回最大人脸区域(x,y,w,h) areas = [w*h for (x,y,w,h) in faces] idx = np.argmax(areas) return faces[idx] def safe_add_watermark(image_path): """安全添加水印主函数""" try: # 读取图像 if image_path.lower().endswith(('.png', '.webp')): img_pil = Image.open(image_path).convert('RGBA') else: img_pil = Image.open(image_path).convert('RGB') # 转为BGR用于人脸检测(仅当启用避让时) img_cv = cv2.cvtColor(np.array(img_pil.convert('RGB')), cv2.COLOR_RGB2BGR) face_roi = detect_face_region(img_cv) # 先加文字水印 if WATERMARK_TEXT.strip(): # 若检测到人脸,微调文字位置避开脸部 if face_roi is not None and TEXT_POSITION in ["center", "bottom-center"]: x, y, w, h = face_roi # 将文字移到右上角 img_pil = add_text_watermark( img_pil, WATERMARK_TEXT, TEXT_FONT_PATH, TEXT_SIZE, TEXT_COLOR, TEXT_ANGLE, "top-right" ) else: img_pil = add_text_watermark( img_pil, WATERMARK_TEXT, TEXT_FONT_PATH, TEXT_SIZE, TEXT_COLOR, TEXT_ANGLE, TEXT_POSITION ) # 再加Logo水印 if LOGO_PATH and os.path.exists(LOGO_PATH): img_pil = add_logo_watermark(img_pil, LOGO_PATH, LOGO_SCALE, LOGO_ALPHA) # 保存(覆盖原图) if image_path.lower().endswith('.png'): img_pil.save(image_path, format='PNG', optimize=True) elif image_path.lower().endswith('.jpg') or image_path.lower().endswith('.jpeg'): img_pil.convert('RGB').save(image_path, format='JPEG', quality=95) elif image_path.lower().endswith('.webp'): img_pil.save(image_path, format='WEBP', quality=90) else: img_pil.save(image_path) # 默认保存 print(f"[INFO] 水印已添加 → {os.path.basename(image_path)}") except Exception as e: print(f"[ERROR] 处理 {image_path} 失败: {e}") def main(): parser = argparse.ArgumentParser() parser.add_argument("--dir", type=str, default=OUTPUT_DIR, help="监控目录") args = parser.parse_args() output_path = Path(args.dir) if not output_path.exists(): print(f"[ERROR] 监控目录不存在: {args.dir}") return # 初始化:处理已有文件(可选) # for f in output_path.glob("*.*"): # if f.suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp']: # safe_add_watermark(str(f)) print(f"[START] 水印服务启动,监控目录: {args.dir}") print(f"[HELP] 修改配置请编辑 watermark_postproc.py 文件") processed_files = set() while True: for f in output_path.iterdir(): if f.is_file() and f.suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp']: if str(f) not in processed_files: # 等待文件写入完成(简单策略:检查大小是否稳定) size1 = f.stat().st_size time.sleep(0.3) size2 = f.stat().st_size if size1 == size2 and size1 > 0: safe_add_watermark(str(f)) processed_files.add(str(f)) time.sleep(CHECK_INTERVAL) if __name__ == "__main__": main()关键说明:
- 脚本默认使用系统自带DejaVu字体,无需额外安装;如需中文字体,将
TEXT_FONT_PATH改为你的.ttf路径(如/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc)- Logo推荐使用带透明通道的PNG,尺寸建议256×256以上,脚本会自动缩放
- 人脸避让为轻量级实现,不追求100%精准,但足以避免文字压在脸上
4. 一键集成部署全流程
现在,我们把脚本真正“装进”你的卡通化工具里。整个过程只需5步,全部命令可复制粘贴执行。
4.1 准备工作:安装依赖与创建目录
# 进入项目根目录(根据你的实际路径调整) cd /root/unet-cartoon # 创建水印专用目录 mkdir -p watermark/{fonts,logo} # 安装OpenCV(用于人脸检测) pip install opencv-python-headless==4.10.0.84 # 下载免费中文字体(可选,如需显示中文水印) wget -O watermark/fonts/NotoSansCJK.ttc https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/Chinese%20(Hans)/NotoSansCJK-Regular.ttc # 示例Logo(白色文字+透明底,可替换为你自己的) echo "科哥AI" | convert -background none -fill white -font /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf -pointsize 64 label:@- watermark/logo/logo.png4.2 配置脚本:填入你的定制信息
用你喜欢的编辑器打开watermark_postproc.py,修改以下几行:
WATERMARK_TEXT = "你的品牌名 · 2026" # ← 改这里 TEXT_FONT_PATH = "/root/unet-cartoon/watermark/fonts/NotoSansCJK.ttc" # ← 中文字体路径 LOGO_PATH = "/root/unet-cartoon/watermark/logo/logo.png" # ← Logo路径 OUTPUT_DIR = "/root/unet-cartoon/outputs" # ← 必须与WebUI输出目录完全一致4.3 启动水印服务(后台常驻)
# 赋予执行权限 chmod +x watermark_postproc.py # 启动并放入后台(使用nohup,断开终端也不影响) nohup ./watermark_postproc.py --dir /root/unet-cartoon/outputs > /root/unet-cartoon/watermark.log 2>&1 & # 查看是否启动成功 ps aux | grep watermark_postproc # 应看到类似:/usr/bin/python3 ./watermark_postproc.py --dir ...4.4 验证效果:上传一张测试图
启动WebUI:
/bin/bash /root/run.sh访问
http://localhost:7860在「单图转换」页上传任意人像照片
点击「开始转换」
切换到服务器终端,查看日志:
tail -f /root/unet-cartoon/watermark.log应看到
[INFO] 水印已添加 → outputs_20260104152233.png到
outputs/目录查看该文件,用图片查看器打开——水印已清晰可见。
4.5 批量模式验证
- 上传5张图,点击「批量转换」
- 观察日志,会逐行打印5次
[INFO] 水印已添加 → ... - 下载ZIP包解压,每张图都已带水印,顺序与上传一致
5. 进阶技巧与维护建议
水印不是一劳永逸的装饰,而是需要随业务演进的“数字签名”。这里分享几个实战中沉淀下来的技巧:
5.1 动态水印:让每次输出都独一无二
你想让每张图的水印包含当前时间戳或输入文件名?只需两行代码改造:
# 在 safe_add_watermark 函数开头添加: from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") dynamic_text = f"{WATERMARK_TEXT} [{timestamp}]" # 然后将 dynamic_text 传入 add_text_watermark这样生成的水印就是科哥AI工坊 · 2026[20260104-152233],便于溯源。
5.2 条件水印:仅对特定用户启用
如果你的WebUI已对接用户系统(如JWT鉴权),可在后处理脚本中读取outputs/下的元数据文件(例如outputs_20260104152233.json),判断用户类型再决定是否加水印:
meta_path = image_path.replace(".png", ".json").replace(".jpg", ".json") if os.path.exists(meta_path): import json with open(meta_path) as f: meta = json.load(f) if meta.get("user_type") == "vip": return # VIP用户不加水印5.3 故障自愈:水印服务挂了怎么办?
为防意外,我们在run.sh中加入守护逻辑:
#!/bin/bash # /root/run.sh (原内容保持不变...) # 启动水印服务(如果未运行) if ! pgrep -f "watermark_postproc.py" > /dev/null; then echo "[INFO] 启动水印后处理服务..." nohup /root/unet-cartoon/watermark_postproc.py --dir /root/unet-cartoon/outputs > /root/unet-cartoon/watermark.log 2>&1 & fi # 启动WebUI(原命令) cd /root/unet-cartoon && python app.py --share每次重启应用,水印服务自动拉起,无需人工干预。
5.4 性能监控:确认它没拖慢你
水印脚本单图处理耗时约80~150ms(i5-8250U,1024px图),远低于模型推理的5~10秒。你可以在日志中加一行计时:
start_time = time.time() safe_add_watermark(str(f)) print(f"[TIME] 处理 {f.name}: {time.time()-start_time:.3f}s")只要稳定在200ms内,就完全不会成为瓶颈。
6. 总结:后处理思维的价值远超水印本身
我们花了30分钟,给一个现成的AI工具加上了水印能力。但真正值得带走的,不是这段代码,而是背后的后处理工程思维:
- 关注边界:不侵入核心模型,只在I/O边界做文章,降低风险;
- 小步快跑:一个脚本解决一个问题,比重构整个系统更高效;
- 配置驱动:所有参数外置,业务方改个文本就能上线;
- 可观测性:日志、计时、错误捕获,让自动化服务不再黑盒;
- 可组合性:今天加水印,明天可以加EXIF信息、加哈希校验、加自动归档——它们都是同一套监听+处理范式。
水印只是起点。当你习惯用这种“轻量后处理”思路,你会发现:AI应用的定制化,原来可以如此简单、可控、可持续。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。