☰
MATLAB硬币识别实战:从图像预处理到金额统计
2026/10/17 19:54:17 网站建设 项目流程

简介:面向MATLAB图像识别初学者的硬币识别示例代码包,以硬币数量和面值自动分类为切入点,演示图像识别的基本流程,适合课程设计、毕业设计以及入门计算机视觉的实践。压缩包共3个文件,主体是一个m脚本主程序,搭配一张用于测试的硬币图片和一个源码使用必读的url快捷方式,整体仅71KB,轻量易用。程序步骤完整,从读取与显示图像、灰度化、二值化、去噪、边缘检测,到区域属性分析与目标计数累计,均有对应实现,清晰展示各环节在MATLAB中的组织方式。开发者可参考代码结构进行修改,将识别逻辑迁移到其他圆形物体或形状检测场景,并通过链接中的使用说明快速了解运行方式。目前已有776人学习下载,资源虽小但含完整的端到端识别思路,适合快速掌握图像识别实践项目的基本框架。 上周有个朋友发我一张照片,桌上散落着一堆硬币,问我能不能写个程序自动数一下数量和金额。我用MATLAB写了个简单图像识别的程序,从读取图像到输出“共几枚、总共多少钱”,核心代码不到100行,日常光照条件下识别率能做到九成以上。这篇文章就把这个项目的完整思路、代码实现和我在实测中踩过的坑一并复盘,所有代码可以直接复制改参数去跑,适合刚接触图像处理、想用一个完整小项目练手的朋友参考。

这个项目本身不复杂,但它把图像识别里最典型的几个环节——预处理、目标检测、特征提取、分类决策——全串起来了。做完之后你会发现,之后再做任何“识别某类物体”的项目,套路都是相通的,这也是我想把它写透的原因。

1. 项目起点:硬币分类要解决什么

1.1 目标看似简单,内核是经典图像识别链路

标题里说的“硬币的数量和表示的数额进行分类”,翻译成技术语言就是两件事:第一,图像里到底有几个硬币目标;第二,每个硬币属于哪个面额类别。数量靠目标检测完成,面额靠特征分类完成,合起来就是一次标准的图像识别任务。

硬币识别有一个天然优势:硬币是圆形,且不同面额的物理直径有明显差异。这意味着我不需要引入深度学习模型,不需要标注几百张训练集,只用几何特征就能完成分类。第五套人民币硬币里,1角直径19毫米,5角直径20.5毫米,1元直径25毫米,三者直径存在可测的梯度,这在图像上直接表现为圆的半径或面积不同。只要能把硬币边缘的圆提取出来,分类就成功了一大半。

所以这个项目的技术栈非常轻:灰度化、滤波、圆检测、按半径分类。每一步都有对应MATLAB现成函数,几乎不用自己写算法。但“不用写算法”不代表“不用理解算法”,后面圆检测的参数坑了我一整个晚上,这个经验后面会细说。

1.2 为什么选MATLAB而不是OpenCV或Python

做这个需求之前我也犹豫过,用Python加OpenCV同样能实现,而且显得更“工业级”。但最终选了MATLAB,原因很实际:

第一,MATLAB图像处理工具箱和计算机视觉工具箱里,imfindcircles、viscircles、regionprops这类函数封装得非常成熟,我只需要关心参数,不用像OpenCV里那样手动组合模糊、边缘检测、霍夫变换、非极大值抑制等一系列操作,开发速度快很多。

第二,MATLAB的脚本式交互体验非常适合调试。我可以把中间结果一行一行打印出来看,灰度图、二值图、检测出的圆叠加图,随时可视化,这对理解算法在干什么非常有帮助。

第三,这个场景对性能没有要求,不是视频实时处理,也不是嵌入式部署,跑一张静态图片的耗时完全可以忽略。MATLAB在这个量级的任务上没有任何短板。

当然它也有缺点,比如最终编译成独立程序比较麻烦,License也不便宜。但如果你的目标就是快速验证一个图像识别思路,或者作为课程设计、毕业设计里的一个模块,MATLAB是性价比最高的选择。

