☰
基于YOLOv12的硬币识别检测系统:从数据集构建到桌面应用部署实践
2026/10/22 14:33:49 网站建设 项目流程

清点整理一大堆零钱的时候,我突然冒出一个想法:能不能直接丢一张硬币照片进去,让程序自己告诉我这里面有几枚25美分、几枚10美分、总额是多少?这个念头后来逐渐落地成了一整套基于YOLOv12深度学习的美国硬币识别检测系统,包括Python项目源码、训练好的模型权重、带UI界面和登录注册功能的完整桌面应用。这篇文章就是我整个开发过程的真实记录,从数据集怎么凑、模型为什么选YOLOv12、训练时踩了哪些坑,到界面怎么集成、部署阶段如何应对反光和遮挡,一步一步说清楚。如果你也想用深度学习做一个实物识别项目,不满足于跑通官方Demo,而是想把它做成一个能给别人演示甚至试用的产品,那这篇应该能帮你少走不少弯路。

1. 数据关:硬币识别系统最难的不是模型,是数据

1.1 先摸清家底:美国硬币的面额体系与识别难点

做任何检测项目的第一步都不是急着训练,而是把检测对象彻底搞清楚。美国硬币常规流通的主要有六种面额:1美分、5美分、10美分、25美分、50美分和1美元。表面看起来就六类,但实际做起来会发现不少麻烦。

先看一组公开的物理参数:

面额直径(mm)边缘颜色视觉特征正面主图案识别难度
1美分(Penny)19.05铜红色,辨识度高林肯头像低
5美分(Nickel)21.21银灰色偏暗杰斐逊头像中
10美分(Dime)17.91银白色,尺寸最小罗斯福头像中
25美分(Quarter)24.26银白色,边缘有齿纹华盛顿头像中
50美分(Half Dollar)30.61银白色,体积大肯尼迪头像低
1美元(Dollar)26.50金色/银色皆有萨卡加维亚或总统像中高

这里有几个容易翻车的点。10美分和1美分的直径只差1.14毫米,单靠尺寸非常容易混淆,但好在颜色差异大,一个银白一个铜红。5美分和25美分直径相差3毫米多,但5美分因为材质原因颜色偏黄暗,25美分则更亮白,如果打了强光,两者视觉上很容易接近。更麻烦的是流通硬币普遍有磨损,边缘齿纹不清晰、图案细节丢失,这些都会直接影响模型的特征提取。

还有一个容易忽略的现实问题:50美分和1美元硬币在国内兑换渠道里并不常见,真实样本采集难度大,后续做数据集时必然会遇到类别不均衡。这些都决定了我们不能把项目当成一个简单的六分类问题来处理。

1.2 三种数据获取方式:公开数据集、实拍补充、合成增强

确定目标后,我开始搭数据集。经验之谈是:永远不要指望一口气找到一个完美的现成数据集,公开数据打底、自己实拍补强、再用合成数据调优,这才是最靠谱的组合。

最初我先从公开的数据集托管平台上下载了一批硬币图片,大概有几千张,涵盖了六种面额在不同背景下的照片。这一步的好处是省时间,但问题也很明显:很多公开数据集的拍摄场景过于理想,背景干净、光照均匀、硬币摆放整齐,跟实际使用场景脱节。如果你训练的模型只在这样的数据上验证,效果一定会虚高。

第二步是实拍补充。我自己找了一批不同品相的硬币,包括大量带有明显划痕、氧化、变色甚至部分图案磨损的旧币,然后换着场景拍:木质桌面、深色绒布、白色A4纸、甚至直接放在键盘上。光照也做了变化,有室内LED灯、窗边自然光、以及昏暗环境下的补光。这一步补了大概1000多张图片,直接决定了模型的泛化能力。

第三步是合成增强。我写了一个简单的脚本,将单枚硬币从原图中抠出来,随机旋转、缩放、摆放到各种纯色或纹理背景上,再叠加高斯噪声、亮度扰动和轻微模糊。这样既扩充了样本量,也模拟了硬币散落、相互靠近的真实场景。合成数据在项目初期的价值很大,能快速验证标注有没有问题,但我必须提醒一句:合成数据和真实图像的分布始终有差异,比例不要超过训练集的30%,否则会拉低真实场景下的表现。

