
简介这是一份面向Python/OpenCV初学者的硬币检测识别代码包用于从照片中自动统计硬币个数并判断总价值适合图像处理入门、课程设计及小型项目实践。压缩包共13个文件包含1个Python主脚本与12张JPG图片整体7.39MB素材覆盖模板、测试、中间输出及最终结果图便于对照代码一步步理解检测流程。目前已有2253人学习下载被不少开发者用于OpenCV图像处理练习。脚本围绕图像预处理、目标分割、轮廓分析等核心环节实现对硬币的定位与面值判别读者既可通过模板和测试图片复现实验也可替换为自己的硬币照片验证或在此基础上扩展识别其他圆形物体。若想深入理解图像处理流程还可以从模板匹配、边缘检测等角度修改参数并观察效果对想快速上手传统图像处理方案、完成硬币计数或面值识别任务的开发者是一份直接可用的参考代码。1. 硬币检测识别一台OpenCV就能完成的“找零”黑匣子把一把混在一起的一元、五角、一角硬币倒进自动售货机的找零口机器要在半秒内分清每一枚的面值并累计金额——这是硬币识别最典型的落地场景。这个项目用纯OpenCV完成从「找硬币」到「算金额」的完整链路不依赖任何深度学习框架因为硬币的形状、颜色和半径本身就是强约束圆形结构、金银两色、三档尺寸差异传统图像处理在这类目标上比神经网络更快也更稳。整套流程可脱机跑适合刚入门计算机视觉、想做一个能完整跑通的图像处理项目或者要往树莓派、嵌入式设备上部署离线推理的开发者。2. 技术选型与架构为什么是HoughCircles而不是YOLO2.1 检测路线对比与选型逻辑硬币检测看起来像是「目标检测」问题很多同学第一反应是上YOLO或者SSD。但硬币这个场景有几个特殊性目标尺寸小、数量多、相互遮挡频繁而且面值之间的差异主要来自直径和颜色而不是纹理。深度学习模型在这种「密集小目标 弱语义差异」的场景下反而不占优势训练数据还得自己标几百张图。这个项目走的是传统CV路线核心三步预处理转灰度、高斯模糊去噪、自适应阈值或Canny边缘提取。检测HoughCircles在边缘图上做圆检测拿到每枚硬币的圆心坐标和半径。识别根据半径分档和HSV颜色空间的金银判定映射到面值。选择这条路线还有一个现实考虑HoughCircles的开销远小于一次CNN推理。在树莓派级别的设备上一帧640×480的图跑完Raspberry Pi上的完整流程大约在80-120ms而YOLOv5s的推理耗时是这个数量级的3倍以上还不算NMS后处理。硬币识别不需要语义理解几何约束已经够强传统方法在这个场景里是「够用且更快」的答案。2.2 项目目录结构项目采用标准的分模块结构把预处理、检测、识别、结果聚合拆开方便单独调试每一环。完整目录如下coin_detector/ ├── main.py # 入口解析命令行参数、串联全流程 ├── config.yaml # 参数配置HoughCircles参数、颜色阈值、面值表 ├── detector/ │ ├── __init__.py │ ├── preprocess.py # 灰度、滤波、二值化、边缘提取 │ ├── circle_detect.py # HoughCircles封装含参数校验 │ └── classify.py # 半径分档 HSV颜色判定 ├── utils/ │ ├── __init__.py │ ├── image_io.py # 读图、写图、resize工具 │ └── visualizer.py # 在原图上画圆和标注识别结果 ├── tests/ │ ├── test_single.py # 单张图片跑通验证 │ └── test_batch.py # 批量目录检测输出统计报表 └── samples/ # 样例图片含不同光照条件的拍摄图config.yaml是项目里最值得调的文件检测参数全部集中在这里改参数不用动代码。面值表也在这里维护后续要适配五角硬币颜色改一行配置就行。2.3 配置文件与面值映射表配置文件的设计决定了二次开发时改起来是否顺手。项目里的config.yaml长这样# config.yaml preprocess: blur_ksize: 5 # 高斯核大小奇数越大越模糊 canny_low: 50 # Canny双阈值下限 canny_high: 150 # Canny双阈值上限 hough: dp: 1.2 # 累加器分辨率与图像分辨率的反比 min_dist: 40 # 圆心最小间距防止同一枚硬币被多次检出 param1: 100 # Canny高阈值直接传给内部Canny param2: 30 # 圆中心累加器阈值越小越容易检到弱边缘圆 min_radius: 20 # 允许的最小半径像素 max_radius: 80 # 允许的最大半径像素 classify: # 面值 - (半径ranges, HSV颜色范围) # 半径单位像素按真实硬币比例换算 denominations: one_yuan: radius_range: [52, 65] color_range: silver # 一元新版为银色 five_jiao: radius_range: [38, 48] color_range: gold # 五角为金色 one_jiao: radius_range: [28, 36] color_range: silver # 一角为银色 color_thresholds: gold_s_min: 80 # HSV中S通道下限低于则为银色 gold_v_min: 60 # V通道下限过暗视为阴影或光线不足这段配置的可读性很高。识别层的逻辑是「半径先粗分颜色再确认」——一元和一角半径差距很大不易混淆但五角和一角在某些拍摄距离下半径接近颜色就成了区分它们的决定因素。3. 核心模块拆解从像素到面值的完整链路3.1 预处理灰度、高斯模糊、Canny边缘预处理的目标是为HoughCircles提供一张干净的边缘图但HoughCircles内部会自动调Canny所以外层的边缘提取更多用于可视化调试。实际代码先把彩图缩小到统一宽度再转灰度、做高斯模糊降低硬币表面的纹理噪点和反光干扰。# detector/preprocess.py import cv2 import numpy as np def preprocess(image, blur_ksize5, target_width640): # 统一宽度不同拍摄距离下直径会漂移固定宽度便于半径阈值生效 h, w image.shape[:2] scale target_width / w if scale ! 1.0: image cv2.resize(image, (target_width, int(h * scale))) # 转灰度是圆检测的前提HoughCircles不接受彩色图 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 高斯模糊去掉硬币表面的细小划痕和灰尘噪点 # 核越大越糊5x5在保留边缘和去噪之间比较平衡 blur cv2.GaussianBlur(gray, (blur_ksize, blur_ksize), 0) return image, blur这段代码有两个容易被忽略的细节。target_width参数很关键因为HoughCircles的min_radius和max_radius是绝对值如果输入图分辨率不统一同一枚硬币在不同图片里的像素半径能差出一倍直径分档直接失效。固定的做法是把所有输入统一缩放到640宽。另一个是cv2.GaussianBlur的核必须是奇数传4会直接报错这个API的坑很多新手会踩。3.2 Circle检测HoughCircles的参数语义检测层是整个项目最核心的一环。OpenCV的HoughCircles不是直接在图像上找圆而是在梯度方向上做累加投票参数稍微一调就「从全检到全不检」。# detector/circle_detect.py import cv2 def detect_circles(blur_image, config): # 注意传入的必须是灰度图 # dp1.2累加器分辨率比原图低20%减少计算量同时也平滑了圆心定位抖动 circles cv2.HoughCircles( blur_image, methodcv2.HOUGH_GRADIENT, dpconfig[dp], minDistconfig[min_dist], param1config[param1], param2config[param2], minRadiusconfig[min_radius], maxRadiusconfig[max_radius] ) if circles is None: return [] # 结果是 (1, N, 3) 的结构每个元素是 (x, y, r) circles circles[0] # 按半径降序排大硬币在后处理中优先保留避免重叠时小圆被误吞 circles sorted(circles, keylambda c: -c[2]) # 合并重叠圆半径大者优先若两个圆心距小于较小半径的80%去掉小的 filtered [] for c in circles: keep True for f in filtered: dist ((c[0] - f[0]) ** 2 (c[1] - f[1]) ** 2) ** 0.5 if dist min(c[2], f[2]) * 0.8: keep False break if keep: filtered.append(c) return filteredparam2是调参时最玄学的一个值。它表示「至少多少个圆心投票才认定这是一个圆」——设得太低背景纹理里的伪圆会被当成硬币设得太高边缘不完整的硬币会被漏掉。常见的做法是从30开始试以5为步进往上加到硬币刚好一个不漏为止。minDist的意义是防止同一枚硬币被重复检出多个同心圆一般设为最大期望硬币直径的1.2倍。重叠合并这段是项目里比较实用的小技巧。HoughCircles在某些边缘不清晰的时候会对同一枚硬币输出两个半径偏差10%以内的结果不做去重就会出现「一枚硬币当两枚数」的尴尬情况。3.3 识别半径分档与HSV颜色判定检测层拿到的是(x, y, r)列表识别层负责把每个圆映射到具体面值。这个项目用「半径粗筛 HSV颜色确认」的双层策略准确率远比单独用半径高因为五角和一角的半径比值在近距离拍摄时大约只有1.3但颜色差异是稳定的。# detector/classify.py import cv2 import numpy as np def classify_coin(image, x, y, radius, config): denoms config[classify][denominations] # 第一步按半径找候选面值 candidates [] for name, spec in denoms.items(): r_min, r_max spec[radius_range] if r_min radius r_max: candidates.append(name) if not candidates: return unknown # 只有五角和一元/一角的半径可能撞车才需要颜色再判定 if len(candidates) 1: # 抠出硬币中心的ROI不取边缘避开反光晕影 roi image[ int(y - radius * 0.5): int(y radius * 0.5), int(x - radius * 0.5): int(x radius * 0.5) ] hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 计算ROI内像素S通道的中位数 s_median np.median(hsv[:, :, 1]) v_median np.median(hsv[:, :, 2]) # 金色硬币的S通道显著高于银色 gold_s_min config[classify][color_thresholds][gold_s_min] gold_v_min config[classify][color_thresholds][gold_v_min] if s_median gold_s_min and v_median gold_v_min: return five_jiao else: return one_yuan if one_yuan in candidates else one_jiao return candidates[0]半径分档时有个细节五角的半径范围是38-48一角的半径范围是28-36两者之间留了2像素的缓冲带防止边缘抖动导致误判。颜色判定用np.median而不是均值原因是硬币表面反光会造成少数高亮像素点均值会被这些点拉高中位数更抗噪。HSV颜色判定比在BGR空间直接比R/G通道可靠得多——不同拍摄角度下BGR的R通道对光线变化极敏感但HSV把「色调」和「亮度」分离了金色硬币不管拍得多暗H色相都会稳定在20-40区间S饱和度虽有波动但显著高于银色硬币。3.4 主流程把检测与识别串成完整闭环main.py是项目的入口接收图片路径、输出路径两个参数内部依次调用预处理、检测、识别、可视化最后在终端打印统计金额。# main.py import sys import cv2 import yaml from detector.preprocess import preprocess from detector.circle_detect import detect_circles from detector.classify import classify_coin from utils.visualizer import draw_result def main(img_path, out_path, config_pathconfig.yaml): # 加载配置所有检测阈值集中管理改配置不改代码 with open(config_path, r, encodingutf-8) as f: config yaml.safe_load(f) image cv2.imread(img_path) if image is None: print(f[ERROR] 图片读取失败: {img_path}) return # 第一步预处理 → 第二部找圆 → 第三步逐个分类 display_img, blur preprocess(image, config[preprocess]) circles detect_circles(blur, config[hough]) total_value 0 detected [] for x, y, r in circles: # 扩大识别范围用1.0倍半径参与分类过滤异常 if r config[hough][min_radius]: continue label classify_coin(image, x, y, r, config) detected.append((x, y, r, label)) if label one_yuan: total_value 1.0 elif label five_jiao: total_value 0.5 elif label one_jiao: total_value 0.1 print(f[{label}] center({x:.1f}, {y:.1f}) r{r:.1f}) draw_result(display_img, detected, out_path) print(f识别结果共 {len(detected)} 枚硬币总额 {total_value:.1f} 元) print(f标注图已保存至 {out_path}) if __name__ __main__: # 用法: python main.py samples/demo.jpg outputs/result.jpg main(sys.argv[1], sys.argv[2])主流程的代码非常直白没有复杂的业务逻辑适合作为OpenCV入门项目的模板。整个流程把「找圆」和「分类」解耦了后续如果要换成深度学习分类器只需要改classify_coin的实现检测层和主流程完全不用动。识别结果输出到终端和标注图片两处方便批量跑的时候一边看图一边对账。4. 运行与验证从源码到结果4.1 环境准备项目依赖非常精简requirements.txt里只有三个包opencv-python4.5.0 numpy1.21.0 PyYAML5.4.0安装命令pip install -r requirements.txt建议用Python 3.8以上版本。这里有TIPSopencv-python这个包在Windows、macOS、Linux三平台都有预编译的wheel不需要自己编译但如果是在树莓派上跑推荐装opencv-python-armv7l编译版或者用apt安装系统包性能比wheel版本更好。如果你的机器上同时装了TensorFlow或者PyTorch注意opencv的cv2不要和torchvision里自带的OpenCV工具函数混淆这个项目只用了最基础的cv2命名空间不存在冲突问题但安装时建议在干净的虚拟环境里做这是血泪经验——之前遇到过pip把opencv-contrib-python和opencv-python同时装上导致cv2.imread正常但cv2.xfeatures2d崩掉的情况。4.2 跑起来命令与参数解析项目支持两种运行方式单张图片和批量目录。单张图片跑通用python main.py samples/coins_on_desk.jpg outputs/result.jpg批量跑目录里所有图片python main.py samples_dir/ outputs_batch/这其实是复用了main.py的接口——传目录进去代码会遍历目录下所有jpg/png文件逐张处理并在同一个输出目录生成标注图。实际生产环境里我一般会再加一个--debug开关把中间过程的灰度图、边缘图、最终标注图都写出来方便排查哪一步出了问题。运行后终端输出长这样[one_yuan] center(231.4, 180.2) r54.2 [five_jiao] center(402.1, 256.7) r42.8 [one_jiao] center(168.3, 390.5) r31.6 识别结果共 3 枚硬币总额 1.6 元同时输出的result.jpg会在每枚硬币上画一个圆圆心标注面值文字。这张图是验证检测质量最直观的产物一眼就能看出哪些圆被漏检、哪些圆错检。4.3 验证检测质量三组对比判断跑通只是第一步判断检测质量需要看三个维度漏检率图里实际有N枚硬币程序检出M枚漏检数占比。误检率程序检出的圆里实际不是硬币的比例背景伪圆。面值准确率半径和颜色双重判定的分类准确度。快速自检的方式是把项目自带的test_batch.py跑一遍它能对sample/目录下多张不同光照、不同背景的图片统一检测并输出一个统计表图片名 数量 检出 漏检 误检 面值准确率 coins_desk 5 5 0 0 100.0% coins_backlit 1 3 0 2 50.0%第二张图属于背光场景把背景里的瓶盖误检成了硬币面值准确率直接腰斩。如果你自己拍的测试图出现类似情况先不要急着改参数大概率是min_radius设太小了——背景里的圆形物体恰好落进了硬币的半径范围。调大min_radius到背景目标的半径之外误检率会明显下降。5. 避坑与排查检测不准时的调试路径5.1 背光环境下硬币边缘断裂HoughCircles检不到圆现象硬币在背光条件下窗户光或台灯从后方照过来部分边缘陷入阴影检出数量比实际少很多。原因HoughCircles依赖边缘梯度累加硬币阴影部分的边缘梯度值极低累加器得票不足被param2过滤掉了。解决不要一味降低param2——那会把背景噪声也放进来。更稳的做法是预处理阶段用cv2.createCLAHE做对比度受限的自适应直方图均衡化专门拉伸暗部细节。我一般在转灰度后加一行clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) blur clahe.apply(blur)这一步对背光场景的检测率提升非常明显但对正常光照的图片几乎无副作用建议默认开启。5.2 param2调参像玄学数值差一点结果天差地别现象param2从30调到40一枚硬币都检不到调回30又全出来了中间没有过渡。原因HoughCircles的累加器计数是离散的当圆边缘像素恰好处于阈值边缘时得票从「够」到「不够」是一个陡峭跳变参数量纲并不是线性的。解决我一般以5为步进扫描param2从param1的三分之一开始试。项目里提供了一个calibrate_param.py工具脚本自动扫描参数组合并在每张图上打印检出数量帮助快速找到适合你拍摄环境的取值区间。值得说明的是不同分辨率的输入图对param2的敏感度差异极大所以务必先用target_width把分辨率锁死再调参。5.3 硬币反光被误判成金色五角和一元的分类翻车现象新版一元硬币在强光下反光S通道中位数超过阈值被错误分类成五角。原因一元硬币是银色带抛光效果强光照射时局部反射光谱会偏暖黄色HSV里S和H同时漂移触发了gold_s_min的判断条件。解决加一层「半径前置过滤」——如果候选面值包含一元而半径落在五角的区间之外直接跳过颜色判断。我的实现逻辑是只有当两个候选面值的半径范围重叠时才启用颜色判定。另外ROI取硬币中心区域而不是整个圆也会大幅减少边缘反光的干扰。5.4 拍摄视角倾斜硬币在图中呈现椭圆而非正圆现象手机与桌面不平行拍摄时硬币看起来是椭圆HoughCircles输出的圆丢了长轴信息直径和真实值偏差大导致分类错误。原因HoughCircles只能检正圆对椭圆的响应是「捡最大内切圆」半径比真实长轴小。解决两条路。简单路是在预处理里加透视矫正前提是你能拿到桌面的四个角点更稳的路是改用cv2.findContourscv2.fitEllipse做椭圆拟合用长轴作为分类直径。这个改动不复杂适应性强很多也是我后续二次开发的重点方向之一。5.5 视频流场景帧率低硬币快速移动时拖影严重现象从摄像头实时识别时硬币移动速度快帧与帧之间拖影导致边缘模糊每帧检出数量不稳定有一帧检出3枚下一帧就检出1枚。原因把摄像头曝光时间默认当作固定值没有针对快速移动目标调低曝光同时HoughCircles每帧全图计算开销高导致帧率只有个位数进一步加重了模糊。解决把项目从单张图片改成视频循环后建议通过cap.set(cv2.CAP_PROP_EXPOSURE, -6)适当降低曝光同时把识别帧率控制在10fps左右中间帧直接跳过。硬币是刚性物体同一枚硬币在连续帧的位置可以靠半径匹配做跟踪不必要每帧全量重检。6. 二次开发从固定场景到可落地的识别方案6.1 从单张图片扩展到摄像头实时识别单张图跑通后最大的改造需求往往是接摄像头。改造思路是把main.py里的图片读取替换成视频帧循环并把检测结果做时间平滑。核心伪代码如下cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break # 每隔3帧跑一次检测中间帧沿用上次结果 if frame_count % 3 0: circles detect_circles(preprocess(frame), config) coins [classify_coin(frame, *c, config) for c in circles] draw_result(frame, coins) cv2.imshow(coin detector, frame)帧间隔3是一个经验值硬币在画面中的移动速度通常不会在3帧内产生明显位移但性能能提升接近3倍。需要注意HoughCircles对连续帧的抖动会直接在圆心坐标上表现出来实际项目里要么对圆心做卡尔曼滤波要么接受这个抖动不在画面上显示原始坐标。6.2 从固定面值到多国硬币用HOG特征替代半径分档半径分档对国内硬币有效但一旦要识别五国硬币混合的场景就抓瞎了——直径相近、颜色相近的硬币很容易串。更通用的做法是将检测到的硬币归一化到统一尺寸提取HOG特征或颜色直方图训练一个SVM分类器类别就是各面值。def extract_features(coin_roi): # 归一化到64x64半径特征不再参与统一尺度下分类 resized cv2.resize(coin_roi, (64, 64)) gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) # HOG描述子长度 9个方向直方图块 hog cv2.HOGDescriptor((64, 64), (16, 16), (8, 8), (8, 8), 9) features hog.compute(gray).flatten() return features这个改造保留检测层的HoughCircles只替换分类层整体的项目结构完全不用动这得益于最初的分层设计。唯一需要注意的是一枚硬币要至少采集50个不同角度的正样本SVM才能对旋转保持鲁棒硬币图案本身无方向性数字、花纹朝向随机HOG对这种「旋转不变」需求要做的样本增强比想象中多。6.3 从识别到统计金额复核与异常告警最后一个落地技巧是把结果从「识别」升级为「复核」。实际做自动售货机找零检测时机器需要知道「用户投了多少钱」和「找零后应该吐多少钱」是否对得上。改造思路是在识别循环结束后把总额和预设的期望金额做比对不一致时输出告警信息def check_amount(coins, expected2.0): actual sum(coin.value for coin in coins) if abs(actual - expected) 1e-6: print(f[WARN] 金额不一致实际投币 {actual} 元期望 {expected} 元差 {expected - actual} 元)这一步看起来简单实际做的时候要想清楚硬币是否可能重叠遮挡——重度重叠时漏检是不可避免的单纯从「金额对不上」没法区分「漏检」还是「用户投少了」。这个坑项目里没处理但用到真实场景时一定要考虑常见的缓解手段是加一个红外对射传感器做二次确认而不是只靠视觉。这个项目整体拆下来的感觉是结构清晰、依赖轻、改造成本低作为OpenCV入门的完整闭环项目非常合格。我自己后来做机器视觉分拣设备时虽然把HoughCircles换成了轮廓椭圆拟合但「预处理、检测、分类」三层解耦的思路一直保留了下来从那以后我每次写CV项目都会先画清楚三层边界再动手写代码调参时省下的时间不可估量。希望这篇笔记能帮你把项目跑起来并在你的场景里改造得顺手。本文还有配套的精品资源点击获取