ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

图像二值化方法详解:全局阈值到自适应阈值的工程实践

图像二值化方法详解:全局阈值到自适应阈值的工程实践 二值化这事看着简单真正做起来坑多。早几年我刚开始接触图像处理时拿到一张灰度图第一反应就是cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)一出手就是固定阈值127。后来被一堆现实图片反复教做人之后才明白阈值怎么选、全局还是局部、要不要先做预处理这些决策直接决定后面OCR、缺陷检测、边缘提取能不能跑通。这篇东西我把这么多年用过的图像二值化方法做一个系统梳理从原理、代码到业务场景里的选型思路都按自己的实际经验来讲不写成说明书尽量把每种方法的底牌翻出来。1. 二值化到底在做什么先想清楚你要留下什么信息很多人把二值化当成“把图变黑白”的一步操作这个理解不能说错但容易让人忽略本质。二值化的真正目标不是变好看而是从灰度图中分离出你关心的前景结构抑制掉不关心的背景和噪声。一张文档扫描图前景是文字笔画背景是纸张一张工业零件图前景是缺陷或字符背景是均匀的材质表面一张显微图像前景可能是细胞或颗粒。不同场景要求的“前景”完全不同所以二值化方法才需要分门别类。这个过程本质上是一个像素级的决策问题对图像中每个像素根据某个判据决定它是属于前景通常置为255或1还是背景置为0。判据可以是全局唯一的也可以是随像素位置变化的还可以是结合了邻域梯度、纹理信息的复杂函数。这就是不同方法之间的根本差异。从信息保留的角度看灰度图每个像素有256种可能而二值化之后只有0或255两种。这个过程一定会丢信息但丢得值不值取决于你丢掉的是不是噪声和背景干扰。比如一条边缘在灰度图里是缓慢过渡的斜坡二值化后变成一条锯齿状的硬边界边缘位置和形状大致保留但过渡细节没了。如果你后面的算法只需要连通区域、轮廓、字符切割这类高层结构这点损失完全可接受但如果你的目标是要做亚像素边缘测量那直接二值化就会丢失精度得考虑阈值分割后再做边缘拟合。还有一点需要明确二值化是预处理手段很少是最终目的。所以评价一个二值化方法好不好不能只看图片效果要看它对接的下游任务表现如何。比如做OCR二值化后笔画是否断裂、字符是否粘连直接影响识别率做缺陷检测二值化后有没有把正常纹理误判成缺陷直接影响误检率。理解了这一点后面所有的方法选择和参数调优就有了真正的判断标准。我在实际项目里一般会先问自己三个问题前景和背景在灰度分布上是否可分离看直方图是否双峰光照是否均匀是否存在阴影、渐变、反光下游任务对笔画断裂、粘连、噪声点的容忍度如何这三个问题基本决定了你要用哪一类方法也决定了后面参数调节的方向。2. 全局阈值三件套固定阈值、Otsu大津法与三角法的原理和各自翻车场景全局阈值的意思是整幅图像共用一个阈值像素灰度大于阈值判为前景小于等于阈值判为背景。这是一维直方图分割的经典思路实现简单、速度极快是很多处理流程的默认起点。2.1 固定阈值最直接但最娇气固定阈值就是人为指定一个灰度值比如127然后逐个像素比较。代码上OpenCV一行就够import cv2 img cv2.imread(input.png, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)双峰直方图两种灰度峰且光照理想的情况下这种方法其实足够用。但翻车场景也很常见光照偏暗整个直方图左移固定阈值会把原本的前景像素误判为背景。前景和背景灰度范围交叠不管选哪个阈值都会同时丢前景和留噪声。同一批图片由不同设备或不同拍摄条件产生固定阈值很难一键通吃。所以固定阈值我一般只用在受控环境里比如实验室打光固定、相机参数不变、背景纯净的检测台。真实场景里直接用它九成要出事。2.2 Otsu大津法自动找阈值但是有前提条件Otsu大津法的核心思想是遍历所有可能的阈值0~255对每个阈值把像素分成前景、背景两类计算类间方差选择使类间方差最大的那个阈值。类间方差刻画的是两类灰度均值之间的分离程度越大说明前景和背景在灰度上拉得越开。下式为类间方差的定义第t级为当前阈值ω₀、ω₁分别为背景、前景像素比例μ₀、μ₁为对应灰度均值μ_T为整图灰度均值σ²_b(t) ω₀(t) · ω₁(t) · [μ₀(t) − μ₁(t)]²代码上OpenCV提供了极简写法_, binary_otsu cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)不用自己指定阈值函数内部自动计算。Matlab里对应的是graythreshimbinarizeT graythresh(img); binary imbinarize(img, T);Otsu在双峰直方图且两峰面积相差不太悬殊时效果很好。但它的前提是“直方图存在比较明显的双峰”一旦不满足就会翻车前景面积远小于背景面积时比如一张图上只有一个很小的零件直方图中前景峰极小类间方差会被大面积背景主导选出的阈值往往偏低或偏高无法准确框出目标。直方图是单峰的例如光照渐变的图像整体像一个大包Otsu会在峰腰上随便切一刀分割结果会按位置出现大片误判。直方图是多峰的比如有三类区域背景、目标A、目标BOtsu强行分成两类结果必然顾此失彼。所以我会说Otsu是对固定阈值的一次重要升级但它解决的是“阈值怎么自动选”的问题没有解决“全局阈值这个模型本身不适合这个场景”的问题。光照不均时不管阈值选在哪都会有一部分区域切错。2.3 三角法Triangle单峰直方图下的备用选择三角法也叫三角阈值法它先统计直方图找到峰值位置然后从峰值向直方图两侧做一条基线再计算直方图曲线上离这条基线最远的点该点对应的灰度值就是阈值。名字里的“三角”就是指峰值点、直方图最右侧边界点和最左侧边界点连成的几何关系。OpenCV里同样有封装_, binary_triangle cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_TRIANGLE)Matlab没有直接等同的单行函数但可以用imhist拿到直方图后手动实现或者直接用imbinarize的默认global方法并在内部切换MATLAB里通过graythresh只能得到Otsu阈值三角法需要自己写。三角法真正的用武之地是直方图接近单峰的情况。医学图像里经常出现背景一片、目标较小且灰度偏暗的情况直方图呈现一个高峰带长尾这时Otsu容易受峰形影响三角法反而能用尾部和峰之间的几何关系切出一个合理阈值。不过三角法也有明显局限它假设直方图有一个明显主峰并且目标像素分布在峰的某一侧长尾上。如果直方图本身就是“双峰夹一谷”或“多峰缠在一起”三角法的分割效果会很随意。2.4 全局阈值的共同软肋把三种全局方法放在一起看它们的共同问题都是整个图像共享一个阈值。光照不均匀、存在阴影、反光、视角倾斜导致亮度变化时图像不同区域的灰度分布差异很大全局阈值只能取一个折中值结果常常是“按了葫芦起了瓢”——亮区前景被保留暗区背景被误判成前景。这时候就需要局部信息介入也就是下一章要说的自适应阈值。3. 光照不均的救星自适应阈值怎么调才不产生伪影自适应阈值adaptive threshold和全局阈值的最大区别是图像每个像素的阈值不是同一个而是根据该像素周围一个邻域窗口的统计量实时计算出来的。这样即使全图光照变化很大每个局部窗口内的前景和背景仍然可以拉开阈值就能跟着局部亮度走。OpenCV的实现有两种模式ADAPTIVE_THRESH_MEAN_C阈值取邻域内像素的均值减去常数C。ADAPTIVE_THRESH_GAUSSIAN_C阈值取邻域内像素的加权均值高斯权重减去常数C。基本调用方式binary_adaptive cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize15, # 必须是奇数且需要经过试验调节 C5 )Matlab里对应的是adaptthreshT adaptthresh(img, 0.4, NeighborhoodSize, 15, Statistic, gaussian); binary imbinarize(img, T);3.1 blockSize和C到底怎么调blockSize是邻域窗口大小它决定了“局部”是多大范围。调这个参数有几个直观规律blockSize太小比如3或5窗口内像素太少统计量噪声大容易在平坦区域产生大量椒盐状伪影。blockSize太大比如51或101局部范围变得接近全局自适应能力慢慢退化又回到全局阈值的效果。经验上blockSize应大于前景特征的尺度。比如文字笔画宽度是5像素窗口至少要能覆盖几个笔画一般取15~31之间如果前景是细线窗口取11左右如果前景是大块区域窗口要更大才能避免把单个目标块切开。C是邻域统计量减去的常数作用是让判定更严格或更宽松。C增大阈值降低更多像素被判为前景前景变粗、噪声变多C减小阈值升高前景变细边缘更脆但也可能漏掉灰度较低的真实前景。从原理上理解均值/高斯加权均值代表了局部背景亮度减去C相当于给“比局部背景暗多少才算前景”设了一个门槛。光照变化剧烈的地方C要适当大一些避免背景波动被误判成前景。3.2 高斯加权和均值模式的区别高斯加权模式对中心附近像素给了更大的权重所以受邻域中心亮度的约束更强处理渐变光照时边缘更平滑在光照复杂场景下通常比均值模式更稳。均值模式计算更快适合能容忍一定噪声、追求速度的场合。我实测下来的感受是文档扫描、拍照文字提取这类场景GAUSSIAN_C比MEAN_C的效果普遍好一截尤其是页面边缘有阴影、中间受光比较亮的情况。工业检测中如果背景是金属拉丝、磨砂等有纹理的表面均值模式反而更不容易被细纹理干扰。3.3 自适应阈值最容易踩的两个坑坑一平坦区域伪影。当图像某一整块区域灰度变化特别小比如白纸空白处、纯色背景任何局部统计量都与中心像素接近减去C后就会在阈值附近抖动结果出现密密麻麻的小黑点。解决办法是调小C或者先做一次轻度的中值滤波/高斯模糊把平坦区域的微小波动抹平。坑二笔画断裂或孔洞。笔画内部灰度偶尔上升局部反光、纸张厚度不均时局部窗口内的均值也跟着上升阈值被抬高笔画内部就被判定为背景形成孔洞。这时调大C能减少孔洞但可能引入噪声更稳妥的办法是先自适应阈值再用形态学闭运算把断裂和孔洞补上。所以自适应阈值从来不是“设置一次参数走天下”它需要配合预处理、后处理一起用。我在做文档类项目时通常的链路是灰度化 → 高斯模糊3x3或5x5→ 自适应阈值GAUSSIAN_CblockSize约等于两倍笔画宽度C取3~8→ 形态学开运算去孤立噪点 → 形态学闭运算补笔画断裂。4. 边缘检测与形态学辅助当普通阈值无能为力时的组合打法全局阈值和自适应阈值都是直接基于灰度值做分割但有些场景灰度信息本身不可靠比如前景和背景灰度几乎一样、只是纹理或边界有差异或者光照极不均匀导致局部灰度信号也被破坏。这时候就要换思路先把边界找出来再通过边界反推前景区域。二值化和边缘检测之间是互相成就的关系——边缘检测提供位置先验二值化负责把位置扩展成完整的区域。4.1 为什么要从梯度信息反推二值图边缘检测算子如Sobel、Canny、Laplacian本质上是求图像的灰度梯度。梯度大的地方是边界梯度小的地方是平坦区域。如果前景和背景灰度值接近直接阈值分割分不开但它们的交界处一定存在梯度变化所以梯度图能给出“哪里可能有边界”的答案。用梯度信息辅助二值化的常见策略有两种策略一梯度图引导下的阈值调整。先算梯度幅值图梯度大的像素倾向于真实边界降低这些位置的二值化阈值确保边界像素被归入前景梯度小的平坦区域提高阈值抑制噪声。这种策略在文献里常叫梯度加权阈值工程里可以简化成把梯度幅值归一化后作为偏置项叠加到原始灰度图上再做Otsu或自适应阈值。import cv2 import numpy as np img cv2.imread(input.png, cv2.CV_8UC1) sobel_x cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize3) sobel_y cv2.Sobel(img, cv2.CV_32F, 0, 1, ksize3) grad_mag cv2.magnitude(sobel_x, sobel_y) grad_mag cv2.normalize(grad_mag, None, 0, 50, cv2.NORM_MINMAX) enhanced cv2.addWeighted(img.astype(np.float32), 1.0, grad_mag, 0.8, 0) _, binary cv2.threshold(enhanced.astype(np.uint8), 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)这段代码的思路是让边界处的灰度被往上拉平坦背景不变然后整体用Otsu切一次。注意grad_mag归一化时最大值设成50而不是255避免边界被拉得过高导致边界区域出现过肥的伪影。策略二边缘图 形态学闭运算重建区域。先用Canny提取完整边缘再做闭运算把断开的边缘线连起来然后找连通域填充最后用填充结果作为掩膜去提取前景。edges cv2.Canny(img, 50, 150) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) closed cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations3) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) mask np.zeros_like(img) cv2.drawContours(mask, contours, -1, 255, thicknesscv2.FILLED)这个策略的局限是Canny漏检的边界会让闭运算也救不回来边缘过于密集时几个目标连在一起填充后变成一个大连通域还需要按面积或形状再拆分。4.2 形态学后处理先开运算后闭运算的顺序别弄反形态学操作在二值化流程里的作用常常被低估。开运算是先腐蚀后膨胀用来去掉孤立的小噪点和细小的突出物闭运算是先膨胀后腐蚀用来填补前景内部的孔洞和连接断开的笔画。先做开运算是因为噪声点如果不先去掉闭运算会把它们膨胀成更大的块越处理越脏。一个常见的错误是上来就闭运算补洞结果把旁边的噪声也连进了目标区域。正确顺序是先开运算去孤立噪点再闭运算补洞和断线。如果目标内部孔洞很大比如文字笔画较粗且内部出现大块空洞可以先用闭运算补洞再开运算去噪但此时要用较小的核防止把细的笔画腐蚀断。核的形状也有讲究。连通性要求不高的场景用矩形核即可核大小一般取3x3或5x5迭代次数1~3轮。笔画较细的场景用椭圆核能减少对角线上的毛刺。4.3 分块阈值把Otsu拆成小Otsu另一种处理光照不均的思路是分块处理把图像切分成M×N个小块对每个块分别做Otsu最后拼回整图。分块后的子图可以看作光照近似均匀Otsu在每块上的效果比全图好很多。h, w img.shape block_h, block_w 64, 64 result np.zeros_like(img) for i in range(0, h, block_h): for j in range(0, w, block_w): block img[i:iblock_h, j:jblock_w] _, block_bin cv2.threshold(block, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) result[i:iblock_h, j:jblock_w] block_bin这个方法的坑在于块边界处会出现明显的接缝因为相邻两块算出的阈值不同边界两侧灰度相仿的区域一个被判前景一个被判背景。要消除接缝可以对每块算出的阈值做一次双线性插值平滑相当于每个像素用“周围几个块阈值按距离加权”的结果来判定。5. 按业务场景选方法文档OCR、工业检测、显微图像里的真实取舍方法学了一堆回到项目里到底怎么选其实是有规律可循的。下面按我实际做过的业务场景分类给出选型经验和参数起点。5.1 文档OCR与拍照文字识别这类场景的特点是前景是文字笔画通常比背景暗光照可能不均匀尤其是手机拍的文档有阴影、有折叠反光文字密度不高笔画像素占总面积比例不大。我的推荐链路是灰度化后先做高斯滤波5x5去除扫描纹路和传感器噪声。用自适应阈值GAUSSIAN_CblockSize取笔画宽度的2~3倍一般15~25C取5~10。开运算核3x3去孤立噪点再用闭运算核3x3或5x5连接断裂笔画。如果文字颜色很浅或者纸张很花可以尝试先做一次顶帽变换提取相对背景更暗的结构再做自适应阈值。顶帽变换是形态学操作tophat 原图 - 开运算结果。开运算先腐蚀再膨胀会把亮背景抹平成近似背景原图减去背景就剩下暗前景。这个预处理在纸张发黄、背景有图案时特别管用。5.2 工业视觉缺陷检测工业场景里背景通常是固定材质相机位置和光源也相对固定但被检工件表面可能有纹理、油污、反光。缺陷检测更关注边缘锐利度和尺寸稳定性二值化的目标是让缺陷区域和正常区域之间有明确边界。我的经验是能上全局阈值就用全局阈值光源稳定的情况下固定阈值Otsu的稳定性远超自适应阈值。因为自适应阈值会跟着局部纹理走容易把正常纹理的灰度波动放大成伪缺陷。如果光源确实不均匀优先用分块Otsu加阈值插值比直接上自适应阈值稳定。后处理阶段形态学操作要克制。缺陷检测最怕把真实缺陷给补掉或膨胀得过大一般只做一次开运算去掉孤立点不做闭运算。5.3 显微图像与生物医学图像显微图像的特点是噪声大、背景灰度波动大、目标形状多样而且往往是亮目标在暗背景上或反之。Otsu在这里是好用但不是万能类间方差在噪声干扰下选出的阈值经常偏。我处理显微图像的经验是先做中值滤波或各向异性扩散去噪保护边缘的同时压噪声再用自适应阈值最后用水填充分割watershed分离开粘连目标。如果只是需要统计目标数量二值化后数连通域就够了但连在一起的细胞需要做一次距离变换找局部极大值再用分水岭切分。Matlab在这类场景有优势内置了imbinarize、bwlabel、regionprops以及watershed的一整套工具箱配合Imaging Processing Toolbox推流程很快。5.4 二值化效果怎么定量评估不能只凭肉眼说“效果好”。我常用的定量方法有如果标注了GTGround Truth掩膜可以算IoU交并比、Dice系数、像素级精确率和召回率。没有GT时可以用连通域数量和平均连通域面积作为稳定性指标。同一批图片好的二值化应产生稳定数量的连通域。对接下游任务时直接用最终指标来评估比如OCR识别准确率、缺陷检出率/误检率这是最务实的评估方式。def iou_metric(pred, gt): intersection np.logical_and(pred, gt).sum() union np.logical_or(pred, gt).sum() return intersection / (union 1e-6)有一点要提醒二值化的“最优”是任务相关的。同一张图OCR要笔画完整缺陷检测要边界准确网格分析要面积接近真实值三个任务可能对应三个不同的最优阈值不存在一个万能参数。6. 代码落地对照OpenCV(Python)和Matlab的核心实现有些读者习惯用OpenCV有些习惯用Matlab两边函数名和参数含义差别挺大容易被绕晕。这里做一张对照表方便换环境时不踩坑。功能OpenCV (Python)Matlab固定阈值cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)imbinarize(img, 127/255)Otsucv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)T graythresh(img); imbinarize(img, T)三角法cv2.THRESH_BINARY cv2.THRESH_TRIANGLE手动实现自适应阈值均值cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15, 5)T adaptthresh(img, 0.4, NeighborhoodSize, 15, Statistic, mean); imbinarize(img, T)自适应阈值高斯cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 5)Statistic, gaussian开运算/闭运算cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)imopen/imclose连通域标记cv2.connectedComponentsWithStats(binary)bwlabel/bwconncomp注意Matlab里imbinarize输入的阈值是0到1之间的小数OpenCV里是0到255的整数换环境时记得换算。adaptthresh第一个参数0.4表示“在前40%的局部统计量基础上做阈值”和OpenCV里直接减C的语义不同它对应的是归一化敏感度默认0.4~0.5是常见区间数值越大越容易判为前景。关于代码里的数据类型cv2.imread读取灰度图的通道是uint8许多形态学、阈值函数只支持uint8或float32。做梯度计算时用CV_32F保持小数精度回到阈值或形态学时再转回uint8否则会出现精度截断导致的异常边缘。我自己的经验里写一次性分析脚本时用Matlab很顺手区域属性统计和可视化都方便但搭正式服务或批量处理流水线时用PythonOpenCV更合适部署轻、内存可控、和后续深度学习模型能直接串起来。两边代码逻辑完全对应唯一要小心的就是阈值取值范围和参数语义的换算。下面给一个可直接运行的Python完整示例覆盖灰度化、自适应阈值、形态学后处理、连通域过滤和可视化import cv2 import numpy as np def preprocess_and_binarize(path, block_size21, C7, min_area15): img cv2.imread(path) if img is None: raise ValueError(f无法读取图像: {path}) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 轻度去噪保留边缘 blur cv2.GaussianBlur(gray, (3, 3), 0) # 自适应阈值 binary cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, block_size, C ) # 形态学后处理先开后闭 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations1) closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations1) # 用连通域过滤小噪点 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(closed, connectivity8) filtered np.zeros_like(closed) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: filtered[labels i] 255 return gray, filtered gray, result preprocess_and_binarize(demo.png) cv2.imwrite(binary_result.png, result)这段代码里min_area按实际目标面积调整比如检测细小的文字或颗粒时设小一些检测大块目标时设大一些。实际项目中这个参数是过滤背景噪点的第一道闸门比反复调二值化参数更直接有效。7. 我踩过的坑噪声、断裂、伪影的排查与修复最后这部分是纯经验分享。二值化流程看起来只有几行代码实际调试时花时间的往往不是方法本身而是被一些不起眼的细节卡住。7.1 椒盐噪声不先滤波后面的形态学操作会把噪声越搞越脏我曾经在一条产线项目里遇到一个奇怪现象同一套二值化代码在样品图上效果还行一上产线图片就出现大量黑点。排查半天发现是相机Sensor在低照度下产生了大量椒盐噪声。噪声点经过自适应阈值后变成孤立前景点再经过闭运算膨胀从1像素变成3~4像素的块看起来就像缺陷。解决办法是在二值化前加一道3x3或5x5的中值滤波。中值滤波对椒盐噪声的处理效果比高斯滤波好很多因为它直接取邻域中值孤立点被邻域正常值替换掉同时对边缘的模糊程度也比高斯滤波小。7.2 直方图的双峰不明显先做对比度拉伸再Otsu另一类常见问题是图片整体灰蒙蒙的直方图集中在60~120这段区间没有明显双峰Otsu选出的阈值基本就是直方图均值附近切不出什么有意义的结果。这时候先把直方图拉满再分割效果会显著改善。线性对比度拉伸、直方图均衡化Histogram Equalization和CLAHE限制对比度自适应直方图均衡化是三个常用手段。CLAHE比普通直方图均衡化好的一点是它在局部窗口内做均衡不放大噪声特别适合光照不均的灰度图。我的习惯是光照方向单一且变化平缓时直接线性拉伸局部有过暗或过亮区域时用CLAHE。二值化前确实应该做灰度预处理但不能把对比度拉伸当作万能药。如果原图的前景和背景本来就不分层拉伸只是把两个峰拉开了一点本质问题没有解决还得靠自适应或边缘引导的方法。7.3 C参数正负理解反导致调试反复自适应阈值里C一个很容易混淆的点OpenCV代码里是邻域统计量 - C如果理解成“减C让阈值变低”就对了但这意味着C越大比局部背景暗的范围越宽前景会变粗C越小判定越严格前景会变细。但Matlab的adaptthresh方向相反它第一个参数是敏感度因子0~1越大越容易判为前景和OpenCV的C增量方向正好相反。如果两边代码来回切换会很容易调反方向越调越乱。我在这上面吃过亏后来固定在代码里写注释标注“增大C→前景变粗”以保证方向一致。7.4 破片边缘的二值化结果不稳定检查一下原图是否过曝有一次检测透明材质表面划痕二值化结果时好时坏。后来发现是光源角度稍微一变划痕区域出现高光过曝区域灰度被顶到255任何阈值都切不出来。这时候调阈值没用要么改光源角度和强度要么在算法里对高光区域做灰度保护要么直接换成分级照明下的两次采集。很多二值化问题表面是算法问题本质是成像问题先改硬件往往比调代码更有效。兜底的小技巧保存一组中间结果图调试二值化流程时我强烈建议把灰度图、滤波图、二值图、形态学后图全部单独保存出来。遇到效果不理想时一眼就能看出问题出在哪个环节滤波前就有问题是成像端二值图有伪影是阈值方法或参数问题二值图还行但后处理变差是形态学顺序或核大小问题。这一步看着简单实际能省下大量反复试参数的时间。图像处理这种流水线式的任务可视化中间结果是最高效的调试手段比光盯着最终输出猜原因靠谱得多。
返回列表