2. 从图像到金额:整体技术路线怎么设计

2.1 预处理三步,先解决“看得清”

图像处理里有个基本共识:算法质量的上限由输入图像决定。预处理不是花架子,它直接决定后面圆检测的稳定度。我的预处理管线是这样设计的:

第一步,灰度化。硬币面额判断主要依赖形状尺寸,颜色信息在这个场景里是次要特征,而且光照会显著改变颜色表现,所以我直接把RGB图像转为灰度图,降低数据维度。

第二步,中值滤波去噪。拍摄过程中传感器会产生噪点,尤其是手机在光线较差时会更明显。中值滤波的优势在于去噪的同时能保留边缘,不会把硬币轮廓模糊掉,这一点比均值滤波更适合后续边缘检测。

第三步,根据背景情况决定是否做二值化。如果背景是纯色深色桌面,图像直方图会有明显的双峰,imbinarize一下就能干净地分离前景背景;如果背景是复杂纹理,二值化反而会把纹理一起变成目标,这种情况我会跳过二值化,直接让圆检测算法在灰度图上工作。

这里有一个容易犯的错:不要在拿到图像后立刻做全套预处理,先看中间结果。我习惯用subplot把原始图、灰度图、滤波图排在一起对比,确认每一步的输出没有引入新噪声再做下一步。机器不会替你做视觉判断,这一步省不掉。

2.2 特征选择:半径为什么比面积更可靠

硬币分类的特征,可以用半径,也可以用面积。很多人第一反应用regionprops提取连通域面积,因为代码写起来最简单。但实测下来,面积特征非常不稳定。

原因在于,面积通过二值化后的像素统计得到,而二值化的阈值设定稍有变化,边缘的几个像素就会被吃掉或冒出来,面积随之出现偏差。尤其是硬币边缘有倒角、反光时,二值化结果经常出现锯齿或孔洞,算出来的面积偏差可能达到5%以上。直径19毫米和20.5毫米的5角与1角,面积差异本来就只有约16%,这个误差足以导致误判。

而半径是从圆的几何拟合中得到的,即使边缘有几个像素的干扰,拟合出的半径也能保持在稳定区间。imfindcircles内部实现的是圆周梯度方向的累加投票,对局部噪声不敏感,这比单纯数像素可靠得多。所以我的方案是直接检测圆、拿半径,而不是先分割再算面积。

2.3 面额判别与金额汇总的逻辑

拿到每个硬币的半径后,需要把它们映射到面额。这里分成两种场景:

第一种,图像里有1元硬币作为参考。1元直径25毫米是最大的,所以把检测到的最大半径作为1元基准,其他硬币半径与它的比值用来判别。1角与1元的半径比是19比25,也就是0.76;5角与1元的半径比是20.5比25,也就是0.82。这两个比例差距不算大,所以阈值要设在两个比例的中间,比如大于0.79判为5角,否则判为1角。

第二种,图像里没有1元,只有5角和1角。这时只能用绝对像素半径,也就是需要做一次标定。方法很简单:用一把尺子放在硬币旁边拍一张照片,量出图像中1厘米对应多少像素。比如标定结果是每毫米2像素,那么1角直径对应38像素左右,5角对应41像素左右,直接按像素半径阈值分类。

我实际代码里用的是第一种方案,因为朋友拍给我的照片里大概率有1元硬币,不用额外标定。

3. 核心代码实现与关键参数解读

3.1 检测代码主体

主程序分成三段:读取与预处理、圆检测、分类汇总。先看前两段。

% 读取图像 img = imread('coins.jpg'); % 灰度化 gray = rgb2gray(img); % 中值滤波去噪,窗口大小根据图像分辨率调整 grayF = medfilt2(gray, [5 5]); % 圆检测:半径范围先给一个宽区间,后续再筛选 [centers, radii] = imfindcircles(grayF, [15 60], ... 'ObjectPolarity', 'bright', ... 'Sensitivity', 0.9, ... 'EdgeThreshold', 0.15);

