MATLAB图像计数实战:从二值化到分水岭分割

发布时间:2026/10/12 18:32:57
MATLAB图像计数实战:从二值化到分水岭分割 我最早接触图像计数是帮某实验室数细菌菌落。那批培养皿照片拍得歪歪扭扭光照还不均匀人工数了一个多小时眼睛都快看瞎了结果两个人数的数量还不一致。后来我用MATLAB写了十几行代码几分钟跑完全部照片误差控制在几个百分点以内从那以后我就养成了习惯凡是遇到“数一数图里有多少个XX”的需求第一反应就是拿MATLAB快速验证一把。图像计数这件事听起来不起眼但应用场景比很多人想象的要广得多。质检线上数零件、工业料仓里统计颗粒数量、生物实验里数细胞和菌落、农业遥感里数树木和作物本质上都是同一个问题在一张二维图像里把“感兴趣的目标”和“背景”分开然后统计目标的个数。MATLAB在这条路上的优势很明显图像处理工具箱Image Processing Toolbox把大量算法封装得比较完善不需要自己从零去写滤波、形态学、连通域这些底层实现可以快速搭建一套能跑的流程。这篇文章我会把整个流程拆开揉碎从最朴素的连通域计数讲起再到粘连目标的分水岭处理最后把边界截断、噪点干扰、光照不均这些实际会遇到的问题整理成排查思路。不管你之前有没有contact过MATLAB只要想解决“图像里数个数”这种问题都可以直接照着操作再结合你手上实际图片微调参数。1. 先盘思路图像计数到底在数什么1.1 一个朴素的流程灰度化 → 二值化 → 区分数图像计数不是一个孤立操作它背后是一条明确的处理链。我用最简单的话来梳理任何计数的前提是让“目标”和“背景”在计算机眼里变得泾渭分明。假如原始图像是彩色的、背景比较杂乱第一步就要灰度化丢掉无关的颜色信息然后通过某种规则最常用的是阈值将像素分成前景和背景两类这就是二值化二值化之后图像变成黑白的目标区域连成一个个“块”这时候再使用连通域分析数出这些块的数量。为什么要强调这个流程因为很多人在一开始就犯方向性错误——直接拿彩色图像的边缘检测结果来数数结果边缘断裂、重叠数量完全没法看。正确的做法是先降维把信息浓缩成“前景/背景”两个状态再考虑分割和计数。整个过程就是不断给图像做减法过滤掉与“数量”无关的信息。1.2 为什么MATLAB适合干这件事我始终觉得工具的选型要结合任务目的。如果是在工业环境里部署到生产线上实时计数我肯定会优先考虑嵌入式方案、OpenCV配合C或者直接用集成好的视觉系统。但如果目标是快速验证、算法调研、离线数据分析MATLAB的性价比就很高。具体来说有三个原因。第一函数接口封装度高。imread读图、rgb2gray灰度化、imbinarize二值化、bwlabel标记连通域、regionprops计算区域属性这一套下来新手也能在一个小时内跑通基础流程。第二可视化调试方便。MATLAB的图片变量可以直接用imshow查看中间结果每一步都能直观看到“这步处理之后图像变成什么样了”排查问题时非常高效。第三矩阵思维和图像天然匹配。图像在MATLAB的底层就是数值矩阵做像素级计算、加减阈值、逻辑掩码操作都非常直接。1.3 需要先做减法想清楚哪些物体要数这一条算是我反复踩坑后的真实体会。在写第一行代码之前先问自己三个问题图像里的目标是什么颜色、什么形状大概多大背景是什么样子是均匀的还是渐变的、有没有纹理目标之间会不会粘连粘连严重到什么程度这些问题的答案直接决定后续阈值怎么选、形态学结构元素用多大、要不要上分水岭。比如数红色圆形药片可以用RGB通道分离加圆形度过滤数灰色零件在均匀光照下可能用固定阈值就够了数细胞这类半透明、互相紧贴的物体则几乎必然要面对粘连分割的问题。先把这些问题想清楚比闷头调参重要得多。2. 核心细节解析每一步在做什么、为什么这么做2.1 灰度化与滤波第一步不处理好后面全部白费计数流程的起点通常是灰度化。MATLAB里一句grayImg rgb2gray(originalImg)就能完成原理是提取R、G、B三个通道的加权和得到单通道亮度图。不过要注意如果目标颜色在灰度图上与背景亮度接近直接灰度化会导致前景背景难以区分。举个实际例子数背景中的深蓝色零件如果直接用rgb2gray灰度图上零件和背景的亮度差异可能不大二值化后根本无法完整分割。这时候如果目标颜色鲜明红、绿、蓝等可以考虑直接提取对应颜色通道或者做通道差分比如红色目标拿R通道减去G通道目标一下就亮了。这一步的处理完全取决于图像内容没有万能公式。接下来是滤波。图像传感器拍出来的照片往往含有噪点而噪点会在二值化后变成细微的白色碎点严重影响计数精度。因此我在预处理阶段通常先做一次平滑滤波最常见的是高斯滤波imgaussfilt(grayImg, sigma)。sigma的值一般取1到2取太小去噪效果不明显取太大会让目标边界模糊、粘连加剧。我常用的做法是先调sigma2看一眼效果如果目标边缘细节很重要就降到1.5左右。滤波不是越强越好它的目的是在保留目标结构的前提下去掉尽可能多的噪声。2.2 二值化阈值的三条路线固定、Otsu、自适应二值化是决定计数成败的关键一环。它的本质是给每个像素一个判定规则亮度大于阈值的设为前景否则设为背景。阈值怎么选通常有三个思路。固定阈值最简单代码就是bw grayImg 120。这个数需要人为判断适合光照非常稳定、目标和背景亮度对比强烈的场景比如强光下拍摄的深色零件。Otsu大津法是自动阈值的主流方案MATLAB里用level graythresh(grayImg); bw imbinalize(grayImg, level);一句话搞定。它的原理是遍历所有可能的阈值找到一个让前景和背景两类像素“类间方差最大”的分界点本质上是自动寻找一个最优分割点使得两类之间差异最大、每类内部差异最小。对于绝大多数图像Otsu比手动阈值靠谱我日常90%的场景都先跑一遍Otsu看效果。自适应阈值则解决了光照不均的问题。当图像一侧亮一侧暗、同一背景在不同位置的灰度差异超过目标与背景的差异时全局阈值必然失效。核心思路是使用imbothat或imtophat做形态学顶帽/底帽变换来消除背景光照变化的影响。顶帽变换用大尺寸结构元素估计背景然后用原图减掉这个背景估计值得到“去除背景趋势”的图片之后再跑Otsu就很干净了。方法代码示例适用场景注意事项固定阈值bw grayscale T光照恒定、对比度高T依赖人工经验Otsu自动阈值level graythresh(im); bw imbinarize(im, level);光照均匀、双峰明显光照不均时失效顶帽/底帽Otsuim imtophat(im, strel(disk, 30)); bw imbinarize(im, graythresh(im));光照不均、背景渐变结构元素尺寸需大于目标直径2.3 形态学操作让计数偏差从“乱七八糟”变成“基本可控”二值化之后图像通常是黑底白目标但常常带着毛刺、空洞、小碎块。如果直接拿这个二值图做连通域计数会得到大量虚假目标。这时候就该形态学上场了。膨胀imdilate让白色区域向外扩张用来连接断裂的区域腐蚀imerode让白色区域向内收缩用来去除细小突起和碎点。开运算先腐蚀再膨胀能有效去除微小噪点同时保持大目标的整体形状和面积不变闭运算先膨胀再腐蚀能填补目标内部的小空洞让目标块更完整、更连贯。具体到计数场景我通常会组合使用先bw imopen(bw, strel(disk, r))扫掉小噪点再bw imclose(bw, strel(disk, r))填补空洞。这里结构元素用圆盘形disk而不是矩形rectangle原因是圆盘更接近大多数自然目标的形状不容易让边界产生棱角。r的选择取决于噪点尺寸与目标直径的比例。如果目标最小直径是20像素噪点直径是2像素那我用r3或者r5的开运算既不会损伤真实目标又能清掉绝大部分噪点。形态学操作还有个额外的好处对轻微粘连的目标可以通过一次腐蚀让它们分开计数后再通过膨胀把面积还原回来。不过在腐蚀的时候要小心过度腐蚀会把小目标直接消掉反而少计数。2.4 连通域分析数数量、量尺寸、做筛选形态学做完之后的二值图已经比较干净了此时可以进入真正的“数数”环节连通域分析。MATLAB里用cc bwconncomp(bw, 8)来获得连通域信息它返回一个结构体里面包含每个连通域的总像素数和像素索引列表。如果想进一步拿到每个区域的面积、质心、外接矩形等属性就交给stats regionprops(table, bw, Area, Centroid, BoundingBox)。这里有个细节必须强调bwconncomp的第二个参数选择4连通还是8连通会直接影响计数结果。4连通表示只有上下左右四个方向相邻的像素才算同一个区域8连通则把对角线上的像素也算作相邻。对于颗粒、菌落这类目标我通常用8连通因为实际物体边缘往往是对角接触的8连通能更接近人的视觉判断。但如果目标和目标之间挨得非常近、甚至中间只有对角线相接8连通容易把它们连成一块这时换成4连通反而更准确。area属性特别有用可以用来做面积筛选。比如噪点清除后仍然剩了一些比真实目标小很多的碎块我可以直接写一条规则validIdx stats.Area 500把面积小于500像素的统统不算数。类似地如果目标都近似圆形我可以用stats.Eccentricity离心率过滤掉异常细长的假目标如果目标是特定颜色我还可以结合原始彩色图提取连通域对应位置的RGB均值来判断颜色是否符合预期。这些属性组合起来相当于给计数逻辑加了一层或多层过滤器让结果稳定可靠得多。3. 从零到一MATLAB计数实操全流程3.1 准备测试图像与实验环境实际操作之前先把实验环境备好。我建议使用较新版本的MATLAB内置的图像处理工具箱一般都比较完整。如果你的环境里没有Image Processing Toolbox用ver命令可以直接查出来缺工具箱的话重新安装或者补装对应的许可证就行不然imread、bwlabel这些函数都会报错。测试图像怎么准备也有讲究。第一张测试图我的建议是目标数量适中比如20到100个之间、目标明显区别于背景、目标之间尽量分离、光照较均匀。先用这样一张“理想图像”把流程跑通再去处理那些“真实世界”里的复杂图像。整个过程会省心很多。3.2 基础版连通域计数适用于目标分开、背景干净的情况下面是基础版计数代码对多数分离目标图像有效。这段代码我按照“读图 → 灰度 → 二值化 → 形态学清理 → 连通域分析 → 可视化标注”的顺序组织每一步都有注释调试时可以插入imshow查看中间结果。% 读取图像 origImg imread(test_image.png); figure; imshow(origImg); title(原始图像); % 灰度化 if size(origImg, 3) 3 grayImg rgb2gray(origImg); else grayImg origImg; end % 高斯平滑去噪 grayImg imgaussfilt(grayImg, 1.5); % Otsu自动阈值二值化 level graythresh(grayImg); bw imbinarize(grayImg, level); figure; imshow(bw); title(Otsu二值化结果); % 形态学清理开运算去噪声闭运算填空洞 bw imopen(bw, strel(disk, 3)); bw imclose(bw, strel(disk, 3)); % 面积筛选去掉太小的碎片 cc bwconncomp(bw, 8); stats regionprops(table, bw, Area, Centroid); minArea 20; filteredIdx find(stats.Area minArea); % 计数 count length(filteredIdx); fprintf(检测到目标数量: %d\n, count); % 标注结果 imshow(origImg); hold on; centroids stats.Centroid(filteredIdx, :); plot(centroids(:, 1), centroids(:, 2), ro, MarkerSize, 8); hold off;这个基本流程对应80%以上“目标清晰、互不粘连”的场景。写完之后重点验证两件事一是面积阈值minArea合不合理二是标注点是否准确落在每个目标中心。如果图上明显有目标没被标记多半是二值化把目标和背景融合了如果有多个红点落在一个目标上说明可能存在过分割。3.3 处理粘连目标分水岭分割怎么用现实场景里目标往往紧贴着细胞成群、零件堆叠、硬币挨着硬币。直接做连通域分析这些目标会被合并成一个连通区域计数自然就不准。这时候需要先把“粘在一起的一堆”分成“一个个单独的个体”最常用的算法是分水岭分割watershed。分水岭的直觉理解是把梯度图想象成地形图目标中心是盆地目标边界是山脊然后从盆地底部慢慢注水水漫到山脊时不同盆地的水就会相遇相遇的地方形成分水岭线这条线就是目标之间的分割边界。具体到MATLAB实现代码逻辑是这样% 输入经过预处理的二值图 bw目标是分出一个一个的独立物体 % 第一步计算距离变换每个前景点到最近背景点的距离 D bwdist(~bw); % 第二步对距离变换图取负让物体中心成为“盆地” D imcomplement(D); % 第三步通过强制最小值来避免过分割 D imhmin(D, 2); % 第四步执行分水岭 L watershed(D); % 第五步把分水岭线之外的区域标记为0从二值图中去掉 bwSeg bw; bwSeg(L 0) 0; % 第六步对分割后的二值图重新做连通域计数 ccSeg bwconncomp(bwSeg, 8); statsSeg regionprops(table, bwSeg, Area, Centroid); countSeg height(statsSeg);这里面要特别注意imhmin这个函数。分水岭算法最著名的坑是“过分割”因为图像里有大量局部极小值算法会在很多无关紧要的小凹陷处也建立分水岭导致一个目标被切成好几块。imhmin通过压低那些深度不足的极小值把它们“抹平”从而控制分割粒度。参数2表示只有深度超过2的局部极小值才会被保留成分水岭起点。这个值越大分割出来的区域越少反之越多。实际操作中我会从1开始试着调到3左右盯住粘连最紧的那一簇目标看效果。分水岭是这一整条任务链里最需要调试的部分没有一个参数是万能解。你面对的图像的粘连程度、目标大小差异都不同每次都要结合可视化结果换参数。3.4 参数调节与可视化验证边调边看结果图像处理很少一次性成功中间产物可视化几乎是必需操作。我在调试时习惯建一个“图像管线”视图也就是在一个figure里用subplot把各阶段结果都摆出来原始图、灰度图、二值图、形态学结果、连通域标注图、分水岭分割图。好处是能快速判断问题出在哪个环节。举一个实际案例之前处理一批药片计数二值化之后总有几个药片被吞掉边缘。从subplot里看到二值图发现药片外圈是白色糖衣、中间是深色药物灰度化后中间部分和背景亮度相近Otsu把中间区域判成了背景导致药片变成“空心的圆环”面积属性异常。解决办法是改用闭运算补洞或者采用imfill填充连通域内部空洞。没有可视化这类问题很难靠直觉定位。给一个新手的提示每次修改参数后都要同时看“数量”和“标注图”不要只看一个数字。数量对不代表标注位置对过分割和漏分割有可能恰好把数量补到一致但这种“巧合”换一张图就崩了。以标注图为准结合具体目标形状来判断参数是否合适这才是可靠的验证方式。4. 常见问题与排查技巧实录4.1 边界截断的物体要不要数如果目标紧贴图像边缘连通域分析时这部分目标会被生硬截断面积比真实目标小很容易被“面积过低”规则过滤掉或者被算成一个异常目标。我的处理习惯是如果是统计“总量”比如在视野内的菌落总数那么边缘截断的菌落也应该算进去因为它们是真实的个体如果是识别“完整目标”——比如质检中只关心完好的药片——那么边缘截断的目标就不参与计数。实现上可以通过比较连通域的边界位置和图像边界来判断。stats.BoundingBox返回每个连通域的左上角坐标和宽高如果左上角x坐标接近1或者x宽度接近图像宽度说明它接触了左右边界y方向同理。在筛选阶段把这些边界接触目标单独分类就行。如果要保留它们就放宽面积阈值的判断——对接触边界的连通域不应用面积下限或使用一个更低的面积下限如果不想保留直接过滤掉。4.2 粘连成簇的目标分割过度或分割不足分水岭的实际表现经常会走两个极端。一是过分割目标内部存在纹理、凹坑距离变换图上有很多局部极小值分水岭就把一个真实目标切成了几块。二是欠分割粘连太紧密、目标之间几乎没有灰度梯度分水岭找不到山脊线一簇目标仍然被当成一个整体。对付过分割优先调整imhmin的参数把抑制深度调大一些减少无效分割。同时检查预处理如果图像本身噪声偏高建议先增强高斯滤波强度。对付欠分割可以试试给二值图做一次距离变换前的小幅度腐蚀把目标收缩让目标之间的“缝隙”变得更明显然后分水岭再膨胀回来。另外如果目标之间黏连处有色彩差异可以考虑在RGB某一个通道上做分水岭有时能比灰度图分割得更准。4.3 噪点被误判成目标图像里的微小亮点、传感器噪声、JPEG压缩块经二值化后经常变成独立的小连通域。面积过滤是个有效办法但需要先知道目标的大概尺寸范围否则容易误伤小目标。更稳妥的思路是先用形态学开运算统一清理一次。开运算有一个特性对于结构元素无法容纳的细小目标会被直接消除而大目标基本不受影响。如果经过开运算后仍然有碎碎的小区域再用bwareaopen(bw, minSize)精确过滤掉像素数少于minSize的连通域。这个函数不需要先bwconncomp直接传入二值图和最小面积就返回清理后的结果代码上更省事。minSize怎么定先在图上框出一个真实目标用regionprops看它的面积然后取该面积的1/2到1/3作为阈值。4.4 光照不均导致阈值失效光照不均是工业落地场景里最普遍的干扰因素。如果图像左上角亮、右下角暗Otsu出来的全局阈值往往在亮区把背景也带成了前景过分割在暗区把目标淹没在背景里漏分割。此时最有效的工具是形态学顶帽变换。顶帽变换的实现思路是先用一个较大的结构元素做开运算估计出“背景亮度面”再用原图减去这个背景面把光照变化带来的缓慢灰度漂移消除掉。注意结构元素尺寸必须大于目标的最大直径否则会把目标本身也纳入背景估计导致目标消失。例如目标直径约20像素背景光照在大约200像素尺度上变化就可以用半径50~80像素的圆盘做顶帽效果通常不错。跑完顶帽再跑Otsu整体分割稳定性会好很多。5. 扩展应用与个人经验5.1 用颜色、形状属性做多层筛选连通域的面积和数量只是基础真实业务往往需要更精细的筛选。例如在同一幅图里数出“红色零件有几个蓝色零件有几个”基础二值化把两种颜色都当成前景连通域数得到的是总数。这时候可以在二值化的基础上进一步利用每个连通域所对应的原始彩色像素均值来判断颜色类别。代码思路是先用labelmatrix得到一个标签矩阵然后遍历每个标签号提取坐标对应的RGB值计算平均色相再根据预设的颜色范围分类。形状属性也能帮助筛选。regionprops提供的Eccentricity离心率接近0表示近似圆形接近1表示细长条Solidity凸度比反映目标轮廓是否规整MinorAxisLength和MajorAxisLength能给出目标长短轴。比如筛选“完整圆形药片”时可以要求面积在一定范围内、离心率低于0.3、凸度比大于0.9。这些规则相互组合即便省去复杂的机器学习分类也能在工程场景下实现较高准确率。5.2 批量处理多张图像的技巧如果一次要处理几十张图片手动一张一张点开看会累死人。我建议先单独调好一张图的参数然后写一个循环把所有文件读进来统一跑。一个大坑是不同图片的光照、目标大小可能有差异直接套用同一套阈值到所有图上很容易翻车。如果图像间差异很大就不能用固定阈值最好每张图都动态计算Otsu阈值或自适应阈值。还可以用dir遍历文件夹、fullfile拼接路径统一读取。处理完成后把每张图的计数结果保存在表格里最后用writetable导出Excel或CSV方便后续统计和做记录。还有一个从实际项目里得到的体会批量处理时最好在每张图处理完之后自动保存一份“标注过”的结果图文件名沿用原图名加后缀_result。这样如果批次处理完毕发现哪张图数量异常可以马上打开标注图定位问题原因而不是重新跑一遍又只看到一个孤立数字。5.3 几个值得记住的调参经验第一结构元素尺寸优先参考目标直径。无论是开运算、闭运算还是顶帽变换结构元素的尺寸都应该根据“目标多大”来定。如果目标是20像素的圆开运算用半径30的圆盘就会出问题目标会被当成噪声清掉。设计一套管线时先从图像里量一下目标像素尺寸所有参数都围绕这个基础量来估计。第二二值化结果差不要急着调二值化参数先回头检查预处理。很多分割问题其实是光照不均和噪声引起的预处理多做一步二值化就轻松很多。我一直把预处理当成两个基本任务打光顶帽/底帽矫正背景和除尘高斯滤波开运算。第三计数结果要设置人工复核机制。可以抽取10%的图片人工核对如果误差超过预期立刻停下来检查流程不要继续跑批量任务。拿我自己来说通常设置5%以内的误差才算可用超过这个范围肯定有某个环节的参数不适合这批数据。写在最后一次实践换来的几点体会说到底MATLAB图像计数的核心不是在“MATLAB”这三个字上而是对图像处理链路每一步的掌握和参数的因地制宜。灰度化、二值化、形态学、连通域分析、分水岭分割这套组合拳掌握之后你会发现迁移到OpenCV或者Python的scikit-image也只是一层语法差异底层逻辑是一样的。从个人经验来说有几个小习惯确实能提升效率一是每次调试都保留中间图像的可视化输出随手截图存下来对比二是参数尽量用变量名集中管理而不是散落在代码各处三是计数之后一定要做标注可视化看到标记位置与目标实际的对应关系才敢信任那串数字。图像处理这东西一眼看结果是很快的但真正让自己放心还得靠对每一步含义的理解。希望这篇实操整理能帮你少走一点弯路。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询