ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python和Pillow实现图片九宫格切割:从坐标到批量处理

用Python和Pillow实现图片九宫格切割:从坐标到批量处理 图片切割成九宫格是我最近被问到最多的 Python 小需求之一。起因是身边一个做自媒体运营的朋友想发九宫格朋友圈但手机上的裁图工具要么强制压缩画质要么只能处理正方形原图稍微不规则的图就会变扭。我随口说了一句“这种活用 Python 写个脚本不就行了”结果他发现我真的几分钟就写完了还顺手把非正方形图片的边界情况也处理了。所以这篇文章我打算把整个实现拆开讲一遍从 Pillow 的 crop 坐标原理到 3×3 九宫格、任意 N×M 网格切分、批量处理再到高频报错的排查方法。无论你是刚学 Python 想找点小项目练手还是日常有发圈、切图、UI 素材整理需求这篇都能让你直接抄作业。1. 整体设计与思路拆解1.1 九宫格切图本质是坐标计算很多人一听“图片切割”就觉得要上 OpenCV、要搞图像识别实际上九宫格切图就是一个纯粹的几何切割问题。一张图片在计算机里可以理解为一个巨大的像素矩阵水平方向是 x 轴垂直方向是 y 轴左上角坐标是 (0, 0)。把一张大图切成 3×3本质上就是算出 9 个矩形区域每个区域用四个数字表示左边界 left、上边界 upper、右边界 right、下边界 lower。拿一张 900×900 的正方形图举例切成 3×3 以后每个小格的尺寸是 300×300。第一行第一列的格子坐标是 (0, 0, 300, 300)第二行第二列是 (300, 300, 600, 600)最后一行最后一列是 (600, 600, 900, 900)。你会发现规律非常清晰如果当前格子在第 row 行、第 col 列宽度是 cell_w高度是 cell_h那么 left col * cell_wupper row * cell_hright left cell_wlower upper cell_h。只要这个公式搞清楚后面所有代码都只是把这个逻辑用 Python 写出来而已。1.2 为什么选 Pillow而不是 OpenCVPython 里能做图像处理的库不少最常被提起的是 Pillow 和 OpenCV。我的建议很直接如果只是做图片切割、拼接、改尺寸这类轻量任务优先用 Pillow。原因有三个第一安装简单。Pillow 一条pip install pillow就装好了而 OpenCV 在部分环境下依赖较多装起来更折腾。第二API 直观。Pillow 的Image.crop(box)就是为裁剪设计的参数就是一个四元组坐标阅读代码的人一眼能看懂。OpenCV 的切片虽然也支持但它的颜色通道顺序是 BGR初学者很容易踩通道顺序的坑。第三场景匹配。我们做的是把一张图规则地切成多块不是做人脸识别、边缘检测、目标跟踪。用 OpenCV 属于杀鸡用牛刀还会让代码变得不必要地复杂。当然不是说 OpenCV 不好。如果你后面想做的功能涉及图像特征提取、模板匹配或者需要基于切割结果进一步识别内容那再上 OpenCV 也不迟。小工具阶段Pillow 完全够用。1.3 整体代码结构应该怎么拆写这类脚本有一个习惯我很推荐不要把逻辑全塞在一个函数里。最简版可以拆成三步——读取图片、循环计算坐标并裁剪、保存结果。进阶版可以把“图片预处理”和“切割逻辑”分开因为这两部分将来都会复用。比如原图不是正方形时需要先处理成正方形这是一个独立需求切割本身是另一个独立需求。拆开以后你以后想切六宫格、十二宫格或者想换成不同的预处理方式改动成本都非常小。2. 核心细节解析与实操要点2.1 必须搞懂的 crop 坐标系Pillow 的Image.crop(box)方法参数box是一个四元组(left, upper, right, lower)。这四个值都表示像素坐标而且要注意right 和 lower 是开区间也就是说实际裁出来的像素范围是[left, right)和[upper, lower)不包含 right 和 lower 这一行/列。这个设计其实很巧妙。因为 Pyxel 这类坐标体系本身就是左闭右开这样可以保证相邻切片之间不会出现重叠像素也不会凭空多出一行空白。举个例子如果第一格是(0, 0, 300, 300)第二格是(300, 0, 600, 300)那么第一格最后一列像素是第 299 列第二格第一列是第 300 列刚好无缝衔接。实际写代码时很多人会忽略这个细节直接用right left cell_w这种写法。如果图片宽高能被 3 整除这没毛病但一旦遇到不能被整除的尺寸比如 1000×1000 的图直接每格取1000 // 3 333最后你会发现三格加起来只有 999 像素多出来 1 像素的边没有被裁进去。正确做法是前两格用固定宽度 333最后一格的 right 直接取图片宽度 1000这样三格分别是 333、333、334总数刚好 1000。不要小看这一个像素切完以后做拼接验证时你就会发现少 1 像素都可能会让图片拼起来有偏差。2.2 非正方形图片的三种处理方式九宫格发朋友圈或做内容配图原图往往不是正方形。这时候直接切 3×3每一块的比例不统一拼回去虽然还是原图但独立看每一块会很奇怪。所以我建议在切割前先把原图统一成正方形。常见方案有三种居中裁剪、填充边缘、拉伸变形。我整理了一个对比表处理方式实现思路优点缺点适用场景居中裁剪取原图较短边作为边长裁掉两边多余部分图片不变形输出统一会丢掉部分内容发朋友圈、配图最推荐填充边缘在较短边上补白边或指定颜色补成正方形保留全部内容会出现白边视觉效果不一定统一需要完整信息的场景拉伸变形直接 resize 成正方形代码最简单图片会被拉伸变形原图本来就接近正方形或不在乎变形我个人最常用的默认方案是居中裁剪。尤其是发朋友圈九宫格如果每张图比例不一致刷起来会显得很乱而居中裁剪以后九张图都是正方形视觉上整齐很多。如果你担心丢内容可以先程序里生成一个检查图看看裁剪后的中心区域是不是保留了主体。居中裁成正方形的代码并不复杂def center_crop_to_square(img): w, h img.size side min(w, h) left (w - side) // 2 top (h - side) // 2 right left side bottom top side return img.crop((left, top, right, bottom))这段代码的含义是先取宽和高中较短的那条边作为正方形边长然后计算左上角坐标让裁切区域居中。原图横构图就裁左右原图竖构图就裁上下。2.3 尺寸整除问题的细节处理切割时每个小格的宽高应该是整数。图片宽高不一定是 3 的倍数所以不能直接拿w // 3硬算所有格子。我推荐一个更稳妥的边界处理方式前cols - 1列用cell_w w // cols最后一列的右边界直接用w行同理。写出来的核心循环逻辑大概是for row in range(rows): top row * cell_h bottom h if row rows - 1 else (row 1) * cell_h for col in range(cols): left col * cell_w right w if col cols - 1 else (col 1) * cell_w box (left, top, right, bottom)这样能保证所有切片在空间上无缝拼接并且不会因为取整导致右下角出现黑色空隙。这个细节在写最简版代码时容易被忽略但一旦图片尺寸不是行列的整倍数就会出现切片后拼接不回去的问题很多初学者的“查错一小时”都花在这里。3. 实操过程与核心环节实现3.1 环境准备Python 环境与 Pillow 安装动手之前先把环境搞定。只要你的电脑上有 Python 3.6 以上的版本就可以直接装 Pillow。没装 Python 的话先去官网下载安装包安装时一定记得勾选 “Add Python to PATH”这一步不勾后面在命令行里pip会找不到命令很多新手都在这里卡住。装 Pillow 的命令很简单pip install pillow如果下载速度慢可以换成国内镜像源pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple装完以后在 Python 交互环境里执行from PIL import Image不报错说明环境就绪。Pillow 的包名是 pillow但导入名是 PIL这是一个历史遗留问题遇到了不用慌。3.2 最简版九宫格切割代码下面这个版本是最小可运行脚本只依赖 Pillow。代码里自带注释你可以直接复制保存为split_grid.py使用。from PIL import Image import os def split_into_grid(image_path, output_diroutput, rows3, cols3): img Image.open(image_path) img img.convert(RGB) w, h img.size os.makedirs(output_dir, exist_okTrue) cell_w w // cols cell_h h // rows for row in range(rows): for col in range(cols): left col * cell_w upper row * cell_h right w if col cols - 1 else (col 1) * cell_w lower h if row rows - 1 else (row 1) * cell_h box (left, upper, right, lower) cropped img.crop(box) filename os.path.join(output_dir, f{row 1}_{col 1}.jpg) cropped.save(filename, quality95) print(f已保存 {filename}) if __name__ __main__: split_into_grid(input.jpg)这里面img.convert(RGB)是一个保险操作。有些图片是透明的 PNG直接保存成 JPEG 会报OSError: cannot write mode RGBA as JPEG先转成 RGB 就能避开这个问题。如果你需要保留透明通道就把保存格式改成.png同时去掉 convert 那行。运行方式python split_grid.py前提是当前目录下有一张input.jpg程序运行完会生成output文件夹里面是1_1.jpg到3_3.jpg九张图。3.3 进阶版支持非正方形图预处理与任意行列最简版有个缺陷原图不是正方形时切出来的九张图比例不一致。所以我把“预处理成正方形”和“切割”两个逻辑都做成可选参数再配合 argparse 做成命令行工具。这样不写代码的人也能通过命令行参数调整行为。import os import argparse from PIL import Image def center_crop_to_square(img): w, h img.size side min(w, h) left (w - side) // 2 top (h - side) // 2 return img.crop((left, top, left side, top side)) def pad_to_square(img, fill(255, 255, 255)): w, h img.size side max(w, h) new_img Image.new(RGB, (side, side), fill) new_img.paste(img, ((side - w) // 2, (side - h) // 2)) return new_img def split_grid(img, rows, cols, output_dir, prefixpiece): os.makedirs(output_dir, exist_okTrue) w, h img.size cell_w w // cols cell_h h // rows for row in range(rows): for col in range(cols): left col * cell_w upper row * cell_h right w if col cols - 1 else (col 1) * cell_w lower h if row rows - 1 else (row 1) * cell_h box (left, upper, right, lower) img.crop(box).save( os.path.join(output_dir, f{prefix}_{row 1}_{col 1}.jpg), quality95 ) def main(): parser argparse.ArgumentParser(description图片切割成九宫格工具) parser.add_argument(image, help输入图片路径) parser.add_argument(--rows, typeint, default3, help行数默认3) parser.add_argument(--cols, typeint, default3, help列数默认3) parser.add_argument(--output, defaultoutput, help输出目录默认 output) parser.add_argument(--mode, choices[crop, pad, none], defaultcrop, help非正方形图处理方式crop居中裁剪pad填充白边none不处理) parser.add_argument(--prefix, defaultpiece, help输出文件名前缀) args parser.parse_args() img Image.open(args.image) if args.mode crop: img center_crop_to_square(img) elif args.mode pad: img pad_to_square(img) split_grid(img, args.rows, args.cols, args.output, args.prefix) print(f切图完成共 {args.rows * args.cols} 张保存在 {args.output} 目录下) if __name__ __main__: main()这个进阶版里有三个关键选择modecrop默认居中裁剪适合发朋友圈modepad会补白边保留全部内容modenone就是直接按原图切不关心比例是否统一。命令行用法python split_grid.py input.png --rows 3 --cols 3 --mode crop --output wechat_grid3.4 用拼接函数验证切割结果切完以后我强烈建议做一步验证把九张图按原位置合并回去检查能不能无缝隙还原成原图。这不只是强迫症而是切图过程中坐标偏移、边界少取一像素这类问题用肉眼直接看小图很难发现但拼回去一眼就能看出来。合并函数也很简单from PIL import Image import os def merge_grid(grid_dir, rows3, cols3, output_pathmerged.jpg): # 先拿第一张图确定单格尺寸这里要求所有小图尺寸一致 first_piece Image.open(os.path.join(grid_dir, 1_1.jpg)) piece_w, piece_h first_piece.size # 创建一张大画布 merged Image.new(RGB, (piece_w * cols, piece_h * rows), (255, 255, 255)) for row in range(rows): for col in range(cols): piece Image.open(os.path.join(grid_dir, f{row 1}_{col 1}.jpg)) merged.paste(piece, (col * piece_w, row * piece_h)) merged.save(output_path)这个脚本会读取1_1.jpg的尺寸然后创建一张piece_w * 3宽、piece_h * 3高的大图再把 9 张图按行列粘贴回对应坐标。如果你切图时的边界处理有问题合并后的图片和原图在右边缘或下边缘会出现细缝这时你就可以回头检查cell_w和right的赋值逻辑。4. 常见问题与排查技巧实录4.1 高频报错对照速查表我在帮别人调这个切图脚本的过程中发现很多问题都是重复出现的。下面这张表基本覆盖了最常见的坑你可以直接对照排查。现象可能原因解决方法ModuleNotFoundError: No module named PIL没有安装 Pillow执行pip install pillowOSError: cannot write mode RGBA as JPEG原图是 PNG带透明通道JPEG 不支持透明转换img.convert(RGB)或保存为 PNG切出来的图右边或下边有黑边边界计算时直接用了(col 1) * cell_w非整除尺寸导致最后一格越界或多余最后一列/行的边界直接用图片宽高图片在 Windows 下报FileNotFoundError路径中有中文或路径格式问题用英文路径避免在路径里混用正反斜杠输出目录不存在导致保存失败没创建目录使用os.makedirs(output_dir, exist_okTrue)大图切图时内存占用高Pillow 在 crop 时会加载部分图片数据超大图尤其明显先img.draft(RGB, (1024, 1024))做预览级处理或用分块思路文件名顺序在相册里乱文件名按1_1、1_10排序时字符串排序会出问题用两位数补零比如1_01.jpg、1_10.jpg4.2 从一次真实报错看排查思路之前有个朋友给我发了一张截图说他跑脚本时一直报错OSError: cannot write mode RGBA as JPEG他用的原图是一张手机截图手机截图在 Android 设备上经常保存为带透明通道的 PNG。虽然截图肉眼看起来没有透明区域但文件的像素模式确实是 RGBAJPEG 格式根本不支持 Alpha 通道所以保存时直接报错。排查思路很简单先在读图后用print(img.mode)把图片模式打出来。如果是 RGBA再决定到底要不要保留透明通道。如果只是发朋友圈用不需要透明那就convert(RGB)如果图片本身有透明区域且你想保留那就把保存后缀从.jpg改成.pngPillow 会自动识别格式。这个案例很典型因为它提醒我们报错的根源往往不是代码逻辑而是图片格式本身的特性。以后遇到图像保存类报错第一步先检查img.mode第二步检查保存格式这两步能解决八成的格式问题。4.3 切图过程的三个独家建议第一文件名建议用行列号而不是纯序号。比如1_1.jpg、2_3.jpg这种命名看名字就知道它在九宫格里的位置。纯序号1.jpg到9.jpg虽然也没问题但如果你想合并回去还得自己记住哪个是第几行第几列很别扭。第二保存 JPEG 时记得设置quality95。Pillow 默认的 JPEG 保存质量是 75如果原图比较精细切完再拼回去会发现画质有损失。设到 95 基本可以做到肉眼无差别文件体积也不会大太多。这个参数只对 JPEG 有效保存 PNG 时不需要。第三批处理前先跑一张测试图。不要直接拿着几百张原图跑完整脚本先用一张有明显特征的图比如带文字、带边框试一遍拼回来看是否对齐。确认没问题以后再批量跑可以避免批量生成一堆不可用文件。5. 扩展从九宫格到任意网格与批量自动化5.1 参数化行列数一个函数搞定任意网格很多人觉得九宫格只能在 3×3 里玩其实把rows和cols抽象成参数以后这个函数可以切任意网格。比如发小红书时图片有时候需要切成 1×3 的纵向长图或者做 UI 设计时要把一张设计稿切成 4×5 的素材切片只需要改两个数字。进阶版脚本里的--rows和--cols就是为此设计的。这个思路也可以用到游戏开发里。做 2D 游戏时美术经常给一张雪碧图里面排列着多个小图标。用类似的切图脚本配合等宽等高网格就能把雪碧图快速拆成单个图标文件。只要网格参数设置正确代码几乎不用改。5.2 与内容创作流程结合的玩法这类脚本在实际工作中最香的地方是流程化。比如我自己的习惯是维护一个简单的素材处理目录原图放raw脚本处理完输出到grid然后配合一个自动拼接的验证脚本所有操作一条命令完成。在这个基础上你还可以把切图脚本接进文件监听工具只要文件夹里出现新图片就自动切好完全不用手动跑命令。对自媒体运营而言价值更明显。每周要发好几组九宫格朋友圈每次手动切图非常浪费时间。跑一次脚本几十毫秒省下来的时间看点别的不好吗。5.3 继续优化的方向如果身边有不会 Python 的同事也想用这个功能你可以用 PyInstaller 把脚本打包成 Windows 下的 exe 文件双击就能运行。打包命令大概是pip install pyinstaller pyinstaller -F split_grid.py这样就能把命令行工具变成一个小软件不需要对方装 Python、不需要配环境。另外如果你想在批处理时看到处理进度可以引入tqdm库在循环外面包一层tqdm几百张图跑起来右下角就有进度条了。在优化内存方面如果原图特别大比如 8000×6000 的相机原图直接 Pillow 裁剪会占用较多内存。一个简单的降载方案是先用img.thumbnail((3000, 3000))把图片缩到适合九宫格的尺寸再切因为朋友圈九宫格每张图最终显示尺寸也就几百像素切前缩图完全不影响发圈效果。最后分享一个我的个人习惯切图之前先看一眼原图的宽高比。如果宽高差得太多我一般直接选居中裁剪而不是填充白边因为九张统一尺寸的方形图排起来更整齐白边做法虽然保住了全部内容但观感上总有拼接感。脚本写完以后我会在正式切图前先跑一张预览拼图确认没有切错再批量处理。这个小小的检查动作帮我省过好几次返工现在也推荐给你。
返回列表