1.3 标注规范:边界框怎么框才不会坑到模型

数据凑齐后是标注环节。我用常见的图像标注工具画边界框,原则只有一条:边界框要贴着硬币的外缘,但不要包含明显的阴影区域。

硬币是圆形物体,边界框天然会带上四个角的背景区域,这没关系,YOLO系列模型本身就能适应这种情况。真正需要注意的,是硬币边缘和背景对比度低时容易把框画大一圈,或者反过来把边齿切掉一小块,这两种情况都会让模型学到错误的边缘特征。

标注类别时我直接用面额英文名加分类编号,比如penny_1c、nickel_5c、dime_10c、quarter_25c、half_dollar_50c、dollar_1,尽量不用纯数字,因为模型输出的类别ID记起来容易乱。遇到多枚硬币叠在一起的情况,我的处理方式是:对于遮挡超过三分之一的硬币直接跳过不标,对于轻微重叠的两枚硬币,各自画框,让模型自己去学习可见部分的特征。这个策略在前期特别管用,模型不会因为大量遮挡样本而变得过于激进。

标注完一定要做一次全量抽查,重点看两类问题:一是类别标错,比如把5美分标成25美分;二是框偏移,框中心偏了半个硬币半径。这些错误哪怕只占1%~2%,也会在训练时产生大量噪声,导致个别类别的AP值莫名上不去。

2. 选型逻辑:为什么我最终锁定了YOLOv12

2.1 从YOLOv5到YOLOv12,版本交替背后的关键变化

目前的YOLO系列经过多次迭代,早已不是当年那个只有检测头的简单结构。YOLOv5的成功在于把工程易用性做到了极致,YOLOv8引入了Anchor-Free和C2f模块,训练和部署变得更加灵活,YOLOv9和YOLOv10又在可逆架构和免NMS推理上做了探索。到了YOLOv12,比较大的变化是引入了区域注意力机制,开始尝试替换结构中一部分传统的卷积模块,这对小目标和类间相似目标的特征提取是有增益的。

硬币检测恰好就是典型的"类间相似、小目标密集"场景:6种面额之间形状高度接近,区别主要藏在颜色、纹路和微小的尺寸差里。YOLOv12的注意力结构能在特征提取阶段更关注这些细节区域,而不是只盯着整体轮廓,这也是我选择它的核心原因。

当然,版本越新不等于效果一定越好,这里有个很现实的前提:你的数据集质量和数量要配得上模型的能力。如果你的样本本身就模糊不清,再强的注意力机制也救不回来。我的判断是,这个项目的数据规模(约5000~8000张)和类别难度,用YOLOv12是完全合理的,尤其是需要部署成桌面应用时,新版本在训练和推理接口上也比老版本更顺手。

2.2 为什么不选传统视觉方案:硬币看似简单,实则难搞

有朋友问过我,硬币不是圆形的吗?直接用边缘检测加霍夫变换找圆,再做模板匹配不就行了?这个思路理论上可行,但实际操作中会被三个问题打得很痛。

一是背景干扰。真实的硬币照片背景往往是凌乱的桌面,边缘检测会把笔、本子、指纹、木纹全部当成边界,霍夫变换找出来的圆里混杂大量假阳性。二是尺度和旋转。同一枚硬币,拍摄距离不同会导致直径从100像素到400像素变化,模板匹配需要多尺度多角度穷举,计算量爆炸,而且一旦硬币磨损导致图案变化,模板就失效。三是颜色和纹理信息的浪费。1美分和10美分尺寸很接近,但颜色完全不一样,传统几何方法只盯着形状,等于把最重要的判别信息丢了。

深度学习方案的好处是把"用什么特征来区分"这件事交给网络自己学习。模型会同时利用颜色、纹理、边缘、尺寸关系等多维信息,对光照变化和背景变化的鲁棒性远高于手工特征。所以在实际项目里,YOLO这类目标检测模型几乎成了标准答案。

2.3 模型规模与部署场景的匹配