这里imfindcircles的输入半径范围[15 60]需要根据实际图像里硬币的像素大小调整。我朋友这张图,硬币直径大概在50到70像素,所以半径范围设在15到60像素是安全的。

ObjectPolarity参数指定检测亮背景上的暗圆还是暗背景上的亮圆。硬币通常比桌面背景亮,所以用'bright'。如果拍摄时硬币放在白色纸面上,就要改成'dark',这个要根据实际图像明暗关系确认。

3.2 imfindcircles 参数怎么定

imfindcircles是我这次项目里最关键也最需要调的函数,三个参数值得单独说明。

Sensitivity控制检测灵敏度,取值范围0到1。数值越大,越容易检测出轮廓不明显的圆,但同时误检率也会上升。我实测下来,0.85到0.92是比较合理的区间。低于0.8会漏检边缘反光强烈的硬币,高于0.95会把杯底、瓶盖、桌面纹理误判成硬币。

EdgeThreshold控制边缘梯度阈值。数值越低,越能检测模糊的边缘,但也会引入更多噪声。默认是0.1,但我在侧光环境下会把值提到0.2左右,过滤掉阴影造成的虚假边缘。

还有一个更隐蔽的问题:imfindcircles的返回结果默认只返回最强的一个圆。要检测多个硬币,必须显式传入参数'Method', 'phasecode'或者使用length(radii)确认是否找到了全部目标。我一开始就吃过这个亏,以为函数返回所有圆,结果只检测到一个硬币。

3.3 分类和金额计算的实现

圆检测完成后,分类和金额汇总的逻辑如下:

% 以最大半径为1元基准 rMax = max(radii); ratio = radii / rMax; % 按半径比例分类 labels = cell(numel(radii), 1); for i = 1:numel(radii) if ratio(i) > 0.92 labels{i} = '1元'; elseif ratio(i) > 0.79 labels{i} = '5角'; else labels{i} = '1角'; end end % 统计数量和金额 totalAmount = 0; for i = 1:numel(labels) switch labels{i} case '1元' totalAmount = totalAmount + 1; case '5角' totalAmount = totalAmount + 0.5; case '1角' totalAmount = totalAmount + 0.1; end end fprintf('共检测到%d枚硬币,总金额:%.1f元\n', numel(centers), totalAmount); % 可视化:在原图上画圆并标记 imshow(img); hold on; viscircles(centers, radii, 'Color', 'r', 'LineWidth', 1.5);

阈值0.92和0.79不是随便拍的。1元与5角的半径比是25比20.5,约0.82,所以1元与5角的分类阈值放在两者中间偏上,我取0.92是为了防止5角边缘检测偏大导致误判成1元。5角与1角的半径比是20.5比19,约0.927,但这两个面额的半径差异非常接近,理论上很容易混淆。实际操作中我会额外加一个约束:如果比例在0.75到0.85之间,还要用亮度辅助判断。5角硬币是钢芯镀铜,颜色偏金黄,整体亮度低于1角,这个特征在灰度图上是有区分度的,可以看单个硬币区域的灰度均值做二次确认。

不过我得提醒一句,用亮度辅助判断的前提是光照均匀。如果硬币表面有强反光,这个特征就会失灵。我的经验是,宁可让程序输出“未知硬币,请人工确认”,也不要强行分类导致金额算错。

4. 实测翻车记录:问题与排查

4.1 反光与阴影导致的误检

第一次跑完整流程,我用手机在台灯下拍了一组硬币,结果检测出来的圆数量比实际多了三四个。把叠加可视化图打出来一看,误检的圆全都在硬币反光的高光边缘附近。

原因很好理解:硬币表面有弧度,侧光照射时产生的高光区域边缘,在灰度图上的梯度变化和真实硬币边缘非常相似,圆检测算法直接把这些高光当成了独立的圆目标。

