资讯详情

资讯详情

Python批量给图片加水印:Pillow脚本实现全图平铺与透明度控制

如果你手上有几百张图片要统一加上水印第一反应是打开修图软件一张张贴还是找个小工具一把梭我以前总是选前者直到有一次被一大批商品图压得喘不过气才老老实实把批量添加全图水印这件事拆成固定流程。这篇文章就把那套流程完整写出来包括直接用得上的Python脚本、底层原理以及那些只有真正跑过一批图才会踩到的坑。1. 为什么批量加全图水印会变成大麻烦1.1 水印不只是一句版权声明还是一次被动曝光很多人一提水印就想到防止盗图实际上它还有另一层作用品牌曝光。比如独立摄影师发布样片、设计师交付预览图、电商团队处理白底商品图水印通常是统一尺寸、统一位置的logo或ID甚至做成半透明斜向平铺铺满整张图。这种全图水印既能防止截图裁剪又能在社交平台被转发的时保持品牌露出。不过麻烦也随之而来。一张一张贴图时间全耗在重复动作上更不说不同图片的尺寸可能不一样每次还要手动调整水印大小和边距。真正让人崩溃的不是贴一张而是800张都要贴每张都要对齐、大小一致、透明度统一。手动操作半小时后眼睛已经分不清哪张贴过、哪张漏了。1.2 全图水印和普通角落水印不是一回事角落水印通常只放在右下角或左上角逻辑很单纯盖住某一小块区域。全图水印一般是平铺模式也就是把同一个logo或文字按固定间距重复铺满整张图像瓷砖一样。这个操作看起来只是重复次数变多实际上它引出了几个新问题水印之间的间距要一致否则视觉上会歪歪扭扭。水印密度太高会压住主体内容密度太低又被轻易裁掉。不同尺寸图片如果按固定坐标平铺铺出来效果会忽密忽疏。这些细节在单图处理时根本不值得想但一旦进入批量流水线就必须定出统一规则。否则同一个文件夹下有的图水印铺成两张有的铺成五张交付出去会很业余。1.3 批量需求里的潜规则统一之外还要不抢内容批量加水印的核心诉求有两个字统一。统一透明度、统一位置、统一尺寸比例甚至统一角落留白。但另一条隐藏原则是不抢内容。特别是电商图和设计预览图主体是商品或设计稿本身水印只是辅助标识。半透明、低饱和度、斜向排列都是为了让水印存在感低一点但攻击盗图者时又能明确宣示所有权。这就意味着水印逻辑不能是把logo直接怼上去而要把透明度、旋转、间距都参数化。手动一张张处理完全没法保证这些参数手工保持一致脚本批量处理反而是最稳定的方案。2. 批量水印方案选型脚本、桌面软件和在线工具怎么选2.1 三类方案的真实差距动手写代码前我其实把桌面端、在线工具和脚本都试了一圈。它们各有各的适用场景放在一张表里看得更清楚方案优点缺点适合场景桌面批量工具上手快图形界面直观参数不够细平铺间距和透明度可调范围有限不同机器装不同软件行为不一致偶尔一次、几十张的小批量在线加水印网站零安装上传即用原图要上传到第三方服务器隐私敏感的不敢用多张高清大图上传慢单文件大小常受限不涉及版权敏感内容、数量少的临时处理本地脚本PythonPillow参数全可控可重复执行不消耗网络流量需要一点代码基础大批量、重复执行、对透明度和位置有自定义要求我常常见到有人把上百张带客户信息的样片传到在线工具里处理传上去之后心里就开始打鼓。其实这个场景用本地脚本是最稳妥的不离开自己电脑跑出问题改代码再跑一次就行。2.2 为什么最终选了Python Pillow选型时我对比过重型的图像框架最后还是决定用Pillow。原因很直接批量加水印这件事本质上是像素级合成不需要目标检测、不需要模型推理只需要把一张图按规则贴到另一张图上。Pillow正好覆盖这块能力安装简单依赖干净跨平台表现也一致。Pillow里核心的操作是paste和alpha_composite配合ImageDraw画文字、配合ImageFont加载字体做平铺水印、单点水印、文字水印基本是十分钟的事。更关键的是它是纯Python库脚本跑完换台机器也能执行几乎不会出现环境水土不服。2.3 为什么不推荐用OpenCV做这件事OpenCV确实强大但用它做批量加水印有点高射炮打蚊子。一是OpenCV的默认通道顺序是BGR跟日常处理图片的习惯不一致新手特别容易在颜色上翻车二是它对中文文字的支持很麻烦需要自己找字体库、配置复杂三是输出图片时编码参数跟Pillow有差异色彩偏差问题更明显。不是说OpenCV不行而是在这个特定任务里Pillow的抽象层级更合适代码量更少可读性也更好。工具不在多顺手最重要。3. 从零写出第一个可用的批量水印脚本3.1 水印叠加不是覆盖而是Alpha合成这是整个脚本里最值得理解的一个点。很多人第一次写水印代码时会直接把水印图paste到背景图上结果发现透明部分要么变成黑色块要么把下面的图完全挡住。原因在于普通paste是把上层像素直接覆盖下去完全不理会上层本身的Alpha通道而真正的水印叠加是一个**Alpha合成Alpha Blending**过程。Alpha合成本质上就是一套加权平均公式结果颜色 背景颜色 × (1 - 水印透明度) 水印颜色 × 水印透明度如果水印像素点的Alpha是100%那这个位置就是纯水印色如果Alpha是0%就完全透出背景。日常见到的半透明水印对每个像素来说都是背景和水印颜色按透明度做加权混合的结果。用生活经验来类比就是透明贴纸你贴上去之后底下的图案还能隐约透出来因为贴纸膜本身有透光性。Pillow里做Alpha合成最稳的是Image.alpha_composite()它处理两张RGBA模式的图片严格按照Alpha通道计算混合结果不会出现把透明区域盖成黑块的问题。3.2 单点水印把logo贴到右下角并保留边距先写一个最基础的函数实现把带透明底的logo贴到图片右下角同时做整体透明度和边距控制。from PIL import Image, ImageOps def add_single_watermark(base_path, logo_path, output_path, positionright_bottom, margin20, opacity0.85): base Image.open(base_path).convert(RGBA) logo Image.open(logo_path).convert(RGBA) # 控制水印整体透明度 alpha logo.split()[3].point(lambda v: int(v * opacity)) logo.putalpha(alpha) w, h base.size lw, lh logo.size if position right_bottom: x, y w - lw - margin, h - lh - margin elif position left_bottom: x, y margin, h - lh - margin elif position center: x, y (w - lw) // 2, (h - lh) // 2 else: x, y margin, margin layer Image.new(RGBA, base.size, (0, 0, 0, 0)) layer.paste(logo, (x, y), logo) result Image.alpha_composite(base, layer) result.convert(RGB).save(output_path, quality95)这段代码里有几个细节值得说明所有图像先统一转成RGBA保证Alpha通道存在避免后续合成报错或不透明区域变成黑块。split()[3]取出的是Alpha通道通过point()方法对每个Alpha值乘以透明度系数就能控制整个logo的淡出程度。合成时先建一个透明layer把logo贴到layer上再通过alpha_composite叠到底图上这样可保留水印的圆角、阴影、半透明渐变这些细节。3.3 平铺全图水印循环铺满整张图单点水印变全图水印其实只是把贴一次改成贴一个规律网格。以平铺间距为步长在整张图上循环写入水印def add_tiled_watermark(base_path, logo_path, output_path, spacing200, opacity0.12, rotate30): base Image.open(base_path).convert(RGBA) logo Image.open(logo_path).convert(RGBA) logo logo.rotate(rotate, expandTrue) alpha logo.split()[3].point(lambda v: int(v * opacity)) logo.putalpha(alpha) w, h base.size lw, lh logo.size layer Image.new(RGBA, base.size, (0, 0, 0, 0)) y 0 while y h: x 0 while x w: layer.paste(logo, (x, y), logo) x lw spacing y lh spacing result Image.alpha_composite(base, layer) result.convert(RGB).save(output_path, quality95)平铺参数有三个最关键spacing控制水印间距opacity控制整体透明度rotate控制水印旋转角度。实际平铺时透明度不要超过20%不然整张图看起来像蒙了一层纱主体内容直接被干扰旋转30度左右会让水印剪裁起来更麻烦对防裁图有点用。这段代码还可以优化成按行偏移错落排列比如奇数行水印右移半个周期视觉上更像马赛克瓷砖防裁切效果也更好。不过基础版已经够日常使用。3.4 批量循环与参数化入口上面两个函数只解决单张图接上批量逻辑才是分分钟的关键。核心做法是用glob扫描某个文件夹下的所有图片再逐个处理import glob import os import argparse def batch_watermark(input_dir, output_dir, modetile, logo_pathNone, textNone, spacing200, opacity0.15, rotate30): os.makedirs(output_dir, exist_okTrue) image_paths glob.glob(os.path.join(input_dir, *)) image_paths [p for p in image_paths if p.lower().endswith((.jpg, .jpeg, .png, .bmp, .webp))] for i, path in enumerate(image_paths, 1): if mode tile: add_tiled_watermark( path, logo_path, os.path.join(output_dir, os.path.basename(path)), spacingspacing, opacityopacity, rotaterotate) else: add_single_watermark( path, logo_path, os.path.join(output_dir, os.path.basename(path)), opacityopacity) print(f处理进度: {i}/{len(image_paths)} - {path}) if __name__ __main__: parser argparse.ArgumentParser(description批量添加水印) parser.add_argument(--input, requiredTrue, help输入目录) parser.add_argument(--output, defaultoutput, help输出目录) parser.add_argument(--mode, defaulttile, choices[tile, single]) parser.add_argument(--logo, defaultlogo.png, help水印图片路径) parser.add_argument(--spacing, typeint, default200, help平铺间距) parser.add_argument(--opacity, typefloat, default0.15, help水印透明度) parser.add_argument(--rotate, typefloat, default30, help水印旋转角度) args parser.parse_args() batch_watermark(args.input, args.output, args.mode, args.logo, spacingargs.spacing, opacityargs.opacity, rotateargs.rotate)这样在命令行执行一次就能搞定整个文件夹python watermark.py --input ./raw_images --output ./watermarked --mode tile --logo mylogo.png --spacing 180 --opacity 0.12 --rotate 30处理过程中打印进度是很有必要的不然批量跑几百张图中间卡住了都很难定位是哪一张出问题。3.5 没有png logo时文字水印怎么做很多人其实没有现成的透明底logo只有一段文字比如个人ID、工作室名、网站域名。这种情况下不需要渲染图片直接用Pillow的ImageDraw画文字就行。from PIL import ImageDraw, ImageFont def add_text_watermark(base_path, text, output_path, font_pathsimhei.ttf, font_size48, opacity0.2, rotate25, spacing260): base Image.open(base_path).convert(RGBA) layer Image.new(RGBA, base.size, (0, 0, 0, 0)) font ImageFont.truetype(font_path, font_size) draw ImageDraw.Draw(layer) w, h base.size # 先测出文字的宽高再按网格平铺 bbox draw.textbbox((0, 0), text, fontfont) text_w, text_h bbox[2] - bbox[0], bbox[3] - bbox[1] y 40 while y h: x 40 while x w: draw.text((x, y), text, fontfont, fill(255, 255, 255, int(255 * opacity))) x text_w spacing y text_h spacing layer layer.rotate(rotate, resampleImage.BICUBIC, expandFalse) result Image.alpha_composite(base, layer) result.convert(RGB).save(output_path, quality95)文字水印有两点容易疏忽字体文件必须真实存在系统自带字体路径各平台不一样写死路径很容易换台机器就报错。建议脚本里做一次字体路径检测。fill的RGBA里Alpha值乘的是opacity也就是文字的透明度。白色低透明度在大部分图片上观感比较好不会像纯黑水印那样显得脏。4. 实测跑批效率数值、踩坑和修正4.1 一批400张大图的真实耗时段我用一批403张图做过测试每张尺寸大概4000×3000像素水印是一张1500×1500的半透明PNG平铺间距200像素。具体环境是中端笔记本Python 3.10Pillow 10.x。结果如下处理模式总耗时单张平均耗时平铺水印约7分30秒约1.1秒单点水印约2分40秒约0.4秒文字水印约5分10秒约0.8秒平铺模式耗时长主要是图层旋转和多次paste导致的计算开销。如果只是赶时间可以去掉rotate参数或降低平铺密度速度会快很多。这个速度对日常交付完全够用毕竟全自动跑批不需要人盯着。4.2 踩坑记录一透明PNG水印贴上去变成黑色块第一次跑的时候我直接把logo用paste贴到底图上结果所有透明区域全都变成了黑色块整整跑完400张之后才发现整批图全废了。问题出在paste的第一种用法paste(image, position)没有把水印图自己的Alpha通道作为mask传进去透明部分被当成纯色覆盖了。正确的做法是传第三个参数masklayer.paste(logo, (x, y), logo) # 第三个参数logo就是mask或者干脆用alpha_composite。这也是为什么前面所有合成逻辑都使用alpha_composite的原因——它可以避免绝大多数透明通道引发的奇怪问题。4.3 踩坑记录二混合目录里JPEG和PNG的文件格式冲突批量处理的文件夹里往往同时有JPG和PNG。JPG本身没有Alpha通道转成RGBA再处理后转回RGB保存反而正常但如果原图是PNG保存时用默认的PNG参数文件体积可能会膨胀。更隐蔽的问题是如果输出目录和输入目录是同一个文件夹脚本会扫描到上一轮已经处理过的图片无限循环处理“水印套水印”。我在脚本里强制输出到独立目录以后再也没有碰见这问题。另外处理完PNG文件时可以加上PNG压缩优化参数result.convert(P, paletteImage.ADAPTIVE).save(output_path, optimizeTrue)当然转成调色板模式会损失颜色精度除非你的流程对文件体积特别敏感否则不建议用。保守做法是保持RGB保存设置合适的qualityJPG一般用85到95之间。4.4 踩坑记录三手机拍摄的图片竖竖横横水印方向不对用Pillow直接打开的JPG很多是带了EXIF旋转信息的预览器能自动转正但脚本处理时读到的像素还是拍摄时的原始方向。结果就是水印按横向图的位置贴出来的图却是竖着的位置完全错位。修复方法很简单利用Pillow自带的ImageOps.exif_transposefrom PIL import ImageOps base Image.open(base_path) base ImageOps.exif_transpose(base).convert(RGBA)这步处理尤其重要只要是手机图、相机原图几乎都带EXIF。加一行代码就能避免水印位置整体偏移。4.5 踩坑记录四中文字体在部分环境里变成方框文字水印在Windows下用默认字体路径一般没事但脚本迁到Linux服务器上之后中文全部变成方框字。原因是Linux环境没有预装中文TrueType字体而Pillow不会自动扫描系统中的中文字体。解决方式是在脚本里做一个字体路径探测按顺序检查几个常见路径import os FONT_CANDIDATES [ C:/Windows/Fonts/msyh.ttc, # Windows 微软雅黑 /usr/share/fonts/truetype/wqy/wqy-microhei.ttc, # Linux 文泉驿微米黑 /System/Library/Fonts/PingFang.ttc, # macOS 苹方 ] def find_font(): for path in FONT_CANDIDATES: if os.path.exists(path): return path raise FileNotFoundError(找不到中文字体文件请手动指定路径)字体文件最好直接放到脚本目录并相对引用这样换机器时行为完全一致。4.6 踩坑记录五透明模式与缩略图预览观感不一致还有个小坑就是平铺水印在100%查看时很淡但在某些缩略图渲染下会显得特别深因为缩略图缩小后水印和背景的像素被混合成更扎眼的色块。这个很难完全规避只能在水印透明度上留点余量。我通常把最终交付图的透明度控制在12%到18%之间既保证水印清晰可辨认又不会在缩略图下喧宾夺主。5. 让批量水印脚本更好用的四个扩展点子5.1 递归处理子目录里的所有图片很多素材库是按日期或主题分子目录的只处理一层目录就得反复跑脚本。给批处理函数加上递归扫描改动很小但非常实用def collect_images(root_dir): result [] for current, _, files in os.walk(root_dir): for f in files: if f.lower().endswith((.jpg, .jpeg, .png, .bmp, .webp)): result.append(os.path.join(current, f)) return result输出时也要保留目录结构否则几百张图全部平铺到一个文件夹里后期找图又会疯掉。5.2 根据原图尺寸动态调整水印大小不同来源的图片尺寸差异可能很大水印固定像素大小在手机上看的很清楚到了4K大图上就变成一个小点。动态缩放水印的规则并不复杂让水印宽度占原图宽度的15%到25%之间再按比例算出高度。target_ratio 0.2 new_width int(w * target_ratio) new_height int(lh * new_width / lw) logo logo.resize((new_width, new_height), Image.LANCZOS)这样就保证了不同尺寸的图水印视觉占比是统一的批量交付时观感更整齐。5.3 按目录或文件名规则套用不同水印比如对外发样片用A版本水印对外发精修预览图用B版本水印。可以在扫描目录后通过文件名前缀来动态决定水印方案。博客里给个简化思路for path in image_paths: if preview in path: use_logo preview_logo.png else: use_logo final_logo.png这种规则化处理的好处是后期不用人工区分只要命名规范脚本可以完全自主决定每一张图用什么水印、什么透明度。真实项目里遇到过的需求还有按客户维度区分水印、按产品线区分水印本质上都是同一套逻辑。5.4 增加干跑预览模式避免整批跑完才发现参数错了这是我最推荐加的一个功能。在脚本里加一个--dry-run参数只处理前3张图输出到preview目录并打印参数不处理全集。这样每次调整透明度、间距、旋转角度后先看几张效果满意后再全量跑。别小看这个小功能它能救回好几次整套图跑完发现水印太大的窘境。if args.dry_run: sample_paths image_paths[:3] else: sample_paths image_paths跑批之前先预览是批量图像处理里成本最低、收益最高的习惯。6. 最后说两个我花了很久才养成的习惯第一个习惯是所有输出目录都和输入目录分开。一开始我图省事直接覆盖原文件结果某次参数配错把一批原图的清晰度永久损掉了后来所有图像批处理都强制写新目录回滚几乎零成本。第二个习惯是保持脚本幂等——同一个输入、同一套参数无论跑多少次输出的图都应该完全一致。这意味着不要依赖随机数、不要用系统当前时间做文件名所有变量都从参数显式传入。坚持这个原则后客户说上次的水印位置往左上挪一点我只需要改参数重新跑一遍出来的结果可预测、可比对。批量加水印这个需求看起来不大但它背后其实是图像合成、文件格式、字体渲染、目录管理等一堆小细节的组合。把这一套流程跑通之后以后再遇到这批图全部要加水印的需求我基本不慌因为脚本几分钟就能完成。希望这份经验对你也有用。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →