查看: 144|回复: 3

Python Pillow图像处理:缩略图水印与EXIF清理

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
很多开发者第一次用脚本处理图片时,卡住的往往不是算法,而是“读进来的到底是什么”。同样是 .jpg,有的带 EXIF 方向标签,手机竖拍存成横的、显示时才旋转;有的带 ICC 色彩配置;有的画质低到放大全是马赛克。.png 又分 8 位/16 位、调色板模式和真彩色模式,.gif 可能是动画,.webp 还分有损无损。只靠文件后缀去猜格式,脚本迟早出错。

Pillow 解决的正是这一层问题:它不负责目标检测、人脸识别这类计算机视觉算法,而是负责图片怎么读、怎么存、怎么缩放裁剪、怎么加水印、怎么清理元数据。批量转格式、生成缩略图、打水印、剥离 GPS 信息这类管线型需求,用 Pillow 最省力;需要做边缘、轮廓、模型推理时,再交给 OpenCV 或 PyTorch,两者是上下游关系而不是替代关系。

一、先建立四个核心模型

1)图像模式 mode 决定每个像素是什么。L 是 8 位灰度,getpixel 返回单个整数;RGB 是三通道 24 位,返回三元组;RGBA 多一个透明通道,返回四元组;P 是调色板索引,常见于 GIF 和小体积 PNG;CMYK 用于印刷。最容易踩的坑是把 RGBA 直接存成 JPEG,结果不是报错就是透明区域变黑,必须先 convert('RGB')。

2)坐标系原点在左上角,x 向右、y 向下,和屏幕坐标一致,和数学坐标相反。getpixel((10, 20)) 里的 10 是列、20 是行,别把顺序写反。

3)Image 对象有四件套:format 是文件格式名,由文件头 magic bytes 判定而不是扩展名;mode 是通道模式;size 是 (宽, 高),与坐标 (x, y) 顺序一致;info 是随文件读入的元信息字典,包含 dpi、icc_profile 等,Pillow 10+ 中 EXIF 建议用 getexif() 单独取。

4)懒加载是性能关键。Image.open() 只读文件头和少量元数据,此时 size、mode、format 已经可用,像素数据还没解码;直到调用 load() 或第一次访问像素时才真正解码进内存。批量扫描目录时,可以先 open 一遍拿到全部尺寸、格式、EXIF,再决定哪些需要解码,避免把上万张图一次性读进内存。

二、常用 API 与参数含义

文件 I/O:Image.open(fp) 打开图片;Image.new(mode, size, color) 新建画布;img.save(fp, format=None, **params) 负责编码,常用参数有 quality、optimize、progressive、dpi、subsampling;img.convert(mode) 做模式转换。

几何变换:crop(box) 裁剪,box 为 (left, top, right, bottom),右下是开区间;resize(size, resample) 会按指定尺寸硬缩放,可能变形;thumbnail(size) 原地等比缩小且绝不放大,是生成缩略图的正确接口;rotate(angle, expand=False) 旋转,expand=True 才会扩大画布防止裁边;transpose 用于镜像翻转;paste(im, box, mask) 做粘贴合成,mask 决定透明度。

像素操作:getpixel/putpixel 适合调试,批量处理不要用;getdata/putdata 整幅读写;split 分离通道,Image.merge 合并通道;getbbox、getextrema、histogram 用于内容范围、最值和直方图分析。

绘制与文字:ImageDraw.Draw(img) 直接在图上绘制;rectangle、ellipse、line、text 是常用方法;字体用 ImageFont.truetype 加载。注意 Pillow 10.1+ 中 draw.text 的 font 参数已成为必填项,老代码不传 font 在新版本会直接报错,这是跨版本兼容的高频坑。

滤镜增强:ImageFilter 提供 GaussianBlur、UnsharpMask、FIND_EDGES、CONTOUR、MedianFilter 等;ImageEnhance 的 Brightness、Contrast、Color、Sharpness 四件套按倍率调整;ImageOps 提供 autocontrast、equalize、flip、mirror、fit 和 exif_transpose。

元数据:img.getexif() 返回类字典对象,274 是方向标签,271、272 是厂商和机型,306 是拍摄时间,34853 是 GPSInfo。手机照片必须先做 ImageOps.exif_transpose 方向转正,再做裁剪缩放,否则后续几何操作全建立在错误方向上。

三、八组可直接运行的示例

