ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OpenCV硬币检测识别实战:从图像处理到金额自动计算

OpenCV硬币检测识别实战:从图像处理到金额自动计算 简介这是一份基于Python与OpenCV的硬币检测与识别项目资源面向计算机视觉初学者和OpenCV实战学习者可解决照片中硬币的自动计数与总价值估算问题适合用来入门图像处理或完成课程设计。压缩包共13个文件由12张jpg图像和1个py脚本组成整体约7.39MB其中图像部分包括原始照片、模板图像、测试图像和检测结果图py脚本则是完整的检测识别程序便于直接运行和对照调试。目前已有2242人学习下载实践参考价值较高尤其适合需要在实验中快速验证算法的开发者和学生。通过学习读者可以掌握图像预处理、边缘检测、圆形目标定位、特征匹配与小物体计数等常用技术路线理解从图像输入到价值判断的完整流程也能基于这套代码进一步扩展用于其他圆形物体识别场景。 先聊点实际的。我当初做这个硬币检测与识别项目起因是帮朋友写一个自动数零钱的小工具——就是拍一张照片程序自己告诉我里面有多少钱。听起来挺简单对吧不就是找圆、看大小、算面值嘛。真动手之后才发现找圆这一步就够喝一壶的反光、阴影、硬币挨得太近、背景乱七八糟全是坑。而看大小算面值还得先解决像素和真实尺寸之间怎么换算的问题。这篇文章就把我完整跑通的思路、代码和踩坑记录都摊开讲给正在做 python-opencv 硬币检测识别的同学一个可以直接参考的路径。先说清楚一件事检测和识别是两个环节。检测负责定位告诉你图像里第几个位置有一枚硬币半径是多少像素识别负责分类告诉你这枚硬币是1元、5角还是1角。两个环节的错误是会叠加的圆没找准后面面值必错圆找准了但分类逻辑太糙照样数错钱。所以每一步都得稳。1. 先拆开找硬币和认面值这个项目的第一道坎1.1 检测和识别是两件事很多人一上来就搜硬币识别然后指望一个函数把结果全吐出来这其实是把两个问题搅在一起了。检测环节说白了就是目标定位。OpenCV 里典型的做法有两种一是用 HoughCircles 做圆形检测二是用 findContours 找轮廓再拟合圆。我不太建议一上来就上深度学习原因很简单硬币是高度规矩的圆形物体传统图像处理在受控环境下完全够用而且可解释性强出了问题你能看到是边缘断了还是阈值错了深度模型给不了这种可调试性。识别环节则是另一套逻辑。硬币的面值判断依据有三个维度按可靠程度排序物理尺寸、颜色、表面图案/文字。物理尺寸最可靠因为人民币硬币的直径是官方固定的只要知道像素和真实尺寸的换算比例量一下半径就能确定面值范围。颜色是第二可靠的维度5角是金黄色1元和大部分1角是银白色。最不可靠的是图案识别——硬币表面磨损、反光、角度变化都会严重影响模板匹配或OCR的效果所以我把图案识别放在了最后的扩展方案里而不是主方案。1.2 识别依据尺寸、颜色、图案的优先级拿我国目前常见的硬币来说直径参数大致是面值直径材质颜色备注1元25.0mm钢芯镀镍银白色新版硬币5角20.5mm钢芯镀铜合金金黄色新版硬币1角19.0mm不锈钢银白色2019年新版注意这里有个容易踩的坑1角和5角的直径只差1.5mm在普通分辨率图像里可能只差十个像素左右单纯靠半径阈值很容易混淆所以我后面增加了一道颜色校验逻辑专门区分这两种面值。把识别依据排个优先级先看尺寸尺寸落在模糊区间的再用颜色兜底。图案文字识别这种重方案只有在硬币版本特别多、或者必须识别年份版别的需求出现时才值得上。1.3 传统CV还是深度学习我为什么选了前者我当时考虑过两套方案一套是本文讲的传统方法另一套是用 YOLO 之类的目标检测模型直接检测并分类硬币。选传统方法的核心原因是工程量匹配。我需要处理的场景非常固定硬币平铺在深色底板上相机固定在正上方固定高度拍摄。在这个前提下硬币就是一个标准圆尺寸是已知的颜色是明确的传统CV可以做到又快又稳。深度学习的优势在复杂场景——硬币随意堆叠、任意姿态、光照剧烈变化、背景杂乱——那种场景下 YOLO 的鲁棒性确实碾压传统方法。但代价是你需要标注几百张图片、训练模型、调参数还要面对新的误检方式。对一个数零钱的需求来说性价比太低了。如果你的场景不再满足平铺、正拍、背景可控这几个条件别硬用传统方法换检测模型是更明智的选择。这是后话我放到最后一节展开。2. 拍摄环境没你想的那么随便背景、光源和镜头位置2.1 环境依赖安装这部分最简单直接在 Python 环境里装 opencv-python 和 numpy 就行pip install opencv-python numpy如果需要读取中文路径下的图片建议加装 opencv-contrib-python 并配合 imdecode 读取这个坑我后面细说。版本方面我用的是 OpenCV 4.xAPI 和 3.x 差别不大老代码基本能直接跑。2.2 用底板和光源把变量锁死我第一版代码是在办公桌上随手拍的结果硬币和木质桌面的纹理混在一起Canny 边缘检测出来的全是木纹线Hough 找圆找出来一堆乱七八糟的结果。后来我找了个深色的纯色鼠标垫当底板误检率立刻大幅下降。这里有个原则图像处理项目里能通过控制拍摄环境解决的问题不要留给算法硬扛。底板的颜色最好和硬币颜色有明显区分。深色底配银色硬币效果最好因为银白色硬币在暗背景下边缘对比度极高。如果你要重点处理金色5角深蓝或黑色的背景也比浅色背景更合适。光源方面均匀光比强光更重要。我当时用了一盏普通台灯从侧上方照明结果每枚硬币靠近光源的一半亮、另一半有阴影Canny 只能检出半圈边缘圆检测全都偏了。后来我把光源改成漫反射环境光散射光均匀照明或者在拍摄时尽量让房间整体明亮、避免单一强光源直射效果就好了很多。2.3 相机镜头要垂直于桌面镜头的拍摄角度会影响硬币成像形状。硬币本身是圆形的但如果相机没有正对桌面圆会变成椭圆HoughCircles 的半径检测就会飘面值判断自然出错。我的建议是镜头光轴尽量垂直于桌面固定高度拍摄。如果实在做不到完全垂直那就保持固定角度然后用一个已知尺寸的标定物做换算——标定物能帮你直接把像素和实际毫米数的比例定死拍摄角度造成的系统误差在换算后会稳定在一个固定范围内反而不影响分类。3. 从噪声到干净边缘预处理与候选圆提取3.1 灰度化、滤波与光照均衡预处理的目标很纯粹让硬币的边缘尽量干净地被检测出来同时让背景纹理的干扰降到最低。第一步是把 BGR 图像转成灰度图。第二步是滤波。我实测下来高斯滤波核大小取 (9, 9)sigma 取 2对硬币这类大目标来说去噪效果和边缘保留之间的平衡最好。中值滤波也可以它对椒盐噪声更有效但边缘会偏钝一点看你的图像情况。如果你拍摄的光照不均匀可以加一步光照均衡处理。当时我的照片左侧亮右侧暗硬币边缘在暗侧几乎断了。解决办法是用形态学的黑帽/顶帽变换分离出光照分量或者直接对灰度图做高斯模糊后与原图相减。OpenCV 里最简单粗暴的写法blurred cv2.GaussianBlur(gray, (31, 31), 0) equalized cv2.subtract(gray, blurred)这行代码的效果是把缓慢变化的光照背景去掉只保留局部变化的高频信息硬币边缘的对比度会被拉回来。很实用尤其是在环境光不受控的场合。3.2 Canny边缘检测的参数门道Canny 的阈值是新手最容易卡住的地方。它有两个阈值高阈值和低阈值。高于高阈值的像素一定被当成边缘低于低阈值的肯定丢弃介于两者之间的只有与强边缘相连才会被保留。我的经验是高阈值和低阈值的比例放在 1:2 到 1:3 之间比较合适。比如高阈值 100、低阈值 50或者 150、60。硬币场景下我常用(100, 200)这个组合边缘干净、断点少。阈值设太低木纹、阴影边缘全进来Hough 投票就乱了阈值设太高硬币边缘断成碎片圆检测可能直接失败。参数的直觉不是调大或调小而是想清楚你当前图像的问题是哪一种再决定往哪边调。3.3 HoughCircles vs 轮廓拟合这是硬币定位的核心环节我两种方法都写了因为实际使用中它们互补。方案一HoughCirclescircles cv2.HoughCircles( gray_gaussian, cv2.HOUGH_GRADIENT, dp1.2, minDist50, param1100, param260, minRadius20, maxRadius60 )HoughCircles 的核心思想是每个边缘点都可以在所有候选圆心位置投票投票数大于阈值的圆心和半径组合才会被保留。所以dp1.2累加器分辨率一般取 1~2 即可越大越快但可能丢小圆。minDist两个圆心之间的最小距离。如果比硬币直径的像素值还小同一枚硬币会被检出好几个重叠圆。这个参数是我踩坑最多的后面细讲。param1内部 Canny 的高阈值。注意 HoughCircles 内部会自己做一次 Canny所以如果你在外部已经做过预处理这里的高阈值不用太小我的经验是 100 左右起步。param2累加器投票阈值。越小检出的圆越多误检也越多越大越严格但可能漏掉边缘不太完美的硬币。60 到 80 是比较常用的区间。minRadius/maxRadius候选圆的半径范围。一定要尽量量准。我在代码里加了一个预处理步骤先用轮廓分析预估硬币的像素半径再把这个范围喂给 HoughCircles误检率直接下降一大截。方案二轮廓分析 圆形度校验_, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area cv2.contourArea(cnt) if area 500: continue perimeter cv2.arcLength(cnt, True) circularity 4 * np.pi * area / (perimeter * perimeter) if circularity 0.8: continue (x, y), radius cv2.minEnclosingCircle(cnt)轮廓法的核心是先用 OTSU 自动阈值把图像转成二值图然后找外轮廓用圆形度公式过滤掉非圆形状。圆形度的定义是4π × 面积 / 周长²完美圆的圆形度是 1一般大于 0.8 的都可以认为接近圆。这两个方案怎么选如果硬币之间离得比较开、图像干净HoughCircles 效率高如果硬币有轻微遮挡、边缘有缺失或者背景中有大量非圆物体轮廓法更稳。我最终的项目里是两套都跑Hough 结果优先轮廓法做兜底两个结果用圆心坐标合并去重。4. 用一枚1元硬币标定像素比再把半径换算成面值4.1 标定像素是怎么变成毫米的要判断面值光知道半径的像素值没用因为照片的像素量和拍摄距离直接相关。我一开始犯过这个错直接在测试图上量出 1 元硬币半径大约 30 像素就拿半径 30 以上是 1 元去套所有照片换一个拍摄距离全变了。正确做法是做一个标定在拍摄画面里放一枚已知尺寸的硬币我用 1 元直径 25mm检测到它的像素直径后算出比例scale ref_diameter_px / 25.0 # 单位像素/毫米把每枚硬币的像素直径除以 scale就得到真实直径毫米数。后面只要拍摄条件不变这个比例就不用重新算。标定有两种思路第一种是把参考硬币放在画面内自动检测并计算 scale第二种是拍一张空白背景带标定物的图预先测好 scale 写死在配置里。我用的第二种——因为我的相机是固定安装的拍一次标定图就够了把 scale 存成配置文件后续所有图片共用同一个比例逻辑更简单。如果你的拍摄距离每次都会变那就必须走第一种让标定逻辑随图片走。4.2 直径阈值表与分类逻辑拿到毫米直径后分类逻辑就清晰了。我实测的直径分布大概是真实面值实测直径范围mm中间阈值1角18.5 ~ 19.519.85角20.0 ~ 21.021.51元24.5 ~ 25.523.0注意一个问题1角19mm和 5角20.5mm只差 1.5mm如果标定稍微偏一点或者硬币因为拍摄角度有一点形变这两个就会互相混淆。所以我的逻辑里做了一个双阈值区间设计直径 23mm → 判为 1元直径在 21.0 ~ 23mm 之间 → 判为 5角直径在 19.8 ~ 21.0mm 之间 → 候选5角/1角交给颜色判断直径 19.8mm → 判为 1角这个设计比单纯一个阈值稳得多。落在中间模糊带的硬币再去提取它的颜色特征做二次判断。4.3 用颜色信息兜底金色占比判断5角硬币是金黄色钢芯镀铜合金1角硬币是银白色不锈钢在 HSV 空间里差异明显。我之前在 BGR 空间直接判断颜色吃了不少苦头因为光照一变金色在 BGR 三通道的比值就乱了。换成 HSV 空间后用色调 H 通道就能轻松区分。判断逻辑是把检测到的圆区域裁剪出来转 HSV 空间统计 H 值在 10~35 之间、且饱和度 S 比较高大于 60的像素占比。如果金色像素在圆区域内占比超过 10%就判为 5角否则判为 1角。hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask_gold cv2.inRange(hsv, (10, 60, 60), (35, 255, 255)) gold_ratio cv2.countNonZero(mask_gold) / (np.pi * radius * radius)这个逻辑在大多数室内光线下都稳定。唯一的坑是硬币反光会产生高光白斑但白斑的饱和度低不会被算进金色像素里所以实际影响不大。反过来如果环境光偏暖银白色硬币也可能被照出一点淡金色所以我只把这个判断放在直径模糊带不作为第一判断依据——这是不少实现容易搞错的地方把颜色当成主判断然后被一个偏黄的灯光坑到怀疑人生。5. 可直接复用的完整代码读图、找圆、分类、算总额下面是我最终落地版本的简化代码保留了完整的流程图预处理、候选圆提取、圆形度校验、标定、面值分类、总额累加。你可以直接抄去跑。import cv2 import numpy as np # ---------- 全局配置 ---------- SCALE_FILE scale.txt # 标定结果文件单位像素/毫米 REF_DIAMETER_MM 25.0 # 标定硬币直径1元硬币25mm def load_scale(): with open(SCALE_FILE, r) as f: return float(f.read().strip()) def save_scale(scale): with open(SCALE_FILE, w) as f: f.write(str(scale)) def read_image(path): # 用imdecode读取支持中文路径 stream np.fromfile(path, dtypenp.uint8) return cv2.imdecode(stream, cv2.IMREAD_COLOR) def detect_coins(image, gray, scale): # 方案一HoughCircles 检测 circles None if scale is not None: min_r int(19.0 * scale * 0.85) max_r int(25.0 * scale * 1.15) circles cv2.HoughCircles( gray, cv2.HOUGH_GRADIENT, dp1.2, minDistint(25.0 * scale * 1.2), param1100, param260, minRadiusmin_r, maxRadiusmax_r ) detections [] if circles is not None: circles np.round(circles[0]).astype(int) for (x, y, r) in circles: detections.append((x, y, r, hough)) # 方案二轮廓分析 圆形度过滤 blurred cv2.GaussianBlur(gray, (9, 9), 2) _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area cv2.contourArea(cnt) if area 1000: continue perimeter cv2.arcLength(cnt, True) if perimeter 0: continue circularity 4 * np.pi * area / (perimeter * perimeter) if circularity 0.8: continue (x, y), r cv2.minEnclosingCircle(cnt) detections.append((int(x), int(y), int(r), contour)) # 去重合并两种方案检测到的同一个圆 result [] for d in detections: x, y, r, source d duplicate False for rx, ry, rr, rs in result: if (x - rx) ** 2 (y - ry) ** 2 (max(r, rr) * 0.5) ** 2: duplicate True break if not duplicate: result.append(d) return result def classify_coin(img, x, y, r, scale): # 计算直径像素 diameter_px 2 * r diameter_mm diameter_px / scale # 裁剪圆区域用于颜色判断 mask np.zeros(img.shape[:2], dtypenp.uint8) cv2.circle(mask, (x, y), r, 255, -1) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) gold_mask cv2.inRange(hsv, (10, 60, 60), (35, 255, 255)) gold_ratio cv2.countNonZero(cv2.bitwise_and(gold_mask, gold_mask, maskmask)) / (np.pi * r * r) # 面值判断直径优先颜色兜底 if diameter_mm 23.0: return 1.0 elif diameter_mm 21.0: return 0.5 elif diameter_mm 19.8: return 0.5 if gold_ratio 0.1 else 0.1 else: return 0.1 def main(): img read_image(coins.jpg) if img is None: print(图片读取失败) return gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 标定流程第一次运行时用1元硬币标定 scale None try: scale load_scale() except FileNotFoundError: print(未找到标定文件请在画面正中放置一枚1元硬币3秒后自动标定...) cv2.imshow(calibration, img) cv2.waitKey(3000) # 这里简化处理手动用Hough检测画面中1元硬币直径 # 实际场景可以增加人工确认逻辑 ref_circles cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp1.2, minDist50, param1100, param260, minRadius30, maxRadius80) if ref_circles is not None: _, _, ref_r np.round(ref_circles[0][0]).astype(int) scale (2 * ref_r) / REF_DIAMETER_MM save_scale(scale) print(f标定完成scale{scale:.3f} 像素/毫米) else: print(标定失败未检测到参考硬币) return coins detect_coins(img, gray, scale) total 0.0 for (x, y, r, source) in coins: value classify_coin(img, x, y, r, scale) total value label {1.0: 1元, 0.5: 5角, 0.1: 1角}[value] cv2.circle(img, (x, y), r, (0, 255, 0), 2) cv2.putText(img, f{label}, (x - 20, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) print(f检测到 {len(coins)} 枚硬币总金额{total:.1f} 元) cv2.imshow(result, img) cv2.waitKey(0) if __name__ __main__: main()代码里的detect_coins同时跑两种检测方案再用去重逻辑合并这样 Hough 的效率和轮廓法的容错性都能兼顾。分类逻辑就是我上面说的双阈值加颜色兜底。read_image用 imdecode 是为了兼容中文路径——很多同学在 Windows 上图片路径带中文直接用 cv2.imread 会返回 None这个问题排查起来非常隐蔽。6. 我踩过的四个坑重复圆、断边、误检与面值混淆6.1 同一枚硬币被重复检测出多个圆这个问题几乎每个用 HoughCircles 的人都会遇到。表现是一枚硬币上叠着两三个半径相近的圆圆心稍微偏移几像素。原因是minDist设得太小了小于硬币的实际直径像素。排查方法很简单把检测到的所有圆都画出来看一眼。修复方法是把minDist调到硬币直径像素的 1.2~1.5 倍。如果你不确定硬币直径像素是多少先跑一次轮廓分析估算一个平均半径再去设 minDist。我代码里用int(25.0 * scale * 1.2)就是为了让这个参数自动跟着标定结果走省得每次换拍摄距离都要重调。6.2 边缘断成两截导致圆检测失败硬币迎光面和背光面的边缘灰度差异很大Canny 只会把对比度够高的部分判成边缘于是出现半圆边缘。Hough 在这种输入下会漏检或者检出一个偏小的圆。我最终的修复方案是组合拳首先用高斯模糊去掉细小噪声然后用顶帽变换/减背景光照的做法拉平光照最后把 Canny 的阈值从 (50, 150) 调到 (100, 200)——提高低阈值会减少微小边缘的干扰让主要边缘更突出。如果你发现硬币边缘本身太弱调高 Canny 低阈值反而会断得更厉害这时先检查光照是不是要修而不是继续加滤波。6.3 背景纹理被误检成圆办公室桌面、报纸、木纹板——这些表面都有大量类圆纹理Hough 会把它们当成硬币。我的解决顺序是换深色纯色底板从源头消灭背景纹理。用圆形度过滤非圆轮廓直接丢弃。限制候选圆半径范围木纹里那些半圆纹理的半径往往和真实硬币半径差很多一限制就过滤掉了。这三步做完误检率降到了几乎为零。核心思路是你先用物理知识把搜索空间缩到最小剩下的交给算法。6.4 5角和1角直径相近分类一直在边缘横跳这个问题在前期最折磨人。1角直径 19mm5角直径 20.5mm差距本来就小如果标定稍有偏差阈值 19.8mm 附近的硬币就会反复横跳。我最后的处理就是前面说的设置一个 19.8~21.0mm 的模糊区间区间内的硬币用金色像素占比来判断。金色占比超过 10% 判 5角否则判 1角。在自然室内光下这套逻辑很稳。有一次在暖黄灯光下银白色的 1 角被照出了明显金色调金色占比超过了 10%被误判成 5角。后来我加了一个条件检测金色区域内像素的平均饱和度是否够高——反光引起的假金饱和度普通偏低而 5角本身的合金颜色饱和度明显更高。两种金色从肉眼上你看得出来差别在 HSV 的 S 通道上更是清清楚楚。7. 方案升级思路如果场景变复杂还是得上模型我前面反复强调传统方法好用前提是场景受限。如果你的实际需求变成一堆硬币随意堆叠在一起从桌面上杂乱环境下直接识别需要识别出硬币的年份版别传统方案的短板就会暴露得很明显。堆叠硬币Hough 找不到完整圆轮廓法也会被粘连搞崩溃这时候需要分水岭算法做粘连分割或者直接用目标检测模型。复杂背景圆检测会把大量类圆物体当成候选过滤逻辑越写越复杂误差不可控。区分年份版别尺寸和颜色完全不够用必须识别正反面的文字图案。硬币表面磨损严重传统模板匹配基本不可靠需要用小数据集微调一个 CNN 分类器或者用现成的 OCR 模型识别图案里的数字。我的建议是先用本文这套方法把数据收集和场景规范做起来——控制背景、固定机位、准备标定物这些工作无论你最后用不用深度学习都是必须的。等你积累了足够多的真实图片再训练一个轻量目标检测模型去替换传统检测环节识别的部分用裁剪出来的硬币图像微调分类模型。这条路走起来平滑很多因为你已经对问题有了足够深的理解知道模型在哪一步帮到你、哪一步反而会引入新的麻烦。最后再分享一个我个人的经验做这类图像处理项目可视化中间结果比调算法参数更节省时间。我当时把灰度图、光照均衡图、Canny 边缘图、Hough 结果图、轮廓二值图全都用 imshow 弹出来看一眼就能定位是预处理的问题、检测的问题还是分类阈值的问题。如果只看最终结果调参你永远在赌运气。另一个小技巧是标定文件单独存一份拍摄距离不变就不需要重新标定但每次跑批量图片前都先在结果图上随便画一两个圆看看半径是否合理——标定错了后面全是白算。本文还有配套的精品资源点击获取
返回列表