查看: 332|回复: 3

Python Pillow加水印拼九宫格:中文路径与EXIF旋转

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
装修现场照片一天几十张,月底要给业主出"施工日记",真正麻烦的不是拍照,而是两点:照片看不出拍摄日期,几百张图散在目录里没法翻。下面这套纯 Python + Pillow 的本地小工具就解决这两件事——批量在照片右下角叠加「日期 + 工种」水印,再按 3×3 拼成日报图,全程不联网、双击即用。文中三个坑(中文路径、EXIF 旋转、中文水印乱码)都是实战里最容易卡住批处理流程的地方。

一、执行流程与依赖

整个脚本只做四步:扫描目标目录下的 .jpg/.jpeg/.png;读取拍摄时间(优先 EXIF,取不到就退回文件修改时间);在图片右下角叠加半透明文字水印(日期 + 自定义工种标签);把当天的照片每 9 张拼成一张九宫格,输出到 日报/ 目录。

依赖只有一个:
  1. pip install Pillow
复制代码

本文示例代码在 Python 3.11 + Pillow 10 上实测通过,字体路径需按本机实际情况调整。

二、坑一:中文路径不能靠字符串硬拼

Windows 下路径带中文(例如 D:/装修照片/水电/)时,最稳的写法是全程使用 pathlib.Path,它内部通过 os.fspath 做正确转码,避免 open() 抛 FileNotFoundError: [Errno 2]。
  1. from pathlib import Path
  2. root = Path(r"D:/装修照片/水电")
  3. imgs = sorted(p for p in root.glob("*") if p.suffix.lower() in (".jpg", ".jpeg", ".png"))
复制代码

反例是用 os.listdir() 拿到文件名再手动拼字符串,这种写法在 open 阶段极易因为 GBK 编码问题直接炸掉。用 Path 一步到位,还能顺便用 suffix 做后缀过滤、用 sorted 保证处理顺序稳定。

三、坑二:EXIF Orientation 导致图片"横着读"

很多手机照片在 Windows 看图里方向正常,Pillow 打开却是旋转 90° 的。原因是旋转信息存在 EXIF 的 Orientation 标签里,看图软件会自动纠正,而 Pillow 默认不纠正。批量处理前必须统一处理:
  1. from PIL import Image, ExifTags
  2. def fix_orientation(img):
  3.     try:
  4.         exif = img._getexif()
  5.         if not exif:
  6.             return img
  7.         for k, v in exif.items():
  8.             if ExifTags.TAGS.get(k) == "Orientation":
  9.                 if v == 3:
  10.                     img = img.rotate(180, expand=True)
  11.                 elif v == 6:
  12.                     img = img.rotate(-90, expand=True)
  13.                 elif v == 8:
  14.                     img = img.rotate(90, expand=True)
  15.     except Exception:
  16.         pass
  17.     return img
复制代码

这里对应的是三个常见的 Orientation 取值:3 表示需要转 180°,6 需要顺时针转 90°(即 rotate(-90)),8 需要逆时针转 90°(即 rotate(90)),expand=True 保证旋转后画布跟着扩展,不会被裁掉边角。异常整体吞掉是为了让个别 EXIF 损坏的图片不至于中断整批任务。

关键顺序:纠正必须放在 resize / thumbnail 之前。不纠正直接拼图,九宫格里会有一半图片是歪的。

四、坑三:中文水印乱码,必须指定字体文件

Pillow 的 ImageDraw.text 默认字体不带中文字形,直接写中文会输出方块。必须显式加载一个中文字体 ttf/ttc:
  1. from PIL import ImageDraw, ImageFont
  2. font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 36)  # 微软雅黑
复制代码

两个细节:一是 truetype 的字体路径同样可能含中文,用 Path 传入即可,不要手工拼接反斜杠;二是字号要跟着图片尺寸走,别把像素值写死,否则大图看不清、小图盖满画面。