YOLOv12提供了不同规模的选择,从轻量版到高精度版依次是n、s、m、l、x。很多初学者一上来就选最大的模型,理由是"精度肯定更高",但在硬币检测这个任务上,实验数据并不完全支持这个直觉。

我最初在训练时对比了s和m两个规模:在单卡环境、batch size为16、训练300轮的情况下,s模型的mAP50能到95.8%,m模型是97.2%,精度差距不到1.5个百分点,但m模型的参数量大约是s模型的两倍多,推理速度下降了接近30%。考虑到这个系统最终要跑在带界面的桌面应用里,用户大概率用的还是普通办公电脑甚至笔记本,选一个推理速度更友好的模型规模更现实。

所以最终方案是:如果只做单张图片上传检测,训练m模型做离线分析没问题;如果要接摄像头实时视频流,就换s模型。一套数据训练出两个档位的模型,通过切换权重文件来适配不同场景,这个思路比死磕单个模型实用得多。

3. 训练实录:三次失败后,我把loss曲线彻底看懂了

3.1 环境准备与数据划分:版本对齐是第一优先级

开始训练前,第一步是确认环境。我的组合是Python 3.10、PyTorch 2.1.0、CUDA 11.8,配合YOLOv12对应的官方Python包。这里要特别强调:不要小看版本对齐这个问题,很多人翻车都是因为PyTorch和CUDA版本不匹配,导致模型在GPU上卡死或者报莫名其妙的显存错误。

数据目录结构我参照官方约定的格式来组织:

# coin_dataset.yaml path: ./datasets/coins train: images/train val: images/val nc: 6 names: ['penny_1c', 'nickel_5c', 'dime_10c', 'quarter_25c', 'half_dollar_50c', 'dollar_1']

数据集划分上,训练集占70%,验证集占20%,测试集占10%。有一个细节值得专门说明:划分时要按"拍摄场景"而不是按"图片张数"来切。如果同一块桌面上连续拍的几十张图全部进了训练集,验证集里只有另一个场景的图片,模型在验证集上的指标就会比较难看。我按场景分组后,先随机选场景,再在场景内分配图片,这样能最大程度模拟真实使用时的分布变化。

3.2 参数调试记录:从默认参数到针对性调整

我第一次训练直接用了默认参数,结果mAP50虽然上了90%,但mAP50-95只有70%出头,而且验证集上25美分和5美分的混淆比较明显。做了几轮针对性调整后,最终参数如下:

参数第一次默认最终调整调整原因
img_size640640硬币目标较小,保持原尺寸
batch1616受显存限制
epochs300400后期AP仍在缓慢上升
lr00.010.005降低初始学习率,减少震荡
mosaic1.00.5大量Mosaic增强会让硬币形变过度
hsv_h0.0150.02略微提升颜色扰动,增强对光照变化适应性
fliplr0.50.5保持默认

其中Mosaic增强的调整是我反复对比后发现的。硬币是刚性物体,过强的Mosaic拼接会在同一张图里产生大量不符合真实比例的混叠区域,模型容易学到"背景混乱"的特征。降到0.5后再配合0.2的MixUp增强,验证集上的稳定性和精度反而都有提升。

3.3 指标解读:mAP50只是及格线,mAP50-95才是真相

训练过程中我会盯着四条曲线看:训练损失、验证损失、mAP50和mAP50-95。很多人只看mAP50到95%就觉得自己模型很好了,实际上mAP50是允许预测框和真实框有50%重叠的宽松指标,根本反映不出定位精度。在硬币检测里,你要的不只是"认出面额",还要"框得准",因为后续金额统计和计数都依赖框的坐标。

mAP50-95是把IoU阈值从0.5到0.95每隔0.05取一个档次的平均值,它更严苛,也更接近真实应用的评价标准。我的模型最终mAP50-95稳定在82%左右,对比第一轮的70%已经有了明显提升。为了加速收敛,我在300轮位置手动降低了学习率,后面的100轮让损失曲线基本走平,然后依靠patience=50的早停机制防止过拟合。

