简介面向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.765角与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做了个界面用户点选图片、点击运行、显示结果整个过程不用碰命令行。如果你也想做类似的功能建议先把核心算法跑通再考虑界面顺序不要反。本文还有配套的精品资源点击获取