news 2026/10/13 16:28:39

图像分割的核心:阈值化、大津法与自适应阈值详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
图像分割的核心:阈值化、大津法与自适应阈值详解

图像分割是我读《图像处理、分析与机器视觉(第四版)》时最有“这页很简单”错觉的内容,尤其是阈值化。原因很简单——公式就一句:像素大于阈值就置为前景,小于等于阈值就置为背景。可真正上手做分割项目之后我才发现,阈值选多少、怎么选、选完之后二值图怎么用,每一步背后都藏着细节。这篇笔记把我在这一章反复推敲的东西整理出来:全局阈值、直方图、大津法、自适应阈值,以及我和二值化结果“搏斗”时踩过的坑。

这篇内容适合刚学图像分割的初学者,也适合那些已经会用threshold和adaptiveThreshold但从没深究过原理的工程师。阈值化不会替代任何深度学习模型,但它是理解分割问题的起点——几乎所有分割思路都能归结为“给每个像素分配一个类别标签”,阈值化只是这个决策过程的最简版本。

1. 为什么我在分割系列里先写阈值化:它是最简单但也最容易用错的分割

1.1 阈值化的数学表达

先复习一下教材上的标准形式。设输入灰度图像为 (f(x,y)),灰度级范围是 ([0, L-1]),选一个阈值 (T),输出二值图 (g(x,y)) 可以写为:

[ g(x,y) = \begin{cases} 1, & f(x,y) \ge T \ 0, & f(x,y) < T \end{cases} ]

1 表示我们关心的目标,0 表示背景;如果你不是提取亮目标而是提取暗目标,把比较方向反过来就行。这个式子把一个连续灰度空间上的问题变成了一个“分类问题”:像素只有两类,决策规则是一维的——灰度值够不够大。

我习惯把阈值想象成一个班上的录取分数线:像素是学生,灰度是考试成绩,过了线就进目标专业,没过线就留在背景学院。问题于是从“要不要分割”变成了“这条分数线的位置怎么定”。这是整章一切讨论的核心。

1.2 阈值化在分割工具箱里的位置

《图像处理、分析与机器视觉(第四版)》的图像分割章节,并不是只讲阈值化,它还把边缘检测、区域生长、分水岭等串在一起。我们先看到的总是阈值化,因为这个方法有三个无法替代的优点:

  • 算得快:一次遍历所有像素就能完成,O(N) 的复杂度,实时系统特别友好。
  • 可解释:阈值就是一个数,谁都能理解;出错也容易定位,不像某些黑盒方法。
  • 是很多高级方法的起点:分水岭分割需要标记前景区域,OCR 需要先把文本变成白底黑字,工业缺陷检测经常先做阈值化再算连通域——这些都得从二值图开始。

很多学习者在听完这些之后会觉得“阈值化就是最简单那个,随便用用就行”,实际恰恰相反。我见过很多翻车项目,一张不均匀光照下的照片,直接拿全局阈值硬切,结果背景一团黑斑。阈值化的门槛低,但它的前提条件非常苛刻。

1.3 适合阈值化的场景长什么样

根据教材的描述和我自己的实践,我总结出三个“适合用阈值化”的前提:

  • 目标和背景的灰度分布重叠尽可能小,直方图上最好能明显看到两个峰。
  • 光照条件均匀,背景灰度在整幅图中相对稳定。
  • 不需要太精细的形状精度,只要大致分离目标区域即可。

如果场景满足这三条,阈值化是最佳选择;如果有一条不满足,比如光照从左到右衰减,那就要往下读,考虑自适应阈值或者前处理校正。

2. 全局阈值的基本功:读直方图、找谷底、判断适不适合

2.1 双峰直方图和谷底

全局阈值的经典选法,是把图像灰度直方图画出来,找两个峰之间的谷底。为什么是谷底?因为如果目标和背景各自近似服从正态分布,那么两个分布交界处就是误分类概率最低的位置。换句话说,谷底附近灰度值的像素本身就“身份不明”,把它们归到哪一边,错误代价都最小。