训练结束后我导出了一份混淆矩阵,发现容易出错的正是5美分和25美分、10美分和1美分这两对。25美分的直径虽然比5美分大3毫米,但在某些透视角度下这3毫米的差距在图像中很小;1美分和10美分则主要靠颜色区分,一旦光线偏黄,银色10美分也可能被染成暖色调。这个结果直接指导了下一步的数据增强策略——我在训练数据中加重了白平衡抖动的比例。

4. 界面落地:登录注册和检测UI是怎么串起来的

4.1 技术栈选型:PySide6与Tkinter的取舍

模型训练好了,接下来就是把它包成一个像样的桌面工具。Python做界面的选择无非是Tkinter、PySide6/PyQt5、Kivy这几个方向。Tkinter胜在轻量、环境自带,但控件样式老旧,做一个带图片缩放、表格展示、状态栏的产品化界面需要大量额外工作量。PySide6是Qt的官方Python绑定,控件成熟、样式现代,支持QSS皮肤定制,还内置了线程信号机制,很适合用来承接模型推理这种耗时操作。

我最终选了PySide6。理由是它能在UI线程里保持流畅交互的同时,通过信号槽把模型推理放到后台线程,避免界面卡死。这点在做检测功能时特别关键,否则用户点一下"检测"按钮就要等几秒,体验非常差。

4.2 登录注册模块实现:密码加密与用户会话

界面第一步从登录注册开始。这个模块看起来简单,但交互逻辑和数据处理有讲究。

用户注册时,前后端直接接触的是用户名和密码,密码绝对不可以明文保存到数据库。我在项目里用了一套加盐哈希的方案:

import hashlib import os def hash_password(password: str) -> str: salt = os.urandom(16).hex() hashed = hashlib.sha256((salt + password).encode()).hexdigest() return f"{salt}${hashed}" def verify_password(password: str, stored: str) -> bool: salt, hashed = stored.split("$") return hashlib.sha256((salt + password).encode()).hexdigest() == hashed

用户数据存储用SQLite就足够,无需引入重型的数据库服务。注册逻辑里我还加了两次密码输入一致性校验、用户名查重、密码长度至少六位且必须包含字母和数字这三道检查。登录成功后生成一个会话标记,记录当前用户ID和登录时间,这样检测记录表格里就能存下"哪个用户在哪次登录后检测了哪些图片"。

4.3 检测主界面:图片上传、实时推理与金额汇总

主界面布局分三个区域:左侧是图片预览区,右侧是检测结果显示区,底部是统计工具栏。用户点击"选择图片"按钮后,用QFileDialog打开文件选择框,预览原图后点击"开始检测",程序调用训练好的模型进行推理。

核心调用逻辑是这样的:

import cv2 from ultralytics import YOLO model = YOLO("best.pt") def detect_coins(image_path: str): img = cv2.imread(image_path) results = model(img, conf=0.4) detections = [] for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cls_id = int(box.cls[0]) conf = float(box.conf[0]) detections.append((cls_id, conf, (int(x1), int(y1), int(x2), int(y2)))) return detections

拿到坐标和类别后,我在原图的副本上用cv2.rectangle画出检测框,框的颜色按类别预设好,类别名称和置信度分数直接标注在框上方。为了让用户看得更直观,右侧表格里同步列出每一次检测的面额、置信度、坐标位置,最后用字典累加出当前图片里各面额的硬币数量,并乘以对应面值计算出总额,显示在底部状态栏。

这里我遇到过一个问题:PySide6显示图片时,QImage的通道顺序是RGB,而OpenCV读取的是BGR,直接转换会导致颜色整体偏蓝偏红。处理办法就一行,cv2.cvtColor(img, cv2.COLOR_BGR2RGB),但忘记这行代码几乎是每个人都会犯的错误。

5. 部署阶段踩过的坑:从反光误检到推理卡顿

5.1 模型导出与推理加速:ONNX和TensorRT的实测对比

模型训练完成后,直接加载.pt权重文件跑推理完全没问题,但如果要分发给别人用,或者想提升CPU上的推理速度,就要考虑模型导出了。

实测下来,我对比了三种推理方式的差异:

