十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于形态学图像处理的Matlab硬币计数实现

基于形态学图像处理的Matlab硬币计数实现 简介本资源是一套面向本科及硕士阶段图像处理教学与实验的硬币计数实践方案聚焦【图像检测-计数】任务基于MATLAB形态学图像处理技术实现自动硬币识别与数量统计适用于数字图像处理、计算机视觉入门课程设计与课程实验。压缩包共4个文件386KB含2幅实测硬币图像JPG、核心处理脚本test.m含完整预处理、二值化、去噪、连通域标记与计数逻辑及配套说明文档DOC内容覆盖从原始图像输入到最终计数结果输出的全流程实现细节与关键参数分析。已有495人学习下载文档中明确标注运行环境为MATLAB 2019a并附带实际运行结果截图便于初学者快速验证效果、理解形态学操作如开闭运算、腐蚀膨胀在目标分离与轮廓精修中的作用是掌握基础图像分析工程化落地的典型小而精案例。 先讲个真实经历我最早做这个Matlab硬币计数项目时第一版用的是Hough圆检测结果拿到实拍硬币图直接翻车——边缘阴影有一圈灰光照稍微不均匀就多检出几十个假圆。后来把方案改成形态学图像处理总共用了几行腐蚀膨胀反而稳得离谱。这篇就聊聊完整的形态学硬币计数方案和源码适合正在做图像处理大作业、或想用传统视觉方法解决计数问题的朋友参考。1. 为什么硬币计数不用Hough圆检测要绕一圈用形态学1.1 Hough圆检测的坑参数太多、噪声敏感很多新手拿到“硬币计数”这个题目第一反应是“硬币是圆的那就用Hough圆检测”。这个思路听着没毛病但真跑到实拍图上Hough圆的稳定性会让你怀疑人生。Hough圆检测的敏感点太多了。先是参数半径范围要预判、圆心间最小距离要设、边缘梯度阈值要调五个六个参数堆在一起出一版效果全看运气。其次Hough变换的核心是“边缘投票”它极度依赖边缘检测的质量。硬币放在桌上的实拍图边缘阴影、反光带、桌面的纹理杂色都会参与投票最终结果往往是在真实硬币之外多出一堆假圆或者在硬币本该出现的位置漏检。我印象最深的一次测试图里一个五角硬币反光特别厉害硬币边缘有一小段的梯度值直接断掉Hough算法硬是把这枚硬币拆成了两个半径几乎一样的圆。这种“半圆残影”问题在Hough里非常难根治因为它本质上是在做全局几何搜索局部边缘一断搜索就乱了。1.2 形态学方案的整体思路与适用边界形态学走的是另一条路不考虑“它是不是圆”只考虑“它是一块连通的、面积足够大的区域”。只要硬币区域能在二值图里被完整抠出来剩下的计数就是连通域统计问题完全不依赖边缘的完整性。这里要明确一下形态学的适用边界。形态学方案最适合的场景是硬币和背景的灰度差异足够明显硬币之间没有大面积重叠硬币形状接近凸多边形或圆形。这个条件在绝大多数桌面拍摄场景下都满足甚至扫描仪、摄像头俯拍的效果更好。相比之下Hough对反光、遮挡的表现就更脆弱一些。所以整个项目的处理链路是灰度化 → 滤波去噪 → 自适应阈值分割 → 形态学开闭运算 → 孔洞填充 → 连通域分析 → 面积过滤 → 计数标定。没有一步是玄学每一步都能解释清楚“为什么这么做”。2. 预处理怎么把硬币从真实背景里干净地抠出来2.1 光照不均时固定阈值为什么不行硬币计数最怕的不是硬币本身复杂而是背景乱、光照乱。我记得有次测试图是在台灯下面拍的画面左上角明显比右下角亮同一枚硬币在亮区和暗区的灰度值差了四五十。这种情况下直接用graythresh求大津阈值做全局二值化暗区的硬币边缘会被直接切断亮区的地面纹理又会混入前景。为什么大津阈值Otsu的核心逻辑是找一个全局灰度阈值让前景和背景的类间方差最大。它假设整张图有“一个明确的前景、一个明确的背景”光照不均直接打破这个假设。所以遇到光照有渐变的图固定阈值就是不可靠的必须换成自适应阈值。2.2 自适应阈值和灰度增强的Matlab写法Matlab里自适应阈值分两种方式一种是imbinarize自带的 adaptive 模式基于局部均值或者高斯加权均值动态计算阈值另一种是自己用strel、roifilt2之类实现滑窗阈值。前者更适合日常工程一句话就能用出来。I imread(coins.png); if size(I, 3) 3 gray rgb2gray(I); else gray I; end % 高斯滤波: 平滑反光噪声, 防止后续阈值分割出现碎点 gray imgaussfilt(gray, 2); % 自适应阈值, Sensitivity 越大, 越容易把暗部判为前景 bw imbinarize(gray, adaptive, Sensitivity, 0.4);Sensitivity这个参数值得多说一句。它的取值范围是0到1数值越大二值化时前景判定的门槛越低。如果发现低对比度的地方被漏检就往大调如果发现背景纹理被大片误判成前景就往小调。我个人的习惯是先设为0.4再根据结果上下浮动0.05比一上来就拉满要稳。2.3 亮背景还是暗背景二值化要不要取反这是一个看起来小、实际上能卡住很多人的问题。imbinarize默认把“亮像素”作为前景输出1也就是白底图上黑色的硬币会被变成黑色背景、白色非硬币区域。可我们后续的形态学分析、连通域统计全都是围绕“白色前景区域”来算的。Matlab自带的coins.png是亮背景暗硬币所以二值化结果需要取反bw ~bw;如果是暗背景下拍亮色硬币这步取反就不要做。判断标准很简单用imshow(bw)看一眼如果硬币是白的、背景是黑的就对了如果反过来加一行取反就行。这个判断在做任何实拍图时都要做一遍不要想当然。3. 形态学三件套腐蚀、膨胀、开运算和闭运算到底在干什么3.1 结构元素是形态学的“模板”形态学的所有操作都离不开一个概念结构元素Structuring Element简称strel。你可以把结构元素理解成一个小模板在图像上逐像素滑动。腐蚀和膨胀的结果完全取决于结构元素的形状和大小。硬币计数里我会用到两个结构元素se1 strel(disk, 4); % 小的圆形结构元素, 用于去噪 se2 strel(disk, 10); % 大的圆形结构元素, 用于补洞用disk是因为硬币本身是圆形的圆形结构元素在腐蚀或膨胀时能最大程度保留硬币的真实形状。如果用矩形结构元素开运算之后硬币边缘会产生明显的方块感面积统计误差也会变大。3.2 开运算去毛刺、闭运算补空洞的完整过程这里用不严谨但好理解的方式解释一下腐蚀操作结构元素在图像上滑动时只有当结构元素覆盖的像素全部为1时中心点才保留为1。效果是让前景区域缩小一圈细小的毛刺和孤立噪点会被直接消掉。膨胀操作结构元素覆盖的像素中只要有1中心点就变成1。效果是让前景区域扩大一圈内部的细小空洞和边缘的断裂会被填补。开运算 先腐蚀后膨胀。先腐蚀把小的噪声点消掉再用膨胀把硬币区域恢复到接近原来的大小。注意恢复不等于复原开运算会“磨掉”那些太细的突起这正是我们要的。闭运算 先膨胀后腐蚀。先膨胀把硬币内部的小洞和边缘缺口填上再腐蚀把尺寸拉回来。实际代码里完整的顺序是% 第一步: 开运算去小噪点 bw imopen(bw, se1); % 第二步: 闭运算填补硬币内部的暗孔 bw imclose(bw, se2); % 第三步: 把剩余孔洞填充掉 bw imfill(bw, holes);为什么顺序必须是“先开后闭”而不是反过来因为反光、水渍这类噪声往往比硬币边缘的缺口更细碎。如果先做闭运算细小的噪声点会被膨胀放大形成一片粘在硬币边缘的毛刺之后开运算很可能无法完全清理边缘就变得脏兮兮的。先开运算把噪声清理掉再做闭运算补洞效果更干净。3.3 结构元素尺寸怎么定一条估算经验很多教程只告诉你“用strel(disk, 5)”但不解释为什么是5。这在换一张图的时候就抓瞎了。结构元素尺寸应该根据硬币在图像里占据的直径比例来反推。我平时的经验公式是观察硬币直径大约为 D 像素开运算去噪的结构元素半径取 D/30 到 D/50闭运算补洞的结构元素半径取 D/15 到 D/25。举个例子在coins.png里单枚硬币直径大约180像素开运算用4到6闭运算用8到12效果都还行。如果你把图像缩放到了1000像素宽硬币直径变成300像素结构元素还停留在5噪声就压不住了。这一步很多人忽略最后形态学效果差根源不在算法在参数没跟着图像尺度走。4. 从二值图到“有几枚硬币”连通域统计与面积过滤4.1 bwconncomp 和 regionprops 的使用逻辑形态学处理完成后的二值图理论上只剩下一块块白色区域每块对应一枚硬币。接下来要做的是把这些区域“编号”然后统计每块的面积和质心。cc bwconncomp(bw); stats regionprops(cc, Area, Centroid); areas [stats.Area];bwconncomp返回连通域信息regionprops根据这些信息计算每个连通域的面积和质心。Area是区域包含的像素总数Centroid是区域的质心坐标。这里很容易踩一个坑stats是结构体数组直接stats.Area在MATLAB里会返回一个逗号分隔的列表赋值给数组时要用方括号包起来。我见过不少初学者在这里报错就是因为少了[...]。4.2 面积阈值过滤别把噪点当硬币形态学开运算能去除大部分与结构元素尺度相当的噪点但总有些顽固的区域残留下来比如硬币周围一圈环境光晕、纸片上的墨迹。这时候需要一个简单粗暴但有效的过滤方式面积阈值。minArea max(areas) * 0.3; validIdx find(areas minArea); numCoins length(validIdx);为什么用max(areas) * 0.3而不是一个固定值因为图像分辨率变了硬币的面积会成二次方关系变化。固定写1000在小尺寸图上可能是半个硬币面积在大尺寸图上可能只是噪点面积。相对阈值天然适应不同分辨率。我试验过多个样本取最大面积的30%作为下界在大多数场景都够用了。如果场景里存在和硬币差不多大的干扰物这种方法会误检——这种情况要加“形状判定”比如计算圆形度4 * pi * Area / Perimeter^2圆形度低于0.8的直接排除。4.3 坐标标注质心和圆心之间的关系把质心坐标画到原图上是很直观的验证手段。对于标准圆形硬币质心就是几何圆心可以直接用figure; imshow(I); hold on; centers zeros(numCoins, 2); for i 1:numCoins centers(i, :) stats(validIdx(i)).Centroid; end plot(centers(:, 1), centers(:, 2), r, MarkerSize, 8, LineWidth, 1.5);需要注意这个结论只对接近完整的圆成立。如果硬币在画面边缘被裁掉一块质心会偏离真正的圆心但计数不受影响只是标注位置会偏移。所以面积过滤后的区域如果发现质心几乎贴着图像边界优先怀疑是边缘残缺目标可以在后期做边缘裁剪判断。5. 完整可运行的Matlab源码与实测结果5.1 源码脚本可直接复制下面给出完整脚本包含前面所有步骤代码注释覆盖了参数调整时需要注意的点%% 基于形态学图像处理的硬币计数 % 依赖: Image Processing Toolbox % 测试: MATLAB R2019b 及以上 clear; close all; clc; %% 1. 图像读取与灰度化 I imread(coins.png); if size(I, 3) 3 gray rgb2gray(I); else gray I; end % 高斯滤波, Sigma2, 平滑反光与传感器噪声 gray imgaussfilt(gray, 2); %% 2. 阈值分割 % 自适应阈值, Sensitivity 可根据实际图片微调 bw imbinarize(gray, adaptive, Sensitivity, 0.4); % 当前测试图是亮背景暗硬币, 需取反; 若是暗背景亮硬币则删除这行 bw ~bw; %% 3. 形态学处理 se1 strel(disk, 4); % 开运算结构元素: 去小噪点 se2 strel(disk, 10); % 闭运算结构元素: 补硬币内部孔洞 bw imopen(bw, se1); % 去毛刺、孤立噪点 bw imclose(bw, se2); % 填内部暗孔、接边缘断口 bw imfill(bw, holes); % 剩余孔洞强制填充 %% 4. 连通域分析与过滤 cc bwconncomp(bw); stats regionprops(cc, Area, Centroid); areas [stats.Area]; if isempty(areas) error(未检测到任何区域, 请检查二值化方向或阈值参数); end % 相对面积阈值: 小于最大面积30%的区域视为噪声 minArea max(areas) * 0.3; validIdx find(areas minArea); numCoins length(validIdx); fprintf(检测到硬币数量: %d\n, numCoins); %% 5. 可视化: 原始图 轮廓 质心 figure(Name, 硬币计数结果, Color, w); subplot(1, 2, 1); imshow(bw); title(sprintf(形态学处理后二值图, 连通域: %d, cc.NumObjects)); subplot(1, 2, 2); imshow(I); hold on; % 绘制每个有效区域的轮廓 boundaries bwboundaries(bw, noholes); for k 1:length(boundaries) b boundaries{k}; plot(b(:, 2), b(:, 1), y, LineWidth, 1.2); end % 标记质心 centers zeros(numCoins, 2); for i 1:numCoins centers(i, :) stats(validIdx(i)).Centroid; end plot(centers(:, 1), centers(:, 2), r, MarkerSize, 10, LineWidth, 1.5); title(sprintf(检测到 %d 枚硬币, numCoins));5.2 运行环境与注意事项脚本依赖Image Processing Toolbox纯Matlab基础安装没有这个工具箱跑不了。自己在课堂或作业里使用之前先执行ver确认一下环境。imbinarize自适应阈值模式是R2016a才引入的如果你还在用R2015b需要替换成自己写滑窗均值或者在graythresh基础上做局部处理。我建议尽早换新版本R2019b以后对图像处理相关的性能优化明显尤其imgaussfilt在多核下快很多。5.3 实测效果与参数调节策略用Matlab自带的coins.png跑完整脚本输出是10枚硬币实测多次没有漏检和误检。但这张图是厂家精心准备的测试图背景单一、光照均匀只能用来验证流程。真正的考验在实拍图。我在桌上用手机俯拍了一组硬币光照有渐变、硬币表面有反光直接把代码套上去效果参差不齐。遇到这种情况优先检查三处第一二值化方向有没有搞反这是最隐蔽的错误。第二Sensitivity太高导致背景桌面纹理进入前景往低调到0.3左右再看。第三开运算结构元素太小噪点没有被完全去除放大到 D/30 以上。这三处调完大部分实拍图都能稳定出数。6. 翻车现场记录粘连、阴影、反光以及后续优化6.1 硬币粘连时的分水岭改进思路硬币完全分离是最理想的情况但实际拍摄时经常会有硬币挨着硬币边缘都贴在一起。这种图在连通域分析时会被当成一个整体计数直接少一枚。处理思路是分水岭算法配合距离变换% 距离变换: 每个前景像素到最近背景的距离 D -bwdist(~bw); D(~bw) -Inf; % 分水岭分割 L watershed(D);核心逻辑是把二值图变成“地形图”距离背景越远的像素海拔越高然后模拟往地形灌水不同的积水盆地之间会形成分水岭也就是硬币的分割线。这套方案对部分粘连有效特别是两枚硬币边缘刚好相切的情况。如果硬币重叠面积超过四分之一分水岭也很难切准这时候要么换角度重拍要么考虑用深度学习实例分割。6.2 光照和阴影导致的边缘断裂问题实拍图里另一个高频问题是硬币的边缘阴影。硬币是立体的边缘会形成一圈渐变阴影阈值分割时处理不好硬币边缘会出现一圈空洞二值图上的硬币看起来像被啃了几口。处理这种问题常规闭运算可能力度不够。我的做法是先用形态学顶帽变换做一次背景校正把光照渐变的影响压掉再做阈值分割。顶帽变换的原图减去开运算结果能提取出比周围背景更亮的细节底帽变换是闭运算结果减去原图能提取暗细节。光照不均的图先用imtophat或imbothat预处理再进imbinarize硬币边缘的断裂问题会缓解很多。se strel(disk, 30); grayCorrected imsubtract(imadd(gray, imtophat(gray, se)), imbothat(gray, se)); bw imbinarize(grayCorrected, adaptive, Sensitivity, 0.4);这段代码是“顶帽加底帽”的经典写法本质上是把亮背景和暗背景同时拉回到同一个灰度基准线附近。注意这里的结构元素半径要远大于硬币直径比如硬币直径200像素这里取30到50。6.3 不同面值混检把“计数”升级成“分类”计数项目做到后期很多人会问“能不能顺便把一元的、五角的、一角的分别数出来”。这个需求完全可以在现有形态学流程上扩展不需要换算法。原理很简单人民币硬币不同面值的直径不同在固定拍摄距离下对应二值图的像素面积也不同。一元的直径约25毫米五角的直径约20.5毫米面积差约1.5倍。在连通域统计阶段拿到每枚硬币的Area之后用kmeans对面积做两类或三类聚类就能自动区分面值。areasValid areas(validIdx); if numCoins 2 clusterIdx kmeans(areasValid, 2); for g 1:2 fprintf(第 %d 组硬币数量: %d, 平均面积: %.1f\n, ... g, sum(clusterIdx g), mean(areasValid(clusterIdx g))); end end这个思路应用在固定角度的俯拍场景下准确率很高。但要注意如果拍摄时手抖导致远近不一致面积聚类就会出错。那就要引入标定参照物比如在画面里放一枚已知直径的硬币根据它推算其他硬币的实际尺寸再把尺寸映射到面值。说实话形态学的计数方案做到这一步已经能把传统视觉方法在这类任务上的潜力榨得差不多了。它的优势在于轻量、可解释、不需要训练数据一枚普通CPU跑几百张图都没压力劣势在于对极端光照、严重遮挡、大幅度重叠这种情况确实捉襟见肘。我个人在实际操作中的体会是形态学方案最怕的不是参数多而是上来就盲目调参。每次调整都先问自己一句当前这一步到底在解决什么问题把流程理清了参数只是按需微调的事。最后再分享一个小技巧跑脚本前先imshow查看每一步的中间结果尤其二值化和形态学处理之后各存一张图出问题回溯时能省下大把时间。本文还有配套的精品资源点击获取
返回列表