举个例子,一张白纸黑字的扫描件,直方图上通常在 0 附近有一个代表黑色文字的峰,在 240 附近有一个代表白色纸张的峰,中间凹下去的位置比如 120 就是很自然的阈值。选 100 也行,选 130 差别也不大,因为谷底那段像素本来就少,阈值稍微移动对结果影响有限。

这一点上手很简单,但很多人不画直方图就直接拍脑袋定 128。128 这个数没有错,但它没有任何信息量——应该去看数据长什么样再定。

2.2 直方图先平滑再找谷底

实际图像的直方图并不总是教科书里那种光滑的双峰曲线,它往往像一把锯齿,一个又一个局部极小值。这种情况下直接找谷底,会找到一堆没有意义的“假谷”。我通常的做法是先对直方图做一次高斯平滑:

import numpy as np def smooth_histogram(hist, sigma=3.0): # 简单的一维高斯平滑 kernel_size = int(sigma * 6) | 1 kernel = cv2.getGaussianKernel(kernel_size, sigma) return np.convolve(hist, kernel.ravel(), mode='same')

平滑之后,只保留那些“左右邻域内都是最小值”的点:

def find_valleys(smooth_hist): valleys = [] for t in range(1, len(smooth_hist) - 1): if smooth_hist[t] < smooth_hist[t-1] and smooth_hist[t] < smooth_hist[t+1]: valleys.append(t) return valleys

这个方法足够简单,能帮我们从微观抖动中找出真正的谷底。当然,如果直方图没有双峰或不平滑,上面这些代码也就没意义了——所以我每次都先打印直方图,再决定要不要继续走全局阈值这条路。

2.3 全局阈值的适用边界

全局阈值看起来很直观,但有几个场景会让它直接失效:

  • 类别不均衡:目标只占整幅图像的 0.1% 时,那个小峰可能在直方图里根本看不见,谷底无从谈起。
  • 灰度重叠严重:目标和背景本身灰度范围大量交织,比如雾天拍车牌,任何一条竖向直线都不能把两类分干净。
  • 光照不均匀:背景本身不是常数,左边亮右边暗,那么左边背景的灰度可能超过右边目标的灰度,一个全局阈值无论如何都救不回来。

遇到这三种情况,不要硬上全局阈值,要么改用局部自适应方法,要么先做光照校正。这也是我写这篇笔记最想强调的一点:读直方图不是为了完成任务,而是为了判断“这张图到底能不能用阈值化来分割”。

3. 大津法的笔记:从公式推导到三十行代码复现

3.1 它到底在优化什么

靠眼睛找谷底虽然可行,但不能自动化,也没法量化“这个阈值比另一个阈值好多少”。教材里给出了一个广为人知的自动选阈值方法——大津法(Otsu)。它的核心思想不是去拟合双峰,而是找一个阈值,使得分割出来的两类“类间差异”尽可能大。

问题来了:什么叫“类间差异大”?大津法把它定义为两类均值之间的加权距离。设阈值 (t) 把像素分成背景类 (C_0) 和目标类 (C_1),两类的像素占比分别是 (\omega_0(t)) 和 (\omega_1(t)),灰度均值分别是 (\mu_0(t)) 和 (\mu_1(t)),那么类间方差定义为:

[ \sigma_B^2(t) = \omega_0(t) \cdot \omega_1(t) \cdot [\mu_0(t) - \mu_1(t)]^2 ]

我们要做的是遍历所有 (t),找到让 (\sigma_B^2(t)) 最大的那个阈值。这个公式里有两部分信息:均值差 ([\mu_0(t)-\mu_1(t)]^2) 度量两类平均灰度隔得多远,(\omega_0(t)\omega_1(t)) 则是一个权重——当两类像素数量越接近时,这个乘积越大。整体含义是:不仅要让两类均值分开,还要让它们都不是“少数人”,否则哪怕均值差再大,也说明你只是抠出了一小撮极端像素,这种分割没有意义。

3.2 公式推导与三十行实现

有了上面的定义,实现就顺理成章了。设直方图为 (h(i)),总像素数为 (N),灰度 (i) 出现的概率记为 (p_i = h(i)/N)。对阈值 (t):