叠加水印的函数如下,思路是先画一条半透明黑底条,再在上面写白字:
  1. def add_watermark(img, text):
  2.     draw = ImageDraw.Draw(img, "RGBA")
  3.     w, h = img.size
  4.     # 半透明黑底条
  5.     draw.rectangle([(w-360, h-60), (w, h)], fill=(0, 0, 0, 120))
  6.     draw.text((w-350, h-48), text, font=font, fill=(255, 255, 255, 230))
  7.     return img
复制代码

ImageDraw.Draw(img, "RGBA") 是让 120 / 230 这类 alpha 值生效的前提,如果底层图是 RGB 模式,半透明会被直接丢掉。另外原文明确提醒:水印要打在缩略图上而不是原图画质上,既看得清又不破坏原图。落到函数组织上,就是把 add_watermark 放到 thumbnail 之后调用;此时注意矩形偏移量(w-360、h-60、w-350、h-48)是按缩略图尺寸调的固定值,需要与实际 thumb 尺寸对齐,否则文字会顶出画面。

五、九宫格拼图

把若干缩略图按 3 列贴到一张灰色底图上:
  1. def make_grid(images, cols=3, thumb=360, gap=8):
  2.     rows = (len(images) + cols - 1) // cols
  3.     grid = Image.new("RGB", (cols*thumb + (cols+1)*gap, rows*thumb + (rows+1)*gap), (240,240,240))
  4.     for i, im in enumerate(images):
  5.         im = fix_orientation(im).convert("RGB")
  6.         im.thumbnail((thumb, thumb))
  7.         r, c = divmod(i, cols)
  8.         x = gap + c*(thumb+gap)
  9.         y = gap + r*(thumb+gap)
  10.         grid.paste(im, (x, y))
  11.     return grid
复制代码

几个参数含义:cols 是列数(默认 3,即九宫格);thumb 是单元格最大边长,thumbnail 会等比例缩放且不拉伸变形,比手动 resize 安全得多;gap 是格子间距,同时参与画布尺寸计算(多留一格,保证四周也有留白)。行数用 (len+cols-1)//cols 向上取整,这样不足 9 张也不会出错。convert("RGB") 在粘贴前执行,避免 RGBA 或 P 模式图片粘到 RGB 画布上出现混色问题。

六、按天分组生成日报

用拍摄日期做分组键,每天输出一张日报图:
  1. from collections import defaultdict
  2. from datetime import datetime
  3. groups = defaultdict(list)
  4. for p in imgs:
  5.     d = get_shoot_date(p)  # 读 EXIF 拍摄时间,取不到退回文件 mtime
  6.     groups[d.strftime("%Y-%m-%d")].append(p)
  7. for day, paths in groups.items():
  8.     day_imgs = [add_watermark(fix_orientation(Image.open(p)), f"{day} 水电") for p in paths[:9]]
  9.     grid = make_grid(day_imgs)
  10.     grid.save(out_dir / f"{day}_施工日记.png")
复制代码

get_shoot_date 的实现要点是:先尝试从 EXIF 中取拍摄时间(DateTimeOriginal 一类标签),解析失败或标签不存在时,用文件状态的修改时间(st_mtime)兜底,两者统一转成 datetime 再格式化。paths[:9] 截断保证单张日报最多 9 格,超出的照片应另起一张图或按批次切分。输出文件名用 Path 拼接,避免再次踩中文路径的坑。

七、避坑清单

1)路径一律用 pathlib.Path,不要 os.listdir() 拼字符串,绕开中文编码雷区;

2)开图先 fix_orientation 纠正 EXIF 旋转,再做缩放和拼图,顺序不能反;

3)中文水印必须用 ImageFont.truetype 指定中文字体文件,否则输出方块乱码;字号按图片尺寸缩放,不写死像素;

4)水印打在缩略图上,原图保持干净;半透明填充要求 Draw 使用 "RGBA" 模式;

5)拼图用 thumbnail 等比缩放,比硬编码 resize 稳,不会出现拉伸变形;

6)单张日报固定 9 格时记得切片,避免一天几十张图全塞进一张画布。

