实现硬币分拣模拟,按直径大小分类,统计金额。

4 阅读10分钟

硬币分拣模拟系统

一、实际应用场景描述

在银行、超市、自动售货机、公交投币机等场景中,经常需要对大量混合硬币进行快速分拣和统计。传统的人工分拣方式存在效率低、易出错、人力成本高等问题。本系统基于工业机器视觉技术,模拟实现硬币的自动检测、按直径分类、数量统计和金额计算功能,为自动化硬币处理设备提供技术参考。

典型应用场景:

  • 银行金库硬币清分
  • 超市收银台零钱整理
  • 自动售货机硬币识别
  • 公交公司票款统计
  • 硬币回收机

二、引入痛点

  1. 人工分拣效率低:人工分拣每分钟约200-300枚,且易疲劳
  2. 分类错误率高:相似直径硬币(如1元和1角)容易混淆
  3. 统计工作繁琐:需要逐枚计数,易漏计重计
  4. 成本持续上升:随着人工成本上涨,运营压力增大
  5. 卫生安全隐患:多人接触现金存在卫生风险

三、核心逻辑讲解

系统架构图

输入图像/视频流 → 图像预处理 → 硬币检测(边缘+轮廓) → 特征提取(直径+面积) → 分类决策 → 统计计数 → 金额计算 → 结果输出

关键技术点

  1. 霍夫圆检测:利用 "cv2.HoughCircles"检测图像中的圆形物体
  2. 几何特征计算:通过像素比例换算实际直径
  3. 多特征融合分类:结合面积、周长、圆度综合判断
  4. 动态校准机制:支持不同分辨率和拍摄距离下的参数调整
  5. 实时统计引擎:维护各类型硬币的累计计数

分类标准(以中国硬币为例)

面值 直径(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

  1. 配置文件 (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, # 保存间隔(秒) }

  1. 图像预处理模块 (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解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!