1. 项目概述
这个基于YOLOv11的硬币识别系统是我最近完成的一个很有意思的计算机视觉项目。作为一个经常需要处理零钱的便利店店主,我一直在寻找能自动识别和统计硬币的解决方案。市面上的商用设备要么太贵,要么识别准确率不够理想。于是我用最新的YOLOv11目标检测算法,开发了这个高精度的硬币识别系统。
系统能够准确识别四种常见美国硬币:Dime(10美分)、Nickel(5美分)、Penny(1美分)和Quarter(25美分)。在实际测试中,即使在复杂背景下(如杂乱的桌面或钱包内),识别准确率也能达到95%以上。除了核心的识别功能外,我还为系统开发了完整的用户界面,包括登录注册、参数配置和多种检测模式,使其成为一个真正可用的工具而非单纯的算法demo。
2. 技术选型与架构设计
2.1 为什么选择YOLOv11
在目标检测领域,YOLO系列一直以速度和精度的平衡著称。我选择最新的YOLOv11主要基于以下几个考虑:
-
实时性需求:硬币识别可能需要处理视频流或摄像头实时画面,YOLOv11的推理速度能满足实时性要求。在我的测试中,在RTX 3060显卡上能达到45FPS的处理速度。
-
小目标检测能力:硬币相对于整个画面来说属于小目标,YOLOv11针对小目标检测做了专门优化,其多尺度特征融合机制能更好地捕捉硬币特征。
-
模型轻量化:YOLOv11提供了从nano到x不同规模的预训练模型,可以根据硬件条件灵活选择。我最终选择了yolov11s模型,在精度和速度间取得了良好平衡。
2.2 系统架构设计
整个系统采用模块化设计,主要分为以下几个组件:
code复制├── 核心检测引擎
│ ├── YOLOv11模型
│ ├── 图像预处理模块
│ └── 后处理模块
├── 用户界面
│ ├── 登录/注册系统
│ ├── 主控制面板
│ └── 结果显示区域
├── 数据管理
│ ├── 账户存储
│ └── 结果保存
└── 工具链
├── 数据集准备工具
└── 模型训练脚本
这种架构使得各个功能模块相对独立,便于后期维护和功能扩展。例如,如果想增加新的硬币种类,只需要更新数据集并重新训练模型,其他模块几乎不需要改动。
3. 数据集准备与模型训练
3.1 硬币数据集的构建
高质量的数据集是模型准确性的基础。我收集了约5000张包含各种美国硬币的图像,涵盖了不同场景:
- 单一硬币特写
- 多硬币堆叠
- 复杂背景下的硬币
- 不同光照条件下的硬币
- 各种角度的硬币图像
使用LabelImg工具手动标注了所有图像,生成YOLO格式的标注文件。标注时特别注意了几个要点:
- 确保标注框紧密贴合硬币边缘
- 对于部分遮挡的硬币也进行标注
- 标注了一定数量的负样本(不含硬币的图像)
数据集按7:2:1的比例划分为训练集、验证集和测试集。目录结构如下:
code复制dataset/
├── train/
│ ├── images/
│ └── labels/
├── val/
│ ├── images/
│ └── labels/
└── test/
├── images/
└── labels/
3.2 数据增强策略
为了提高模型的泛化能力,训练时采用了多种数据增强技术:
python复制# 数据增强配置示例
augmentations = {
'hsv_h': 0.015, # 色相增强
'hsv_s': 0.7, # 饱和度增强
'hsv_v': 0.4, # 明度增强
'rotate': 10, # 旋转角度
'translate': 0.1, # 平移
'scale': 0.5, # 缩放
'shear': 0.0, # 剪切
'perspective': 0.0005, # 透视变换
'flipud': 0.0, # 上下翻转
'fliplr': 0.5, # 左右翻转
'mosaic': 1.0, # 马赛克增强
'mixup': 0.1 # MixUp增强
}
特别针对硬币识别任务,我增加了旋转和明度增强的比例,因为硬币在实际场景中可能以各种角度出现,且反光情况各异。
3.3 模型训练过程
使用Ultralytics框架进行模型训练,主要参数配置如下:
python复制model = YOLO('yolov11s.pt') # 加载预训练模型
results = model.train(
data='data.yaml',
epochs=100,
batch=8,
imgsz=640,
device='0', # 使用GPU 0
workers=4,
patience=10, # 早停机制
lr0=0.01, # 初始学习率
lrf=0.01, # 最终学习率
momentum=0.937,
weight_decay=0.0005,
warmup_epochs=3,
warmup_momentum=0.8,
box=7.5, # box损失权重
cls=0.5, # 分类损失权重
dfl=1.5 # DFL损失权重
)
训练过程中观察到几个关键指标的变化:
- mAP50-95:从初始的0.68提升到最终的0.92
- 精确率:达到0.94
- 召回率:达到0.93
训练完成后,模型大小约35MB,在保持高精度的同时保持了轻量级特性。
4. 系统实现细节
4.1 核心检测逻辑实现
检测系统的核心是一个继承自QThread的DetectionThread类,实现了多线程检测以避免阻塞UI:
python复制class DetectionThread(QThread):
frame_received = pyqtSignal(np.ndarray, np.ndarray, list)
def __init__(self, model, source, conf, iou):
super().__init__()
self.model = model
self.source = source # 可以是图片路径、视频路径或摄像头ID
self.conf = conf # 置信度阈值
self.iou = iou # IoU阈值
self.running = True # 控制线程运行的标志
def run(self):
if isinstance(self.source, int) or self.source.endswith(('.mp4', '.avi')):
# 视频或摄像头处理逻辑
cap = cv2.VideoCapture(self.source)
while self.running and cap.isOpened():
ret, frame = cap.read()
if not ret: break
# 执行检测
results = self.model(frame, conf=self.conf, iou=self.iou)
annotated_frame = results[0].plot()
# 提取检测结果
detections = []
for box in results[0].boxes:
cls_id = int(box.cls)
conf = float(box.conf)
x, y = box.xywh[0][:2].tolist()
detections.append((self.model.names[cls_id], conf, x, y))
# 发送信号更新UI
self.frame_received.emit(
cv2.cvtColor(frame, cv2.COLOR_BGR2RGB),
cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB),
detections
)
cap.release()
else:
# 图片处理逻辑
frame = cv2.imread(self.source)
results = self.model(frame, conf=self.conf, iou=self.iou)
# ...类似处理...
def stop(self):
self.running = False
这种设计使得检测过程不会阻塞主线程,UI可以保持响应。当检测到新帧时,通过PyQt的信号槽机制通知UI更新。
4.2 用户界面设计
UI采用PyQt5实现,主要特点包括:
- 双画面显示:左侧显示原始图像,右侧显示检测结果
- 实时结果表格:展示检测到的硬币类型、置信度和位置
- 参数控制面板:可以动态调整置信度阈值和IoU阈值
- 多检测模式:支持图片、视频和摄像头三种输入源
UI的核心代码结构:
python复制class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 初始化模型
self.model = YOLO('best.pt') # 加载训练好的模型
# 创建UI组件
self.create_ui()
# 连接信号槽
self.image_btn.clicked.connect(self.on_image_clicked)
self.video_btn.clicked.connect(self.on_video_clicked)
self.camera_btn.clicked.connect(self.on_camera_clicked)
self.stop_btn.clicked.connect(self.on_stop_clicked)
def create_ui(self):
# 创建主布局
main_layout = QHBoxLayout()
# 图像显示区域
self.original_image = QLabel()
self.result_image = QLabel()
main_layout.addWidget(self.original_image)
main_layout.addWidget(self.result_image)
# 控制面板
control_panel = QVBoxLayout()
# 模式选择按钮
self.image_btn = QPushButton("图片检测")
self.video_btn = QPushButton("视频检测")
self.camera_btn = QPushButton("摄像头检测")
self.stop_btn = QPushButton("停止检测")
# 参数控制
self.conf_slider = QSlider(Qt.Horizontal)
self.conf_slider.setRange(0, 100)
self.conf_slider.setValue(50)
self.conf_slider.valueChanged.connect(self.on_conf_changed)
# 结果表格
self.result_table = QTableWidget()
self.result_table.setColumnCount(4)
self.result_table.setHorizontalHeaderLabels(['类型', '置信度', 'X', 'Y'])
# 组装UI
control_panel.addWidget(self.image_btn)
control_panel.addWidget(self.video_btn)
control_panel.addWidget(self.camera_btn)
control_panel.addWidget(self.stop_btn)
control_panel.addWidget(QLabel("置信度阈值:"))
control_panel.addWidget(self.conf_slider)
control_panel.addWidget(self.result_table)
main_layout.addLayout(control_panel)
# 设置中心窗口
central_widget = QWidget()
central_widget.setLayout(main_layout)
self.setCentralWidget(central_widget)
4.3 登录注册系统实现
为了保证系统安全性,实现了基于本地JSON文件存储的账户系统:
python复制class LoginWindow(QDialog):
def __init__(self):
super().__init__()
# 加载已有账户
self.accounts = self.load_accounts()
# 创建UI
self.username_input = QLineEdit()
self.password_input = QLineEdit()
self.password_input.setEchoMode(QLineEdit.Password)
login_btn = QPushButton("登录")
register_btn = QPushButton("注册")
login_btn.clicked.connect(self.handle_login)
register_btn.clicked.connect(self.handle_register)
# ...布局代码...
def load_accounts(self):
try:
with open('accounts.json', 'r') as f:
return json.load(f)
except:
return {} # 文件不存在时返回空字典
def save_accounts(self):
with open('accounts.json', 'w') as f:
json.dump(self.accounts, f)
def handle_login(self):
username = self.username_input.text()
password = self.password_input.text()
if username in self.accounts and self.accounts[username] == password:
self.accept() # 登录成功
else:
QMessageBox.warning(self, "错误", "用户名或密码不正确")
def handle_register(self):
username = self.username_input.text()
password = self.password_input.text()
if len(password) < 6:
QMessageBox.warning(self, "警告", "密码长度至少为6位")
return
if username in self.accounts:
QMessageBox.warning(self, "警告", "用户名已存在")
else:
self.accounts[username] = password
self.save_accounts()
QMessageBox.information(self, "成功", "注册成功")
5. 性能优化与实际问题解决
5.1 实时性优化
在实际测试中,发现了几个影响实时性的瓶颈:
- 图像预处理开销:原始实现中对每帧图像都进行了完整的预处理,包括尺寸调整和归一化。通过分析发现,这部分占用了约30%的处理时间。
优化方案:将预处理操作移到模型加载时进行配置,利用OpenCV的GPU加速:
python复制model = YOLO('best.pt')
model.export(format='onnx', simplify=True, dynamic=False) # 导出为ONNX格式
# 使用TensorRT加速
model = cv2.dnn.readNetFromONNX('best.onnx')
model.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
model.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
- 结果后处理开销:原始NMS操作在CPU上执行,成为性能瓶颈。
优化方案:使用CUDA加速的NMS实现:
python复制def cuda_nms(boxes, scores, threshold):
# 使用PyCUDA实现GPU加速的NMS
import pycuda.autoinit
from pycuda import gpuarray
# ...具体实现...
经过这些优化后,处理速度从原来的22FPS提升到了45FPS,完全满足实时性需求。
5.2 常见问题与解决方案
在实际部署中遇到了一些典型问题:
问题1:硬币堆叠时识别率下降
当多个硬币堆叠在一起时,模型有时会将它们识别为一个硬币。通过分析发现,训练数据中缺少足够的堆叠硬币样本。
解决方案:
- 收集更多硬币堆叠情况的图像
- 在数据增强中增加随机堆叠的模拟
- 调整损失函数中定位损失的权重
问题2:反光硬币识别困难
硬币表面反光会导致特征提取困难,特别是在强光环境下。
解决方案:
- 增加各种光照条件下的训练数据
- 在预处理阶段加入光照归一化:
python复制def normalize_lighting(img): lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8)) cl = clahe.apply(l) limg = cv2.merge((cl,a,b)) return cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
问题3:边缘设备部署性能不足
在树莓派等边缘设备上运行时帧率过低。
解决方案:
- 使用更小的模型版本(yolov11n)
- 量化模型到INT8精度:
python复制model.export(format='onnx', int8=True, simplify=True) - 使用OpenVINO优化:
python复制from openvino.runtime import Core core = Core() model = core.compile_model('best.xml', 'CPU')
6. 系统功能扩展
基础功能实现后,我又为系统添加了几个实用的扩展功能:
6.1 金额统计功能
在检测结果的基础上,增加了自动计算总金额的功能:
python复制COIN_VALUES = {
'Penny': 0.01,
'Nickel': 0.05,
'Dime': 0.10,
'Quarter': 0.25
}
def calculate_total(detections):
total = 0.0
for class_name, _, _, _ in detections:
total += COIN_VALUES.get(class_name, 0)
return total
6.2 历史记录与导出
增加了检测结果的保存和导出功能,支持CSV和Excel格式:
python复制def save_to_csv(detections, filename):
with open(filename, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['类别', '置信度', 'X', 'Y', '时间'])
for det in detections:
writer.writerow([*det, datetime.now().strftime('%Y-%m-%d %H:%M:%S')])
6.3 多摄像头支持
扩展了摄像头检测功能,支持多摄像头切换:
python复制def get_available_cameras(max_test=5):
available = []
for i in range(max_test):
cap = cv2.VideoCapture(i)
if cap.isOpened():
available.append(i)
cap.release()
return available
7. 项目部署与实际应用
7.1 打包为可执行文件
使用PyInstaller将项目打包为可执行文件,方便在没有Python环境的机器上运行:
bash复制pyinstaller --onefile --windowed --add-data "best.pt;." --add-data "accounts.json;." main.py
7.2 实际应用场景
这个系统已经在几个实际场景中得到应用:
- 便利店收银辅助:自动统计收银台中的硬币金额,减少人工清点时间。
- 自助售货机:用于识别投入的硬币面额,替代传统机械式硬币识别器。
- 银行硬币清分:快速清点大量硬币,并与纸币识别系统集成。
7.3 性能实测数据
在不同硬件环境下的性能测试结果:
| 硬件配置 | 分辨率 | FPS | 功耗 |
|---|---|---|---|
| RTX 3060 | 640x640 | 45 | 120W |
| Jetson Xavier NX | 640x640 | 28 | 15W |
| Raspberry Pi 4 | 320x320 | 8 | 5W |
| Intel i5-1135G7 | 640x640 | 22 | 28W |
8. 项目总结与改进方向
经过这个项目的开发,我总结了几个关键经验:
-
数据质量至关重要:硬币识别看似简单,但要达到高精度需要大量多样化的训练数据,特别是各种边缘情况。
-
模型选择需要权衡:在边缘设备上部署时,需要在模型大小和精度之间找到平衡点。
-
用户体验不容忽视:即使是技术Demo,良好的UI设计和交互流程也能大大提升实用性。
未来的改进方向包括:
- 支持更多国家和地区的硬币识别
- 增加纸币识别功能
- 开发移动端应用版本
- 集成数据库管理系统,实现更完善的用户管理和历史记录查询
这个项目完整展示了从算法选型、数据准备、模型训练到系统实现和优化的全过程。通过不断的迭代和改进,最终实现了一个既准确又实用的硬币识别系统。所有代码和模型都已开源,希望能为有类似需求的开发者提供参考。