大津法(Otsu)自动阈值分割:从原理到代码实战
2026/9/8 9:23:55 网站建设 项目流程

简介:Otsu最大类间方差法(大津法)是1979年提出的经典自适应阈值分割算法,利用类间方差最大化自动寻找最佳灰度阈值,将图像分离为前景与背景,适合学习图像二值化、文字识别、医学图像分析等场景的开发者。压缩包共54个文件,大小17.27MB,包含C++源码(.cpp/.h)、Visual Studio工程配置(.sln/.vcxproj)、编译产物以及多组BMP测试图,便于直接构建运行。目前已有1384人学习下载。源码完整实现直方图统计、类间方差计算、阈值遍历与二值化输出,流程清晰,可结合lena、airplane等经典图像的分割结果对比验证算法效果。同时能观察到该方法对均匀光照图像表现良好,但对噪声和非均匀光照较为敏感,有助于深入理解阈值分割的适用边界,亦可作为图像处理课程设计或毕业设计的参考资料。 做图像处理这些年,有点意思的一个算法就是 Otsu 最大类间方差法,也就是大家常说的“大津法”。很多刚入门的人一听到这个名字,以为是什么高深莫测的数学理论,实际上它解决的是一个非常朴素的问题:一张灰度图,我到底该选哪个阈值来做二值化才最靠谱?如果你也遇到过“手动调阈值调到头秃,换个光照环境又要重新调”的情况,那这篇文章就是为你准备的。我会从数学原理、代码实现、工程上的坑,再到进阶玩法,一次性讲清楚,保证你读完能直接用起来。

1. 大津法到底在干什么:用一句话解释和最朴素的直觉

1.1 先把核心思想说人话

大津法的核心就一句话:自动找到一个灰度值,把这个值当分界线,让分开后的“前景”和“背景”两类之间差别最大。“差别最大”用什么来衡量?就是类间方差。所以这个算法的全名才叫最大类间方差法,Otsu 是提出者大津展之的姓氏发音。

打个比方,现在有一箱苹果和一箱橙子混在一起,你要找个重量界限把它们分开,最佳的分界点一定是让“苹果这堆”和“橙子这堆”的内部重量尽量统一、两堆之间的平均重量差距尽量拉大的那个位置。大津法干的就是这件事,只不过对象换成了像素灰度值,范围从 0 到 255。

这个算法在 1979 年提出,直到现在仍然是工业视觉、文档扫描、医学图像分割里的常青树。原因很简单:它是全自动的,不需要人工设置阈值,对大部分双峰分布的直方图都能给出一个理论上的最优阈值。

1.2 数学公式拆解:类间方差是怎么算出来的

上面说的只是直觉,我们得看公式才能理解大津法每一步在求什么。假设图像灰度级是 L(通常 L=256),灰度值为 i 的像素个数是 n_i,像素总数是 N,那么每个灰度级出现的概率是:

p_i = n_i / N

现在选一个阈值 k,把像素分成两类:

  • C0 类(背景):灰度值在 0 ~ k 之间的像素
  • C1 类(前景):灰度值在 k+1 ~ L-1 之间的像素

那么两类分别出现的概率和均值是:

w0 = sum(p_i)(i 从 0 到 k) w1 = 1 - w0

u0 = sum(i * p_i) / w0 u1 = sum(i * p_i) / w1

全局像素的灰度均值是 uT = w0 * u0 + w1 * u1

最后,类间方差的公式是:

σ_B² = w0 * (u0 - uT)² + w1 * (u1 - uT)²

我们要做的,就是把 k 从 0 到 255 全部试一遍,找出让 σ_B² 达到最大的那个 k,它就是最终阈值。

注意这里有很多资料会把公式简化成:

σ_B² = w0 * w1 * (u0 - u1)²

这个等价形式计算更快,因为读到 uT = w0u0 + w1u1,带入展开就能推导出来。实际写代码的时候用这个简化版,少一次减法和乘法,循环 256 次没什么感觉,但如果将来要扩展到大动态范围图像,就有意义了。

