Python批量处理图片:自动缩放、重命名与加水印脚本

程一诺
程一诺
发布时间:2026-11-08 | 7人浏览过
原创

使用Python的Pillow库编写实用的批量图片处理脚本,涵盖EXIF方向校正、保持纵横比等比缩放、独立透明图层文字水印添加以及格式化批量重命名与防覆盖保存。

自动化运维 / 图像处理
Python批量处理图片:自动缩放、重命名与加水印脚本
在日常网站维护、素材归档或电商上架过程中,手动处理成百上千张尺寸不一、命名混乱且缺乏版权标识的原始图片耗时且容易出错。本文介绍一个基于 Python Pillow 库的完整批处理脚本,通过一次遍历自动完成三项核心任务:按最大边界约束进行等比缩放、按规范数字序列批量重命名,以及在指定边角无损合成半透明文字水印。整个流程严格遵循输入与输出目录分离原则,并在处理过程中校正 EXIF 拍摄方向,避免原图被覆盖或手机照片异常倒置。

一、批处理整体流水线架构与准备工作

构建一个稳定可靠的批量图片处理脚本,首要原则是数据安全与幂等性。处理程序绝不能在原始文件上直接执行原地覆写(In-place Overwrite),而应采用独立的输出目录。一旦脚本在处理到第 500 张图片时因磁盘写满或格式损坏意外中断,未完成的批次和原始文件依然完好无损。

Python 生态中处理位图的首选库是 Pillow(PIL 的现代分支)。它底层依赖高度优化的 C 扩展库,支持 JPEG、PNG、WebP、TIFF 等主流图像格式的解码与编码。可以通过标准包管理器进行安装:

pip install Pillow

整套批处理工作流包含五个阶段:目录过滤扫描、EXIF 方向校正、等比约束缩放、独立透明图层水印合成,以及按序列号规范导出。完整数据流如下所示:

Python批量处理图片:自动缩放、重命名与加水印脚本
图 1:批量图片处理流水线架构与数据流示意图

在遍历目录时,我们使用 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 像素,然后计算原图在此约束盒内的最大等比容纳尺寸。

Python批量处理图片:自动缩放、重命名与加水印脚本
图 2:约束盒 (Bounding Box) 等比缩放与重采样几何原理

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):

  1. 将已缩放的底图转换为 RGBA 模式;
  2. 创建一张与底图尺寸完全一致的独立全透明空白画布:Image.new("RGBA", size, (0, 0, 0, 0));
  3. 在独立透明画布上绘制指定颜色与透明度(如白色、Alpha 值为 120)的水印文本;
  4. 调用 Image.alpha_composite(base_rgba, watermark_layer) 执行逐像素的无损透明度混合。
Python批量处理图片:自动缩放、重命名与加水印脚本
图 3:九宫格水印坐标锚定与 Alpha 独立图层混合模型

文本尺寸度量与九宫格坐标定位

在现代 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 张。

六、生产环境避坑指南与质量把控

在真实业务流水线中,输入图片来源极其复杂。为了确保数千张图片批量跑批时不中途崩溃、不产生畸变图像,必须注意以下工程细节:

Python批量处理图片:自动缩放、重命名与加水印脚本
图 4:生产环境批处理常见陷阱与技术决策对照表
关键技术点 常见错误做法 标准稳健方案 潜在影响与危害
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% 以上的文件体积。
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表