ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python+Pillow批量图片处理:缩放裁剪压缩实战

Python+Pillow批量图片处理:缩放裁剪压缩实战 在稿件整理、内容排版或需求交付时很容易遇到一种情况对方直接发来几张“大头”原图。这里的“大头”通常指两类图一类是分辨率高、体积大、动辄好几 MB 的高清大图另一类则是证件照、工作照这类以人物头部为主的大头照。图片太大直接放进文档或上传到平台要么被压缩得模糊要么因为尺寸不对被驳回图片数量一多手动用画图工具一张张改又非常浪费时间。这篇文章就用“一次处理 3 张大图”作为完整案例分享一套基于 Python 的批量图片处理脚本。从读取图片信息开始逐步完成等比缩放、中心裁剪、大头照裁切、压缩瘦身和格式转换最终把三张原始大图变成符合稿件使用要求的输出文件。整个流程会拆成几步来讲每步都带可运行的代码和解释零基础也能照着敲有开发经验的读者可以直接跳到第 4 节复用完整脚本。1. 背景为什么需要批量处理大图1.1 稿件图片处理的现实痛点在写技术博客、做产品说明书、整理项目文档或者给客户交付素材时配图质量直接影响阅读体验。但原始图片往往并不“听话”图片来源多样可能是相机拍摄、手机截图、设计稿导出或者扫描件尺寸从几百像素到几千像素都有。文件体积差异大有的原图超过 10MB直接上传到内容平台会被自动压缩清晰度反而下降。显示场景不同线上预览只需要 1200 像素左右的宽度打印存档则需要 300 DPI 以上的分辨率。部分图片存在方向错误手机拍照时如果横竖屏没抓好图片里会写入 EXIF 旋转信息打开后方向不对。如果只有一张图手动处理还能接受。但稿件过程往往是成批的一次收到三五张甚至几十张图再靠人工打开 Photoshop 或画图工具逐张调整效率和一致性都很难保证。批量脚本的价值就在这里用一套固定规则处理所有图片结果统一、过程可重复、原始文件还能保留。1.2 本文适用的场景这篇文章的案例围绕“3 张大头”展开具体指两种常见情况三张分辨率高、体积大的原始配图需要统一压缩和缩放到指定尺寸。三张以人像头部为主的证件照或工作照需要按标准尺寸裁剪并导出。这两种需求在实际工作中很常见。前者对应博客插图、产品图、公众号配图后者对应简历照片、工牌照片、考试报名照片。虽然图片主体不同但处理流程的核心逻辑是一样的读取原图信息按目标尺寸缩放按区域裁剪最后压缩保存。1.3 为什么选择 Python 和 PillowPython 生态里处理图片最常用的两个库是 OpenCV 和 Pillow。OpenCV 擅长人脸检测、图像识别等计算机视觉任务安装包较大Pillow 是 PIL 的继承者轻量、易安装、文档完善适合处理日常的尺寸调整、裁剪、旋转、格式转换和压缩。本文案例只需要完成“读图—缩放—裁剪—保存”这一基础链路所以使用 Pillow 完全够用而且代码更短、更容易理解。如果后续要做自动人脸定位裁切再引入 OpenCV 也不迟。2. 环境准备与版本说明2.1 运行环境说明本文示例基于以下环境编写读者不需要完全一致但建议使用 Python 3.8 以上版本操作系统Windows 10/11 或 macOS 均可代码使用pathlib处理路径跨平台兼容。Python 版本3.10 左右即可运行3.8 以上基本没问题。图片处理库Pillow实际安装时以pip install Pillow为准。不需要安装 OpenCV、numpy 等额外依赖保持最小依赖原则。需要注意的是Pillow 的 API 在不同版本之间整体稳定但个别细节有差异。本文示例基于常见的最新稳定版写法如果读者使用的是较老版本个别参数可能需要微调。2.2 安装依赖库建议先创建一个独立项目目录再在虚拟环境中安装 Pillowmkdir image-batch-process cd image-batch-process python -m venv venvWindows 下激活虚拟环境venv\Scripts\activatemacOS 或 Linux 下激活虚拟环境source venv/bin/activate激活后安装 Pillowpip install Pillow安装完成后可以确认版本python -c from PIL import Image; print(Image.__version__)如果能看到版本号输出说明环境已经就绪。2.3 项目目录结构为了便于管理我们按下面的结构组织项目image-batch-process/ ├── input/ # 放置原始大图 │ ├── photo_01.jpg │ ├── photo_02.png │ └── photo_03.jpg ├── output/ # 处理结果输出目录脚本自动创建 └── batch_process.py # 核心处理脚本input目录手工创建把三张原始大图放进去output目录由脚本自动生成不需要提前手动建立。这样原图和结果图分开避免误覆盖也方便追溯处理前后的差异。3. 图片处理核心概念在写代码之前先花点时间把图片处理的几个核心概念讲清楚。理解了这些后面看代码就不会觉得参数是“魔法数字”。3.1 像素、尺寸与分辨率一张图片由若干像素点组成常见的1920×1080指的就是宽 1920 像素、高 1080 像素。像素越多画面细节越多但文件体积通常也越大。分辨率是另一个容易混淆的概念它表示每英寸包含的像素数单位是 DPIDots Per Inch。同一张图在屏幕上显示时 72 DPI 和 300 DPI 看起来尺寸差别很大但像素总量没变。打印场景通常要求 300 DPI屏幕展示则只需 72 DPI 或 96 DPI。在脚本中我们主要操作的是像素尺寸DPI 只在保存时通过参数写入文件信息。不要为了追求“高 DPI”而无脑放大图片那只会增加文件体积不会增加真实细节。3.2 图片格式与色彩模式常见的图片格式有 JPEG、PNG、WebP、BMP 等。JPEG 适合照片类图片压缩率高但有损PNG 支持透明背景适合截图和图标属于无损格式体积通常较大。WebP 是新一代格式压缩率更高但部分老平台不支持。色彩模式也很关键。JPEG 不支持透明通道如果一张 PNG 图片带透明背景直接保存成 JPEG透明区域会变成黑色这是个高频踩坑点。所以代码里遇到 RGBA 或调色板模式P的图要先转成 RGB 再保存为 JPEG。3.3 压缩质量与文件体积的平衡JPEG 压缩通过quality参数控制取值范围通常为 1 到 95。质量越高画质越好文件越大。日常稿件配图quality85是一个比较稳妥的选择肉眼很难看出和原图的差别但体积能明显下降。另外Pillow 的save方法还支持optimizeTrue可以在保持画质的前提下进一步优化编码减少体积。多次重复保存 JPEG 会持续损失画质因此处理链路上应该做到“原图读取一次处理完只保存一次”。4. 实战批量处理三张大图的完整流程下面进入正题。我们按步骤拆解从读取图片信息到最终输出结果的完整流程每一步都给出代码和解释最后合成一个完整的可运行脚本。4.1 第一步读取图片基本信息处理图片前先要把每张图的基本信息读出来包括尺寸、色彩模式和原始格式。这有助于确认图片是否正常、是否需要特殊处理。先创建一个脚本batch_process.py写入以下代码# 文件路径batch_process.py from PIL import Image from pathlib import Path def show_info(image_path: Path): with Image.open(image_path) as img: print(f文件名: {image_path.name}) print(f尺寸: {img.size[0]} x {img.size[1]}) print(f色彩模式: {img.mode}) print(f原始格式: {img.format}) print(- * 40) if __name__ __main__: input_dir Path(input) for img_path in sorted(input_dir.iterdir()): if img_path.suffix.lower() in (.jpg, .jpeg, .png, .bmp, .webp): show_info(img_path)这里有两个值得注意的细节。第一用with Image.open(...)打开图片可以在读取完信息后自动释放文件句柄避免大量图片时出现文件占用问题。第二用pathlib处理路径脚本在 Windows 和 macOS 上都能正常运行。运行方式python batch_process.py预期输出类似文件名: photo_01.jpg 尺寸: 4032 x 3024 色彩模式: RGB 原始格式: JPEG ---------------------------------------- 文件名: photo_02.png 尺寸: 2560 x 1440 色彩模式: RGBA 原始格式: PNG ---------------------------------------- 文件名: photo_03.jpg 尺寸: 3024 x 4032 色彩模式: RGB 原始格式: JPEG ----------------------------------------看到这些信息后就可以根据实际需求决定每张图如何处理。比如photo_02.png是 RGBA 模式后面保存为 JPEG 时必须转换色彩模式。4.2 第二步等比缩放图片稿件配图最常见的需求是限制最大宽度或最大高度同时保持图片宽高比不变。如果直接指定固定宽高去resize图片会发生拉伸变形。正确做法是使用thumbnail方法。def smart_resize(img: Image.Image, max_size: tuple) - Image.Image: 等比缩放使图片的长边不超过 max_size。 img.thumbnail(max_size, Image.LANCZOS) return imgmax_size是一个元组比如(1200, 1200)表示缩放后宽和高都不超过 1200 像素。thumbnail会自动按比例计算新尺寸不会变形。Image.LANCZOS是重采样算法缩放质量较好适合缩小图片。如果图片本来比目标尺寸小thumbnail不会放大图片这一点对避免把小图放大变模糊很有用。4.3 第三步中心裁剪与大头照裁切缩放解决的是“太大”的问题裁剪解决的是“比例不对”的问题。比如原图是 4:3目标位置只留 1:1 正方形区域这时就需要裁剪。对于普通大图采用中心裁剪即可。对于证件照、大头照这类以人物头部为主的图片人脸通常位于画面上部区域所以裁剪基准点应该偏上避免把头部截掉。def cover_crop(img: Image.Image, target_ratio: float, focus_top: bool True): 按目标宽高比裁剪focus_top 为 True 时裁剪区域偏上适合大头照。 w, h img.size current_ratio w / h if current_ratio target_ratio: # 原图偏宽需要裁掉左右两侧 new_w int(h * target_ratio) left (w - new_w) // 2 box (left, 0, left new_w, h) else: # 原图偏高需要裁掉上下部分 new_h int(w / target_ratio) if focus_top: top int((h - new_h) * 0.3) # 取上部 30% 区域避开头像 else: top (h - new_h) // 2 box (0, top, w, top new_h) return img.crop(box)这里的关键思路是先比较原图宽高比和目标宽高比判断该裁左右还是裁上下然后计算裁剪框的位置。focus_top参数控制纵向裁剪时是偏上还是居中大头照场景建议偏上。4.4 第四步压缩与格式转换处理完尺寸和裁剪后最后一步是保存。保存时组合运用格式转换和质量参数把文件体积降下来。def save_compressed(img: Image.Image, output_path: Path, quality: int 85): 统一保存为 JPEG并压缩体积。 if img.mode in (RGBA, P): img img.convert(RGB) img.save(output_path, JPEG, qualityquality, optimizeTrue, dpi(300, 300))代码中先判断色彩模式如果是 RGBA 或调色板模式P先转换为 RGB否则保存成 JPEG 会出现透明区域变黑的问题。optimizeTrue让编码器在保证画质的前提下进一步优化文件体积。dpi(300, 300)写入打印分辨率信息方便后续需要印刷或打印的场景。如果目标平台不支持 JPEG也可以把JPEG换成PNG或WEBP但要注意 PNG 是无损格式压缩效果和 JPEG 不同。4.5 第五步组织批量输出批量处理时输出文件命名要能对应到原始文件避免覆盖。这里给每个输出文件加上处理标识后缀def build_output_name(original_name: str, suffix: str) - str: stem Path(original_name).stem return f{stem}_{suffix}.jpg比如photo_01.jpg处理后生成photo_01_processed.jpg这样原图不受影响结果图也能一眼看出是谁的派生文件。4.6 完整可运行脚本把上面几个函数组合起来再加上批量遍历逻辑和输出目录自动创建就得到一个完整的脚本# 文件路径batch_process.py #!/usr/bin/env python3 # -*- coding: utf-8 -*- 稿件图片批量处理脚本读取、缩放、裁剪、压缩、格式转换。 from PIL import Image, ImageOps from pathlib import Path INPUT_DIR Path(input) OUTPUT_DIR Path(output) MAX_SIZE (1200, 1200) # 缩放宽高上限 TARGET_RATIO 1.0 # 目标宽高比1.0 表示正方形 QUALITY 85 SUPPORTED_SUFFIX (.jpg, .jpeg, .png, .bmp, .webp) def load_image(image_path: Path) - Image.Image: 打开图片自动修正 EXIF 旋转信息。 with Image.open(image_path) as img: img ImageOps.exif_transpose(img) return img.copy() def smart_resize(img: Image.Image, max_size: tuple) - Image.Image: 等比缩放使图片长边不超过 max_size。 img.thumbnail(max_size, Image.LANCZOS) return img def cover_crop(img: Image.Image, target_ratio: float, focus_top: bool True) - Image.Image: 按目标宽高比裁剪focus_top 为 True 时裁剪区域偏上。 w, h img.size current_ratio w / h if current_ratio target_ratio: new_w int(h * target_ratio) left (w - new_w) // 2 box (left, 0, left new_w, h) else: new_h int(w / target_ratio) if focus_top: top int((h - new_h) * 0.3) else: top (h - new_h) // 2 box (0, top, w, top new_h) return img.crop(box) def save_compressed(img: Image.Image, output_path: Path, quality: int QUALITY) - None: 统一保存为 JPEG并压缩体积。 if img.mode in (RGBA, P): img img.convert(RGB) img.save(output_path, JPEG, qualityquality, optimizeTrue, dpi(300, 300)) def format_size(size_bytes: int) - str: 把字节数转成可读的 KB/MB。 if size_bytes 1024 * 1024: return f{size_bytes / (1024 * 1024):.2f} MB return f{size_bytes / 1024:.1f} KB def process_one_image(input_path: Path, output_dir: Path) - None: 处理单张图片读入、缩放、裁剪、保存。 img load_image(input_path) img smart_resize(img, MAX_SIZE) img cover_crop(img, TARGET_RATIO, focus_topTrue) output_path output_dir / f{input_path.stem}_processed.jpg save_compressed(img, output_path) original_size input_path.stat().st_size new_size output_path.stat().st_size print(f{input_path.name} - {output_path.name}) print(f 尺寸: {img.size[0]} x {img.size[1]}) print(f 体积: {format_size(original_size)} - {format_size(new_size)}) print(- * 40) def main() - None: OUTPUT_DIR.mkdir(exist_okTrue) image_files [ p for p in sorted(INPUT_DIR.iterdir()) if p.suffix.lower() in SUPPORTED_SUFFIX ] if not image_files: print(input 目录下没有找到支持的图片文件。) return print(f共发现 {len(image_files)} 张图片开始处理...\n) for image_file in image_files: process_one_image(image_file, OUTPUT_DIR) print(全部处理完成输出目录, OUTPUT_DIR.resolve()) if __name__ __main__: main()整个脚本的逻辑链路是读入目录 → 逐张打开图片 → 修正方向 → 等比缩放 → 按目标比例裁剪 → 转 RGB → 压缩保存。每一步都是独立函数方便后续单独修改参数。运行方式python batch_process.py4.7 运行与验证脚本运行结束后先检查输出目录下是否生成了三个_processed.jpg文件。再用 Python 快速验证输出文件的实际情况from PIL import Image from pathlib import Path output_dir Path(output) for img_path in sorted(output_dir.glob(*_processed.jpg)): with Image.open(img_path) as img: print(f{img_path.name}: 尺寸{img.size}, 模式{img.mode})这个验证步骤很重要。只看到“处理成功”字样还不够必须确认每个输出文件的尺寸、模式符合预期。如果发现某张图比例不对优先检查原始图片的宽高比和TARGET_RATIO设置是否合理。5. 常见问题与排查思路批处理脚本看起来简单实际跑起来仍会遇到各种问题。下面整理几个高频问题按“现象—原因—解决方案”的方式逐个排查。问题现象常见原因解决思路打开图片时报UnidentifiedImageError文件不是有效图片或扩展名与内容不符去掉后缀限制用Image.open直接尝试或先检查文件头中文文件名或路径报编码错误Windows 环境编码处理不一致使用pathlib.Path避免手工拼接路径字符串保存 JPEG 后透明区域变黑RGBA 或调色板模式未转 RGB保存前执行img.convert(RGB)图片方向不对横图变竖图图片含 EXIF 旋转信息但未被应用用ImageOps.exif_transpose修正后再处理处理超大图时内存占用过高原图分辨率极大Pillow 一次性载入内存先用Image.draft采样缩略或对大图单独降采样压缩后文件反而变大把 PNG 反复保存为 PNG或 JPEG 质量参数过高统一输出为 JPEG 并检查 quality 与 optimize 参数处理后图片变模糊缩放时用了默认的最近邻插值或把小图强行放大使用Image.LANCZOS重采样小图不做放大重点说一下 EXIF 旋转问题。手机或相机拍照时传感器可能横着放但图片文件会写入一条旋转信息告诉看图软件应该按哪个方向显示。Pillow 的ImageOps.exif_transpose会把旋转信息真正“落”到像素上处理后再保存图片方向就正确了。很多批量处理脚本忽略这一步导致辛苦处理完的图方向是错的返工成本很高。内存问题也值得单独说明。现在手机拍一张图动辄 4000 万像素直接Image.open后全量载入内存再同时处理多张图内存占用很容易飙升。如果处理 ultra 大图建议先调用img.draft(mode, size)生成一个低分辨率预览确认信息后再决定是否全量处理或者用Image.open的流式读取特性处理完一张立刻释放。6. 最佳实践与工程建议6.1 文件与目录规范批处理脚本最怕“跑完找不到结果”“结果覆盖原图”。建议强制规定三点输入目录和输出目录严格分离原图永远放在input下不动。输出文件命名带上处理标识如_processed、_resized避免和原图重名。每次处理前先看输入文件清单确认数量、类型无误后再执行。如果处理的是证件照这类有严格要求的图片建议把output目录按日期归档例如output/20250101/这样不同批次的结果不会互相混淆也方便回溯。6.2 异常处理与日志当前脚本在遇到异常图片时会直接中断。在实际项目中更推荐对单张图片做异常隔离某一张图失败了记录日志后继续处理下一张。def process_one_image_with_try(input_path: Path, output_dir: Path) - bool: try: process_one_image(input_path, output_dir) return True except Exception as exc: print(f[失败] {input_path.name}: {exc}) return False批量处理时把成功和失败的数量分别统计最后输出汇总。这样即使三张图里有一张出了问题也能快速定位是哪一张、为什么失败而不必从头重跑。6.3 安全与授权提醒处理他人提供的图片时需要注意素材使用权限。批量脚本本身只是工具但处理对象如果是他人肖像、版权图片或敏感资料必须确保处理行为获得了合法授权且处理结果不会被用于未经允许的场景。尤其在证件照、人脸照片这类涉及个人信息的场景应当只在本地环境处理不要随意把原图上传到第三方在线工具。6.4 性能优化建议当图片数量从 3 张扩展到 300 张时有几个优化方向使用多进程并行处理。Pillow 的操作是 CPU 密集型的用concurrent.futures.ProcessPoolExecutor可以充分利用多核 CPU。避免重复解码。同一张图只打开一次所有操作在内存中完成后再保存。输出前检查目标文件是否已存在如果已存在且时间较新可以跳过处理避免重复劳动。大批量处理前先在input目录里放 1 到 2 张测试图确认效果后再全量执行。6.5 代码可维护性建议把尺寸、质量、比例等参数统一放在脚本顶部的常量区域而不是散落在函数调用中。这样后续调整需求时只需要改几个常量不需要改动核心逻辑。如果需要支持不同批次不同尺寸可以把参数抽成配置文件YAML 或 JSON脚本启动时读取这样非技术人员也能自行调整。7. 进阶扩展方向三张图的批处理只是个开始掌握这套流程后可以往两个方向继续深入。第一个方向是自动识别人脸并裁剪。当前脚本的大头照裁剪用的是“上部 30%”的近似规则遇到构图特殊的照片仍可能裁偏。引入 OpenCV 的CascadeClassifier或更现代的mediapipe人脸检测模型可以自动定位人眼或人脸位置从而实现更精准的证件照自动裁切。调整思路是先用检测模型拿到人脸边界框再以人脸为中心规划裁剪区域。第二个方向是把脚本接入自动工作流。比如用watchdog监听某个目录有新图片放入就自动触发处理或者封装成 Flask 接口让编辑在网页上上传图片、选择尺寸、下载结果。核心处理逻辑不变改变的是入口和出口。另外如果经常处理内容平台配图还可以在处理后调用平台 API 直接把图片上传省去手动上传下载的环节。这一步通常会涉及 token 权限配置操作时要注意密钥不要提交到公开仓库建议使用环境变量保存敏感信息。8. 动手建议建议读者不要只复制脚本而是亲手走一遍完整的调试流程先在input目录放三张风格差异较大的图比如横图、竖图、带透明背景的 PNG 各一张运行脚本后观察输出结果体会smart_resize和cover_crop在不同宽高比下的表现差异。把整个过程跑顺之后再替换成自己的真实数据。处理图片这件事难点从来不在 API 本身而在于对“原图情况多样、目标要求明确、处理过程可重复”这三点的把握。脚本能保证批量任务的一致性和效率但最终效果仍然需要人工确认。拿到一批图时先花两分钟看清楚原始图片的尺寸、比例、模式和体积再决定怎么处理这会比反复试错高效得多。
返回列表