把这套脚本跑起来,每天收工前几十秒就能生成一张可直接发给业主的施工日报,比零散发图专业得多。真正需要改动的通常只有三处:照片根目录、字体文件路径、以及拼接在水印里的工种标签文本。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Python Pillow加水印拼九宫格:中文路径与EXIF旋转

感谢分享,这套思路很实用,尤其是把三个坑单独拎出来讲,做批处理时确实最容易在这些地方反复踩。 中文路径用 pathlib 全流程处理这点非常认同,之前用 os.listdir 加字符串拼接在带中文的目录上吃过亏,换成 Path 之后稳定很多。EXIF Orientation 那段也说到点子上了,手机照片在资源管理器里看方向正常,Pillow 打开却是歪的,很多人一开始会以为是图片本身的问题。把纠正放在 resize/thumbnail 之前这个顺序提醒得也很关键,否则九宫格里歪几张,整张日报图就废了。 另外看帖子末尾好像被截断了,make_grid 里 fix_orientation 那行之后的内容没显示出来,九宫格函数后半段和实际调用部分是不是漏了?方便的话可以补一下,尤其是缩略图 thumbnail 之后水印坐标要跟着实际尺寸走这块,能贴个动态计算偏移量的写法就更好了,固定值在批量处理不同尺寸照片时容易顶出画面。 还有个小疑问:水印文字里的工种标签如果是中文,字体加载没问题的话应该可以正常显示,但如果在 Linux 或 Mac 上跑,字体路径就得换,楼主有没有考虑过做成跨平台自动探测字体的逻辑?再次感谢,收藏了。
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Python Pillow加水印拼九宫格:中文路径与EXIF旋转

这套思路挺实用的,尤其是把三个坑单独拎出来讲,做过批量图片处理的人应该都有共鸣。顺着楼主的思路补充几个自己踩过的点: EXIF 方向纠正其实 Pillow 有现成的 `ImageOps.exif_transpose()`,一行就能覆盖 Orientation 的各种取值,比手写 rotate 分支更省心,而且它会把处理后的 Orientation 标记重置掉,避免同一张图重复处理时又被转一次——手写 rotate 的话这一点容易漏。如果楼主想保留更多兼容性,可以在 `fix_orientation` 里先试 `exif_transpose`,异常再回退到手动分支。 另外半透明水印这里有个容易忽略的收尾问题:用 `ImageDraw.Draw(img, "RGBA")` 画完,图的模式还是 `RGB` 的话没问题,但如果后续某一步把图转成了 `RGBA`,保存成 JPEG 时会直接抛 `cannot write mode RGBA as JPEG`,记得 `img.convert("RGB")` 一下再 `save()`。九宫格拼图那张底图用 `RGB` 起就规避了这个问题,但单张加水印后落盘那步值得多看一眼。 水印偏移量按 thumb 固定值 360 来调这一点,楼主自己也提醒了,实际用的时候建议把矩形宽高改成 `thumb` 的比例,比如 `int(thumb * 0.35)`,
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Python Pillow加水印拼九宫格:中文路径与EXIF旋转

这套流程挺实用的,尤其是把中文路径、EXIF Orientation、中文水印字体这三个坑单独拎出来讲,批量处理时确实很容易卡在这些地方。用 pathlib.Path 全程处理路径,再在 resize/thumbnail 之前先纠正方向,这个顺序提醒得很关键,不然九宫格里很容易出现歪图。水印放在缩略图之后、用 RGBA 模式画半透明底条,也更符合实际出日报图的需求。 有个小提醒:末尾 make_grid 的代码贴到 im = fix_orientat 就断了,后面缩略图缩放和贴图部分没显示完整,如果方便的话可以补全一下,大家照着跑会更顺。另外 add_watermark 里的矩形偏移和字号目前是固定值,后续如果 thumb 尺寸调整,建议按比例算,这样换尺寸时不容易把文字顶出画面。整体思路很清楚,感谢分享。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-4 08:25 , Processed in 0.021653 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部