公司动态

形态学处理与连通域分析:基于Matlab的硬币计数方案

📅 2026/8/31 17:05:35
形态学处理与连通域分析:基于Matlab的硬币计数方案
简介本资源是一套面向本科及硕士阶段图像处理教学与实践的硬币计数实验方案基于MATLAB形态学图像处理技术实现自动计数适用于数字图像处理、计算机视觉等课程的算法验证与项目实训。压缩包共4个文件386KB包含2幅硬币原始与处理后图像JPG、核心算法脚本M文件及详细原理与步骤说明文档DOC覆盖从图像预处理、二值化、噪声抑制、连通域分析到最终计数的完整流程。已有495人学习下载配套运行结果截图与清晰注释代码便于初学者理解形态学开闭运算、面积滤波、标记连通区域等关键操作文档中还梳理了常见误检原因与参数调优建议可直接运行复现结果显著降低图像计数类实验的入门门槛。 开头部分先交代这个项目的来龙去脉别急着贴代码。上学期给数字图像处理课当助教碰到不少学生选“硬币计数”这个题目。说实话这类任务看着简单好像数个数就行但真上手做一遍就会发现坑不少光照不均匀、硬币互相粘连、背景里有干扰物、反光导致的孔洞……一张照片里硬币数量稍微多一点传统方法立刻露馅。我当时整理了一套基于形态学处理加连通域标记的方案顺手写成Matlab源码打包发给了学生后来在社区里也传开了不少朋友问能不能把思路讲透。这篇文章就把这套方案的完整思路、源码结构和参数调整经验一次性说清楚想交课程作业的、想练手图像处理的、甚至是准备面试算法岗的朋友都可以直接拿去参考。这套方案的核心思路其实只有四步预处理、二值化、形态学处理、连通域计数。但每一步展开都有讲究尤其是形态学那一步直接决定粘连硬币能不能分开、孔洞能不能补上。下面我从头拆解顺便把网上经常搜到但讲得含糊的概念比如膨胀、腐蚀、开运算、闭运算放到实际场景里面说清楚。1. 整体设计思路为什么偏偏选形态学1.1 硬币计数到底难在哪硬币计数的本质是图像中圆形目标的检测与统计。用边缘检测、霍夫变换也能做但我最终选择形态学路线是因为它更稳、更简单而且对硬件要求极低。先看硬币图像的几个典型问题光照不均匀硬币是金属材质表面有反光拍摄时不同位置的亮度差异很大。直接固定阈值分割要么把暗部硬币丢了一部分要么把亮部背景也划进来。硬币互相粘连随手把硬币铺在桌上难免有几枚挨在一起。如果只按连通域计数两枚贴着的硬币会被当成一个大区域数量直接错。背景干扰桌面纹理、纸张边缘、阴影都可能形成类圆形噪声干扰计数。硬币内部孔洞反光导致硬币内部高亮二值化后硬币中心变成白色空洞如果不处理一枚硬币可能被拆成多个区域。霍夫变换能检测圆形但它对参数敏感而且当硬币相互遮挡、边缘不完整时检测效果很差。边缘检测加形态学拟合的思路反而绕开了这个问题。1.2 形态学方案的出发点和优势形态学处理回答了一个问题能不能通过“形状”而不是“灰度”来理解图像。膨胀让白色的区域变大腐蚀让白色的区域变小组合起来就能实现断开粘连、填补孔洞的效果不需要依赖边缘完整性。具体到硬币计数形态学处理的收益非常明确开运算先腐蚀再膨胀把硬币之间细小的粘连断开同时去掉背景上的小噪点。闭运算先膨胀再腐蚀把硬币内部因为反光产生的孔洞补上保证每枚硬币都“实心”。连通域标记处理后每个白色区域对应一枚硬币数量就是区域数量。我从第一次实现到稳定运行大概花了两个晚上调参。主要精力都花在结构元素尺寸和阈值选择上形态学本身的逻辑只要想清楚“断开”和“填上”两个动作就够了。2. 形态学基础膨胀、腐蚀、开运算、闭运算到底做了什么网上关于形态学的资料很多但大多数都绕不开数学公式反而让人望而却步。我用最直白的方式解释一遍。想象二值图像是一张黑白方格纸黑色是背景白色是目标也就是硬币。形态学的所有操作都围绕一个“结构元素”展开它本质上是一个小模板比如3×3的正方形、5×5的圆盘等。2.1 膨胀与腐蚀膨胀拿着结构元素在图像上滑动只要结构元素覆盖的区域内有一个点是白色中心点就变成白色。效果是白色区域向外扩张一圈小孔被填住细缝被桥接。腐蚀拿着结构元素在图像上滑动只有结构元素覆盖的区域内所有点都是白色中心点才保持白色。效果是白色区域向内收缩一圈细小的白色噪点被消除细连接被切断。打个比方膨胀是“把白糖堆在纸片上受潮黏连成一坨”腐蚀是“拿小刀把糖块外面刮掉一圈看看里面还剩多少”。2.2 开运算与闭运算开运算和闭运算是上面两个操作的组合顺序不同效果也完全不同。开运算 先腐蚀再膨胀腐蚀把黏连的白色区域细脖子断开去掉小白点膨胀再把剩下的白色区域恢复原状。所以开运算的用途是断开误连接、去除小噪声。硬币边缘的细小毛刺经过开运算也能被抚平。闭运算 先膨胀再腐蚀膨胀把白色区域内部的孔洞填住把邻近白色区域桥接起来腐蚀再把扩张后的边界恢复到正常大小。所以闭运算的用途是填补孔洞、连接断裂。硬币反光产生的内部空洞闭运算可以直接处理掉。补充说明一点这里的“先腐蚀再膨胀”和“先膨胀再腐蚀”都不能交换顺序。先腐蚀后膨胀和先膨胀后腐蚀的结果差异很大原因很简单第一个操作改变了图像的形态第二个操作是在改变后的基础上继续处理。第一次用的时候我没细想交换顺序试了一次结果硬币边缘全部变成锯齿状直接劝退。这个细节不多说你在代码里验证一次就知道了。2.3 结构元素怎么选结构元素的形状和大小直接决定形态学处理效果常见的选项有形状适用场景square正方形通用处理速度快disk圆盘更贴合圆形目标边缘更平滑line线段处理条纹状目标硬币是圆形的理论上用disk最合适。但我实测发现当硬币数量不多、粘连不严重时square和disk的差别几乎可以忽略。真正影响结果的是结构元素的尺寸——也就是半径或边长。结构元素太小腐蚀不掉黏连区开运算无效。结构元素太大硬币本身的边缘也被腐蚀掉面积缩小甚至小硬币直接消失。一个简单的经验是结构元素半径取硬币直径的1/15到1/10需要根据粘连程度微调。这是整套方案里最需要人工介入的参数也是我给学生们强调过最多的地方。别指望一次调好多试几次就熟练了。3. 完整源码与核心实现步骤下面给出完整的Matlab实现。这段代码我针对硬币图像做了精简但仍然保留了所有关键环节。我尽量让代码结构清晰方便你直接跑通再修改。3.1 主流程代码%% 基于形态学的硬币计数系统 % 适用场景: 桌面/纸张上放置的硬币, 光照相对均匀, 硬币之间允许轻微粘连 % 作者: 博主整理发布 % 依赖工具箱: Image Processing Toolbox clc; clear; close all; %% 1. 读取图像 img imread(coins.jpg); figure; subplot(2,3,1); imshow(img); title(原始图像); %% 2. 灰度化 if size(img, 3) 3 gray rgb2gray(img); else gray img; end subplot(2,3,2); imshow(gray); title(灰度图像); %% 3. 二值化 % 使用Otsu全局阈值, 对光照不均的图像效果一般, 可改用自适应阈值 threshold graythresh(gray); bw imbinarize(gray, threshold); % 注意: 硬币是深色还是浅色, 取决于拍摄环境 % 如果硬币是亮色、背景是暗色, 需要取反 % 判断准则: 二值图中硬币目标应为白色(1), 背景为黑色(0) if mean(bw(:)) 0.5 bw ~bw; end subplot(2,3,3); imshow(bw); title(二值化图像); %% 4. 形态学处理 % 4.1 开运算: 断开粘连, 去除小噪点 se_open strel(disk, 5); bw_open imopen(bw, se_open); % 4.2 闭运算: 填补硬币内部孔洞 se_close strel(disk, 8); bw_close imclose(bw_open, se_close); % 4.3 清除与边界相连的目标(可选) % 如果硬币都在画面内部, 这步可以去掉 % bw_close imclearborder(bw_close); % 4.4 再次开运算: 清理闭运算可能产生的微小连接 se_open2 strel(disk, 3); bw_final imopen(bw_close, se_open2); subplot(2,3,4); imshow(bw_final); title(形态学处理后); %% 5. 连通域标记与计数 % 8邻域连通是最常用的选择 [labels, num] bwlabel(bw_final, 8); fprintf(检测到硬币数量: %d\n, num); %% 6. 面积过滤(去除残留噪声) stats regionprops(labels, Area, Centroid, BoundingBox); areaList [stats.Area]; % 过滤掉面积过小的区域(如灰尘、边缘碎片) % 阈值可根据图像分辨率调整, 经验值: 最大面积的1/20 minArea max(areaList) / 20; validIdx find(areaList minArea); finalNum length(validIdx); fprintf(过滤后硬币数量: %d\n, finalNum); %% 7. 可视化标注 subplot(2,3,5); imshow(img); hold on; for i 1:length(validIdx) idx validIdx(i); centroid stats(idx).Centroid; plot(centroid(1), centroid(2), r, MarkerSize, 10, LineWidth, 2); text(centroid(1)10, centroid(2), num2str(i), Color, yellow, FontSize, 12); end title(计数结果标注); hold off; %% 8. 输出每个硬币的参数 subplot(2,3,6); imshow(bw_final); title([最终二值图, 数量, num2str(finalNum)]); % 如果需要保存结果 % saveas(gcf, result.png);3.2 代码分步解读第1步到第3步是预处理流程。灰度化利用rgb2gray把三通道压缩到单通道减少计算量。二值化用graythresh自动计算Otsu阈值这一步对光照均匀的图像很好用但光照不均时建议改用adaptthresh做自适应阈值。我实测下来adaptthresh对阴影区域的适应性明显更好缺点是计算稍慢、参数需要微调。第4步形态学处理是核心。我用了一个开-闭-开的组合流程第一次开运算主要目的是断开硬币之间的轻微粘连。结构元素用半径5的圆盘。闭运算填补硬币内部反光产生的孔洞。结构元素用半径8的圆盘比开运算的稍大因为孔洞面积往往不小。第二次开运算闭运算在填补孔洞的同时可能把原本不相连的噪点也连进来或者把边缘搞得不圆润用小半径结构元素再开一次清理这些瑕疵。这个流程不是一次想出来的是我反复测试后的结果。第一次实验时我只做了开运算发现硬币内部孔洞没补上导致一枚硬币被拆成两三个区域。加上闭运算后孔洞问题解决了但边缘又出现了一些微弱连接。再加一次小半径开运算整个流程才稳定下来。第5步到第7步是计数和可视化。bwlabel按8邻域给每个白色区域打上编号之后用regionprops提取面积、质心、边界框。我加了一个面积过滤把小于最大面积1/20的区域直接剔除因为灰尘或桌面纹理碎片偶尔会形成小块白色区域。这个1/20的经验值不是随便定的我用不同硬币数量测试过小于这个比例的区域基本都是False Positive。3.3 为什么用bwlabel而不是bwconncomp很多教程用bwconncomp它返回的是结构体索引访问不够直观。bwlabel返回矩阵每个连通域一个编号配合regionprops非常顺手。两者原理一样但bwlabel的代码可读性更好适合新手理解。性能方面对于单张图片的离线处理两者的差异完全可以忽略。4. 参数调整技巧让方案适配不同场景固定参数的代码是跑不通所有图片的。我见过太多同学拿着同一套代码换一张图就翻车然后回头怀疑算法有问题。实际上问题绝大多数出在参数上。4.1 结构元素尺寸的调整逻辑结构元素的尺寸与硬币在图像中的像素直径直接相关。假设你的图像里硬币直径约占100像素开运算结构元素半径建议5到8像素。太小断不开粘连太大硬币会被“削”掉一圈。闭运算结构元素半径建议8到12像素。硬币内部孔洞的直径通常在10像素以上太小补不上。如果你不确定硬币直径可以在Matlab里直接量figure; imshow(img); [x, y] ginput(2); % 在硬币边缘点两个点 diameter sqrt((x(1)-x(2))^2 (y(1)-y(2))^2); fprintf(硬币直径约: %.1f 像素\n, diameter);用这个直径除以15到20就能得到初始的结构元素半径。再根据实际效果微调即可。4.2 二值化阈值的坑Otsu全局阈值有一个隐含假设目标和背景的灰度分布近似双峰。但硬币拍摄场景经常不满足这个假设尤其是当硬币数量多、相互遮挡产生投影时直方图可能变成多峰。遇到这种情况我推荐改用自适应阈值bw imbinarize(gray, adaptive, Sensitivity, 0.4);Sensitivity控制阈值对局部亮度变化的响应程度默认0.5暗背景下建议调低到0.3-0.4亮背景下可以调高到0.6左右。这个参数没有通用最优值和结构元素一样需要针对具体图片实验。一个小技巧在调试阶段把二值图直接显示出来用肉眼判断是“背景噪点多”还是“硬币有空洞”再决定调阈值还是调形态学参数。我见过不少初学者在阈值上使劲调结果越调越差原因是真正的问题在形态学环节。4.3 边缘不完整的硬币怎么处理如果硬币超出了图像边界bwlabel会把边缘外的部分也算成一个连通域但实际上这可能是一枚不完整的硬币。要不要计入取决于你的计数规则。如果目标是“画面内完整可见的硬币”用imclearborder删除与边界相连的区域即可。如果目标是“画面内出现过的所有硬币”保留这些边界区域但要在结果里注明。我在代码注释里加了imclearborder的调用默认注释掉你可以按需开启。5. 常见问题与排查技巧实录5.1 最容易翻车的几个问题下面是我整理的高频问题排查表每条都是真实案例现象可能原因排查方向计数偏多一枚硬币被拆成多块硬币内部孔洞未补上增大闭运算结构元素半径计数偏少两枚硬币被当成一枚硬币之间粘连未断开增大开运算结构元素半径背景噪点太多计数严重偏多阈值过低或结构元素太小调高阈值或增大开运算半径硬币边缘出现锯齿结构元素形状不合适把square改成disk小硬币消失开运算结构元素太大减小结构元素半径运行报错“Index exceeds array bounds”regionprops结果为空检查二值图是否全黑或全白二值图中目标和背景反了取反逻辑不对检查mean(bw(:))判断逻辑5.2 一个真实的调试案例有个学生拿来一张图桌面是深棕色的硬币是银色的。按理说目标和背景灰度差异很大很好分割。但他的二值图里一堆噪声计数结果从17到33乱跳。我让他把二值图直接显示出来一看就发现问题桌面纹路在灰度图上形成了一些细长的高亮区域经过闭运算后这些小纹理被连成一片直接当成了硬币。解决方案很简单在二值化之后、形态学处理之前先加一步面积过滤把小于硬币面积1/30的区域直接置0。这个操作看起来多余但对这种带纹理背景的图非常有效。% 预处理: 删除极小区域 bw bwareaopen(bw, round(max(areaList)/30));bwareaopen专门用来删除小于指定面积的连通域放这里正好。5.3 性能优化与批量处理处理单张图片整个流程耗时不到100毫秒。如果你要批量处理几十张图建议把imshow和figure都注释掉只保留计算逻辑速度能提升好几倍。批量处理时还可以加入并行计算Matlab的parfor很直接parfor i 1:numFiles img imread(fileList{i}); gray rgb2gray(img); % ... 中间步骤 counts(i) finalNum; end我第一次把批量处理的40张图跑完只用了不到10秒。当然前提是每张图的拍摄条件一致否则就需要针对每张图单独调参。6. 扩展思路与实际应用6.1 从“硬币计数”到通用圆形目标检测硬币计数其实是一个非常好的练手项目因为它的核心逻辑——预处理、二值化、形态学、连通域分析——可以无缝迁移到其他任务细胞计数生物实验中的显微图像细胞统计螺母/垫片分拣工厂流水线零件计数菌落计数培养皿里的细菌菌落统计气泡检测工业液体中的气泡数量与大小分析换场景时只需要调两个东西图像分割的策略和形态学结构元素的设计。整体流程完全不用变。6.2 如何应对更复杂的场景如果你的图片里硬币堆叠严重硬币边缘互相遮挡形态学方案就会失效。这时候需要升级到分水岭算法或基于深度学习的目标检测。分水岭算法处理粘连目标的核心思路是把图像看成地形图灰度值代表高度硬币内部的“山峰”和“山谷”自然形成分水岭边界。Matlab里用watershed函数即可实现。但分水岭对噪声敏感通常需要先用距离变换预处理。深度学习方案比如YOLO或Mask R-CNN精度更高但需要标注数据训练成本也高。学生作业或者小规模应用形态学方案完全够用。6.3 这套方案的价值在哪里从工程角度看形态学硬币计数的核心价值不在于“新”而在于“稳”。它不依赖昂贵的GPU、不需要训练神经网络、不需要标注数据集只要一台普通电脑和MATLAB就能跑通。对于资源有限的场景这种简单可靠的方案往往比“高大上”的深度学习更有实用价值。我在实际指导过程中发现能把这一套流程完全吃透的人再去看Hough变换、分水岭、连通域分析这些方法都会觉得轻松很多。形态学虽然是图像处理里的“老古董”但正是这种基础方法才能真正锻炼对图像本质的理解。6.4 给入门者的几个建议最后给刚接触这个项目的人几点我的实操体会。第一先跑通再调参。不要上来就想办法一次到位先把代码复制下来找一张硬币图片跑一遍看看输出什么结果再逐步改参数。没有基准结果谈优化没有意义。第二一定把每一步的中间结果打印出来看。我在代码里特意加了多个subplot显示中间结果就是为了方便调试。很多人跑完只看最终数量一旦结果不对就抓瞎不知道问题出在哪一步。第三你的图片如果和我的假设差别较大建议先用图像标注工具看下硬币的大致像素直径再反推结构元素尺寸。这一步看似简单能省下大量反复试错的时间。我记得有一次处理一张硬币密集堆叠的图硬币数量多达25枚很多硬币边缘都被遮挡。形态学方案愣是把粘连区域断开并且正确计数为23枚。那一次我意识到这种方法虽然朴素但在可控场景下其实相当可靠。希望这篇文章能帮你少走一些弯路把更多时间花在真正有意思的扩展上。本文还有配套的精品资源点击获取