本节摘要:阈值分割把灰度图一刀切成二值图,是"检测目标 vs 背景"最便宜的手段。本节讲 threshold 的固定阈值与五种阈值类型、adaptiveThreshold 的局部自适应方案、Otsu 大津法的自动定阈值,以及三者按光照条件分档的选择逻辑。核心结论:光照均匀用 Otsu 自动档,光照不均用自适应局部档,纯固定阈值只留给光照可控的产线场景。
阅读完本节,你应当能够:
import cv2 gray = cv2.imread('part.jpg', cv2.IMREAD_GRAYSCALE) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
第二个参数 127 是阈值:灰度大于 127 的像素变 255(白),其余变 0(黑)。第三个参数是 maxValue,即"命中区域"填充的值。返回的第一个值容易被忽略——它是实际使用的阈值,固定阈值模式下就是你传的数;但等会儿用 Otsu 时,实际阈值由算法计算,全靠这个返回值拿回来,养成接收它的习惯。
三种进阶玩法,各一行:
_, auto = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) adaptive_mean = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 25, 10) adaptive_gauss = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 25, 10)
第一行把阈值传 0 并追加 OTSU 标志,OpenCV 会在整张图的灰度直方图上自动找一个最优分界点。后两行走局部路线:每个像素的阈值由它周围 25×25 邻域算出,减去常数 10 作为修正。同一张照度不均的图(比如侧光的纸质文档,一半亮一半暗),固定阈值必然顾此失彼——亮区全白、暗区全黑;自适应版本则两边都能干净分离。这个对比实验强烈建议亲手做,看一次终身不忘。
把灰度图统计成直方图(横轴 0–255,纵轴每档像素数),理想的分割场景会呈现"双峰一谷":目标物体一个峰、背景一个峰、中间一道谷。阈值就该落在谷底——分错类 的像素最少。固定阈值法相当于你用肉眼估计谷底位置;Otsu 法让程序系统地找谷底。
Otsu 的思路可以一句话讲明白:枚举所有可能阈值,把图分成前景背景两类,计算"类间方差"(两类平均灰度差距的度量),使类间方差最大的阈值就是最优分界——分得越开,说明两类差异越大、切得越干净。前提是直方图确实接近双峰。目标占画面极小(比如 1% 的小缺陷)时双峰退化成单峰加一根小刺,Otsu 会切偏,这类场景要改用形态学礼帽先增强目标,或干脆换第 4 章的检测方法。
THRESH_BINARY 只是五种切法之一:
| 类型 | 规则 | 输出效果 |
|---|---|---|
| BINARY | 大于阈值为 maxval,否则 0 | 标准黑白二值图 |
| BINARY_INV | 大于阈值为 0,否则 maxval | 反相二值图 |
| TRUNC | 大于阈值的截到阈值,其余不变 | 压掉高光,保留暗部层次 |
| TOZERO | 小于阈值置 0,其余不变 | 只保留亮区,暗区清空 |
| TOZERO_INV | 大于阈值置 0,其余不变 | 只保留暗区,亮区清空 |
后三种的输出仍是灰度图不是二值图,它们更像"灰度域手术刀":TRUNC 适合压过曝天空的细节,TOZERO 适合把暗背景清空只留亮目标。日常说"二值化"默认指前两种。
全局阈值是"一把刀切全图",要求光照均匀。扫描仪盖着盖子、产线相机带环形光源,光照可控,全局刀够用。但手机随手拍、自然光场景、大画幅监控画面,光照必然不均,一把刀无解。
adaptiveThreshold 的解法是给每个像素配一把小刀:以该像素为中心的 blockSize×blockSize 邻域内,算出局部基准(MEAN_C 是邻域均值,GAUSSIAN_C 是高斯加权均值),再减去常数 C 当作该处的阈值。C 是"留白量":它把阈值从邻域均值再压低一点,容忍噪声与纹理的波动。blockSize 决定"多局部":取 25 意味着约 25 像素尺度内的光照差异被抹平;取太大就退化回全局阈值,取太小则纹理被当成"光照"过度响应,会出碎斑。
两个参数的推荐起点:blockSize 取目标宽度的 2 到 3 倍并保持奇数,C 从 10 起步按碎斑程度上调。MEAN 与 GAUSSIAN 的差别通常不大,GAUSSIAN 的过渡更平滑,跑得稍慢,我默认 GAUSSIAN。
阈值分割对噪声极其敏感:一个 ±20 的灰度抖动就可能让像素在黑白之间跳变。所以标准链路是"滤波 → 阈值 → 形态学":滤波压灰度噪声,阈值切分,形态学清理切分后残余的碎斑与空洞。跳过第一步,后面两步就得加倍补偿。
调阈值前先看直方图,一分钟避免一小时瞎调:
hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
双峰清晰就上 Otsu;单峰说明目标背景灰度重叠,任何阈值都会有大量误分,该回去改光照或换方法,而不是继续在阈值上死磕。阈值救不了糟糕的成像,这是工业视觉的第一课。
很多算法(findContours、形态学)默认白区为前景。当你的目标是暗物体(白纸上的黑字)时,直接二值化的白区是背景。加 THRESH_OTSU 加 THRESH_BINARY_INV 组合,一步得到"黑字变白"的正确朝向,省一次反相。
灰度区分不开的目标,颜色可能分得开。比如黄底黑字的标签、红绿混合的工件,转到 HSV 后按色调通道(H)做阈值筛选,往往一刀干净。inRange 函数可以做"区间版阈值":同时约束 H、S、V 三个通道的上下界,输出直接是二值掩码,这是颜色目标提取的标准姿势(第 4 章识别部分还会用到)。
⚠️ 常见坑:对没滤过波的图直接 Otsu,阈值天天漂。噪声让直方图变"毛",谷底位置随机移动,Otsu 算出的值在几十的幅度上抖动。先高斯滤波再 Otsu,阈值立刻稳定——这也是 OpenCV 文档明示的建议。另一个坑是 blockSize 传了偶数,函数直接报错,该参数必须为奇数。