推理方式适用场景单张平均耗时(CPU)备注
PyTorch原模型开发调试约1.8秒灵活,方便改代码
ONNX导出跨平台部署约1.1秒体积小,兼容性好
TensorRTNVIDIA显卡优化约0.2秒需要额外编译引擎

如果用户机器没有NVIDIA独立显卡,只做单张图片检测的话,ONNX导出后跑CPU完全够用。导出命令也很简单,模型对象调用导出方法指定format=onnx即可。需要提醒的一点是,导出ONNX时最好固定输入尺寸(比如640x640),否则动态尺寸会让推理时间抖动明显。

5.2 反光、磨损与重叠硬币的真实影响

部署到实际场景后的第一个教训,来自硬币表面的反光。尤其是在灯光直射下,25美分和5美分表面的高光区域会形成一片亮白色的色块,模型在高光干扰下偶尔会把25美分误判成5美分,或者直接漏检。

解决思路有两个方向。一是数据层面,我在训练集里有意识地加入了一批用侧光拍摄、带有明显反光斑的硬币图片,让模型学会忽略这些局部高光。二是推理层面,我保留了OpenCV的高光抑制后处理开关,检测前先对图片做一次简单的亮度通道均衡化处理,把高光区域的过曝像素压回正常范围。两者叠加之后,实测误检率下降了不少。

重叠硬币则是另一个麻烦。当两枚硬币有三分之一面积重合时,模型的预测框往往会偏向可见部分,导致另一枚硬币的置信度骤降。我在测试集中专门统计过,重叠超过30%的样本漏检率大约是正常样本的三倍。这个问题要完全解决并不容易,目前的做法是把置信度阈值从0.45降到0.35,代价是会有少量背景误检,需要再配合一个按面积过滤的后处理规则来压制。

5.3 相似面额混淆的定位与缓解

从混淆矩阵来看,最容易分不清的是5美分和25美分。为了定位问题,我把错误样本全部导出到本地,按预测正确的和预测错误的分别建立文件夹,人眼逐个对比。看了两百多张图之后,我总结出两个规律:

  • 25美分的边缘齿纹比5美分更细密,但照片模糊时这个差异完全消失。
  • 5美分表面由于镍铜合金氧化,整体偏灰黄色;25美分偏亮白色。但在白色LED灯直射且白平衡不准时,两者的整体颜色分布很接近。

针对这两个规律,我在数据增强里做了两件事:一是增加低分辨率模糊样本的占比,确保模型见过"模糊但有正确类别标签"的硬币;二是加强白平衡抖动和色温偏移,让模型不把某个固定的颜色数值当成唯一的判断依据。效果上,5美分和25美分两类的F1分数都回到了0.9以上。

5.4 从毕业设计到可用的系统:功能扩展清单

模型和界面跑通之后,我把项目整理成了一个带登录注册、图片检测、金额统计、检测记录保存的完整系统。如果想让这个系统更进一步,有几个扩展方向值得做:

  • 批量检测模式:选择整个文件夹,后台逐张检测并合并统计结果,导出CSV报表。
  • 摄像头实时检测:切换模型为s档位,接入视频流,检测结果叠加到视频画面上。
  • 检测记录数据库化:把每次检测的图片路径、检测结果、用户ID写入数据库,支持按时间查询。
  • 模型热切换:在界面里提供模型选择下拉框,让用户根据当前使用场景切换s或m档位。

这几个方向的工程量都不算大,架构上只需要在现有代码上增加对应的接口即可。尤其是批量检测和CSV导出,在实际使用中价值很高。如果你是在做类似的钱币清点项目,建议优先把这两个功能加上。

训练一个泛化性好的硬币检测模型,我最大的体会是:真正消耗时间精力的环节不是调模型参数,而是和数据较劲。公开数据集打底、实拍样本补全、合成数据调优,这三板斧缺一不可。还有一个小技巧分享给你:我通常先拿一小批数据快速跑20轮,专门用来检查标注文件有没有格式错误、类别名有没有拼错,确认这一切正常之后再进行长轮次训练,可以省下好几个小时的无效等待。

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

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

立即咨询