示例 1:读取、查信息、转格式、保存。
  1. from PIL import Image
  2. img = Image.open('photo.jpg')
  3. print(img.format, img.mode, img.size)
  4. # RGBA 直接存 JPEG 会报错或黑底,先转 RGB
  5. img.convert('RGB').save('photo.webp', quality=85, method=6)
  6. img.convert('RGB').save('photo_opt.jpg', quality=85, optimize=True, progressive=True)
复制代码
quality 对 JPEG 和 WebP 都是 0 到 100,80 到 90 之间通常能兼顾体积与画质;optimize=True 会做二次编码压缩但更慢;progressive=True 输出渐进式 JPEG,适合网络加载。

示例 2:批量缩略图流水线。
  1. from pathlib import Path
  2. from PIL import Image, ImageOps
  3. def make_thumb(src: Path, dst: Path, size=(800, 800)):
  4.     with Image.open(src) as img:
  5.         img = ImageOps.exif_transpose(img)   # 先按 EXIF 方向转正
  6.         img.thumbnail(size)                  # 原地等比缩小,不放大
  7.         img.convert('RGB').save(dst, quality=82, optimize=True)
  8. src_dir = Path('raw_photos')
  9. dst_dir = Path('thumbs')
  10. dst_dir.mkdir(exist_ok=True)
  11. for f in src_dir.glob('*.jpg'):
  12.     make_thumb(f, dst_dir / f.name)
复制代码
thumbnail 和 resize 的区别要记牢:thumbnail 保持宽高比且绝不放大,resize 会把图硬拉到指定尺寸导致变形。生成缩略图应始终用前者。

示例 3:EXIF 隐私清理与方向修正。
  1. from PIL import Image, ImageOps
  2. def sanitize(src, dst):
  3.     with Image.open(src) as img:
  4.         img = ImageOps.exif_transpose(img)   # 先把方向转正
  5.         img = img.convert('RGB')
  6.         img.save(dst, quality=88, optimize=True)  # 不传 exif 即剥离
  7. sanitize('origin.jpg', 'clean.jpg')
  8. print(Image.open('clean.jpg').getexif())  # 空 Exif 对象
复制代码
保存时不传 exif 参数就能实现隐私清理,GPS 坐标在 GPSInfo 字段中,剥离 EXIF 时会一并清除。顺序上必须先是 exif_transpose,再是 crop、resize,否则方向信息丢失后无法补救。

示例 4:文字水印与透明角标。
  1. from PIL import Image, ImageDraw, ImageFont
  2. def add_watermark(img, text='© 2026 Album'):
  3.     img = img.convert('RGBA')
  4.     overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
  5.     draw = ImageDraw.Draw(overlay)
  6.     font = ImageFont.truetype('C:/Windows/Fonts/arial.ttf', size=36)
  7.     bbox = draw.textbbox((0, 0), text, font=font)
  8.     tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
  9.     x, y = img.width - tw - 30, img.height - th - 30
  10.     draw.text((x, y), text, fill=(255, 255, 255, 180), font=font)
  11.     return Image.alpha_composite(img, overlay)
  12. photo = Image.open('vacation.jpg')
  13. add_watermark(photo).convert('RGB').save('wm.jpg', quality=90)
复制代码
水印的正确姿势是先画到透明 overlay 上再 alpha_composite 合成,而不是直接在原图上画,这样不破坏原图、可反复叠加,fill 的 alpha 值还能实现半透明。draw.text 的坐标是文字左上角,所以先用 textbbox 量出宽高才能定位到右下角。字体路径按系统替换即可。

示例 5:圆角头像与遮罩合成。
  1. from PIL import Image, ImageDraw, ImageOps
  2. def rounded_avatar(src, dst, size=256, radius=48):
  3.     img = Image.open(src).convert('RGBA')
  4.     img = ImageOps.fit(img, (size, size))     # 居中裁剪成正方形
  5.     mask = Image.new('L', (size, size), 0)    # 单通道灰度掩码
  6.     draw = ImageDraw.Draw(mask)
  7.     draw.rounded_rectangle((0, 0, size, size), radius=radius, fill=255)
  8.     result = Image.new('RGBA', (size, size), (0, 0, 0, 0))
  9.     result.paste(img, (0, 0), mask=mask)      # 白色不透明,黑色透明
  10.     result.convert('RGB').save(dst)
  11. rounded_avatar('avatar.jpg', 'avatar_round.png', size=256, radius=48)
