硬币分拣模拟系统
一、实际应用场景描述
在银行、超市、自动售货机、公交投币机等场景中,经常需要对大量混合硬币进行快速分拣和统计。传统的人工分拣方式存在效率低、易出错、人力成本高等问题。本系统基于工业机器视觉技术,模拟实现硬币的自动检测、按直径分类、数量统计和金额计算功能,为自动化硬币处理设备提供技术参考。
典型应用场景:
- 银行金库硬币清分
- 超市收银台零钱整理
- 自动售货机硬币识别
- 公交公司票款统计
- 硬币回收机
二、引入痛点
- 人工分拣效率低:人工分拣每分钟约200-300枚,且易疲劳
- 分类错误率高:相似直径硬币(如1元和1角)容易混淆
- 统计工作繁琐:需要逐枚计数,易漏计重计
- 成本持续上升:随着人工成本上涨,运营压力增大
- 卫生安全隐患:多人接触现金存在卫生风险
三、核心逻辑讲解
系统架构图
输入图像/视频流 → 图像预处理 → 硬币检测(边缘+轮廓) → 特征提取(直径+面积) → 分类决策 → 统计计数 → 金额计算 → 结果输出
关键技术点
- 霍夫圆检测:利用 "cv2.HoughCircles"检测图像中的圆形物体
- 几何特征计算:通过像素比例换算实际直径
- 多特征融合分类:结合面积、周长、圆度综合判断
- 动态校准机制:支持不同分辨率和拍摄距离下的参数调整
- 实时统计引擎:维护各类型硬币的累计计数
分类标准(以中国硬币为例)
面值 直径(mm) 特征描述 1元 25.0 最大,银白色 5角 20.5 中等,金黄色 1角 19.0 较小,铝色
四、代码模块化实现
项目结构
coin_sorting_system/ ├── main.py # 主程序入口 ├── config.py # 配置文件 ├── detector/ │ ├── init.py │ ├── preprocessor.py # 图像预处理模块 │ ├── coin_detector.py # 硬币检测模块 │ └── classifier.py # 硬币分类模块 ├── analyzer/ │ ├── init.py │ └── statistics.py # 统计分析模块 ├── utils/ │ ├── init.py │ ├── image_utils.py # 图像工具函数 │ └── calibration.py # 校准工具 ├── gui/ │ ├── init.py │ └── visualizer.py # 可视化界面 ├── test_images/ # 测试图片目录 ├── output/ # 输出目录 ├── requirements.txt └── README.md
- 配置文件 (config.py)
""" 系统配置参数模块 包含硬币检测、分类、统计的各项可调参数 """
硬币规格定义 (中国现行流通硬币)
COIN_SPECS = { '1_yuan': { 'name': '1元硬币', 'diameter_mm': 25.0, 'value': 1.0, 'color': (200, 200, 210), # 银白色BGR 'tolerance': 0.5, # 直径容差(mm) }, '5_jiao': { 'name': '5角硬币', 'diameter_mm': 20.5, 'value': 0.5, 'color': (0, 180, 255), # 金黄色BGR 'tolerance': 0.4, }, '1_jiao': { 'name': '1角硬币', 'diameter_mm': 19.0, 'value': 0.1, 'color': (180, 180, 180), # 铝色BGR 'tolerance': 0.3, } }
检测参数
DETECTION = { 'dp': 1.2, # 累加器分辨率与图像分辨率的反比 'min_dist': 30, # 检测到的圆心之间的最小距离 'param1': 100, # Canny边缘检测的高阈值 'param2': 30, # 累加器阈值,越小检测越多 'min_radius': 15, # 最小圆半径(像素) 'max_radius': 80, # 最大圆半径(像素) }
校准参数
CALIBRATION = { 'reference_diameter_mm': 25.0, # 参考硬币直径(mm) 'reference_pixel_diameter': 125, # 参考硬币像素直径 'pixel_to_mm_ratio': None, # 像素到毫米的转换比率(运行时计算) }
统计参数
STATISTICS = { 'enable_persistence': True, # 启用持久化统计 'save_interval': 60, # 保存间隔(秒) }
- 图像预处理模块 (detector/preprocessor.py)
""" 图像预处理模块 负责将原始图像转换为适合硬币检测的状态 """
import cv2 import numpy as np from config import DETECTION
class ImagePreprocessor: """图像预处理器类"""
def __init__(self):
pass
def enhance_contrast(self, image: np.ndarray) -> np.ndarray:
"""
增强图像对比度,突出硬币边缘
Args:
image: BGR格式彩色图像
Returns:
对比度增强后的图像
"""
# 转换到LAB色彩空间
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
# 对L通道应用CLAHE自适应直方图均衡化
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
l_enhanced = clahe.apply(l)
# 合并通道并转回BGR
enhanced_lab = cv2.merge([l_enhanced, a, b])
enhanced_bgr = cv2.cvtColor(enhanced_lab, cv2.COLOR_LAB2BGR)
return enhanced_bgr
def reduce_noise(self, image: np.ndarray) -> np.ndarray:
"""
降噪处理,减少背景干扰
Args:
image: 输入图像
Returns:
降噪后的图像
"""
# 高斯模糊
blurred = cv2.GaussianBlur(image, (5, 5), 0)
return blurred
def apply_edge_enhancement(self, image: np.ndarray) -> np.ndarray:
"""
边缘增强,使硬币轮廓更加明显
Args:
image: 输入图像
Returns:
边缘增强后的图像
"""
kernel = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]], dtype=np.float32)
enhanced = cv2.filter2D(image, -1, kernel)
return enhanced
def preprocess_pipeline(self, image: np.ndarray) -> np.ndarray:
"""
执行完整的预处理流水线
Args:
image: 原始BGR图像
Returns:
预处理后的图像
"""
# Step 1: 对比度增强
enhanced = self.enhance_contrast(image)
# Step 2: 降噪
denoised = self.reduce_noise(enhanced)
# Step 3: 边缘增强
final = self.apply_edge_enhancement(denoised)
return final
def create_detection_mask(self, image: np.ndarray, method: str = 'adaptive') -> np.ndarray:
"""
创建硬币检测掩码,排除背景干扰
Args:
image: 输入图像
method: 掩码生成方法 ('adaptive', 'threshold', 'color')
Returns:
二值掩码图像
"""
if method == 'adaptive':
# 自适应阈值方法
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
mask = cv2.adaptiveThreshold(
gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV,
11, 2
)
elif method == 'threshold':
# 全局阈值方法
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
else:
# 基于颜色的掩码
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
lower_bound = np.array([0, 0, 100])
upper_bound = np.array([180, 50, 255])
mask = cv2.inRange(hsv, lower_bound, upper_bound)
# 形态学操作清理掩码
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
return mask
3. 硬币检测模块 (detector/coin_detector.py)
""" 硬币检测模块 使用多种方法检测图像中的硬币 """
import cv2 import numpy as np from typing import List, Dict, Optional, Tuple from config import DETECTION, CALIBRATION from detector.preprocessor import ImagePreprocessor
class CoinDetector: """硬币检测器类"""
def __init__(self):
self.preprocessor = ImagePreprocessor()
self.coins_detected = [] # 存储检测到的硬币信息
def detect_by_hough_circles(self, image: np.ndarray) -> List[Dict]:
"""
使用霍夫圆检测算法检测硬币
Args:
image: 预处理后的图像
Returns:
检测到的硬币列表,每个硬币包含位置和半径信息
"""
coins = []
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 应用高斯模糊减少噪声
gray_blurred = cv2.GaussianBlur(gray, (9, 9), 2)
# 霍夫圆检测
circles = cv2.HoughCircles(
gray_blurred,
cv2.HOUGH_GRADIENT,
dp=DETECTION['dp'],
minDist=DETECTION['min_dist'],
param1=DETECTION['param1'],
param2=DETECTION['param2'],
minRadius=DETECTION['min_radius'],
maxRadius=DETECTION['max_radius']
)
if circles is not None:
circles = np.uint16(np.around(circles))
for circle in circles[0, :]:
x, y, r = int(circle[0]), int(circle[1]), int(circle[2])
coin_info = {
'center_x': x,
'center_y': y,
'radius': r,
'diameter_pixels': 2 * r,
'method': 'hough_circle'
}
coins.append(coin_info)
return coins
def detect_by_contour_analysis(self, image: np.ndarray, mask: np.ndarray = None) -> List[Dict]:
"""
通过轮廓分析检测硬币
Args:
image: 输入图像
mask: 可选的掩码图像
Returns:
检测到的硬币列表
"""
coins = []
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
if mask is not None:
# 应用掩码
gray = cv2.bitwise_and(gray, gray, mask=mask)
# 二值化
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 形态学操作
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
# 查找轮廓
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
# 计算轮廓面积
area = cv2.contourArea(contour)
# 过滤太小的轮廓
if area < 500:
continue
# 计算最小外接圆
(x, y), radius = cv2.minEnclosingCircle(contour)
center = (int(x), int(y))
radius = int(radius)
# 计算圆度 (轮廓面积与外接圆面积之比)
circularity = area / (np.pi * radius * radius)
# 过滤非圆形物体
if circularity < 0.7:
continue
# 过滤过大的轮廓
if radius > DETECTION['max_radius'] or radius < DETECTION['min_radius']:
continue
coin_info = {
'center_x': center[0],
'center_y': center[1],
'radius': radius,
'diameter_pixels': 2 * radius,
'area': area,
'circularity': circularity,
'method': 'contour_analysis'
}
coins.append(coin_info)
return coins
def merge_detections(self, hough_coins: List[Dict], contour_coins: List[Dict]) -> List[Dict]:
"""
合并两种检测方法的结果,去除重复检测
Args:
hough_coins: 霍夫圆检测结果
contour_coins: 轮廓分析结果
Returns:
合并后的硬币列表
"""
merged = []
used_contour_indices = set()
# 首先添加所有霍夫圆检测结果
for hough_coin in hough_coins:
merged.append(hough_coin)
# 然后添加未与霍夫圆检测重叠的轮廓检测结果
for i, contour_coin in enumerate(contour_coins):
is_duplicate = False
for hough_coin in hough_coins:
# 计算两个圆心之间的距离
dist = np.sqrt(
(contour_coin['center_x'] - hough_coin['center_x'])**2 +
(contour_coin['center_y'] - hough_coin['center_y'])**2
)
# 如果距离小于两圆半径之和的一半,认为是同一枚硬币
min_dist_threshold = (contour_coin['radius'] + hough_coin['radius']) * 0.4
if dist < min_dist_threshold:
is_duplicate = True
break
if not is_duplicate:
merged.append(contour_coin)
used_contour_indices.add(i)
return merged
def detect(self, image: np.ndarray, use_mask: bool = True) -> List[Dict]:
"""
执行完整的硬币检测流程
Args:
image: 输入BGR图像
use_mask: 是否使用掩码辅助检测
Returns:
检测到的所有硬币信息列表
"""
self.coins_detected = []
# 预处理
preprocessed = self.preprocessor.preprocess_pipeline(image)
# 创建掩码
mask = None
if use_mask:
mask = self.preprocessor.create_detection_mask(preprocessed)
# 方法1: 霍夫圆检测
hough_coins = self.detect_by_hough_circles(preprocessed)
print(f"[INFO] 霍夫圆检测找到 {len(hough_coins)} 个圆形")
# 方法2: 轮廓分析
contour_coins = self.detect_by_contour_analysis(preprocessed, mask)
print(f"[INFO] 轮廓分析找到 {len(contour_coins)} 个候选")
# 合并结果
merged_coins = self.merge_detections(hough_coins, contour_coins)
print(f"[INFO] 合并后共检测到 {len(merged_coins)} 枚硬币")
self.coins_detected = merged_coins
return merged_coins
def calculate_diameter_mm(self, pixel_diameter: float) -> float:
"""
将像素直径转换为实际毫米直径
Args:
pixel_diameter: 像素单位的直径
Returns:
毫米单位的直径
"""
if CALIBRATION['pixel_to_mm_ratio'] is None:
# 使用默认比例计算
ratio = CALIBRATION['reference_diameter_mm'] / CALIBRATION['reference_pixel_diameter']
else:
ratio = CALIBRATION['pixel_to_mm_ratio']
return pixel_diameter * ratio
def calibrate(self, known_diameter_mm: float, measured_pixel_diameter: float):
"""
根据已知直径的硬币进行系统校准
Args:
known_diameter_mm: 已知的实际直径(mm)
measured_pixel_diameter: 测量得到的像素直径
"""
CALIBRATION['pixel_to_mm_ratio'] = known_diameter_mm / measured_pixel_diameter
CALIBRATION['reference_diameter_mm'] = known_diameter_mm
CALIBRATION['reference_pixel_diameter'] = measured_pixel_diameter
print(f"[INFO] 校准完成,像素到毫米比例: {CALIBRATION['pixel_to_mm_ratio']:.4f}")
4. 硬币分类模块 (detector/classifier.py)
""" 硬币分类模块 根据检测到的特征对硬币进行分类 """
import cv2 import numpy as np from typing import List, Dict, Tuple, Optional from config import COIN_SPECS, CALIBRATION from detector.coin_detector import CoinDetector
class CoinClassifier: """硬币分类器类"""
def __init__(self):
self.detector = CoinDetector()
self.classification_history = [] # 分类历史记录
def classify_by_diameter(self, coin_info: Dict) -> Tuple[str, float, float]:
"""
基于直径进行硬币分类
Args:
coin_info: 硬币信息字典
Returns:
(分类名称, 置信度, 实际直径mm)
"""
diameter_pixels = coin_info['diameter_pixels']
diameter_mm = self.detector.calculate_diameter_mm(diameter_pixels)
best_match = None
best_confidence = 0.0
best_diff = float('inf')
for coin_type, specs in COIN_SPECS.items():
expected_diameter = specs['diameter_mm']
tolerance = specs['tolerance']
# 计算与预期直径的差距
diff = abs(diameter_mm - expected_diameter)
# 计算置信度 (差距越小,置信度越高)
if diff <= tolerance:
confidence = 1.0 - (diff / tolerance) * 0.5
else:
confidence = max(0.0, 1.0 - (diff - tolerance) / tolerance)
if confidence > best_confidence:
best_confidence = confidence
best_match = coin_type
best_diff = diff
if best_match is None:
best_match = 'unknown'
best_confidence = 0.0
return best_match, best_confidence, diameter_mm
def classify_by_color(self, image: np.ndarray, coin_info: Dict) -> Tuple[str, float]:
"""
基于颜色特征辅助分类
Args:
image: 原始图像
coin_info: 硬币信息字典
Returns:
(分类名称, 颜色置信度)
"""
cx, cy, r = coin_info['center_x'], coin_info['center_y'], coin_info['radius']
# 提取硬币区域
mask = np.zeros(image.shape[:2], dtype=np.uint8)
cv2.circle(mask, (cx, cy), r, 255, -1)
# 计算平均颜色
mean_color = cv2.mean(image, mask=mask)[:3]
best_match = None
best_similarity = 0.0
for coin_type, specs in COIN_SPECS.items():
expected_color = np.array(specs['color'])
detected_color = np.array(mean_color)
# 计算颜色相似度 (欧氏距离归一化)
distance = np.linalg.norm(expected_color - detected_color)
max_distance = np.linalg.norm(np.array([255, 255, 255]))
similarity = 1.0 - (distance / max_distance)
if similarity > best_similarity:
best_similarity = similarity
best_match = coin_type
return best_match, best_similarity
def classify_by_combined_features(self, image: np.ndarray, coin_info: Dict) -> Dict:
"""
使用多特征融合进行分类
Args:
image: 原始图像
coin_info: 硬币信息字典
Returns:
完整的分类结果字典
"""
# 特征1: 直径分类
diameter_class, diameter_conf, actual_diameter = self.classify_by_diameter(coin_info)
# 特征2: 颜色分类
color_class, color_conf = self.classify_by_color(image, coin_info)
# 特征融合决策
# 如果两个特征指向同一类别,置信度相乘
# 否则取较高置信度,但降低权重
if diameter_class == color_class and diameter_class != 'unknown':
final_class = diameter_class
final_confidence = diameter_conf * 0.7 + color_conf * 0.3
elif diameter_conf > color_conf:
final_class = diameter_class
final_confidence = diameter_conf * 0.8
else:
final_class = color_class
final_confidence = color_conf * 0.8
# 构建结果
result = {
'coin_type': final_class,
'confidence': final_confidence,
'actual_diameter_mm': actual_diameter,
'detected_diameter_pixels': coin_info['diameter_pixels'],
'center_position': (coin_info['center_x'], coin_info['center_y']),
'radius': coin_info['radius'],
'diameter_classification': diameter_class,
'diameter_confidence': diameter_conf,
'color_classification': color_class,
'color_confidence': color_conf,
}
# 添加面值信息
if final_class in COIN_SPECS:
result['coin_name'] = COIN_SPECS[final_class]['name']
result['face_value'] = COIN_SPECS[final_class]['value']
else:
result['coin_name'] = '未知硬币'
result['face_value'] = 0.0
return result
def classify_all(self, image: np.ndarray, coins: List[Dict]) -> List[Dict]:
"""
对检测到的所有硬币进行分类
Args:
image: 原始图像
coins: 检测到的硬币列表
Returns:
分类结果列表
"""
results = []
for i, coin in enumerate(coins):
print(f"[INFO] 正在分类第 {i+1}/{len(coins)} 枚硬币...")
classification = self.classify_by_combined_features(image, coin)
results.append(classification)
if classification['coin_type'] != 'unknown':
print(f" → {classification['coin_name']}, "
f"直径: {classification['actual_diameter_mm']:.2f}mm, "
f"置信度: {classification['confidence']:.2%}")
else:
print(f" → 未知硬币, 直径: {classification['actual_diameter_mm']:.2f}mm")
return results
def filter_by_confidence(self, classifications: List[Dict], threshold: float = 0.6) -> List[Dict]:
"""
根据置信度过滤分类结果
Args:
classifications: 分类结果列表
threshold: 置信度阈值
Returns:
过滤后的结果列表
"""
filtered = [c for c in classifications if c['confidence'] >= threshold]
rejected = [c for c in classifications if c['confidence'] < threshold]
if rejected:
print(f"[WARN] {len(rejected)} 枚硬币因置信度低于 {threshold:.0%} 被过滤")
return filtered
5. 统计分析模块 (analyzer/statistics.py)
""" 统计分析模块 负责硬币计数、金额计算和统计报告生成 """
import json import csv from datetime import datetime from typing import List, Dict, Optional from collections import defaultdict from config import COIN_SPECS, STATISTICS
class StatisticsAnalyzer: """统计分析器类"""
def __init__(self):
self.reset_statistics()
self.history_records = []
def reset_statistics(self):
"""重置统计数据"""
self.counts = defaultdict(int) # 各类硬币计数
self.total_value = 0.0 # 总金额
self.total_coins = 0
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!