2. 从公式到代码:一步步手写一个可运行的大津法

2.1 不依赖库,用 Python + NumPy 从零实现

理解了公式以后,自己写实现其实是很快乐的体验,因为代码量出乎意料地少。我先把完整实现贴出来,再逐步解释每一段在做什么。

import numpy as np def otsu_threshold(gray): # 1. 统计灰度直方图 hist = np.bincount(gray.ravel(), minlength=256).astype(np.float64) total = gray.size # 2. 归一化,让直方图变成概率分布 hist_norm = hist / total # 3. 全局灰度均值 gray_levels = np.arange(256) sum_total = np.dot(gray_levels, hist_norm) # 4. 遍历所有可能的阈值 sum_bg = 0.0 # 背景像素灰度累计值 w_bg = 0.0 # 背景像素占比 max_var = -1.0 best_threshold = 0 for t in range(256): w_bg += hist_norm[t] if w_bg == 0: continue w_fg = 1.0 - w_bg if w_fg == 0: break sum_bg += t * hist_norm[t] mean_bg = sum_bg / w_bg mean_fg = (sum_total - sum_bg) / w_fg # 类间方差(简化版公式) var_between = w_bg * w_fg * (mean_bg - mean_fg) ** 2 if var_between > max_var: max_var = var_between best_threshold = t return best_threshold

这里的核心技巧是增量更新。传统的写法在每一个阈值 t 下都要从头累加计算 w0 和 u0,复杂度是 O(L²),L 小的时候无所谓,但养成用累积变量的习惯以后,不管灰度级多少都保持 O(L),代码也更简洁。

2.2 用 OpenCV 一行代码验证结果

如果你只是想在项目里快速用上,OpenCV 已经帮你封装好了,根本不需要自己写。

import cv2 gray = cv2.imread("demo.png", cv2.IMREAD_GRAYSCALE) threshold, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) print(f"Otsu threshold = {threshold}")

注意这三个细节:

  • 阈值参数位置传 0,因为大津法会自动计算,传入的数值会被忽略。
  • 要使用 cv2.THRESH_BINARY + cv2.THRESH_OTSU 这种组合标志,单纯传 cv2.THRESH_OTSU 是不生效的。
  • 返回的 threshold 就是你最终需要的分割阈值,可以直接打印出来看。

我自己测试下来,手写实现和 OpenCV 算出的结果通常完全一致。偶尔有 1 个灰度级的误差,多半是浮点精度问题,工程上可以忽略。

2.3 给小白补充:什么是直方图,为什么二值化要选阈值

这里多解释一句,因为很多做工程的朋友可能对图像基础概念不熟。灰度直方图就是把图像里每个灰度值出现的次数画成柱状图。一张对比度正常的图,直方图往往呈现两个明显的“山头”,一个对应暗背景,一个对应亮目标。大津法要做的就是在两个山头之间的“山谷”地带找一个最合适的位置当分割线。

如果没有大津法,你可能得看直方图自己猜一个值,比如看到山谷在 127 附近,那就手动填 127。问题在于,光照一变、镜头光圈一调整,直方图整体偏移,山谷位置变了,原来填死的阈值就不适用了。大津法每次自动算,等于把“看图猜阈值”这个动作自动化了,这就是它在工业自动化场景里受欢迎的根本原因。

3. 实战中的边界与坑:大津法什么时候会翻车

3.1 光照不均匀会选出离谱的阈值

大津法最怕的场景之一,就是图像左右亮度不一致。左边被灯照得很亮,右边落在阴影里,直方图的双峰结构会被彻底打乱,变成一个大而宽的平缓分布。这时候大津法算出来的阈值往往会落在某个中间灰度,导致亮区的背景被误判成前景,暗区的前景又跟背景粘连在一起。