[ \omega_0(t) = \sum_{i=0}^{t} p_i, \quad \omega_1(t) = 1 - \omega_0(t) ]

[ \mu_0(t) = \frac{\sum_{i=0}^{t} i \cdot p_i}{\omega_0(t)}, \quad \mu_1(t) = \frac{\sum_{i=t+1}^{L-1} i \cdot p_i}{\omega_1(t)} ]

为了在线性时间内完成扫描,可以维护累计像素数cnt_b和累计灰度值sum_b,这样每个阈值只做常数级运算:

def otsu_threshold(gray): hist = cv2.calcHist([gray], [0], None, [256], [0, 256]).ravel() total = gray.size sum_all = np.dot(np.arange(256), hist) cnt_b = 0.0 sum_b = 0.0 max_var = -1.0 best_t = 0 for t in range(256): cnt_b += hist[t] if cnt_b == 0: continue cnt_f = total - cnt_b if cnt_f == 0: break sum_b += t * hist[t] mean_b = sum_b / cnt_b mean_f = (sum_all - sum_b) / cnt_f var_between = cnt_b / total * cnt_f / total * (mean_b - mean_f) ** 2 if var_between > max_var: max_var = var_between best_t = t return best_t

这段代码里,cnt_b / total * cnt_f / total就是 (\omega_0 \omega_1)。很多地方的实现里省去这个归一化因子,因为对所有 (t) 来说分母一样,不影响找最大值,但我还是会把完整形式写进去,方便对照公式理解。

3.3 大津法的边界情况与失败模式

大津法不是银弹。我自己跑实验时最常遇到两类失败:

第一类是直方图单峰。如果图像中背景很干净、目标很大,或者噪声把灰度分布拉得很难看,直方图上根本看不到两个峰,大津法也会硬生生找一个阈值出来,结果往往是把同一类目标切成了两半,或者把背景的某个灰度带单独抠出来。遇到单峰直方图,应该换思路,而不是继续调阈值。

第二类是目标占比极小的缺陷检测场景。假设要检测一块金属表面上的细微划痕,划痕只占 0.3% 像素,那么 (\omega) 和 (1-\omega) 极端不平衡,( \omega_0\omega_1) 会很接近 0,大津法为了避开这种不平衡,会把阈值偏向“把大背景完整保留、牺牲小目标”的方向。结果是划痕几乎全被滤掉。

所以我的使用原则是:大津法适合目标和背景像素比例不悬殊、灰度分布大致呈双峰的情况;它适合自动得到一个“不差的阈值”,但不保证结果符合人的视觉期望。把它和 OpenCV 内置的threshold(gray, 0, 255, THRESH_BINARY + THRESH_OTSU)对照过,两者输出一致,但我自己实现的版本更容易调试,也更容易二次扩展。

4. 光照不均匀会让全局阈值直接失效:自适应阈值与光照校正

4.1 全局阈值的隐藏假设

全局阈值所有方法都默认了一个隐藏假设:图像里的背景灰度是一个常数。等式 (g = f \ge T) 里并没有“局部”概念,如果左侧墙面的亮度是 200,右侧是 100,一个全局阈值根本无法同时处理两边。

我最早在实拍文档照片上栽过跟头。纸面中间平整,边缘因为弯曲或阴影变暗,全局阈值后暗边文字全灭背景还被误判。后来才明白,这类问题应该当作“背景光照不是常数”来处理,而不是简单把阈值调低。

4.2 自适应阈值:每个像素一个局部阈值

自适应阈值的思路很直接:不要用一个全局 (T),而是在每个像素周围的邻域里,根据局部灰度统计量估计出该位置的分割线。OpenCV 的adaptiveThreshold就是典型实现:

th = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, # blockSize,必须为正奇数 10 # C,常数减量 )

参数只有两个:blockSize和C。blockSize是计算局部阈值用的邻域尺寸,C是一个偏移量。实际判断是:

[ g(x,y) = 1 \quad \text{if} \quad f(x,y) > \text{neighborhoodMean}(x,y) - C ]

