做图像处理这几年有一类问题最不起眼却也最容易翻车——阈值分割。你说它简单四行代码就能跑读图、转灰度、比大小、出二值。你说它难只要图像里混进一点光照阴影或者传感器噪声那个“合适的阈值”就像会躲一样怎么调都不对。这次我专门做了一组“泾渭分明”式的微实验把固定阈值、Otsu大津法、迭代均值法、自适应局部阈值四种常见方法放在三张可控的合成图上逐个对比量化指标和主观表现一起看。实验不复杂但把每种方法的计算原理、适用场景和翻车点都暴露得清清楚楚。这篇文章就是完整记录并附上可以直接跑的MATLAB代码给正在做图像分割、二值化、目标提取的读者一个选型参考。1. 实验设计与对比思路1.1 阈值分割到底在解什么问题图像阈值分割的本质是把连续灰度图像映射成离散的二值标签。假设灰度图记为f(x,y)阈值T理想的分割结果是目标区域内灰度低于T背景区域内灰度高于T或相反。但这个假设在真实图像里几乎总是被打破。光照变化会改变整个灰度水平导致同一物体在不同位置灰度值不同噪声会把部分像素推到阈值另一侧产生椒盐似的误分点目标与背景交界处灰度连续过渡无论如何都无法用单一阈值干净切开。这就让“选T”从一次简单的比大小变成了一个有约束的最优化问题。我的实验思路就围绕这个“最优化”展开。固定阈值是基线代表人工经验粗选Otsu和迭代法代表自动化的全局最优求解自适应阈值则放弃了“全图一个T”的约束改用逐像素可变阈值。四种方法形成两条对比维度人工 vs 自动全局 vs 局部。这种设计的目的不是比谁跑得准而是看清楚每种方法背后的假设是什么、在什么条件下假设成立、又在什么条件下失效。1.2 测试图与评价指标的设计逻辑要做公平对比测试数据必须满足三个条件ground truth可获取、场景能模拟典型问题、结果可重复。合成图正好满足全部要求。我设计了三个场景对应实际项目中三种最常见的干扰场景一光照均匀、目标灰度稳定的标准图用来检验各方法的基线能力。场景二背景亮度沿对角线渐变灰度范围一路延伸到接近目标的灰度附近专门考验全局阈值在光照不均下的鲁棒性。场景三在场景一之上叠加高斯噪声模拟传感器噪声或暗光环境的干扰看各方法对噪声的容忍度。评价指标选用错分率、IOU和F1。错分率衡量全局准确度IOU更关注目标区域的重合程度F1则平衡了漏检和误检。三个指标配合使用可以避免被单一指标带偏判断。程序方面我统一把图像归一化到[0,1]并转为double处理排除量纲干扰。这个细节看着小实际很关键——后续计算指标、混用图像类型时很容易因为类型不对而出bug。为了不让结果只停留在合成图我还留了一张真实拍摄的纹理图做视觉验证先看分割效果是否合理再对照合成图上的量化结论确认方法间的相对优劣。毕竟合成图再可控也只是对真实情况的抽象模拟最后一步总要回到真实数据上感受一下。2. 四种阈值分割方法的原理拆解2.1 固定全局阈值最简单也最看脸固定阈值是最原始的分割方式人工观察灰度直方图找到目标峰和背景峰之间的谷底手动设定T。在MATLAB里就是一行bw img T。它的优势是极快、结果可预期、完全不需要训练过程。缺点是T一旦定死整张图像的亮度变化都会直接影响分割质量。这个方法适合目标与背景对比强烈的场景比如白纸上扫描的黑字、票据上的印章、显微镜下的均匀染色样本。但固定阈值的“看脸”程度远超想象。如果图像经历过光照不均或高动态范围拍摄同一个阈值在不同空间位置上的语义就会完全不同。关于直方图选阈值我提供一个经验双峰明显的时候谷底通常是最优位置附近但不是绝对。因为直方图反映的是像素数量随灰度的分布而分割质量还和像素的空间位置有关。之前遇到过用双峰谷底选出来的阈值把大片阴影区域当作背景的情况。所以对固定阈值只建议在图像采集条件受控时使用。2.2 Otsu大津法自动拿捏类间方差Otsu法是目前最常用的自动阈值选择方法。核心思想很直白把阈值当作决策边界将像素分为两类让这两类之间的方差最大。类间方差公式可以写为σB² ω0 × ω1 × (μ0 - μ1)²其中ω0、ω1是两类像素的占比μ0、μ1是两类灰度均值。当两类均值距离最大时类间方差最大说明分割后两类“分得最开”。实现上并不复杂统计直方图后遍历所有可能的灰度级找出使类间方差最大的T。MATLAB中直接调用graythresh即可。用Otsu有两点要心里有数。第一它隐含假设直方图大致呈双峰分布当目标占比很小或灰度差异不大时类间方差曲线会变得平坦阈值漂移明显。第二它对光照不均没有抵抗力因为全局统计量会被渐变的背景“拉平”。但Otsu的抗噪性相对不错统计均值天然对独立噪声有一定抑制作用这一点在我后面的场景三测试里会明显体现出来。2.3 迭代均值法一个手工版的自适应基线迭代均值法可以理解为k-means聚类在灰度空间上的特例。算法流程很经典先取整幅图像均值作为初始阈值T用T将像素分为前景和背景两类分别计算两类灰度均值取两者的中点作为新的T重复这个步骤直到T的变化小于某个容差。数学上它收敛到使得两类类内距离平方和最小化的解过程相当于不断向最优分割边界移动。它的优点是容易理解、完全无参数运行速度也快。但弱点同样明显如果两类像素数量极不平衡均值会被大数类拉走新阈值会比Otsu更偏向占优势的那一侧导致小目标丢失。我在代码实现里加了一个迭代上限和单类崩溃检查防止分类过程中某一类像素数量变成0时死循环。实际跑下来迭代法在干净图上与Otsu几乎一样好在噪声图上略逊一点作为Otsu的替代方案完全可以接受。2.4 自适应局部阈值让每个像素拥有自己的T全局阈值再怎么优化也逃不过“全图一个阈值”的约束。自适应局部阈值正是为了打破这个约束而生。它的逻辑是对图像中的每个像素取一定尺寸的邻域窗口在窗口内计算局部统计量推导出该像素位置上的阈值T(x,y)。这样亮区域的阈值会偏高暗区域的阈值会偏低光照渐变造成的灰度漂移被逐点抵消。MATLAB中由adaptthresh实现。adaptthresh有一个参数sensitivity需要特别留意它控制阈值相对局部统计量的偏移程度。sensitivity越高阈值越靠近局部灰度下限更多像素被判为前景过低则前景被大量漏检。我一般从0.4附近起步再根据结果微调超过0.6后往往会出现明显的过分割噪声。自适应阈值不是没有代价。它需要处理大量局部窗口运算速度明显慢于全局阈值它对局部噪声的放大作用也很明显——一个局部窗口内只要有少数噪点大幅偏离均值该窗口的阈值就会失真。所以它并不总能“碾压”Otsu在图像噪声较高且目标占比较大时全局方法反而可能赢。3. 代码实现与关键参数解析3.1 环境准备与合成图生成我用的是桌面版MATLAB版本R2021a工具箱为Image Processing Toolbox。准备好之后先写一个合成图生成函数。这个函数同时输出二值化的ground truth之后和分割结果逐像素比较。场景一是矩形暗目标0.2、亮背景0.75场景二是背景对角线渐变0.30到1.00、圆形暗目标0.15场景三在场景一基础上加高斯噪声方差0.02。function [img, gt] synth_data(scene) sz 256; [X, Y] meshgrid(1:sz, 1:sz); switch scene case 1 img 0.75 * ones(sz); region X 60 X 196 Y 80 Y 176; img(region) 0.2; gt region; case 2 img 0.30 0.70 * ((X Y) / (2 * sz)); circle (X - 128).^2 (Y - 128).^2 40^2; img(circle) 0.15; gt circle; case 3 img 0.75 * ones(sz); region X 60 X 196 Y 80 Y 176; img(region) 0.2; img imnoise(img, gaussian, 0, 0.02); gt region; end end三点说明。第一meshgrid返回的X、Y是矩阵X对应行坐标、Y对应列坐标所以矩形区域的判断要写对否则会得到斜着切掉的错误结果。第二imnoise加高斯噪声时第二个参数均值是0第三个参数方差是0.02输入输出都在[0,1]范围。第三由于合成图生成后已经是[0,1]的double类型后续不需要再额外归一化如果是真实照片务必先用im2double做转换再进入分割流程。3.2 四种分割方法的代码实现固定阈值不需要封装函数img 0.5就是。Otsu、迭代法和自适应阈值分别封装成函数方便统一调用和评估。function bw otsu_seg(img) T graythresh(img); % 返回0~1之间的全局阈值 bw img T; % 暗目标在前景 end function [bw, T] iter_seg(img) T mean(img(:)); for k 1:100 fg img T; bg ~fg; if ~any(fg(:)) || ~any(bg(:)) break; end T_new (mean(img(fg)) mean(img(bg))) / 2; if abs(T_new - T) 1e-3 T T_new; break; end T T_new; end bw img T; end function bw adaptive_seg(img, sensitivity) if nargin 2 sensitivity 0.4; end Tmap adaptthresh(img, sensitivity, NeighborhoodSize, 48); bw img Tmap; end这里有几个容易踩的坑。第一个是imbinarize的实际行为它返回的是img T默认把亮区域当作前景。而我的目标是暗区域所以我直接写img T不用imbinarize避免二次取反带来的符号混乱。第二个是迭代法的初始阈值一定要用全图均值不要用灰度中值因为均值能更快地接近两类质心的中点。第三个是adaptthresh中NeighborhoodSize对速度和效果影响很大。48在256×256图上属于较小窗口目标是让阈值拟合光照渐变同时又不过度放大噪声。3.3 评估函数与主流程脚本评估函数计算错分率、IOU和F1顺便测一下运行耗时function [mis, iou, f1, elapsed] evaluate(img, gt, segFunc) tstart tic; bw segFunc(img); elapsed toc(tstart); TP sum(bw(:) gt(:)); FP sum(bw(:) ~gt(:)); FN sum(~bw(:) gt(:)); mis (FP FN) / numel(gt); iou TP / (TP FP FN eps); precision TP / (TP FP eps); recall TP / (TP FN eps); f1 2 * precision * recall / (precision recall eps); end主流程脚本负责把三个场景和四个方法都过一遍并打印汇总表格scenes {1, 2, 3}; methodNames {Fixed, Otsu, Iterative, Adaptive}; methodFuncs { (im) im 0.5; (im) otsu_seg(im); (im) iter_seg(im); (im) adaptive_seg(im, 0.4) }; for s 1:numel(scenes) [img, gt] synth_data(scenes{s}); fprintf( Scene %d \n, s); for m 1:numel(methodNames) [mis, iou, f1, elapsed] evaluate(img, gt, methodFuncs{m}); fprintf(%-10s | mis%5.2f%% | iou%5.3f | f1%5.3f | %6.1f ms\n, ... methodNames{m}, mis * 100, iou, f1, elapsed * 1000); end end注意脚本里匿名函数句柄的使用好处是把方法实现和评估循环解耦。固定阈值方法甚至可以随意换T比如改成(im) im 0.45再跑一遍成本几乎为零。这也是微实验的优势参数的影响可以快速反馈。4. 实测对比结果分析4.1 场景一基线能力全员在线在光照均匀、目标灰度0.2、背景灰度0.75的干净图上四种方法的错分率都是0.0%IOU都等于1.000F1也都是1.000。这个结果看起来平淡但意义不小它画出了所有方法在“理想条件下”的上限说明这类简单分割任务已经不需要纠结算法选型固定阈值就够了。反而是在这种图上我提醒自己别被“自动方法更高级”的念头带偏能用一行img 0.5解决的问题就不要引入更贵的方法。耗时差距在此时就开始显现固定阈值和Otsu都在1毫秒级迭代法约1.8毫秒自适应约42毫秒。在256×256的小图上勉强能接受但如果把分辨率提升到1920×1080自适应阈值的时间可能涨到秒级对实时应用很不友好。4.2 场景二全局方法集体翻车场景二里背景从左下角0.30渐变到右上角1.00目标圆形区域的灰度0.15。固定阈值0.5的问题最直观左下角大块背景灰度低于0.5被整体误判为目标错分率达到16.2%。Otsu好一点但错分率依然11.3%因为全局直方图无法区分“左下角的暗背景”和“真正的暗目标”。迭代法结果介于两者之间错分率13.0%。这三种全局策略输在同一个地方——它们试图用一个数字去解释一个灰度跨度很大的图像内容。自适应阈值在这一场景的优势彻底释放错分率只有1.0%左右IOU达到0.889。它成功跟随了背景的亮度变化把目标与背景在局部层面分开了。这个结果让我印象很深如果拿到一块明显光照不均的图不要在固定阈值上浪费时间直接上自适应先看结果再调参。4.3 场景三噪声面前各有取舍场景三在干净图上叠加了方差0.02的高斯噪声。最让我意外的是自适应阈值在这里反而表现不佳错分率上升到6.1%IOU降到0.790。原因是局部邻域内的噪声会让阈值估计产生抖动噪点本身也可能单独形成伪前景区域。Otsu错分率只有2.3%IOU 0.912因为全局直方图统计相当于一种平滑单个噪声点的贡献被平均掉了。固定阈值0.5错分率3.7%迭代法2.6%也都维持在可用水平。这说明一个容易被忽视的结论全局阈值对“均匀噪声”其实挺有抵抗力而局部自适应阈值对噪声更敏感。工程上如果目标占比较大且图像噪声明显可以先做轻量滤波比如3×3中值滤波再分割效果通常好于直接上自适应。4.4 横向对比表与方法选择建议把三张合成图上的结果汇总如下测试场景分割方法错分率IOU耗时(ms)场景一 均匀光照固定阈值 T0.50.0%1.0000.7场景一 均匀光照Otsu0.0%1.0001.2场景一 均匀光照迭代法0.0%1.0001.8场景一 均匀光照自适应0.0%1.00042.0场景二 光照不均固定阈值 T0.516.2%0.3370.7场景二 光照不均Otsu11.3%0.4151.2场景二 光照不均迭代法13.0%0.3891.9场景二 光照不均自适应1.0%0.88948.0场景三 高斯噪声固定阈值 T0.53.7%0.8650.7场景三 高斯噪声Otsu2.3%0.9121.2场景三 高斯噪声迭代法2.6%0.9002.0场景三 高斯噪声自适应6.1%0.79051.0表三场景四方法的量化对比结果。合成图的数值会随随机种子和环境略有浮动但相对趋势稳定。方法选择的大致规律可以总结成四句话。目标与背景对比强且光照均匀时固定阈值或Otsu都行优先Otsu。图像存在明显亮度渐变时自适应阈值是默认选择若速度敏感就先降采样、自适应、再上采样。噪声大且目标占比不小优先全局方法配合中值滤波预处理能进一步稳住。目标在图中占比极小或目标灰度本身有内部渐变时任何阈值方法都难以解决需要换用连通域筛选、边缘检测或语义分割等手段。这套流程我拿真实图验证过。反光严重的证件照上光照不均占大头自适应阈值效果立刻改善显微镜下均匀染色的样本图Otsu加一次形态学开运算就足够干净。选方法前先判断干扰源比盲目堆高级算法重要得多。5. 常见问题与调参经验5.1 问题速查表现象原因解决办法Otsu阈值明显偏低/偏高输入图像不是灰度图或未归一化转灰度后用im2double归一化分割结果亮暗反了目标实际的灰度关系与代码假设相反用~bw取反或改为img T光照不均导致大片误分全图共用单一阈值改自适应阈值或先做背景估计后扣除二值图中有大量颗粒状噪点噪声被阈值放大分割前中值滤波分割后用bwareaopen去小区域自适应分割结果过碎sensitivity过高或邻域过小调低sensitivity到0.3附近增大NeighborhoodSize自适应运行时间过长逐像素计算局部统计代价高减小图像尺寸、增大邻域或改用全局Otsu配合背景扣除同一算法不同机器结果不一致随机噪声或随机种子未固定关键实验固定rng和输入图像保证可复现表格最后一条我特别有感触。自适应阈值本身不引入随机性但如果你在测试之前没有固定随机种子叠加噪声图的生成结果每次都会变对比实验就失去意义。凡是涉及噪声的评测先写rng(42)再去生成图像已经是我的条件反射了。5.2 三个亲测有效的调参技巧第一个技巧图像噪声明显时先做一步轻量中值滤波比如3×3邻域再进阈值分割。多数情况下错分率能下降一到两个百分点而且基本不牺牲边缘细节。反之如果用均值滤波虽然噪声压得更狠但边缘会被磨糊小目标容易被吞中值滤波对边缘的保留明显更好。第二个技巧当光照不均是主要矛盾而不是噪声时可以先用大半径中值滤波估计背景再用原图减去背景估计后做Otsu。这一步本质是把“变化的光照”提前拆掉让全局阈值恢复效力。处理时间通常比自适应阈值快一个数量级。具体思路是取一个半径为目标尺寸数倍的窗口做中值滤波得到背景亮度图相减后目标与背景的灰度对比会变得非常稳定。形态学开运算估计背景的思路类似哪个顺手用哪个。第三个技巧固定阈值代码里不要把阈值写死在算法内部而是单独设置参数。我习惯把T写成脚本顶部的变量或者封装成函数参数这样换图时不用去翻代码。微实验的核心价值在于快速试错参数要是散落在代码各处一次调整的成本就上去了。5.3 后续扩展与工程落地方向这套对比实验做成模板之后很自然就能往几个方向扩展。比如加入双阈值分割利用低阈值保留弱目标、高阈值剔除噪声并在两阈值之间做连通域约束常用于边缘提取或者弱信号检测。又比如把评估脚本接到视频帧上检查阈值分割在连续帧之间的稳定性这在工业检测里很常见——一帧分割得好不代表帧间跳动可以被接受。从算法选型回到工程落地我个人的体会是阈值分割永远不是一个孤立决策它必须和前处理、后处理打包成一个链路来看。前处理负责把图像“拉”到更接近理想假设的状态后处理负责修复分割结果的细碎误差。真正有价值的不是知道Otsu比固定阈值高级而是能判断什么时候需要加前处理、什么时候应该换模型。实验做完了这套链路的每一环都值得再单独深挖尤其是最后一步往往比算法本身的调参更能决定项目的成败。
阅读完成 · 觉得有帮助?