解决办法有两个方向。第一个是拍摄端处理,尽量用散射光源或者环境光,避免直射强光,让硬币表面亮度均匀。第二个是算法端处理,在圆检测之前适当增大中值滤波窗口,或者加一层高斯模糊,把高频的高光边缘抹平。我用imgaussfilt(grayF, 2)后,误检数量立刻降到0。

4.2 背景干扰的解决方案

还有一次测试,朋友把硬币放在木纹桌面上拍,木纹里的圆形节疤被imfindcircles当成候选圆。检测结果里出现了两三个明显半径不对的目标。

这种问题靠调参很难根治,最好的方式是规范输入。我在代码里加了一个简单的前置判断:先算灰度图的直方图,如果背景纹理复杂导致方差过大,就提示用户换纯色背景重新拍摄。另外也可以用imcrop手动框选硬币所在区域,把检测范围限制在ROI内,背景干扰自然被排除。

实际产品里当然不能依赖用户手动裁剪,但作为一个小工具、一个学习项目,这个折中方案完全够用。

4.3 新旧硬币直径差异导致的误分类

这是个隐蔽的坑,我差点翻车。朋友那堆硬币里混了几枚旧版硬币,虽然都是1角,但直径和第五套人民币的1角不一样。旧版菊花1角直径22.5毫米,和新版5角硬币的20.5毫米非常接近,程序直接把它归到了5角,金额一下子差了四毛钱。

这个问题的本质是:硬币家族本身存在版本差异,同一面额在不同版别下直径不同。我的分类模型假设“最大半径=1元”,但如果图像里最大的是旧版1角,整个比例基准就错了。

对此我只做了一件事:在结果输出前加一个人工确认步骤。程序把分类结果和置信度打印出来,对于落在阈值边界附近的硬币,会提示“该硬币直径介于5角和1角之间,请人工确认”。在纯自动化的条件满足之前,加一道人工校验远比追求全自动更稳妥。这也是我在做这类小项目时的一个原则:先做半自动,跑稳定了再考虑全自动。

4.4 硬币重叠的边界与处理

重叠硬币是这类视觉识别项目里最头痛的问题。当两枚硬币叠在一起时,被遮挡部分的边缘不完整,imfindcircles检测圆的弧段不够,很容易漏检。

我实测过,重叠面积小于15%时算法还能勉强识别,超过这个比例基本就会漏。硬解的办法不是没有,用分水岭分割、椭圆的弧段拼接,理论上可以做到,但代码量和调参难度会指数级上升,对于这个项目来说性价比很低。

我的最终方案是:在程序开头加一行提示“请将硬币平铺分开,避免重叠”,同时检测结果里如果出现某个圆心与其他圆心距离小于两倍最小半径的情况,就把这个区域标记为“疑似重叠,请调整后重拍”。这是最务实的处理方式——用拍摄规范规避算法难题。

5. 项目还能怎么扩展

这个硬币识别项目做完之后,我试着往几个方向做了扩展,效果都还可以,也给你做个参考。

第一个扩展方向是加入颜色特征。新版5角硬币颜色偏金黄,1角和1元偏银白色,HSV空间的色相和饱和度可以作为补充特征,用来应对直径相近的情况。但前提是光源色温稳定,否则颜色偏移会干扰判断。

第二个方向是批量处理。把单张图片处理的代码包成一个函数,用dir遍历文件夹里所有图片,自动生成一个Excel表格记录每张图的硬币数量和金额。这样如果有一堆历史照片需要统计,几分钟就能跑完。

第三个是结合光学工具箱做畸变校正。手机广角镜头在近距离拍摄时会产生桶形畸变,导致画面边缘的硬币半径偏大。如果是正式项目,在预处理阶段用cameraParameters做一次畸变校正会显著提升边缘区域硬币的分类准确率。不过我实测下来,硬币在画面中心附近时畸变影响很小,不做校正在日常场景下也可以接受。

这个项目最终被我封装成了一个带UI的简单工具,用uifigure做了个界面,用户点选图片、点击运行、显示结果,整个过程不用碰命令行。如果你也想做类似的功能,建议先把核心算法跑通,再考虑界面,顺序不要反。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询