这个判断的意思是:当前像素只要比它附近背景的平均亮度高出一小截,就视为目标。加C是为了抑制噪声,避免原本凹凸不平的表面上大量像素因为微小的灰度波动而乱跳。

调参经验上,首先blockSize要比目标最大尺寸大,最好大到目标内部也能包含足够多的背景像素,否则阈值会被目标自身带偏;C一般从 10 附近开始尝试,增大则目标更“紧”、噪点更少,增大太多会把弱边缘一同吞掉。不同图像理性值变化很大,记录参数并对比直方图是我常用的调试方式。

4.3 形态学背景估计与顶帽变换

自适应阈值并不是唯一出路。另一种我常用的做法是“光照校正 + 全局阈值”:先估计背景光照图,再从原图里把它除掉。

具体操作是:对灰度图做一个大结构元素的形态学开运算,比如cv2.morphologyEx(gray, MORPH_OPEN, kernel)。开运算先腐蚀再膨胀,能去掉小于结构元素的亮目标,剩下的就是“没有目标、只有背景”的估计图。将这个背景图从原图减去,再做一次全局阈值,就能把光源不均匀造成的低频变化抹掉。

如果目标是亮区域、背景是暗区域,还有一个更经典的做法:顶帽变换(Top-hat)。形态学顶帽的结果是原图减去开运算结果,相当于“专门提取比背景更亮的小块”,同时滤掉低频背景变化。我也用它在显微图像里提取细胞这一类亮目标,效果比直接全局阈值稳定得多。

这四种方法放在一起,可以直观对比:

方法核心思想适用场景主要局限
固定阈值一个全局 (T)光照均匀、双峰明显对光照和分布变化极其敏感
大津法自动最大化类间方差双峰自动分割单峰、类别极端不均衡时失效
自适应阈值局部统计量作局部阈值光照不均匀的文档、背景慢变参数不好调,块大小敏感
形态学前处理 + 全局阈值估计背景并扣除目标亮度与背景差异稳定结构元素尺寸影响大

4.4 为什么前处理比阈值本身更重要

当全局阈值和自适应阈值都能用时,我会先做一个低成本实验:画直方图,看背景峰是否偏移;如果不偏移,就用大津法;如果偏移,就用自适应或开运算背景扣除。很多教程会把这两类方法并列讲,但在实际项目里,判断“这张图该走哪条路”比“阈值调成几”重要得多——预处理和选阈值不是割裂的两步,而是一套完整的决策链。

5. 阈值化输出只是半成品:形态学清洗与连通域过滤

5.1 二值图的三类典型瑕疵

阈值化之后的输出,正常情况下都不会是一张“完美的黑白画”。常见瑕疵有三类:孤立噪点,比如某个像素因为传感器噪声被切成白点;目标内部空洞,比如文字笔画的内部因为灰度偏高被误判成背景;边缘毛刺,让本来平滑的轮廓像锯齿一样。

如果不处理这些瑕疵就直接计数或测量,结果会很糟。所以教材在阈值化之后都会接形态学,我现在也把它当成二值化的默认后续动作,而不是可选动作。

5.2 开闭运算的标准动作

形态学里最常用的两个组合:

  • 开运算 = 先腐蚀再膨胀,用于去除小的亮斑和细的连接桥。
  • 闭运算 = 先膨胀再腐蚀,用于填充目标内部的小洞和断裂。

总流程通常是“先开后闭”:开运算把孤立的白色噪点去掉,闭运算把目标里的空洞填平。这里有一个很关键的参数——结构元素尺寸。它必须跟目标尺寸匹配:如果目标直径是 20 像素,那么结构元素选 3×3 太小,选 15×15 才有效;反过来如果目标只有几个像素,15×15 会把目标整个抹掉。

5.3 连通域分析与面积过滤

形态学清洗完之后,就可以用连通域分析把“一块块的目标”提取出来。OpenCV 里有现成函数:

n, labels, stats, centroids = cv2.connectedComponentsWithStats(binary, connectivity=8)

stats里每一行是一个区域的x, y, width, height, area等信息。按面积过滤是我最常用的操作:

