1. 写在前面:为什么搞懂这几个函数就够了
说到用Python做图片处理,很多人第一反应就是OpenCV,然后去找教程,噼里啪啦装了一堆库,结果第一行import cv2就报错。其实日常处理图片,Pillow + numpy这对组合就够用了,而且轻量、好上手,几乎不会遇到依赖地狱。OpenCV更适合做检测、跟踪、特征匹配这些偏视觉算法的任务,如果只是改尺寸、裁剪、加水印、转灰度、批量处理,Pillow加几个基础函数完全能搞定。
这篇笔记我按自己的实际使用经验,把Python图片处理里最常用的函数整理成一份“速查+避坑”合集。每个函数我都会讲清楚它到底是干什么的、参数怎么理解、实际用的时候容易踩什么坑。最后还会给两个可以直接抄的实战脚本,一篇看下来,你自己就能写批量处理图片的小工具了。
适合谁看?刚入门Python、想在项目里快速处理图片的人,以及把图片处理当辅助技能、不想在视觉算法里陷太深的同学。我以前带过不少新人,发现大家卡住的点往往不是函数本身,而是“不知道该用哪个函数”“参数填什么”和“为什么按教程写还是报错”,这些我会重点写。
2. 环境准备与工具选型:别一上来就装OpenCV
2.1 Pillow还是OpenCV,按任务选工具
先把我真实的建议说清楚。Pillow(PIL分支出来的维护版本)是Python里最老牌的图片库,接口设计得很直观,读图、缩放、滤镜、保存都是“一句话”的事。OpenCV功能确实强,但它的设计目标是计算机视觉,很多函数参数又多又底层,对只想把图片改一下的使用者来说,学习成本不划算。
我自己常用的分配方式是:
| 任务 | 推荐工具 | 原因 |
|---|---|---|
| 读取、保存、格式转换 | Pillow / OpenCV均可 | 都很简单,选Pillow更直白 |
| 缩放、裁剪、旋转、翻转 | Pillow | 接口清晰,坐标逻辑好理解 |
| 亮度、对比度、饱和度调整 | Pillow | ImageEnhance一行搞定 |
| 灰度化、二值化、边缘检测 | OpenCV | 算法函数更丰富、速度更快 |
| 人脸检测、物体识别 | OpenCV | 有训练好的分类器与深度模型 |
| 图像拼接、批量处理 | Pillow + numpy | 代码量少,逻辑清晰 |
简单说,Pillow负责“改图”,OpenCV负责“看图”。如果你拿到的任务核心是“把图片变成另一种图片”,优先用Pillow;如果是“从图片里找出什么东西、判断什么内容”,再考虑OpenCV。这样选型,你十有八九不会走弯路。
有人会问,那matplotlib呢?matplotlib的plt.imread也能读图,但它的强项是画图,不是改图,把图读进来还得靠Pillow或OpenCV转型,多绕一步,没必要。等你要可视化对比处理效果时再用它不迟。
2.2 安装和验证:跨过最基础的坑
安装很简单,但我在各种机器上帮人处理过环境问题,发现“装不上”绝大多数时候不是因为网络,而是pip本身出了问题。最经典的是在命令行窗口输入pip install pillow,结果提示“pip 不是内部或外部命令,也不是可运行的程序或批处理文件”,这个热搜词里反复出现,确实是新人高频翻车点。
我推荐两条路绕开它:
# 方式一:用python -m pip调用,绕过pip命令找不到的问题(推荐) python -m pip install pillow python -m pip install opencv-python python -m pip install numpy # 方式二:如果你登录了国内镜像,可以加速 python -m pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple为什么用python -m pip?因为这条命令会明确告诉Python解释器“去site-packages里找pip模块运行”,只要你的python能跑,pip就一定能被找到,完全不依赖PATH环境变量。如果你连python命令都提示找不到,那大概率是安装时没勾选“Add Python to PATH”,重新跑一遍安装包勾上就行,或者用安装包自带的完整路径运行。
装完记得验证一下,别急着写业务代码:
from PIL import Image import cv2 import numpy as np print(Image.__version__) # Pillow版本 print(cv2.__version__) # OpenCV版本 print(np.__version__) # numpy版本这里有个历史遗留坑:老教程会写import Image,但现在的Pillow要求from PIL import Image,少写前面的PIL包名,直接“No module named 'Image'”。看到这个报错,第一反应不是去装Image,而是检查是不是漏了PIL。另外,新版Pillow和OpenCV对Python版本都有要求,建议Python 3.8以上,太低版本装新库容易触发编译错误,不想折腾就把版本升上去。
3. 图片读写与基本信息获取:一切处理的起点
3.1 三种读取方式,通道顺序是重点
图片处理的第一个动作一定是“把图片读进来”。Pillow、OpenCV、matplotlib都能读,但三种方式返回的数据结构不一样,这个差异决定了你后续代码怎么写。
Pillow读图返回的是Image对象,它有很多方便的方法,比如.size、.convert()、.resize();OpenCV读图返回的是numpy数组,形状是(高, 宽, 通道数),而且通道顺序是BGR;matplotlib读图也是numpy数组,通道顺序是RGB。如果混着用,没做转换,就会出现“图片颜色怪怪的”的经典现象,尤其是红蓝互换。
我平时最顺手的方式是:
from PIL import Image import numpy as np # 方式一:Pillow读进来,保持Image对象操作 img = Image.open("test.jpg") print(img.size) # (宽, 高),注意Pillow先宽后高 print(img.mode) # RGB / L / RGBA等 # 方式二:转成numpy数组,方便做像素级运算 arr = np.array(img) print(arr.shape) # (高, 宽, 3),这里又是先高后宽这里建议背下来一个结论:Pillow的.size是(width, height),numpy数组的shape是(height, width, channels),这两个顺序搞反是低级错误里出现频率最高的。写代码时养成习惯,拿到图片先打印size和shape看一眼,再动手。
如果你用OpenCV读图:
import cv2 img_bgr = cv2.imread("test.jpg") print(img_bgr.shape) # (高, 宽, 3) # 需要转回RGB再显示或处理吗?看场景: img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)只要记住:OpenCV读图默认BGR,用其他库显示或保存时记得转。如果不转,直接cv2.imwrite保存倒是没关系,因为imwrite按BGR存回去,颜色还是对的;但一旦用Pillow的Image.fromarray转成Image对象,或者用plt.imshow显示,颜色立马变蓝红调。很多人在这里排查半天,其实是通道顺序的问题。
3.2 拿到图片先问三句话
图片读进来之后,我建议先问三句话:什么尺寸?什么模式?什么数据类型?这三句对应的代码非常高频,分别回答“图有多大”“颜色是RGB还是灰度还是带透明通道”“像素值范围是不是0到255”。
from PIL import Image import numpy as np img = Image.open("photo.jpg") print("尺寸:", img.size) # 例:(1920, 1080) print("模式:", img.mode) # RGB / L / RGBA / CMYK arr = np.array(img) print("数组形状:", arr.shape) # 例:(1080, 1920, 3) print("像素类型:", arr.dtype) # uint8,一般就是0~255的整数为什么要关心mode?因为mode="L"是灰度图,只有一个通道,如果你拿它做Channel操作会直接报维度错;mode="RGBA"有四个通道,多一个透明度Alpha,存成jpg会把Alpha丢掉,存成png才能保留。为什么要关心dtype?因为很多算法函数要求输入是float32,直接拿uint8去算会出现溢出,比如像素值255加1变成0,这个等做像素级操作时非常恼火。
还有一个细节:Pillow的Image.open()其实是延迟加载的,刚打开时并不会真把完整图片数据加载到内存,而是在第一次调用.load()或转换成数组、保存时才真正解码。所以处理大批量图片时,不要让Image对象一直占着变量不放,用完就及时转数组或del掉,否则内存会被大量图片撑爆。我做批量图片抽取时遇到过几次内存飙升,后来养成习惯:单张处理完立即del img,再配合gc.collect(),稳了很多。
4. 几何变换:缩放、裁剪、旋转、翻转
4.1 resize和thumbnail,一个改尺寸一个改“比例”
缩放图片是最高频的操作之一。Pillow里有两个容易混淆的函数:resize和thumbnail。
resize是你指定目标宽和高,直接把图片拉伸或压缩到那个尺寸,不管原图比例。但如果你只指定一个方向,另一个方向用-1,在OpenCV里可以自动等比;在Pillow里则没有这个特性,你需要自己算比例。
from PIL import Image img = Image.open("test.jpg") # 强行改成800x600,图片可能变形 img_resized = img.resize((800, 600)) # 等比缩放,宽度改成800,高度按比例算 w, h = img.size new_w = 800 new_h = int(h * new_w / w) img_resized = img.resize((new_w, new_h))thumbnail则是“缩小且保持比例”,而且它的特点是只可能缩小,不可能放大,原地修改Image对象。它的参数是一个“最大尺寸的框”,图片会被等比缩放到恰好放进这个框内。
img = Image.open("test.jpg") img.thumbnail((400, 400)) # 直接改img,尺寸不会超过400x400,且比例不变 print(img.size)实际做网站缩略图、微信头像压缩、商品图裁剪时,我基本都用thumbnail,很少用resize,因为用户上传的图片比例千奇百怪,等比缩略永远不会被骂“变形”。
再说说OpenCV的resize,它是另一个写法:
import cv2 img = cv2.imread("test.jpg") resized = cv2.resize(img, (800, 600)) # 指定尺寸 resized_fx = cv2.resize(img, None, fx=0.5, fy=0.5) # 按系数缩放关于缩放的一个提醒:小图放大会模糊,这是信息缺失,任何库都无法真正“补”出细节。如果必须放大,可以试试cv2.INTER_CUBIC或者Pillow里resample=Image.LANCZOS,视觉效果会好一点点,但也只是“好一点点”。我的态度是:能在拍摄或生成阶段保证分辨率,就别指望后期放大救回来。
4.2 crop、rotate、transpose的坐标与参数细节
裁剪的坑主要在坐标系理解上。Pillow的crop接收一个四元组(left, upper, right, lower),分别是左、上、右、下,坐标原点在左上角,而且是左闭右开区间。很多人写(0, 0, w, h)是没问题的,但一旦要裁剪中间区域,就经常把位置搞反。
from PIL import Image img = Image.open("test.jpg") # 裁剪出中间200x200的区域 w, h = img.size left = (w - 200) // 2 upper = (h - 200) // 2 right = left + 200 lower = upper + 200 box = (left, upper, right, lower) cropped = img.crop(box)记住一个判断技巧:right和lower是“不包含”的边界,所以不存在“边界少一像素”需要头疼的问题。想裁剪右上区域,就把left设为w的一半,right设为w;想保留透明通道,crop会原样保留模式。
旋转用rotate,重点参数是expand。默认expand=False时,旋转后Canvas大小不变,图像超出部分会被裁剪掉,图片边角变成背景色;设置expand=True时,画布会跟着旋转后的外接矩形扩展开,图片内容完整保留,但尺寸变大,且多出来的区域默认填黑色,可以通过fillcolor改成白色或别的颜色。
img = Image.open("test.jpg") # 顺时针转90度(Pillow里角度是逆时针的,负号表示顺时针) img_rot = img.rotate(-90, expand=True)翻转用transpose,参数只有两个常用值:Image.FLIP_LEFT_RIGHT是水平翻转(镜子效果),Image.FLIP_TOP_BOTTOM是垂直翻转。注意不是Image.FLIP_TOP_BOTTOM缩写成FLIP_Y之类,别记错。
OpenCV里对应的是cv2.flip(img, 1),第二个参数0表示上下翻转,1表示左右翻转,负数表示两者都做。这个参数设计容易被新手搞混,我的记忆口诀是“0竖,1横,负号双翻”。
5. 颜色空间与通道操作:灰度、RGB拆分与合并
5.1 灰度化公式和convert("L")
灰度化就是把彩色图变成黑白图。别小看这个操作,很多后续算法(边缘检测、阈值分割、模板匹配)都默认先转灰度,因为减少了一个维度,计算量大幅下降,而且很多特征在灰度下已经很清晰。
Pillow一行就能转:
img = Image.open("test.jpg") gray = img.convert("L") gray.save("gray.jpg")convert("L")的L代表Luminance(亮度),它内部用的是这个加权公式:
Y = 0.299 * R + 0.587 * G + 0.114 * B记住这个公式,面试或写算法时经常用到。它本质是在说:人眼对绿色最敏感、对蓝色最不敏感,所以绿色分量权重最大,蓝色权重最小。如果自己用numpy实现这个转换,千万别用三个通道取平均((R+G+B)/3),那样出来的灰度图灰蒙蒙的,丢失了人眼的感知特点。
OpenCV里灰度化是:
import cv2 img = cv2.imread("test.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)注意OpenCV的灰度化结果和Pillow的convert("L")在数值上略有差异,因为OpenCV默认用的Rec.601系数、舍入方式不同,但对绝大多数业务场景没影响。追求像素级完全一致时,直接用numpy自己按公式算最可控。
5.2 split/merge通道操作与BGR坑
彩色图本质是三个通道叠加。有时你需要单独操作某个通道,比如“把天空做得更蓝”“去掉图片里的红色偏色”,这时就要拆通道。
OpenCV里拆通道再合并非常直观:
import cv2 img = cv2.imread("test.jpg") b, g, r = cv2.split(img) # 注意顺序是B, G, R # 对通道做点操作 b = cv2.add(b, 30) # 蓝色通道统一加30,图片偏蓝 result = cv2.merge([b, g, r])Pillow里拆通道用split(),返回三个独立灰度Image,合并用merge("RGB", (r, g, b)):
from PIL import Image img = Image.open("test.jpg") r, g, b = img.split() # 重新合并,但把R和B交换,会出现蓝红互换的效果 img_swap = Image.merge("RGB", (b, g, r))我发现很多人在自写看图程序时,会把Pillow读出来的RGB数组直接交给OpenCV处理,结果颜色偏蓝偏红。原因就是上面说的通道顺序。解决方案有两个:要么只用Pillow处理,要么在转成数组后手动调整通道顺序:
import cv2 from PIL import Image import numpy as np img = Image.open("test.jpg") arr_rgb = np.array(img) # 按OpenCV期望的BGR顺序重新排列 arr_bgr = cv2.cvtColor(arr_rgb, cv2.COLOR_RGB2BGR)通道操作还常用来做简单的颜色区域筛选,比如红灯检测:分离出红色通道,再加一个高阈值,就能把“很红”的像素圈出来。这类技巧做工业视觉、交通信号识别时非常常用,但注意光照变化影响很大,实际项目里要做归一化或直方图均衡,那是另一个话题了。
6. 滤镜与增强:亮度、对比度、模糊和边缘
6.1 ImageEnhance快速调亮度对比度
把一张灰蒙蒙的照片调亮,或者把过度曝光的图片拉回来,这类需求用Pillow的ImageEnhance模块,体验可以用“丝滑”来形容。
from PIL import Image, ImageEnhance img = Image.open("dark.jpg") # 亮度增强,1.0是原始状态,小于1变暗,大于1变亮 enhancer = ImageEnhance.Brightness(img) bright_img = enhancer.enhance(1.3) # 对比度增强,同样1.0为原始 enhancer2 = ImageEnhance.Contrast(img) contrast_img = enhancer2.enhance(1.5) # 颜色饱和度增强 enhancer3 = ImageEnhance.Color(img) color_img = enhancer3.enhance(1.2) # 锐度 enhancer4 = ImageEnhance.Sharpness(img) sharp_img = enhancer4.enhance(2.0) bright_img.save("bright.jpg")参数理解很简单:enhance(1.0)就是原图,>1加强,<1减弱。这个模块底层也只是对像素做线性变换,但封装得好,代码很干净。我经常在一个批量工具里写个循环,给一组图片统一调亮度,比如把所有偏暗的手机照片提亮到1.2倍,效果稳定且速度快。
有一类场景要特别注意:JPG的亮度提升会放大噪点。如果照片本身是在暗光下拍的,JPG压缩已经让暗部出现色块和噪点,用enhance拉高亮度后噪点会更明显。这种情况与其调亮度,不如先用去噪滤波器,再轻微提亮,最后用一点锐度找回细节。处理的顺序不同,最终观感差距很大。
6.2 高斯模糊、Canny边缘与二值化
模糊和边缘检测我一般直接用OpenCV,因为函数全、速度快,处理大图优势明显。
高斯模糊用来去噪、磨皮、制造景深效果:
import cv2 img = cv2.imread("test.jpg") # (5, 5)是高斯核大小,必须是正奇数;0表示标准差自动计算 blurred = cv2.GaussianBlur(img, (5, 5), 0)高斯核越大,模糊越强,但运算量也变大。一个5x5的核处理1080p图片几乎瞬秒,99x99就会慢起来。做预览调试时先从小核开始,别一上来就上大核。
边缘检测最常用的是Canny:
import cv2 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 两个阈值:低于min的像素肯定不是边缘,高于max的肯定是边缘,中间看连接性 edges = cv2.Canny(gray, 100, 200)Canny的阈值选择是很多新人的困惑点。我的经验是:图片比较干净、边缘清晰,用(50, 150);图片噪声多、纹理复杂,用(100, 200)甚至(150, 250)。阈值高了,弱边缘会被过滤;阈值低了,噪声会变成一堆杂乱线条。可以先用(100, 200)起步,再根据结果微调。
二值化就是把灰度图变成“非黑即白”。最简单的是固定阈值:
import cv2 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 大于127的变成255(白),小于等于127的变成0(黑) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)但固定阈值对光照不均匀的图片效果很差,比如文档扫描件一半亮一半暗。这时候用自适应阈值:
adaptive = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 )这里11是邻域大小,2是常量修正值。自适应阈值的原理是:每个像素的阈值由它周围一个小窗口内像素的加权平均值减去常量得到,所以能适应局部的明暗变化。处理扫描件、拍歪的名片时,我几乎无脑用自适应阈值,效果比固定阈值好一大截。
7. 实战脚本:批量缩略图、加水印与图片拼接
7.1 批量生成缩略图:一个脚本省一天时间
理论知识讲再多,不如一个完整脚本。我经常遇到的情况是:设计师丢给我几百张高清图,让我生成一套宽度800px的缩略图。手工一张张开、缩放、另存,能折磨死人。用Python写个脚本,几秒钟搞定。
import os from PIL import Image input_dir = "photos" # 原始图片目录 output_dir = "thumbs" # 缩略图输出目录 target_width = 800 # 目标宽度,高度等比 os.makedirs(output_dir, exist_ok=True) for filename in os.listdir(input_dir): if not filename.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")): continue img_path = os.path.join(input_dir, filename) img = Image.open(img_path) # 等比缩放 w, h = img.size if w <= target_width: # 本来就小于目标宽度,直接复制过去或跳过 continue new_h = int(h * target_width / w) img_scaled = img.resize((target_width, new_h), Image.LANCZOS) out_path = os.path.join(output_dir, filename) img_scaled.save(out_path, quality=90) print(f"已处理 {filename}: {w}x{h} -> {target_width}x{new_h}")这个脚本有几个细节值得说。一是os.makedirs(output_dir, exist_ok=True),不用担心目录不存在报错;二是扩展名判断用lower(),避免大小写问题,“.JPG”也能识别;三是quality=90,save JPG时如果不指定quality,默认是75,文字边缘会出现明显锯齿,放大看特别明显;四是img.resize的Image.LANCZOS重采样,在缩小时能保留更多细节,效果比默认的最近邻插值好很多。如果图片量特别大,还可以在循环里del img,释放内存,防止几千张图撑爆内存。
7.2 文字水印与多图拼接
给图片批量加个版权水印,也是高频需求。Pillow里往图片上写字的核心是先建一个叠加图层,再用Image.paste贴上去,最后合并。如果直接往原图上draw,原图的像素会被永久修改,一旦画错就没法撤销。我习惯在副本上操作:
from PIL import Image, ImageDraw, ImageFont img = Image.open("test.jpg").convert("RGBA") # 转RGBA才能有透明效果 # 建一个透明图层 overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) # 中文字体需要显式指定,默认字体不支持中文,否则会变方框 font = ImageFont.truetype("simhei.ttf", 60) text = "示例水印" # 获取文字尺寸,方便居中放置 bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] x = (img.width - text_width) // 2 y = (img.height - text_height) // 2 draw.text((x, y), text, fill=(255, 255, 255, 128), font=font) # 半透明水印叠回原图 out = Image.alpha_composite(img, overlay) out.convert("RGB").save("watermarked.jpg")关于字体,新手最常踩的坑就是“中文字变成方块”。原因很简单:Pillow默认字体只有英文和数字。解决方式是指定一个系统里存在的中文字体文件路径,我上面写的"simhei.ttf"是Windows的黑体,如果你在Mac上,可以换成"/System/Library/Fonts/PingFang.ttc"或"/System/Library/Fonts/STHeiti Medium.ttc"。设计水印时,fill=(255, 255, 255, 128)里的第4个数是透明度,128表示半透明,这样既不影响看图,又能起到版权提示作用。如果直接填255不透明,水印会显得很突兀,像贴了一块创可贴。
多图拼接,横向拼、纵向拼也一样很简单,核心思想是:先创建一张大画布,然后用paste把图片按位置填进去:
from PIL import Image images = [Image.open(f"pic{i}.jpg") for i in range(1, 4)] widths, heights = zip(*(im.size for im in images)) canvas_w = sum(widths) # 横向拼接,总宽是所有宽度之和 canvas_h = max(heights) # 总高取最高的图 canvas = Image.new("RGB", (canvas_w, canvas_h), (255, 255, 255)) x_offset = 0 for im in images: canvas.paste(im, (x_offset, 0)) x_offset += im.size[0] canvas.save("combined.jpg")如果要纵向拼接,把canvas_w和canvas_h换一下,粘贴时改y_offset就行。需要注意paste时图片和画布颜色模式最好一致,不一致时先convert,否则可能出现奇怪的通道错误。
8. 常见报错与排查技巧实录
8.1 环境问题速查表
把我在实际里见过的高频报错整理成表格,每一条都是真实的“翻车现场”:
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
| pip不是内部或外部命令 | Python没把Scripts目录加进PATH | 用python -m pip代替pip;或者重装Python时勾选Add to PATH |
| ModuleNotFoundError: No module named 'PIL' | 没装Pillow或装错包 | python -m pip install pillow |
| ModuleNotFoundError: No module named 'cv2' | 没装OpenCV | python -m pip install opencv-python |
| AttributeError: module 'PIL.Image' has no attribute 'open' | Open可能拼成了小写open,或者Image是文件不是模块 | 检查文件名不要叫image.py,检查from PIL import Image |
| OSError: cannot identify image file | 路径不对或文件损坏 | 检查路径是否存在,用os.path.exists验证;文件可能不是有效图片 |
| TypeError: Cannot handle this data type | 数组dtype是float64或uint32,Image.fromarray不支持 | 先arr.astype(np.uint8)再转换 |
| 中文文件名读不了 | OpenCV的imread对中文路径支持差 | 用np.fromfile+cv2.imdecode读取 |
最后一行的中文路径问题是Windows用户的重灾区。很多人代码在英文路径下跑得飞起,一放到“C:\用户\我的图片\测试.jpg”就返回None,还以为图片损坏。OpenCV的cv2.imread确实没处理好中文路径,我的通用解法是:
import cv2 import numpy as np # 用numpy读字节流,再用imdecode解码,绕开imread的路径限制 img = cv2.imdecode(np.fromfile("测试/中文路径.jpg", dtype=np.uint8), cv2.IMREAD_COLOR) # 保存时也一样 cv2.imencode(".jpg", img)[1].tofile("输出/结果.jpg")8.2 运行逻辑上最容易翻车的几个点
环境问题看表格能解决大部分,但运行逻辑上的坑更隐蔽,经常是“代码没报错,但结果不对”。我挑了三个频率最高的。
第一个是坐标顺序。前面反复提过,Pillow的size是(宽, 高),numpy数组的shape是(高, 宽),OpenCV的shape也是(高, 宽)。写循环遍历像素时,先写哪个维度决定了你的遍历方向。比如for y in range(h): for x in range(w):是先按行扫,for x in range(w): for y in range(h):是先按列扫。搞反了之后,校验、裁剪、缩放的区域全错。我的建议是:所有图像处理代码的开头先写一行注释,注明当前变量是(width, height)还是(height, width),别嫌丑,关键时刻能救命。
第二个是像素值越界和类型溢出。numpy数组如果是uint8,直接做arr + 100,超过255的部分会循环回绕,255加1变成0,图片出现大量黑点白点。正确做法是把数组先转成int16或float,运算完再np.clip(arr, 0, 255).astype(np.uint8)。我看过好几个人用numpy调亮度,出来的图全是斑点,就是这个原因。
第三个是延迟加载和资源释放。Pillow的Image.open不立即读入全部像素,如果你在循环里把几百个Image对象保存在列表里,内存占用会非常吓人。处理完一张后立即del img并gc.collect(),或者用with Image.open(...) as img的方式,确保用完就关。做批量任务时,这两行代码能让你从内存崩溃边缘稳住。另外,如果图片尺寸非常大,比如几亿像素的扫描图,连Pillow都扛不住时,可以考虑Image.open后先thumbnail再load,只加载缩略部分,不碰原始全分辨率。
写在最后的一点个人体会
最后想分享一个我自己的习惯:不管脚本多简单,我都会把处理函数封装成def,参数尽量单独定义,比如def process_image(input_path, output_path, target_width=800, quality=90)。一开始图省事直接写成一坨脚本,后来改需求时痛苦得想死——宽度改一下、输出目录改一下,要在好几处代码里同步改,还容易漏。封装成函数之后,再配合个配置文件或命令行参数,改起来就轻松多了。
另外,图片处理一定要养成“分步调试”的习惯。先读进来看一眼原图,处理一步保存一版,最后再拼到一起。别指望一次性写出完美代码,绝大多数时候都会出点小问题,分步看中间结果,一分钟就能定位是哪一步出了问题。做批量处理前,建议先拿两三张图试跑,确认效果符合预期再丢全量数据,别一上来就全跑,万一逻辑写反,几千张图全毁了,重新处理倒是小事,用户给你的时间可不会重来。
这套函数组合我用了很多年,从简单的爬虫验证码识别预处理,到上万张商品图的统一缩略与加水印,一直稳得很。希望这篇笔记能让你省下一些绕弯的时间。