不能直接叠加(一个全局一个局部,机制互斥),但可以分而治之:光照不均的图先用大的背景建模(或形态学礼帽)拉平亮场,再对结果做 Otsu——"先去趋势再切分"的思路等价于手工版自适应。另有一个冷知识:threshold 加 OTSU 标志时传入的阈值参数会被忽略,所以写 0 是惯例,写别的数也不影响结果。
不建议。第 2 章第 1 节说过,灰度域的滤波对二值斑点的处理会引入中间灰度值。二值图上的"滤波"由形态学接管(开闭运算),两套工具各守各的域。
它们是并列的两条路,不是前后两步。阈值切"区域"(面),边缘提取"边界"(线),下游要区域(计数、测面积)走阈值,要结构(找直线、配准)走边缘。偶尔串联的场景是"阈值后用形态学梯度描边界",那是二值域的描边,与 Canny 无关。
组装一次完整实战,场景设定为"传送带上的深色零件提取"(光照基本均匀的产线视角)。
第一步看直方图定路线。零件与传送带有稳定的灰度差,直方图双峰清晰,走全局路线;用 Otsu 自动定阈值并打印返回值记录(同一相机同一光照下它应当非常稳定,漂移超过十说明成像链路有问题)。
第二步一次二值化到位。零件是暗目标,加 INV 标志让零件变白(前景),为形态学与轮廓做准备——本节 3.2 的逆向思维直接套用。
第三步形态学净化。按第 2 章第 3 节的配方清斑补洞,核尺寸按实测的噪声团与空洞口径定。
第四步轮廓提取与面积过滤。findContours 加面积上下限(下限砍碎块,上限防误把传送带边缘当目标),输出目标区域列表。至此"灰度图进、目标区域出"的产线级流水线完成,四个环节各两三行代码,但每一步的参数都有依据可回溯——这就是工程代码与脚本小样的区别。
换一个光照不均的场景(车间自然光),只有第二步换成自适应阈值、参数按本节 3.3 的联动规则重标,其余三步原样复用。配方化的价值在迁移成本趋近于零。
有,但救法往往不在阈值函数里。先做一次形态学闭运算或中值滤波把谷底填平,再重新统计直方图,很多"看起来单峰"的图会显出双峰结构。若仍旧平缓,说明光照不均主导了灰度分布——此时局部自适应阈值更合适,或者干脆回到采集端:加个遮光罩、换个打光角度,比在参数里反复横跳有效得多。
还有一种容易被忽视的情况:目标占比太小,峰值几乎看不见。这时可以先做一次 ROI 裁剪再取阈值,把统计范围收窄到目标所在区域。Otsu 返回的阈值本质是对整幅图直方图的最优分割,ROI 一换,同一个函数立刻好用。工程里"阈值不稳"的老大难,一半以上最后都是靠改 ROI 或改光照解决的。
二值图到手,我们换一个视角看图像结构——边缘。下一节的 Canny 会把"物体的边界"从灰度变化里提取出来。