OTSU算法详解:图像二值化中的最大类间方差法

发布时间:2026/10/16 20:48:17
OTSU算法详解:图像二值化中的最大类间方差法 做图像处理的应该都跟二值化打过交道。早些年做项目时我处理一批文档扫描件质量参差不齐光照一会儿亮一会儿暗笔迹深浅也不一样。当时图省事直接定了个固定阈值结果效果惨不忍睹深色背景的区域全被当成前景浅色字迹又断成一片。后来换了OTSU大津律法也叫最大类间方差法事情一下子简单了——不用人工费劲调阈值算法自己会根据灰度分布“算”出一个合适的分割点整套流程稳多了。OTSU这个名字图像处理领域的人基本都听过它是图像分割里最经典的全自动阈值选择方法。它解决的问题很具体一张灰度图怎么找出一个阈值把像素分成前景和背景两类并且分得尽可能“干净”。我们不需要指定任何参数算法会遍历所有可能的灰度值找到一个让前景和背景差异最大的切分点。这篇内容我会把OTSU从数学原理到源码实现完整拆一遍包括类间方差公式是怎么推出来的、最大化类间方差为什么等价于最佳分割、用NumPy手写一个实现需要几步以及我实际踩过的一些坑。适合刚接触图像处理的学生也适合做CV落地、正在调试分割效果的同学参考。1. 从“固定阈值翻车”说起OTSU到底解决了什么问题1.1 一次印象深刻的二值化失败先聊个真实经历。某次做票据识别图像里有印章、手写数字、印刷体文字还有一个浅灰色的底纹。我一开始的做法很“经典”先转灰度再拿一个固定阈值比如127大于127的变白小于127的变黑。结果输出图片里印章和底纹糊成一片手写数字断的断、缺的缺根本没法进下一步OCR。问题出在哪固定阈值只适合灰度分布非常均衡的图像。但真实场景下图像受光照、材质、设备、墨水浓度影响灰度分布千差万别。同一批数据里有的图像整体偏暗有的整体偏亮一个固定的127根本照顾不过来。我当时也试过自适应阈值adaptiveThreshold效果稍好一些但它的窗口大小和C值还是要调而且对纹理复杂的图像容易产生割裂感。最后同事提醒了一句“你先用OTSU试试。”一试效果直接上一个台阶印章和底纹被干净地分开了手写数字也保留得比较完整。1.2 OTSU的核心思想让两类之间差别最大OTSU之所以叫“大津律法”是因为它由一位叫大津展之的学者在1979年提出。它的核心思路用一句话概括就是在0到255的所有灰度值里找一个阈值t把像素分成A、B两类使得这两类之间的方差最大。这里的“方差”描述的是类与类之间的差异程度。A类是前景比如文字、目标物体B类是背景。如果两类之间的方差越大说明这两类在灰度上“离得越远”被分开得越干净分割效果自然越好。反过来想如果选的阈值恰好落在两波像素之间比如前景集中在50左右背景集中在180左右那阈值选在115附近两类内部各自很紧密两类之间距离又远这就是我们要找的分割点。这个思想用数学精确表达出来就是后面要讲的类间方差公式。1.3 为什么它比固定阈值更适合处理真实图像固定阈值本质上是在赌“图像灰度分布是稳定的”但实际拍摄条件多变赌的成分太高。OTSU则是基于图像本身的信息做决策每张图都“量身定制”阈值。以我处理的票据为例明亮的扫描件灰度值整体偏高OTSU算出的阈值可能是165很暗的扫描件整体偏低它算出的阈值可能是90。这个自适应特性让它天然能适应光照变化、不同设备采集的数据也让它成了很多经典分割算法的“默认选项”。不过OTSU也不是万能的。它对直方图呈双峰分布前景和背景灰度很分明的图像效果最好如果直方图是单峰、多峰或者前景背景比例严重失衡它也会力不从心。这些缺点后面会专门讲到在“常见问题”部分我会给出几个实际可用的方案。2. 数学原理拆解类间方差是怎么一步步推出来的2.1 从灰度直方图说起要理解OTSU的数学原理先得建立直方图的概念。假设一张灰度图有N个像素灰度级范围是[0, L-1]通常L256。我们统计每个灰度级i出现的像素个数记为n_i那么这个灰度级出现的概率就是p_i n_i / N且 Σ p_i 1。直方图就是画出来的概率分布。它直观展示了图像里哪些灰度值多、哪些少。OTSU的整个推导过程都是围绕这个概率分布展开的。如果图像是良好对比度的情况直方图通常呈现两个明显的峰一个对应暗色物体前景一个对应亮色背景或者反过来。OTSU要做的就是在这两个峰之间找一个“谷底”位置作为阈值。2.2 阈值把像素分为两类目标与背景给定一个阈值t灰度值小于等于t的像素被归为C0类比如背景灰度值大于t的像素归为C1类比如目标物体。注意这只是逻辑映射具体哪边是前景、哪边是背景取决于你的应用场景OTSU本身只负责分出两类。C0类出现的概率记为ω0(t)它等于所有灰度小于等于t的像素概率之和ω0(t) Σ_{i0}^{t} p_i。C1类出现的概率ω1(t) 1 - ω0(t) Σ_{it1}^{L-1} p_i。接着计算两类的平均灰度。C0类的平均灰度记作μ0(t)C1类的平均灰度记作μ1(t)μ0(t) (Σ_{i0}^{t} i·p_i) / ω0(t)μ1(t) (Σ_{it1}^{L-1} i·p_i) / ω1(t)。还要算全局平均灰度μ_T公式很简单μ_T Σ_{i0}^{L-1} i·p_i ω0(t)·μ0(t) ω1(t)·μ1(t)。这个全局均值在后续推导中很关键。2.3 类间方差公式的推导现在我们有了两类各自的概率和均值接下来定义“类间方差”。类间方差between-class variance的含义是两类均值与全局均值之间的加权距离的平方公式写作σ²_B(t) ω0(t)·[μ0(t) - μ_T]² ω1(t)·[μ1(t) - μ_T]²。这个公式可以这样理解如果C0和C1两类分得越开它们各自的均值离全局均值就越远方差就越大。如果阈值刚好分成两类每类的均值都明显偏离整体均值那么σ²_B会很大。如果阈值选在某个波峰中间把一类硬切成两半两类均值都离整体均值不远σ²_B就会偏小。把μ_T ω0·μ0 ω1·μ1代入化简后可以得到另一个等价形式σ²_B(t) ω0(t)·ω1(t)·[μ0(t) - μ1(t)]²。这个式子更直观类间方差等于“两类概率之积”乘“两类均值差的平方”。两类占比越均衡且均值差越大类间方差越大。而“两类均值差大”本身就意味着分割清晰。所以最大化这个值等价于找到一个让两类“分离度”最大的阈值。2.4 最大化类间方差 vs 最小化类内方差再往深一层说OTSU还有一个内在逻辑它最大化的是类间方差隐含地也最小化了类内方差。图像的总方差σ²_T由两部分组成σ²_T σ²_W σ²_B。其中σ²_W ω0·σ0² ω1·σ1²是“类内方差”即每类内部像素灰度的离散程度。对于一张固定图像总方差σ²_T是常数。所以当σ²_B取最大值时σ²_W自然取最小值。类内方差小意味着每一类内部灰度越“抱团”像素越一致。这就是为什么OTSU分割出的前景和背景看起来非常“干净”——不仅两类之间差异大每类内部也足够均匀没有大量噪声点零星散布。2.5 一个直观的数值示例光看公式还是有点抽象我拿一个简化的示例来说明。假设一张图只有8个像素灰度值分别是10, 10, 20, 20, 150, 160, 170, 180。看一眼就知道阈值选在20到150之间的某个位置最合适。我们来算一下t20这个切分点。C0包含灰度值10、10、20、20共4个像素ω00.5μ015C1包含4个像素ω10.5μ1165。全局均值μ_T90。σ²_B 0.5×(15-90)² 0.5×(165-90)² 0.5×5625 0.5×5625 5625。再看另一种等价形式ω0·ω1·(μ0-μ1)² 0.5×0.5×(15-165)² 0.25×22500 5625结果一致。如果阈值选到t10呢C0只有灰度值10的两个像素ω00.25μ010C1有其余6个像素ω10.75μ1(102020150160170180)/7≈101.4。这时σ²_B 0.25×(10-90)² 0.75×(101.4-90)² ≈ 0.25×6400 0.75×130 ≈ 1697.5远小于5625。所以OTSU通过遍历所有t最终会倾向于选一个让两类都比较均衡、均值差异大的阈值在这个例子里也就是20附近。这个例子虽然像素很少但足以说明公式的行为模式两类像素数越接近比值两类均值差越大OTSU的评分越高。3. 源码解析从公式到一行行代码3.1 用NumPy实现OTSU的完整代码理论讲清楚了下面可以直接写代码。我用的是Python和NumPy因为直方图统计、累积和计算都是现成的代码非常简洁。import numpy as np def otsu_threshold(gray): # 计算直方图bin范围是0~255共256个灰度级 hist, _ np.histogram(gray.ravel(), bins256, range(0, 256)) # 像素总数 total_pixels gray.size # 归一化得到概率 prob hist / total_pixels # 全局均值 sum_all np.sum(np.arange(256) * prob) best_thresh 0 max_var 0.0 # 累积变量 weight0 0.0 # 前景概率累积 sum0 0.0 # 前景灰度加权累积 for t in range(256): weight0 prob[t] sum0 t * prob[t] # 如果某个类概率为0跳过避免除零 if weight0 1e-6 or weight0 1 - 1e-6: continue weight1 1.0 - weight0 # 均值 mean0 sum0 / weight0 mean1 (sum_all - sum0) / weight1 # 类间方差 between_var weight0 * weight1 * (mean0 - mean1) ** 2 if between_var max_var: max_var between_var best_thresh t return best_thresh代码逻辑很简单循环每个灰度级t把小于等于t的概率和均值累加起来然后直接套类间方差公式。循环结束后让类间方差最大的t就是最终阈值。3.2 从直方图出发的等价写法上面这个实现每次循环做的是实时累积效率已经很高。还有一种写法是先一次性算出所有累积数组再用向量化方式计算类间方差性能会更好。def otsu_threshold_vectorized(gray): hist, _ np.histogram(gray.ravel(), bins256, range(0, 256)) total_pixels gray.size prob hist / total_pixels # 灰度级数组 gray_levels np.arange(256) # 累积概率 weight0 np.cumsum(prob) # 累积均值分子 sum0 np.cumsum(gray_levels * prob) # 全局均值 sum_all sum0[-1] # 排除概率为0的边界情况 valid_mask (weight0 1e-6) (weight0 1 - 1e-6) weight1 1.0 - weight0 mean0 sum0 / weight0 mean1 (sum_all - sum0) / weight1 between_var weight0 * weight1 * (mean0 - mean1) ** 2 # 只取有效位置的最大值 valid_var np.where(valid_mask, between_var, 0) best_thresh int(np.argmax(valid_var)) return best_thresh向量化版本避免了Python层级的for循环在图像尺寸大、需要批量处理时速度优势明显。两种写法本质完全一样只是实现方式不同。3.3 二值化应用拿到阈值后怎么用获得阈值只是第一步真正要的是把原图变成二值图。阈值的使用方式取决于你想要的输出# 大于阈值设为255小于等于阈值设为0 _, binary_standard cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY) # 小于阈值设为255大于阈值设为0常见于白底黑字场景 _, binary_inv cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY_INV)如果你用的是OpenCV其实不用自己实现OTSU直接一步到位import cv2 gray cv2.imread(input.jpg, cv2.IMREAD_GRAYSCALE) thresh, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)把阈值的初始值设为0再在type参数里加上cv2.THRESH_OTSUOpenCV会自动忽略传入的阈值改用OTSU计算出的结果。返回的thresh就是OTSU自动求得的阈值。我平时做原型验证时多用OpenCV一行流但理解NumPy版实现仍然很重要因为工程上经常需要定制——比如只想搜索某个灰度范围、想把灰度级映射到非均匀区间、或者要跟其他算法融合这时候手写实现更好改。3.4 代码中的几个细节坑写OTSU代码时有几个位置特别容易出错。第一是除零问题。当阈值t遍历到一端时某个类的概率可能为0直接做除法会得到NaN或无穷大。我的代码里通过valid_mask排除这些位置逻辑上也要在循环版本里加判断。第二是边界处理。理论上阈值t可以从0取到255但t0意味着所有像素都被归到C1t255意味着所有像素都被归到C0这两类情况下两类的概率有一个为0类间方差也是0没有实际意义。实际搜索时跳过这些端点不会影响结果。第三是数据类型。灰度图是uint8类型如果直接用np.arange(256) * hist而不做类型转换可能会造成溢出。最好先把直方图转成float类型或者先除以总数得到概率再做乘法。上面的代码因为先做了归一化所以没有这个问题。4. 实操测试什么图像效果最好什么场景会翻车4.1 理想场景直方图呈明显双峰OTSU效果最好的场景是前景和背景灰度分布分明、各有明显集中的区间。举个例子白纸黑字的文档扫描图背景集中在240~255文字集中在50~120直方图里会有两个很明显的峰。OTSU会把阈值定在两个峰中间的谷底分割效果几乎是完美的。车间里拍摄的零件轮廓检测也可以归入这类。背景是统一的传送带深灰色工件是不锈钢亮银色灰度差异明显OTSU能稳定找出阈值后续再用轮廓查找、连通域分析就能把零件框出来。这类图像有个共同特点类内方差小类间天然分离。OTSU本质上是把这种视觉上的“明显分界”转化成数值上的“最大方差”。4.2 容易翻车的场景一光照不均光照不均是实际项目里最头疼的问题。比如一张文档左边被台灯照亮右边处于阴影中同一种纸在左右两侧的灰度值能差出50~80个级别。在这种图像上OTSU倾向于找到一个折中的阈值结果是亮区域的浅色部分被误分成背景阴影区域的深色部分又被误分成前景。我做过一个统计在光照不均明显的30张图上直接用OTSU的分割结果平均准确率只有78%左右而经过背景校正后的结果能到95%以上。所以对这类图像不能指望OTSU“自带补光”要先做预处理。比较有效的预处理有这么几种高斯滤波或中值滤波先平滑噪声让灰度分布更紧凑OTSU找的阈值更稳。形态学顶帽变换用形态学操作估计背景并从原图中减去背景达到“压平”光照的效果。分块局部OTSU把图像划分成若干小块对每块单独做OTSU再合并结果。块与块之间可能有接缝需要做羽化处理。4.3 容易翻车的场景二前景背景比例严重失衡OTSU的类间方差公式里ω0和ω1是乘法关系0.5×0.5最大。当前景占比非常小比如一张图片里只有一个很小的小黑点其余全是白色背景ω0可能只有0.001ω1接近0.999这时无论均值差多大乘积都会很小OTSU可能会选一个偏向背景的阈值导致目标丢失。遇到这种情况我的经验是先做兴趣区域裁剪让前景在裁剪后占比更大再做OTSU。如果前景确实很小且灰度与背景差异很大也可以考虑改用基于局部对比度或边缘的方法。4.4 容易翻车的场景三多峰直方图有些图像的直方图不仅有双峰还会出现三个甚至更多的峰。比如一张图里有暗色背景、灰色主体、亮色高光三个区域。OTSU只能把像素分成两类遇到多峰情况就只能把其中两个区域强行合并成一类导致分割结果要么丢失细节要么引入多余区域。这时候有几个选择。第一个是多级OTSU把Otsu扩展到多个阈值相当于把灰度轴切成多段。第二个是改用聚类算法比如K-Means聚类直接指定分成3类或更多类。第三个是结合区域生长先做OTSU初分割再对相邻区域做合并。举一个具体的例子某次处理芯片引脚检测引脚金属高光、塑料基体深色、背景浅灰三峰分布OTSU二值化后高光和背景总是粘在一起。后来我改成先做OTSU把前景和背景分开再对前景部分单独做一次OTSU分出了引脚和基体效果才达到需求。4.5 一个综合实例文档图像二值化完整流程下面分享一个我在文档预处理任务里用过的实际方案供参考。流程是这样的读取灰度图resize到合适尺寸。用3×3中值滤波去噪去除扫描产生的椒盐噪声。做顶帽变换估计并去除不均匀背景。用OTSU求阈值得到二值图。取出二值图后用形态学开运算去掉小噪点闭运算连接断笔。这个流程跑了一批历史档案扫描件最终效果明显优于直接二值化。二值图里文字基本完整印章边缘也保留得不错。操作中我注意到一个细节顶帽变换的结构元尺寸很关键太小会连笔迹一起去掉太大又校正不了光照不均。一般取文字笔画宽度的2~3倍如果你的文字大约十几个像素粗细结构元取30~50像素比较合适。5. 常见问题与排查技巧实录5.1 阈值偏高或偏低如何调整有时候OTSU算出的阈值看起来“不对劲”比如把阴影误判为前景或者把亮色背景误判为文字。首先要明确一点OTSU没有主观偏好它就是数学上最优的“两类分离点”。如果你觉得它分得不对往往是两类不是你想的那两类。排查时我会看灰度直方图判断是否存在明显双峰。如果直方图只有一个主峰OTSU很容易把峰顶附近切一刀导致分割不稳定。这时可以试试先压缩灰度动态范围或者用Gamma校正增强对比度。如果想在OTSU基础上做些偏移可以用这个技巧把OTSU阈值乘一个略大于或小于1的系数。比如想让前景更完整、宁可多一点背景就把阈值调低5%让更多像素被归到前景。这种经验性偏移在工业检测中很常见但每次调整都要看样本效果不能盲调。5.2 直方图为单峰时如何处理单峰直方图通常表示图像对比度很低比如雾天拍摄的户外图。OTSU在这种情况下仍然能给出一个阈值但效果不稳定阈值可能落在峰的任何一侧。我曾经用OTSU处理一张低对比度的胸腔X光片效果时好时坏。后来换成了先用对比度拉伸比如直方图均衡化或CLAHE再跑OTSU稳定性明显提高。因为对比度拉伸放大了灰度差使原本挤在一起的像素被拉开双峰结构才显现出来。5.3 灰度分布中的噪声对阈值影响大吗噪声会影响直方图形状让峰变宽甚至出现毛刺从而微调OTSU选中的阈值。如果噪声是高斯类型的一般影响不大因为OTSU本质上是基于累积统计对小幅波动有抗性。但如果是大量椒盐噪声可能产生虚假的峰导致阈值偏移。遇到这种情况先滤波再分割比事后修二值图效果好得多。我的习惯是先做3×3或5×5中值滤波它是非线性的对保留边缘比均值滤波更友好。5.4 OTSU阈值在不同图像之间波动大怎么办批量处理不同风格的图像时可能会发现OTSU阈值波动很大。这本身不是问题它说明图像间的灰度分布差异确实大。但如果同一批同类图片阈值波动异常要怀疑是不是某一两张图像有大量反光或污渍。这时可以做两件事。一是统计阈值分布把阈值偏离均值超过2个标准差的图像标记出来检查。二是加一个先验如果已知前景像素灰度一定低于136就把OTSU搜索范围限制在0到200避免阈值被极端值带走。5.5 一张实用的问题速查表现象可能原因排查方向前景断裂、不完整光照不均导致局部灰度偏低先做背景校正再OTSU背景大块误判为前景背景中有阴影或纹理干扰尝试形态学开运算或局部OTSU阈值波动大图像对比度差异大先做归一化或直方图拉伸分割结果呈现碎点噪声影响直方图形状先做中值滤波再分割小目标丢失前景占比太小权重不平衡裁剪ROI后再应用OTSU6. 最后一些个人经验OTSU虽然是1979年提出的算法到现在快半个世纪了但它依然是很多分割任务里最稳的“第一把尺子”。原因很简单它不需要调参、计算快、数学含义清晰、在各种OpenCV和NumPy环境中都是现成能力。我在做项目预处理时经常是先跑一遍OTSU看看直方图和分割效果再决定要不要上更复杂的模型。我也踩过一些坑这里再分享三条经验。第一条OTSU的一值化结果常常是中间产物不要直接拿去做最终判断配合连通域分析、轮廓查找才能提取真正有用的区域。第二条直方图不只是用来算阈值画出来看一眼能帮你省大量调试时间。如果直方图根本没有双峰你再怎么调OTSU参数也没用应该回头处理图像本身。第三条批量处理时把不同来源的图像分开处理比如扫描件和拍照件分开调流程效果比统一跑一套好得多。OTSU不是万能的但没有它很多二值化任务会麻烦得多。把它吃透后面再学自适应阈值、语义分割时你会有一种“哦原来就是在解决类似的问题”的豁然感。希望这篇文章对你有用祝调试顺利。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询