复制代码
paste 的 mask 必须是 L 或 1 模式的单通道灰度图,白色不透明、黑色透明。圆角的本质就是拿一张圆角灰度图当透明度模板。

示例 6:九宫格拼图。
  1. from PIL import Image, ImageOps
  2. def make_grid(tiles, cols=3, cell=300, gap=10, bg=(255, 255, 255)):
  3.     rows = (len(tiles) + cols - 1) // cols
  4.     W = cols * cell + (cols + 1) * gap
  5.     H = rows * cell + (rows + 1) * gap
  6.     canvas = Image.new('RGB', (W, H), bg)
  7.     for i, path in enumerate(tiles):
  8.         with Image.open(path) as im:
  9.             im = ImageOps.exif_transpose(im)
  10.             im = ImageOps.fit(im, (cell, cell))
  11.             r, c = divmod(i, cols)
  12.             x = gap + c * (cell + gap)
  13.             y = gap + r * (cell + gap)
  14.             canvas.paste(im, (x, y))
  15.     return canvas
  16. make_grid(['a.jpg', 'b.jpg', 'c.jpg', 'd.jpg']).save('grid.jpg', quality=90)
复制代码
divmod(i, cols) 一次拿到行号和列号;粘贴坐标等于间隙加上序号乘以“格子加间隙”,公式写清楚能省大量调试时间。ImageOps.fit 负责等比裁剪到目标尺寸,不会留白。

示例 7:图像增强与风格滤镜。
  1. from PIL import Image, ImageEnhance, ImageFilter, ImageOps
  2. def vintage_filter(img, strength=1.0):
  3.     img = ImageEnhance.Color(img).enhance(0.65 * strength)
  4.     img = ImageEnhance.Brightness(img).enhance(1.05 * strength)
  5.     img = ImageEnhance.Contrast(img).enhance(1.15 * strength)
  6.     overlay = Image.new('RGB', img.size, (255, 214, 170))
  7.     img = Image.blend(img, overlay, 0.15 * strength)
  8.     return img.filter(ImageFilter.GaussianBlur(radius=0.5))
  9. photo = Image.open('street.jpg')
  10. vintage_filter(photo).save('vintage.jpg', quality=90)
  11. bw = ImageOps.equalize(photo.convert('L'))
  12. bw.filter(ImageFilter.UnsharpMask(radius=2, percent=120)).save('bw.jpg')
复制代码
Image.blend(im1, im2, alpha) 要求两张图 mode 和 size 完全一致,alpha 表示叠加权重,这里 0.15 就是 15% 的暖色。滤镜链本质上是把一串像素级操作串行组合,性能和手写循环不是一个量级。

示例 8:GIF 逐帧处理与转 WebP。
  1. from PIL import Image, ImageSequence
  2. def gif_to_webp_frames(src, dst_dir):
  3.     img = Image.open(src)
  4.     for i, frame in enumerate(ImageSequence.Iterator(img)):
  5.         if i % 3 == 0:                       # 每 3 帧抽 1 帧
  6.             frame.convert('RGB').save(f'{dst_dir}/frame_{i:03d}.webp', quality=80)
  7. gif_to_webp_frames('anim.gif', 'frames')
复制代码
ImageSequence.Iterator 返回的帧对象共享底层数据,处理完就失效,不要长期持有。逐帧另存会丢失 duration、loop 等控制信息,这些参数在 img.info['duration'] 里;要保留动画需要手动回填 save_all=True 和 append_images 参数,否则输出只是一张静态图。

四、常见错误与排查思路

RGBA 存 JPEG 报错或黑底:先 convert('RGB') 再保存。
缩略图变形:用了 resize 而不是 thumbnail,后者才保持比例且不放大。
方向不对:exif_transpose 调用太晚,必须在所有几何操作之前。
EXIF 没清掉:保存时仍传了 exif 参数,Pillow 10+ 下只要不传就默认不保留。
draw.text 报参数错误:Pillow 10.1+ 中 font 已是必填。
blend 报错:两张图 mode 或 size 不一致,先统一再混合。
paste 全透明或全黑:mask 不是 L 或 1 模式。
批量处理极慢:在循环里用 getpixel/putpixel,应改用 getdata 或与 numpy 互操作。
目录扫描占满内存:open 是懒加载,扫描阶段不要 load。
GIF 转出来不动:逐帧保存没带 save_all 和 append_images。

