Python批量处理图片:自动缩放、重命名与加水印脚本
使用Python的Pillow库编写实用的批量图片处理脚本,涵盖EXIF方向校正、保持纵横比等比缩放、独立透明图层文字水印添加以及格式化批量重命名与防覆盖保存。
一、批处理整体流水线架构与准备工作
构建一个稳定可靠的批量图片处理脚本,首要原则是数据安全与幂等性。处理程序绝不能在原始文件上直接执行原地覆写(In-place Overwrite),而应采用独立的输出目录。一旦脚本在处理到第 500 张图片时因磁盘写满或格式损坏意外中断,未完成的批次和原始文件依然完好无损。
Python 生态中处理位图的首选库是 Pillow(PIL 的现代分支)。它底层依赖高度优化的 C 扩展库,支持 JPEG、PNG、WebP、TIFF 等主流图像格式的解码与编码。可以通过标准包管理器进行安装:
整套批处理工作流包含五个阶段:目录过滤扫描、EXIF 方向校正、等比约束缩放、独立透明图层水印合成,以及按序列号规范导出。完整数据流如下所示:
在遍历目录时,我们使用 Python 标准库的 pathlib.Path。相比老旧的 os.listdir,Path.iterdir() 提供了面向对象的路径操作方法,能够准确过滤 .jpg、.jpeg、.png、.webp 等扩展名,同时忽略隐藏文件(如 macOS 自动生成的 .DS_Store)。
二、保持纵横比的等比缩放原理与实现
许多初学者在处理缩放时常直接调用 img.resize((1920, 1080))。若原图是正方形(1:1)或竖向拍摄的手机人像(9:16),强行指定固定的宽高将直接导致画面被横向或纵向拉伸,产生严重的形变畸变。
正确的工程方案是设定一个目标约束盒(Bounding Box),例如宽上限 1920 像素、高上限 1080 像素,然后计算原图在此约束盒内的最大等比容纳尺寸。
1. 数学缩放系数计算
假设原图尺寸为 \((W_0, H_0)\),目标约束盒为 \((W_, H_)\)。宽向与高向缩放比分别为:
scale = min(W_max / W_0, H_max / H_0)
如果计算得到的 scale >= 1.0,说明原图本身小于目标尺寸,此时应根据业务需求选择保持原样,避免将小图硬性放大导致像素马赛克;若 scale < 1.0,则新尺寸为 (int(W_0 * scale), int(H_0 * scale))。
2. 重采样滤镜的选择
Pillow 库内置了 image.thumbnail((max_w, max_h), resample=Image.Resampling.LANCZOS) 方法,该方法直接在图像对象内部完成等比计算与原位收缩。需要注意的是,缩小图像必须使用 Image.Resampling.LANCZOS(在老版本 Pillow 中写作 Image.ANTIALIAS),它基于 Sinc 插值核过滤高频成分,能够最大限度保留细节并消除斜线锯齿与摩尔纹。
三、独立透明图层水印与九宫格定位
在图片上叠加文字水印时,直接在原图的 Draw 画布上绘制半透明文本(例如使用带 Alpha 通道的 RGBA 色值)在部分 Pillow 模式下无法获得平滑的半透明融合效果。更严重的是,一旦绘制失误,原图的像素便无法还原。
业界规范的做法是采用双图层 Alpha 合成法(Alpha Composite):
- 将已缩放的底图转换为
RGBA模式; - 创建一张与底图尺寸完全一致的独立全透明空白画布:
Image.new("RGBA", size, (0, 0, 0, 0)); - 在独立透明画布上绘制指定颜色与透明度(如白色、Alpha 值为 120)的水印文本;
- 调用
Image.alpha_composite(base_rgba, watermark_layer)执行逐像素的无损透明度混合。
文本尺寸度量与九宫格坐标定位
在现代 Pillow 版本中,度量文本像素尺寸应使用 font.getbbox(text) 替代已废弃的 getsize()。getbbox() 返回 (left, top, right, bottom) 四元组,由此可精确得出文本实际渲染宽度 text_w = right - left 与高度 text_h = bottom - top。
以最常用的“右下角”水印为例,设定的安全边距(Margin)为 20 像素,则水印绘制的起始坐标计算公式为:
pos_x = img_width - text_width - margin
pos_y = img_height - text_height - margin
ImageFont.load_default(),只能绘制基本 ASCII 英文字符且无法调整字号。绘制中文必须传入有效的 TrueType/OpenType 字体文件路径(如 Windows 下的 msyh.ttc、macOS 下的 PingFang.ttc 或 Linux 下的开源 NotoSansCJK-Regular.ttc)。脚本中应设置字体回退机制,确保路径不存在时不会导致脚本崩溃。
四、规范化批量重命名与防覆盖保存
相机或手机导出的原始文件名通常为 IMG_20241024_152301.jpg 或 DSC08942.JPG,长短不一且夹杂大写后缀。在批处理中,重命名模块应实现:
- 排序稳定性:在遍历文件前先执行
sorted()排序,确保多次运行或在不同操作系统文件系统(如 NTFS 与 APFS)下文件的序号分配严格一致。 - 零填充序列号:使用格式化字符串
f"{prefix}_{idx:04d}.{ext}"(例如product_0001.jpg、product_0002.jpg),方便操作系统按名称正确自然排序,避免出现1, 10, 2的排版错乱。 - 色彩空间转码与 JPEG 陷阱:由于前序水印合成阶段将图像转换为了含 Alpha 透明通道的
RGBA模式,而 JPEG 规范不支持透明通道。若直接调用img.save("out.jpg"),Pillow 会直接抛出OSError: cannot write mode RGBA as JPEG。必须在存盘前通过img.convert("RGB")将透明背景填充为纯白或消除透明通道。
五、完整工程级批处理脚本代码
以下是整合了 EXIF 方向纠正、约束盒等比缩放、独立图层水印合成与规范序号重命名的完整 Python 脚本。代码结构清晰,可直接作为命令行工具或导入其他模块使用:
# -*- coding: utf-8 -*-
"""
批量图片自动化处理脚本:等比缩放、数字序号重命名与独立透明图层水印
运行环境要求:Python 3.8+,Pillow >= 9.0.0
"""
from pathlib import Path
from typing import Tuple, Optional
from PIL import Image, ImageDraw, ImageFont, ImageOps
def get_available_font(font_size: int = 24) -> ImageFont.ImageFont:
"""
尝试从常见系统路径加载中文字体,若未找到则安全降级到默认字体。
"""
candidate_fonts = [
# macOS 常见中文字体
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STHeiti Light.ttc",
# Windows 常见中文字体
"C:\Windows\Fonts\msyh.ttc",
"C:\Windows\Fonts\simhei.ttf",
# Linux 常见开源中文字体
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
]
for font_path in candidate_fonts:
if Path(font_path).is_file():
try:
return ImageFont.truetype(font_path, font_size)
except Exception:
continue
# 降级:使用 Pillow 默认位图字体
return ImageFont.load_default()
def apply_watermark(
base_img: Image.Image,
text: str,
font: ImageFont.ImageFont,
opacity: int = 120,
margin: int = 20
) -> Image.Image:
"""
通过独立全透明 RGBA 画布合成半透明文字水印(默认右下角对齐)。
"""
# 统一将底图转为 RGBA 模式
rgba_base = base_img.convert("RGBA")
w, h = rgba_base.size
# 创建相同尺寸的全透明图层
watermark_layer = Image.new("RGBA", (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(watermark_layer)
# 测量文本渲染包围盒
bbox = font.getbbox(text)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
# 右下角坐标锚定(考虑文本包围盒的 offset 漂移)
pos_x = max(0, w - text_w - margin - bbox[0])
pos_y = max(0, h - text_h - margin - bbox[1])
# 绘制半透明白色文字 (R, G, B, Alpha)
text_color = (255, 255, 255, max(0, min(255, opacity)))
draw.text((pos_x, pos_y), text, font=font, fill=text_color)
# 无损 Alpha 通道融合
return Image.alpha_composite(rgba_base, watermark_layer)
def batch_process_images(
src_dir: str,
dst_dir: str,
max_box: Tuple[int, int] = (1920, 1080),
watermark_text: str = "© 版权保护",
prefix: str = "img",
jpeg_quality: int = 85
) -> None:
"""
批量执行图片处理流水线
:param src_dir: 源图片目录
:param dst_dir: 结果输出目录
:param max_box: 最大等比约束盒 (宽, 高)
:param watermark_text: 水印文字内容
:param prefix: 重命名文件名前缀
:param jpeg_quality: JPEG 输出质量 (1-95)
"""
input_path = Path(src_dir)
output_path = Path(dst_dir)
if not input_path.exists():
raise FileNotFoundError(f"输入目录不存在: {src_dir}")
output_path.mkdir(parents=True, exist_ok=True)
# 过滤支持的图像格式
valid_exts = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"}
image_files = sorted([
p for p in input_path.iterdir()
if p.is_file() and p.suffix.lower() in valid_exts
])
total_count = len(image_files)
if total_count == 0:
print(f"[提示] 在 {src_dir} 中未找到可处理的图片文件。")
return
print(f"[开始批处理] 发现有效图片 {total_count} 张,目标目录: {dst_dir}")
font = get_available_font(font_size=28)
success_count = 0
fail_count = 0
for idx, file_path in enumerate(image_files, start=1):
try:
with Image.open(file_path) as raw_img:
# 1. EXIF 方向物理纠正
img = ImageOps.exif_transpose(raw_img)
# 2. 等比约束缩放(保持纵横比,不发生拉伸)
img.thumbnail(max_box, resample=Image.Resampling.LANCZOS)
# 3. 叠加透明图层水印
watermarked = apply_watermark(
img,
text=watermark_text,
font=font,
opacity=128,
margin=24
)
# 4. 存为 JPEG 需转换为 RGB 模式,消除 Alpha 通道
final_output = watermarked.convert("RGB")
# 5. 格式化序列号重命名
dest_filename = f"{prefix}_{idx:04d}.jpg"
dest_path = output_path / dest_filename
final_output.save(dest_path, "JPEG", quality=jpeg_quality)
success_count += 1
print(f"[{idx:04d}/{total_count:04d}] 已生成: {dest_filename} (来自 {file_path.name})")
except Exception as err:
fail_count += 1
print(f"[错误] 处理文件 {file_path.name} 失败: {err}")
print(f"\n[任务完成] 成功: {success_count} 张,失败: {fail_count} 张。")
if __name__ == "__main__":
# 使用示例:将 input_photos 下的图片缩放至 1920x1080 盒内,并加水印导出至 output_photos
SRC_DIR = "./input_photos"
DST_DIR = "./output_photos"
# 模拟调用(执行前请确保源目录已准备)
print("示例执行命令:")
print("batch_process_images('./input_photos', './output_photos', max_box=(1920, 1080))")
运行终端输出示例
[示例终端输出]
[开始批处理] 发现有效图片 3 张,目标目录: ./output_photos
[0001/0003] 已生成: img_0001.jpg (来自 DSC_0021.JPG)
[0002/0003] 已生成: img_0002.jpg (来自 sample_wide.png)
[0003/0003] 已生成: img_0003.jpg (来自 mobile_portrait.jpeg)
[任务完成] 成功: 3 张,失败: 0 张。
六、生产环境避坑指南与质量把控
在真实业务流水线中,输入图片来源极其复杂。为了确保数千张图片批量跑批时不中途崩溃、不产生畸变图像,必须注意以下工程细节:
| 关键技术点 | 常见错误做法 | 标准稳健方案 | 潜在影响与危害 |
|---|---|---|---|
| EXIF Orientation | 直接调用 Image.open() 后缩放 |
立即执行 ImageOps.exif_transpose(img) |
手机人像照片在网页端被顺时针旋转90度倒置 |
| 通道转换 | RGBA 图像直接 .save("a.jpg") |
存盘前 .convert("RGB") 转三通道 |
触发 OSError: cannot write mode RGBA 导致整个脚本崩溃中断 |
| 缩放重采样 | 使用 NEAREST 或未传参 |
显式传入 Image.Resampling.LANCZOS |
高频细节丢失,文字或线条出现明显马赛克毛边 |
| 文件锁与句柄 | 未用 with 上下文直接打开 |
使用 with Image.open(...) as img |
Windows 下出现 PermissionError,文件句柄泄露 |
| 并发与内存 | 把所有图片加载至全局 list |
生成器单图迭代,即读即存即释放 | 批量处理高分辨率原图时内存暴涨直至 OOM 崩溃 |
七、总结与日常配置检查清单
使用 Python 编写图像批量处理脚本的核心在于“几何尺寸不失真、色彩模式不冲突、源目标目录严隔离”。在正式批量运行前,建议通过以下清单逐项自检:
- [ ] 目录隔离确认:输出目录路径(
dst_dir)与输入目录(src_dir)不同,且具备写入权限; - [ ] 字体文件完备:若需在无桌面界面的 Linux 服务器(Docker 容器)运行,确保宿主机预装了开源中文字体文件并指定了有效路径;
- [ ] 边界样本试跑:先挑选 2~3 张包含极宽横图、极高竖图与带透明通道 PNG 的样本进行小规模测试;
- [ ] 压缩体积平衡:JPEG 质量值
quality建议设在 80~88 之间;既能消除肉眼可见压缩伪影,又能较原图压缩 60% 以上的文件体积。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