我踩过这个坑是在一次瓶盖缺陷检测的项目里,产品在线运行,顶部打光不均匀,ROI 区域左侧偏亮右侧偏暗。直接用大津法阈值分割,误检率一度冲到 20% 以上。后面换了方案:先对 ROI 做分块大津法,把图像按网格切块,每块独立算阈值做分割,再拼回去。分块大小选 64x64 像素,整体效果立刻稳下来。代价是耗时略有增加,但在电源纹波和帧率要求不高的产线上完全可接受。

3.2 目标占比极端时,类间方差会骗人

大津法的另一个隐含假设是前景和背景的像素数量不能差得太离谱。当目标区域很小,比如一张 1000x1000 的图像里只有 1000 个像素是目标点,w0 和 w1 的乘积 w0*w1 会变得很小,类间方差的绝对数值被压得很低。算法还是会选出一个使得方差最大的阈值,但那个阈值往往不是期望的目标点分割阈值,而是背景内部一个噪声级别的区分。

这种情况下,直接套大津法基本等于碰运气。我建议的做法是:

  • 先用形态学滤波或者连通域分析锁定目标可能存在的小范围 ROI。
  • 在 ROI 内部再使用大津法,而不是对整个原始图操作。
  • 或者改用固定阈值 + 统计分析,比如取“背景均值减去三倍标准差”作为阈值,反而更可靠。

3.3 噪声和毛刺会扰乱直方图

二值化之前要不要滤波?这个话题争论了很久。我的经验是:大津法对轻微噪声并不敏感,因为它是全局统计量,个别像素的灰度变化对直方图影响很小。但如果是椒盐噪声比较重的情况,直方图会多出很多诡异的毛刺,导致最优阈值在小范围内抖动。

处理办法不复杂,进大津法之前先做一次高斯滤波或者中值滤波,平滑掉孤立的噪声点。对于一个 3x3 的高斯核,几乎不增加处理耗时,效果却立竿见影。但我提醒一句,滤波半径不要太大,否则边缘模糊,后续分割的轮廓精度会下降,反而得不偿失。

3.4 多峰直方图:一个阈值根本不够用

工业场景里常有这种情况:背景是一种灰度,目标内部还有两种不同材质的纹理,整个直方图变成三个甚至四个峰。大津法只能找一个全局阈值,这时候无论怎么选,都会把其中某一个峰劈成两半。

面对多峰直方图,解决思路是扩展成“多阈值大津法”,也就是通过递归地对子类再次执行大津法来切出更多阈值。一个比较实用的做法是先用大津法分一次,得到前景和背景,然后对前景再做一次大津法,把前景再细分。比如金属表面上划痕检测,背景均匀,得到阈值 T1 分割出前景;前景里划痕和反光区域灰度不同,就用第二个大津法阈值 T2 把划痕单独挑出来。这种做法不需要什么高级数学,工程上非常容易实现。

4. 进阶玩法:大津法的几个实用变体和扩展

4.1 二维大津法,抗噪能力更强的选择

二维大津法不再只看像素本身灰度值,还引入了邻域灰度均值,构成二维直方图。简单说,每个像素点有两个特征:自己的灰度值和周围某个窗口(比如 3x3)的平均灰度值。大津法在这两个维度上同时做划分,目标是让分类同时考虑局部一致性。

效果上,这种方法对噪声的抵抗能力明显提升,尤其是对高斯噪声和低对比度图像。代价是计算量和内存占用变大,原来一维直方图是 256 个桶,二维直方图是 256x256 个桶。如果用普通遍历,复杂度是 O(L²),L=256 时已经比一维慢不少。实际工程中,很少全图使用二维大津法,更多是作为 ROI 区域内的精细分割手段。

4.2 局部大津法,处理光照不均的主力方案

前面提到的分块大津法,本质上就是局部自适应阈值的一种。OpenCV 里的 adaptiveThreshold 提供了更成熟的局部阈值策略,但默认用的是高斯加权或者均值减常数,并不是大津法逻辑。如果你想严格用局部大津法,可以自己写一个滑动窗口版本:

