简介这是一份Python图像处理入门PDF面向对图像分析与处理感兴趣的初学者也适合需要快速回顾常用工具方法的技术人员。文档从图像在计算机中的表示讲起单通道灰度图像对应8位无符号整型矩阵多通道图像则由红绿蓝三通道构成并用NumPy数组承载。围绕显示图像、裁剪、翻转、旋转、图像分割、分类与特征提取、图像恢复和图像识别等常见任务简明梳理PIL、OpenCV、scikit-image、matplotlib等库的定位与典型用途同时给出医学影像、自动驾驶、安防监控、社交媒体等真实应用场景。全文以概念梳理和工具概览为主便于初学者建立知识框架后再深入实战。资源包共1个PDF文件大小约82KB非常轻量便携。截至目前已有2669人学习下载适合作为Python图像处理的入门参考与知识速查手册。1. Python图像处理不是魔法一条从像素到结论的流水线拿到一批零件拍摄图要统计每张图里有多少个标准圆形工件顺带量出直径分布。有人以为“调个库、点个按钮”就能出结果真正动手之后才发现同样的程序、差不多的图换个批次就会出现几十个假目标。python图像处理不是记住几个函数的事核心是把整条流水线理顺读取、预处理、分割、测量、验证。这条路在一百张图上跑得通换到两千张图才不会散架。自动化质检、显微镜图像测量、批量截图分析都可以用新手按步骤走能落地熟手也能在后面的避坑清单里找到想要的东西。我会从工具选型讲起把像素操作和连通域分析一次说透最后给出一段可直接改用的批处理骨架。2. 先选对工具栈Pillow、OpenCV 和 NumPy 的分工边界2.1 没有万能图像库只有分工明确的组合Pillow、OpenCV、NumPy 是三条腿。Pillow 最轻负责读图、保存、转格式、加画框这类 I/O 操作API 对新手友好中文路径的处理也比较省心。OpenCV 的功能密度最大灰度化、滤波、形态学、轮廓提取、连通域分析几乎都齐了而且核心算法在原生层实现大批量处理时优势明显。NumPy 提供的是数组语义图像进入程序后就是数字矩阵逐像素计算、条件筛选都得靠它。三者各有强项重叠部分很多强行只用其中一个也能干活但我更建议按场景分工需要快速看一张图时用 Pillow需要做几何变换和轮廓分析时用 OpenCV需要自己写像素级逻辑时把图像转成 NumPy 数组。为什么强调分工因为代码可读性和性能往往因此改变。Pillow 的 Image 对象不是二维数组OpenCV 的图像天生是三维数组两者混用还要反复转换还不如一开始就定好数据格式。常见做法是程序入口用 Pillow 读取立即转成 BGR 或 RGB 的 NumPy 数组后续统一走 OpenCV输出时再交给 Pillow 或 cv2.imwrite。这样每条数据的数据形状都很明确不会有“某个函数接受的是什么类型”的困惑。选型只要定下来整个项目都不会因为类型不匹配而中途重写。2.2 最小环境搭建与首张图的读取建立一个虚拟环境用标准库避免污染全局环境。命令如下python -m venv venv source venv/bin/activate python -m pip install --upgrade pip python -m pip install pillow opencv-python numpyWindows 上激活命令是 venv\Scripts\activateLinux 和 macOS 是 source venv/bin/activate。opencv-python 这个包装好之后导入名是 cv2不是 opencv。这三个包体积都不小建议在虚拟环境中安装不要往系统环境里塞。装完后先读一张图确认 I/O 正常from PIL import Image import cv2 import numpy as np # 用 Pillow 读适合格式转换和快速预览 pil_img Image.open(sample.jpg) print(Pillow size:, pil_img.size, mode:, pil_img.mode) # 用 OpenCV 读默认返回 BGR 三通道数组 cv_img cv2.imread(sample.jpg) print(OpenCV shape:, cv_img.shape) # Pillow 图像转成 NumPy 数组 arr np.array(pil_img.convert(RGB)) print(NumPy array shape:, arr.shape) # BGR 转 RGB后续保存、显示或做像素比较时颜色才一致 rgb_img cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)这段代码展示了两个最关键的差异。Pillow 的 size 是(宽, 高)OpenCV 的 shape 是(高, 宽, 通道数)两者顺序正好相反刚入门时经常因为这个问题把横竖弄混。OpenCV 默认读出来是 BGRPillow 读出来是 RGB如果直接用 cv2.imread 读图再用 Pillow 保存画面会明显偏蓝这就是通道顺序导致的颜色错位。正确的做法是在跨库传递前统一颜色顺序。这里有一个常见误用想提醒一下cv2.imread 在遇到路径包含中文时会返回 None程序往往在下一行就报错。遇到这类场景我会改用 np.fromfile 配合 cv2.imdecode把文件先读成字节流再解码import numpy as np import cv2 def imread_unicode(path): data np.fromfile(str(path), dtypenp.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) return img参数说明np.fromfile 以二进制方式读取路径内容dtype 指定为 uint8对应图像的原始字节cv2.imdecode 的第二个参数 IMREAD_COLOR 表示强制按三通道 BGR 解码。这个函数可以让中文目录下的图片不再成为黑匣子文件只要真实存在就能读进来。保存时如果也有中文路径可以反过来用 cv2.imencode 把图片编码成字节再配合 ndarray.tofile 写盘。2.3 保存格式与压缩参数别让输出变成新的坑输出图像时很多人直接 cv2.imwrite但不同格式的参数会影响画质和体积。比如 JPEG 是有损压缩压缩质量直接决定文件大小和画面细节PNG 是无损但压缩等级写得太高会拖慢保存速度。常见的做法是按输出扩展名设置参数ext str(out_path).lower().rsplit(., 1)[-1] if ext in (jpg, jpeg): cv2.imwrite(str(out_path), img, [cv2.IMWRITE_JPEG_QUALITY, 95]) elif ext png: cv2.imwrite(str(out_path), img, [cv2.IMWRITE_PNG_COMPRESSION, 3])JPEG 质量参数范围是 0 到 10095 适合需要保留细节的场景PNG 压缩等级范围是 0 到 9数字越大文件越小但耗时更长3 是一个读写速度和体积兼顾的经验值。如果做图像处理的中间结果建议优先用 PNG因为无损格式不会让下一步的阈值分割受压缩伪影干扰。保存前还要确认扩展名与编码一致否则 OpenCV 会根据扩展名决定写盘格式扩展名写错会直接报错或者生成非预期文件。工具栈确认好之后下一步就可以进入像素级处理。3. 像素级处理实战灰度化、二值化与形态学去噪3.1 灰度化和二值化阈值参数该选多少图像从彩色到二值图通常走两步。第一步是灰度化把三通道压缩成单通道第二步是阈值分割把 0 到 255 的灰度区间映射成 0 和 255 两个值。灰度化不是简单的平均OpenCV 的 cvtColor 内部使用一组加权系数更贴近人眼对亮度的感知。二值化时最常踩的坑是把阈值写死。import cv2 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 固定阈值适合光照波动不大的场景 _, binary_fixed cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY) # 大津阈值自动计算适合背景和前景灰度差异明显的图 _, binary_otsu cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)cv2.threshold 的第三个参数是阈值第四个参数是阈值类型。固定阈值 128 简单粗暴但图片偏暗或偏亮时会把大片背景划成前景也会把前景直接漏掉。大津法让算法按灰度直方图自动找阈值传入的第一个参数 0 只是占位真正生效的是最后追加的 cv2.THRESH_OTSU。使用大津法的前提是灰度直方图最好呈现双峰如果图像光照严重不均直方图像一个斜坡大津法也会失效。判断是否适合大津法最直观的办法是先看灰度直方图hist cv2.calcHist([gray], [0], None, [256], [0, 256])cv2.calcHist 的参数表示输入图像列表、需要统计的通道索引、掩膜、每个维度的直方图长度、像素值范围。hist 是一个 256 行、1 列的数组第 i 个值表示灰度级 i 出现了多少个像素。把 hist 打印成折线图如果看到两个明显的峰大津法通常能给出不错的结果如果只有一个宽峰就要考虑局部阈值或在预处理阶段做背景扣除。还有一个细节二值化支持 cv2.THRESH_BINARY_INV它把前景和背景对调适合白底黑目标或黑底白目标两种不同情况。3.2 形态学去噪核的大小是最先要调整的参数二值化之后会有两类噪声前景内部的空洞以及背景区域散落的小点。形态学操作就是处理这类问题的常用手段。开运算是先腐蚀再膨胀能去掉前景周边的孤立小点闭运算是先膨胀再腐蚀能填上前景内部的小孔。两者不会像滤波那样引入新的灰度值。kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations1) closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations1)cv2.getStructuringElement 的第一个参数决定核的形状。MORPH_ELLIPSE 是椭圆核对颗粒状目标更友好MORPH_RECT 是矩形核速度快但会把圆角目标的边缘搞得更方正MORPH_CROSS 适合细长目标。核尺寸一般从 (3, 3) 开始试过小压不住噪点过大会把相邻目标粘在一起。iterations 表示重复执行次数不是核大小的替代品重复多次和加大核不完全等价一般保持 1 到 2 次。操作顺序也很重要。先开后闭是常见流程开运算先去孤立噪点闭运算再补目标内部的孔洞。颠倒顺序也有效果但先闭后开容易把小噪点扩大再被腐蚀留下痕迹。如果噪点不是单个小点而是细长划痕椭圆核比矩形核更贴合真实边缘不容易把目标整体切掉。想快速预览效果时先用开运算通常就够了要分离粘在一起的颗粒后面还需要连通域分析或距离变换不能只靠形态学。3.3 预处理流水线目标颗粒识别前的标准姿势把灰度、二值化、形态学串成一个函数每一层的前后都保留中间结果方便后面验证。以下是我常用的函数框架def preprocess_to_binary(img, use_otsuTrue, kernel_size3): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if use_otsu: _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) else: _, binary cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations1) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations1) return gray, binary参数 use_otsu 控制是否自动计算阈值kernel_size 控制核的边长。这样设计的好处是在不同批次图片上由统一入口调整参数而不需要改上游函数。运行后把 binary 打印成黑白图检查噪点是否被滤除、目标边缘是否还完整。如果想要固定阈值调用时可以写 preprocess_to_binary(img, use_otsuFalse, kernel_size5)方便对比不同参数产生的分割效果。实际项目里常见的问题是照着教程做完发现阈值分割结果“差不多”但连通域统计出的数量忽高忽低。问题往往不出在阈值而出在预处理顺序。灰度化和二值化只是把像素分类成前景和背景形态学去噪才是影响计数准确率的关键一步。核太小噪点会混进目标核太大两个靠得近的目标被并成一个。所以调整形态学参数时需要来回看原始二值图和最终连通域结果而不是只盯着一个输出看。这一章属于整个处理流程的中心理解透之后再去做连通域分析就顺了。4. 连通域分析与目标测量从二值图到面积和轮廓4.1 连通域标记与轮廓提取两种路线怎么选二值图里每一个白色区域代表一个目标或一块噪声。要把它们变成可测量的对象常用的有两套方案findContours 与 connectedComponentsWithStats。findContours 返回的是轮廓坐标序列适合画边界和计算周长connectedComponentsWithStats 直接给出每个连通域的像素数量、外接框、质心适合批量统计。import cv2 num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(binary, connectivity8) for i in range(1, num_labels): x stats[i, cv2.CC_STAT_LEFT] y stats[i, cv2.CC_STAT_TOP] w stats[i, cv2.CC_STAT_WIDTH] h stats[i, cv2.CC_STAT_HEIGHT] area stats[i, cv2.CC_STAT_AREA]参数说明binary 必须是单通道二值图connectivity8 表示上下左右以及四个斜角都算相邻connectivity4 只算上下左右。返回值里 labels 是每个像素的标签图stats 是一个二维数组一行对应一个区域第 0 行是背景所以循环从 1 开始。centroids 是每个区域的质心坐标。这个方法特别适合计数类需求因为不用遍历轮廓点也能拿到像素面积。findContours 的优势是能拿到边界几何。在当前版本的 OpenCV 中cv2.findContours 返回两个值轮廓列表和层级关系如果是从网上找到的旧代码经常会出现三个返回值那是旧版本写法直接编译会报错。轮廓的层级关系在嵌套场景时很有用比如检测圆环外轮廓和中间孔洞靠层级可以把内外轮廓关联起来。如果只是统计面积和数量我一般优先用连通域标记代码短返回维度也更紧凑。4.2 面积、外接框、长宽比三个指标过滤噪声连通域标记之后很多“目标”其实只是形状不规则的噪声。用面积、长宽比、填充率三个指标可以过滤掉大部分假目标。面积过滤最直接小于阈值的区域直接丢弃长宽比能识别横向长条的阴影填充率是轮廓面积与外接矩形面积的比值圆形目标填充率接近 π/4细长线状物填充率很小。min_area 30 for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] if area min_area: continue x stats[i, cv2.CC_STAT_LEFT] y stats[i, cv2.CC_STAT_TOP] w stats[i, cv2.CC_STAT_WIDTH] h stats[i, cv2.CC_STAT_HEIGHT] aspect w / h if h 0 else 0 if aspect 3 or aspect 1 / 3: continue fill_rate area / (w * h) if fill_rate 0.2: continuemin_area 是一个经验值需要根据图像分辨率而定。同样面积在 4000 像素宽的图里是正常目标在 800 像素宽的图里可能就是噪声。长宽比阈值 3 适合过滤横向或纵向的细长反光如果目标本身是长条零件就得放宽。填充率阈值 0.2 适合过滤树叶、划痕这类结构不规则的对象如果目标是实心颗粒填充率正常在 0.5 以上。三个参数按顺序执行每有一个条件不满足就跳过能显著降低误报。过滤时还要注意一个边界情况目标紧贴图像边缘时外接框宽高和面积会被截断长宽比和填充率都会失真。如果业务要求严格就直接丢弃接触边界的区域如果不要求严格统计至少在结果里给一个 is_edge 标记方便后续人工复核。这个细节在批量处理中影响很大因为大批量图里总有几张目标恰好压边。4.3 把测量结果写成结构化数据从像素值到可用结论测量结果如果只是 print 在终端下一轮处理还得重新跑。常见做法是把每次测量的结果收集到列表再用 csv 模块输出。为了建立“像素到结论”的完整闭环可以保存每个目标的坐标、宽高和面积也可以再加一个矩形框预览图方便人工核对。import csv rows [] for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] if area min_area: continue x stats[i, cv2.CC_STAT_LEFT] y stats[i, cv2.CC_STAT_TOP] w stats[i, cv2.CC_STAT_WIDTH] h stats[i, cv2.CC_STAT_HEIGHT] rows.append([x, y, w, h, area]) with open(measure_result.csv, w, newline) as f: writer csv.writer(f) writer.writerow([x, y, w, h, area]) writer.writerows(rows)csv 是标准库不需要额外安装输出字段顺序按照测量需求排列。若想统计直径可以再用外接框宽高平均或最小外接圆直径来估算。OpenCV 的 cv2.minEnclosingCircle 能返回最小外接圆的圆心和半径适合圆形目标cv2.minAreaRect 返回最小外接矩形适合倾斜生长的长条目标。选择哪种测量方式需要回到业务目标而不是哪个函数看着高级就用哪个。到这里单张图的处理流程已经走通下一步就是把流程放到批量和复杂场景里接受检验。5. 图像处理避坑指南我反复踩过的4个技术坑5.1 颜色通道错位为什么保存出来的图“变蓝了”现象用 cv2.imread 读取图片再用 Pillow 保存或者直接用 matplotlib 显示时画面整体偏蓝原本红色的区域变成暗蓝色。这个问题在纯灰度图上不出现一旦换成彩色图立刻暴露。原因OpenCV 读取后按 BGR 顺序存储而 Pillow、matplotlib 和大多数显示工具按 RGB 解释数据。如果不做转换相当于把红色通道和蓝色通道交换了解释颜色自然错位。颜色通道不是“图对不对”的问题它会影响后面所有依赖通道顺序的判断逻辑。解决所有从 OpenCV 出来的彩色图输出前统一执行 cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。如果走的是 Pillow 入口先 np.array(pil_img.convert(RGB))再交给 OpenCV 时执行 cvtColor 转成 BGR。给处理流程加一条铁规矩程序内部统一用一种颜色顺序只在边界处转换避免散落一堆转换调用。最简单的封装是自定义读写函数把转换放在一个函数里其他模块拿到数据时已经默认是约定好的格式不会再有人重复转换导致颜色越转越乱。5.2 光照不均固定阈值为什么在角落翻车现象同一批图片中央区域目标分割得很干净边缘区域却出现大块黑色或者大面积白色导致计数结果和人工数出来的差很多。这种情况在自然光拍摄的照片里特别常见。原因阈值是全局的。整张图只有一个固定阈值而光照并不是均匀的靠近光源的一侧灰度偏高远离光源的一侧灰度偏低。固定阈值只能照顾一个亮度水平无法同时适配暗角和亮区。大津法也属于全局阈值在直方图呈现单峰或坡度曲线时同样无能为力。解决先做背景估计再在原图上减去背景。常用做法是对灰度图做一次尺寸较大的高斯滤波得到低频背景再用原图减去背景把光照梯度去掉最后做阈值分割。示例代码如下gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (51, 51), 0) diff cv2.subtract(gray, blur) _, binary cv2.threshold(diff, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)GaussianBlur 的核尺寸 (51, 51) 是经验起点要远大于目标颗粒的尺寸保留背景梯度而抹掉前景细节。核太小会把目标也算进背景核太大则背景扣除不彻底。diff 里负数会被饱和为 0所以有时需要先调整对比度或者加上一个固定偏移。实际项目中这招比反复调阈值有用得多因为它在根源上去掉了光照差异而不是硬找一个中间值。如果核尺寸不好判断可以先统计目标大小再用目标最大宽度乘以 3 到 5 作为高斯核直径。5.3 目标粘连形态学参数一放大就把两个目标并成一个现象想要去掉小噪点把形态学核从 (3, 3) 调到 (7, 7)结果两个距离较近的目标被连成一个整体统计数量直接少了一半。跑完后再看预览图原本清晰的分界线已经消失。原因开运算和闭运算的核尺寸变大后会改变目标的边界和间距。开运算本质是腐蚀再膨胀核越大对前景边界的切削越强当两个目标之间的间隙小于核的尺寸时膨胀阶段可能又连上了特别是先闭后开或者连续多次使用时更容易粘连。解决不要为了去噪无脑放大核。先看二值图上的噪点尺寸确定核的直径应该略大于噪点但明显小于目标间距。处理颗粒目标时我用 (3, 3) 或 (5, 5) 起步最多到 (7, 7)并且每次只调一档跑完马上看连通域结果。如果目标仍然粘连改用距离变换做分水岭或者把分类目标从“面积”改成“轮廓凹陷程度”。距离变换是处理圆形颗粒的常用方案但代码量比形态学大也要额外调一个局部峰值参数。不要在没有可视化的情况下调大核这是最常见的教训。5.4 内存爆满批量读图时进程直接消失现象对几千张图做同样处理跑不到一半内存占用飙升操作系统把进程杀掉前面处理的结果因为没保存全部丢失。重启之后换了一种输入顺序进程存活时间还不一样看起来像随机翻车。原因一次性把所有图片路径读进列表或者用列表收集全部处理后的数组。图像数组不是小数一张 4000x3000 的彩色图转成 NumPy 数组就是 30 多 MB几千张同时滞留内存再大的机器也会被拖垮。更隐蔽的是在循环里把结果 append 到全局列表用完没有释放内存只增不减。解决改用手写生成器分批读取并尽早把结果落盘不要攒到最后统一写。生成器每次只提供一个路径处理完一个就释放一个循环结束后数组对象被垃圾回收内存压力会小很多。保存结果时用追加模式写 CSV或者每处理一张就单独写一个 JSON这样即使中途崩掉已完成的文件还能继续用。给批量任务加一个 try/except 也是好习惯单张图解码失败不应导致整批任务终止。这些都是把脚本变成可用工具前必须跨过的门槛。以上4条经验每一条都能对应到一个具体的现象。图像处理看起来是在调参数实际上是在修数据通路。通道顺序、光照分布、形态学尺寸、内存生命周期这四类问题处理干净绝大多数批量流程就不会翻车。6. 把流程封装成可复用工具批处理、日志与结果验证6.1 生成器配进程池批量处理的骨架用生成器一次给一个路径再用进程池并行处理每张图的数组不会长期占用内存。以下形态是我跑几千张图很常用的骨架from pathlib import Path from concurrent.futures import ProcessPoolExecutor import csv def iter_images(root, exts{.jpg, .jpeg, .png}): for p in Path(root).rglob(*): if p.suffix.lower() in exts: yield p def process_one(path): # 返回一行测量结果图片对象在函数结束后释放 return [path.name, 0, 0, 0] if __name__ __main__: with ProcessPoolExecutor(max_workers4) as pool: rows pool.map(process_one, iter_images(samples)) with open(batch_result.csv, w, newline) as f: writer csv.writer(f) writer.writerow([name, x, y, area]) writer.writerows(rows)iter_images 是生成器rglob 会递归遍历所有子目录pool.map 会把路径逐个交给进程池。多进程适合 CPU 密集型的图像处理能多核并行但注意传给 process_one 的参数要可以序列化返回结果也要尽量短不要直接把整张图像数组返回给主进程。脚本入口用 ifname main 是多进程必需的否则在 Windows 上会反复创建子进程。6.2 参数留痕与结果预览给后续调整留条后路进程池只是加快速度调参时最需要的是记录。我习惯把阈值方式、核尺寸、最小面积和输入目录放到一个 JSON 配置里程序结束时原样写入结果目录。import json config { threshold_mode: otsu, kernel_size: 3, min_area: 30, input_folder: samples, } with open(run_config.json, w, encodingutf-8) as f: json.dump(config, f, indent2, ensure_asciiFalse)这段代码解决的问题很实际两周之后翻看结果还能知道当年是哪组参数跑出来的。图像处理的高频操作是改阈值、改核尺寸、改过滤条件没有留痕就只能靠记忆时间一长就是一个黑匣子。再加一步结果验证把原始图和标注框并排拼成一张预览图import cv2 import numpy as np def write_preview(out_path, origin, annotated): h min(origin.shape[0], annotated.shape[0]) canvas np.hstack([origin[:h], annotated[:h]]) cv2.imwrite(str(out_path), canvas)np.hstack 要求两图高度一致所以先取较小高度截断宽度可以不同。验证时把原始图、二值图、标注图三张并排保存任何一步处理异常都能从预览图快速定位。这些年我给自己立了一个规矩所有图像处理脚本第一版都会带上结果预览和参数输出绝不直接跑完整批数据。有了中间态的可视化调整参数才有方向批量处理才不是拿数据做赌注。这也是我处理图像时最想分享的基础习惯希望帮到你。本文还有配套的精品资源点击获取