for i in range(1, n): area = stats[i, cv2.CC_STAT_AREA] if 30 <= area <= 5000: # 保留该区域,可继续计算重心或外接矩形 pass

这一步能把小噪点区域扔掉,也能把超出合理大小的连通块单独拿出来做异常检测。我经常在细胞计数或者表面缺陷统计里这么干:阈值化出二值图,形态学清洗,连通域过滤,得到目标数量和面积分布。整个过程没有任何机器学习,却已经能解决很多工业视觉问题。

6. 灰度之外:多阈值、彩色空间与阈值化的边界

6.1 多阈值和递归分割

如果一张图里有三个灰度水平,比如亮背景、中灰目标、暗目标,单阈值只能分出两类,需要两个阈值甚至更多。教材里把它称为多阈值化。实践中最简单的做法是递归地应用大津法:先用大津法把图像分成两类,再对其中较暗的那一类继续做一次大津法,得到第二个阈值。这个方法可解释性很强,但要注意每轮分割都会引入误差,递归层数多了以后边界会很乱。

6.2 最大熵阈值的思想

除了大津法,还值得了解一下最大熵阈值。它的想法是:阈值化后目标和背景两类的灰度分布都应当“保留尽可能多的信息”,具体表现为两个类的灰度熵之和最大。大津法追求的是平均灰度的分离,最大熵法追求的是灰度概率分布的复杂度最大化,所以它在分布重叠较多时有时能给出和大津法不同的结果。我一般不把它当主要工具,但会在双峰不明显时跑一下作为替补方案。

6.3 HSV 空间的彩色阈值化

很多人第一次做彩色分割时,会直接把 RGB 三通道分别阈值化,再把三个布尔结果按“与”“或”组合。这个做法坑很多:RGB 三个通道高度相关,亮度一变三个通道一起变,阈值规则非常脆弱。

更适合的做法是转到 HSV 颜色空间。H 表示色调,S 表示饱和度,V 表示亮度。这样可以把光照信息和颜色信息大致分离:颜色容易受到亮度影响的通道分开处理后再阈值化,效果会稳定很多。比如提取黄色车道标线,可以直接在 H 通道上限定一个范围,同时用 S 通道过滤掉灰白区域。这是阈值化思想在更高维特征空间里的自然拓展——同样是给每个像素定一条“分数线”,只不过现在这条线是多个维度联合的决策边界。

6.4 什么时候该换方法

阈值化的本质是一个基于像素独立判断的分类器,它不考虑邻居关系。如果图像中目标形状复杂、边界模糊、背景纹理丰富,那么无论如何调阈值都不够,此时应该转向区域生长、分水岭或深度学习方法。这个判断很重要——阈值化不是所有分割问题的万灵丹,它有明确的能力边界。

7. 学习阈值化时我踩过的坑,以及一套可复现的练手实验

7.1 我踩过的几个具体坑

这里列几个我真实碰到过的问题,给后来者提个醒:

  • 忘记确认输入是灰度图:直接拿彩色图调threshold,结果输出看上去全黑或全白,因为 OpenCV 对多通道图是逐个通道比较,语义完全不同。
  • 光照不均的图还强行用全局阈值:要么把暗部细节全丢掉,要么把背景切成一大块一大块的“污渍”,怎么调阈值都没用。
  • 把大津法当万能自动挡:单峰直方图硬切,目标区域出现大片断裂。大津法只负责“在给定分布下找最大分离线”,不负责“这张图一定能切”。
  • THRESH_BINARY和THRESH_BINARY_INV用反:在很多检测流程里,背景估计和前景提取黑白是反着的,颜色一颠倒,后面的形态学参数全都要跟着变。
  • 自适应阈值里blockSize超过图像尺寸时报错:如果图像被缩放得很小,而参数还是 31、51,就会直接异常。我在一个数据集增强脚本里踩过一次,之后都加了对图像尺寸的保护判断。
  • 不记录参数:同一张图换了不同C值,效果差很多,但一周之后就忘了上次用的参数是多少。现在我会给每组实验写一个参数卡片。
  • 默认开闭结构元素越大越好:结构元素比目标还大时,目标会被当噪声直接抹掉。