import numpy as np import cv2 def local_otsu(gray, block_size=64): h, w = gray.shape result = np.zeros_like(gray) for i in range(0, h, block_size): for j in range(0, w, block_size): block = gray[i:i+block_size, j:j+block_size] t, binary_block = cv2.threshold( block, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU ) result[i:i+block_size, j:j+block_size] = binary_block return result

这段代码就是最简单的分块大津法框架。实际项目中我通常会在块之间留一些重叠,再用线性插值生成每个像素的阈值,避免块边界产生明显的拼接痕迹。块大小选择上,经验值大约是图像短边的 1/8 到 1/16,可以多试几个值看分割效果再定。

4.3 自动判断是否适合使用大津法

一个更聪明的做法是,在调大津法之前先分析直方图形状,判断它到底适不适合用大津法。可以计算直方图的峰度或者双峰性指标(比如 valley depth),一旦发现直方图是一个明显的单峰,就直接改用固定阈值或 Triangle 法,避免大津法硬生生选出一个没意义的阈值。

我在实际代码里会先算一个简单指标:前景和背景的类内方差之和与类间方差的比值。比值过大说明分割效果不理想,可以抛出警示或者切换到备用分割方案。这个做法在长时间无人值守的设备上特别有用,至少能在算法失效时给你一个明确的信号,而不是默默输出一张废图。

5. 常见问题速查与提效技巧

5.1 问题排查表

我把自己这些年遇到过的典型问题整理成了一个速查表,大家可以直接对照排查:

现象可能原因排查思路与对策
阈值算出来偏向 0 或 255图像直方图严重单峰,前景与背景比例极端查看直方图,改用 Triangle 法或固定阈值
分割结果有大片误检光照不均匀,全局阈值不适用换分块大津法或局部自适应阈值
同一张图每次结果不稳定有噪声源干扰直方图先做中值/高斯滤波,再去算阈值
目标边缘毛刺明显阈值压得太低或太高配合形态学开闭运算做后处理
小目标完全检测不到前景占比太小,类间方差被削弱先提取 ROI,再在 ROI 内使用大津法
图像偏暗或偏亮对比度过低,直方图集中先做直方图均衡化再分割,或改用二维大津法

5.2 提高执行效率的几种实现

大津法原本只需要遍历 256 个灰度级,速度非常快,但如果你处理的是超大图像或者视频流,还是有几个提效空间可以关注:

  • 如果灰度范围本身只有 0~100(比如红外图),可以先做一次 min/max 归一化,把有效灰度范围缩小,这样不仅减少遍历次数,还能提高数值稳定性。
  • 使用 NumPy 的向量化操作代替 Python 循环。我自己测试过一个全向量化版本,对 4096x4096 图像的二值化耗时能压到 10ms 以内,如果用纯 Python 循环遍历像素,直接就是另一个数量级。
  • 在实时系统里,可以把大津法放到 ROI 预处理之后,而不是全图计算。ROI 越小,直方图统计越快,阈值计算越稳。

5.3 一个快速验证大津法效果的实验流程

最后分享一个我调试分割算法时常用的验证流程,简单有效:

  1. 任意找一张有代表性的图像,先用 OpenCV 自带的 Otsu 跑一遍,打印阈值。
  2. 把灰度直方图画出来,肉眼确认直方图是不是双峰,山谷位置跟大津法计算出的阈值是否吻合。
  3. 手动把阈值分别改成比大津法结果大 20 和小 20,对比分割结果,看看波动的敏感程度。
  4. 加一层高斯滤波,再跑一次大津法,确认阈值有没有明显漂移。

这套流程走完,你对这张图什么时候该信大津法、什么时候该换方案,心里基本就有数了。图像分割没有万能钥匙,大津法是一个很强的起点,但真正的工程能力体现在知道它的边界在哪里、在失效的时候怎么补位。这个算法本身很简单,但用好的话,能省掉你大量手工调参的时间,值得花半小时把它吃透。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询