五、性能与工程落地建议

批量场景先做轻量索引:只用 open 读取 size、format 和 EXIF,不触发像素解码,再决定哪些图需要真正处理,这是“先索引后处理”架构的基础。

避免逐像素 Python 循环,优先使用 Pillow 内置的 resize、filter、enhance 等 C 层实现;需要与 numpy 交互时走 tobytes 与 Image.frombytes,注意字节序和步长必须严格匹配。

编码参数按场景取舍:optimize=True 体积更小但更慢,progressive=True 利于网络加载,WebP 的 method=6 压缩更充分但耗时更长,quality 建议落在 80 到 90 之间。

以一个“家庭相册整理与隐私清理工具”为例,可以拆成三层:扫描层用懒加载遍历目录,收集尺寸、格式、方向和 GPS 字段;处理层统一做 exif_transpose 和 convert('RGB'),再按需生成缩略图或水印;输出层保存时不带 exif,顺便统一成目标格式。处理链中始终用 img.copy() 保护原图,几何操作前先转正,透明图存 JPEG 前先转 RGB,这三条能挡掉大部分线上事故。

六、总结

Pillow 的价值在于让你对一张图的每个像素、每段元数据、每种格式拥有完整控制权。记住四个点:mode 决定像素结构,thumbnail 才是缩略图正道,exif_transpose 要放在最前面,save 不传 exif 就是最干脆的隐私清理。把这几条和批量管线结合起来,上千张手机照片的转格式、加水印、去 GPS,几十行脚本就能稳稳跑完。
回复

使用道具 举报

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

Re: Python Pillow图像处理:缩略图水印与EXIF清理

楼主这篇整理得很系统,尤其“四个核心模型”那部分,把 format 由 magic bytes 判定、懒加载、坐标系这些容易忽略的点讲清楚了,比单纯罗列 API 有用得多。 几个我实际踩过、和帖子里对得上的坑:RGBA 直接存 JPEG 确实会出问题,先 convert('RGB') 最稳;手机照片如果不在裁剪缩放前做 exif_transpose,后面所有几何操作都白搭;thumbnail 不会放大这一点也很关键,批量生成缩略图时比 resize 省心。另外 Pillow 10.1+ 里 draw.text 的 font 必填,老脚本升级时很容易突然报错,这个提醒很及时。 有个小地方:示例 2 的代码到 for f in src_dir.glob('*.jpg 这里好像被截断了,后面异常处理和保存路径的部分没贴完,方便的话可以补全一下。批量流水线里单张图损坏或模式异常很常见,建议每张图单独 try 一下,别让一张坏图中断整批任务。期待后续示例。
回复 支持 反对

使用道具 举报

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

Re: Python Pillow图像处理:缩略图水印与EXIF清理

这篇整理得很系统,尤其是把 mode、坐标、四件套和懒加载先讲清楚,再进入 API 和示例,读起来不容易乱。RGBA 直接存 JPEG 透明区域变黑、手机竖拍要先 exif_transpose 再裁剪缩放,这两个坑确实很典型;thumbnail 原地等比缩小且不放大这个点也容易被 resize 混用。 示例 2 最后 glob 那里好像被截断了,不知道后面有没有加异常处理和跳过非图片文件的逻辑?批量跑目录时如果混进损坏文件或 .webp、.png,感觉这部分也很值得展开。整体上这篇对做批量缩略图、水印和 EXIF 清理的管线很有参考价值。
回复 支持 反对

使用道具 举报

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

Re: Python Pillow图像处理:缩略图水印与EXIF清理

楼主这篇总结得很系统,尤其是把“文件后缀不等于真实格式”和懒加载这两点讲透了。我之前批量做缩略图时就踩过 RGBA 直接存 JPEG 的坑,透明区域变黑,后来统一先 convert('RGB') 才稳定;另外 exif_transpose 一定要放在 thumbnail 和 crop 之前,不然手机竖拍图的方向会全乱,这个顺序确实很关键。Pillow 10.1+ 里 draw.text 的 font 必填也是升级时容易中招的地方,感谢提醒。 想请教一下,像 GIF 动画和 16 位 PNG 这种在批量缩略图管线里,楼主一般是怎么分流的?是先按 format/mode 判断再统一转 RGB,还是直接 try 后回退处理?
回复 支持 反对

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-11-3 04:55 , Processed in 0.022232 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部