7.2 一套可复现的练手实验

如果你跟我一样是自学,我给你一套我的练手路径:

  1. 找一张光照均匀的文档照片,画灰度直方图,手动找一个谷底阈值,用threshold分割并保存。
  2. 对同一张图改用大津法,打印阈值,看是否接近手动选的谷底。
  3. 再找一张侧面打光的文档照片,分别用大津法和adaptiveThreshold分割,对比两种结果的文字完整性。
  4. 连接形态学清洗和connectedComponentsWithStats,统计文字连通域个数,并过滤掉面积小于 50 的噪点区域。

这套流程看起来很基础,但它把直方图、自动阈值、自适应阈值、形态学、连通域这几件事串成了一条完整的小项目流水线。我把每步结果都保存成图片,和直方图叠在一起看,很快就理解了“阈值不是算出来的,是数据本身决定的”。

最后再分享一个我自己的小习惯:学习阈值化时,不要第一时间打开库函数,而是先用几十行代码自己实现一遍大津法和自适应阈值。哪怕只是随便写写,再和 OpenCV 结果对照,你会比我直接抄 API 学到的多得多。这个动手过程带来的手感,是读多少页教材都换不来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 16:28:37

VS Code Python远程调试实战:从Docker到K8s的全场景配置

1. 项目概述&#xff1a;为什么远程调试不是“锦上添花”&#xff0c;而是Python工程落地的刚性需求你有没有遇到过这样的场景&#xff1a;本地写好的Python脚本&#xff0c;在自己电脑上跑得飞快、逻辑清晰、日志干净&#xff1b;一扔进服务器——直接报错&#xff0c;错误信息…

作者头像 李华
网站建设 2026/10/13 16:26:56

基于Matlab的LED元器件自动计数系统开发

1. 项目背景与核心价值在电子元器件生产与质检环节中&#xff0c;LED元器件的快速准确计数一直是个痛点问题。传统人工计数方式效率低下且容易出错&#xff0c;特别是在大批量生产场景下&#xff0c;人工清点不仅耗时费力&#xff0c;还难以保证100%的准确率。这个基于Matlab开…

作者头像 李华
网站建设 2026/10/13 16:26:18

开放代码审查:构建可追溯、可学习的工程协作范式

1. 项目概述&#xff1a;这不是代码检查&#xff0c;而是一场协作范式的重构“open-code-review”这个词组乍看像一个技术工具名&#xff0c;实则它指向的是一种正在快速演进的工程实践方式——把代码审查&#xff08;Code Review&#xff09;从封闭、异步、权限受限的内部流程…

作者头像 李华
网站建设 2026/10/13 16:26:11

Litellm 统一大模型API:一套代码接入所有LLM服务的网关实践

写代码这几年&#xff0c;最让我上头的不是模型效果不够好&#xff0c;而是每个模型服务商的接口格式都不一样。今天要聊的 litellm&#xff0c;就是专门治这个毛病的工具。它用一个统一的标准格式去调各家大模型的服务&#xff0c;相当于给模型API装了一个通用转接头。如果你干…

作者头像 李华
网站建设 2026/10/13 16:24:37

代码视频五步流水线:LLM生成代码+专用工具链渲染的工程实践

1. 项目概述&#xff1a;别被“AI生成视频”带偏了——代码视频的本质是精密流水线最近刷到不少标题党内容&#xff0c;说什么“Claude Opus 5.5已支持原生视频生成”“大模型终于能直接吐出MP4了”&#xff0c;点进去一看&#xff0c;全是把一段Python脚本跑完后自动合成的GIF…

作者头像 李华
网站建设 2026/10/13 16:23:23

Everpano 3D 立体编辑:全景图转可漫游三维场景与 Krpano 对接实战

简介&#xff1a;everpano 3D是一款面向全景漫游与虚拟现实内容创作者的立体编辑软件&#xff0c;将3D深度映射与krpano 1.20的Depthmap能力结合&#xff0c;让普通2D全景图无需昂贵立体扫描设备即可生成具有纵深感的三维效果。它适合房地产展示、旅游推广、艺术创作等场景&…

作者头像 李华