毕业设计实战:基于机器视觉的药品分拣系统(从算法优化到硬件落地全流程)

57 阅读13分钟

一、项目背景:为什么需要机器视觉药品分拣?

传统药品分拣靠人工,存在两大核心问题:

  • 效率低:医院/药厂日均分拣 thousands 瓶药品,人工每小时仅能处理200-300瓶,高峰期易堆积;
  • 误差高:相似药瓶(如包装颜色、尺寸接近)易混淆,人工分拣错误率约3%-5%,可能引发用药安全事故。

我的毕业设计目标就是用机器视觉解决这些问题:通过相机采集药瓶图像→算法处理(分割+定位+识别)→机器人分拣的流程,实现“自动识别国药准字、错误药品精准剔除”,最终达到分拣准确率≥97%、效率提升3倍以上的效果。

二、核心技术栈:从软件算法到硬件选型

整个系统分“图像采集→算法处理→执行控制”3层,技术栈覆盖机器视觉全链条,本科生可通过开源工具快速上手:

技术层级具体工具/算法核心作用
图像采集层CCD工业相机+工业镜头采集药瓶清晰图像(分辨率1280×960,帧率40fps),避免模糊或畸变;
算法处理层OpenCV + Python + MXNet图像预处理(灰度化/平滑/增强)、分割(改进Canny+鲸鱼算法优化二维最大熵)、识别(改进LeNet-5);
执行控制层分拣机器人+PLC控制器根据算法识别结果,控制机器人抓取错误药品,完成分拣动作;
软件开发环境Visual Studio 2017 + Matlab算法开发(C++写OpenCV逻辑,Python调深度学习模型)、实验仿真;
数据库/界面MySQL + MFC存储分拣日志(正确/错误数量)、设计人机交互界面(登录/参数设置/结果显示)。

三、项目全流程:5步实现药品分拣系统

3.1 第一步:硬件选型与相机标定

硬件是基础,重点解决“图像清晰采集”和“坐标精准映射”问题:

3.1.1 核心硬件选型

硬件组件型号/参数选型理由
工业相机维视图像MV-EM120M/CCCD传感器(成像比CMOS更清晰)、千兆网接口(传输距离100m,稳定无延迟);
工业镜头AFT-1214MPC口镜头(与相机匹配),焦距12mm,适合20-50cm距离拍摄药瓶标签;
光源环形LED光源均匀打光,避免药瓶反光导致标签模糊;
分拣机器人协作机器人(重复精度±0.02mm)负载1kg,足够抓取药瓶,运动灵活不卡顿;

3.1.2 相机标定(张正友标定法)

相机存在畸变(如桶形畸变),需通过标定获取内外参数,确保“像素坐标→世界坐标”精准转换:

  1. 准备标定板:打印棋盘格标定板(如11×8格,格子大小10mm);
  2. 采集标定图像:用相机从不同角度拍摄15-20张标定板图像;
  3. 计算参数:用OpenCV的calibrateCamera函数,输出内参(焦距、主点坐标)和外参(旋转矩阵、平移向量);
  4. 矫正畸变:用undistort函数对后续采集的药瓶图像做畸变矫正,确保标签无拉伸。

核心标定代码(Python+OpenCV):

import cv2
import numpy as np

# 1. 定义棋盘格参数(内角点数量)
chessboard_size = (10, 7)  # 11×8格的内角点是10×7
objp = np.zeros((np.prod(chessboard_size), 3), dtype=np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) * 10  # 格子大小10mm

# 2. 存储标定板角点(世界坐标+像素坐标)
obj_points = []  # 世界坐标
img_points = []  # 像素坐标

# 3. 读取标定图像并检测角点
images = [f"calib_{i}.jpg" for i in range(1, 21)]  # 20张标定图
for img_path in images:
    img = cv2.imread(img_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 检测棋盘格内角点
    ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
    if ret:
        obj_points.append(objp)
        # 亚像素级优化角点坐标
        corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), 
                                          (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
        img_points.append(corners_refined)
        # 绘制角点(可选,用于验证)
        cv2.drawChessboardCorners(img, chessboard_size, corners_refined, ret)
        cv2.imshow("Calibration", img)
        cv2.waitKey(500)

# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, 
                                                  gray.shape[::-1], None, None)

# 5. 保存标定参数(后续矫正图像用)
np.savez("camera_calib.npz", mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
print("标定完成!内参矩阵:")
print(mtx)
cv2.destroyAllWindows()

3.2 第二步:图像预处理与分割(核心算法优化)

采集的药瓶图像有噪声、标签畸变,需通过3步处理提取“国药准字”区域:

3.2.1 图像预处理(灰度化→平滑→增强)

  1. 灰度化:用加权平均值法(兼顾人眼对RGB的敏感度),公式:
    gray = 0.299*R + 0.587*G + 0.114*B,避免彩色信息干扰;
  2. 平滑去噪:3×3高斯滤波,核矩阵为[[1,2,1],[2,4,2],[1,2,1]]/16,保留边缘同时去除噪声;
  3. 图像增强:分段线性增强,将标签灰度区间[a,b]拉伸到[c,d](如a=50, b=150, c=20, d=230),扩大标签与背景对比度。

处理效果对比:

  • 原始图像:标签模糊,背景有反光;
  • 预处理后:标签文字清晰,背景灰度均匀。

3.2.2 改进Canny边缘检测(分割药瓶轮廓)

传统Canny用2×2邻域计算梯度,易受噪声干扰,改进后用3×3邻域,同时优化双阈值(自适应计算,无需人工调参):

def improved_canny(img):
    # 1. 高斯平滑(已做,此处省略)
    # 2. 3×3邻域计算梯度
    grad_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
    grad_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
    grad_mag = np.sqrt(grad_x**2 + grad_y**2)  # 梯度幅值
    grad_dir = np.arctan2(np.abs(grad_y), np.abs(grad_x))  # 梯度方向
    
    # 3. 非极大值抑制(保留边缘像素)
    h, w = img.shape
    non_max = np.zeros_like(grad_mag)
    for i in range(1, h-1):
        for j in range(1, w-1):
            # 确定梯度方向对应的邻域像素
            if (0 <= grad_dir[i,j] < np.pi/4) or (7*np.pi/4 <= grad_dir[i,j] <= 2*np.pi):
                neighbors = [grad_mag[i,j-1], grad_mag[i,j+1]]
            elif (np.pi/4 <= grad_dir[i,j] < 3*np.pi/4):
                neighbors = [grad_mag[i-1,j+1], grad_mag[i+1,j-1]]
            elif (3*np.pi/4 <= grad_dir[i,j] < 5*np.pi/4):
                neighbors = [grad_mag[i-1,j], grad_mag[i+1,j]]
            else:
                neighbors = [grad_mag[i-1,j-1], grad_mag[i+1,j+1]]
            # 非极大值抑制
            if grad_mag[i,j] >= max(neighbors):
                non_max[i,j] = grad_mag[i,j]
    
    # 4. 自适应双阈值(基于图像灰度均值计算)
    mean_val = np.mean(non_max)
    low_thresh = mean_val * 0.5
    high_thresh = mean_val * 1.5
    # 双阈值分割
    edges = np.zeros_like(non_max)
    strong_i, strong_j = np.where(non_max >= high_thresh)
    weak_i, weak_j = np.where((non_max >= low_thresh) & (non_max < high_thresh))
    edges[strong_i, strong_j] = 255  # 强边缘
    # 弱边缘连接(基于8邻域是否有强边缘)
    for i, j in zip(weak_i, weak_j):
        if np.any(edges[i-1:i+2, j-1:j+2] == 255):
            edges[i,j] = 255
    return edges

3.2.3 改进鲸鱼算法优化二维最大熵(分割国药准字)

传统二维最大熵易陷入局部最优,用改进鲸鱼算法(加自适应权重ω)优化阈值选择,分割国药准字区域:

  1. 鲸鱼算法改进:引入ω = 0.6 + 0.4*(1 - iter/T)iter为当前迭代,T为最大迭代),前期扩大搜索范围,后期精细寻优;
  2. 二维最大熵:以“像素灰度+邻域平均灰度”建立二维直方图,计算目标区与背景区的总熵,熵最大时的(s,t)即为最优阈值;
  3. 分割效果:对比大津法、传统二维最大熵,改进算法能精准分割出国药准字,背景噪声减少60%。

3.3 第三步:国药准字定位与字符识别

分割出药瓶区域后,需定位“国药准字”标签并识别字符,确保分拣正确:

3.3.1 数学形态学+行扫描定位

  1. 形态学处理:先膨胀(填充字符间隙)再腐蚀(去除细小噪声),将国药准字字符连成一个连通域;
  2. 行扫描验证:统计每行像素跳变次数(字符区跳变26-42次,背景区<10次),筛选出符合条件的区域,即为国药准字位置。

定位代码核心逻辑:

def locate_approval_code(img):
    # 1. 形态学处理(膨胀+腐蚀)
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
    dilate = cv2.dilate(img, kernel, iterations=2)
    erode = cv2.erode(dilate, kernel, iterations=1)
    
    # 2. 行扫描统计跳变次数
    h, w = erode.shape
    jump_counts = []
    for i in range(h):
        row = erode[i, :]
        jump = 0
        for j in range(1, w):
            if row[j] != row[j-1]:
                jump += 1
        jump_counts.append(jump)
    
    # 3. 筛选国药准字区域(跳变次数26-42)
    valid_rows = [i for i, cnt in enumerate(jump_counts) if 26 <= cnt <= 42]
    if not valid_rows:
        return None  # 未找到
    # 确定区域上下边界
    top = min(valid_rows)
    bottom = max(valid_rows)
    # 确定左右边界(列扫描,找字符区的左右端点)
    col_sum = np.sum(erode[top:bottom+1, :], axis=0)
    left = np.where(col_sum > 0)[0][0]
    right = np.where(col_sum > 0)[0][-1]
    # 提取国药准字区域
    approval_code = erode[top:bottom+1, left:right+1]
    return approval_code

3.3.2 改进LeNet-5识别字符

传统LeNet-5适配32×32手写数字,针对“国药准字(32×20像素,21类字符:4汉字+10数字+7字母)”做3点改进:

  1. 输入层:改为32×20像素,避免字符拉伸;
  2. 激活函数:用ReLU替代Sigmoid,解决梯度消失问题;
  3. 网络结构:取消C5卷积层,减少4万+参数,加Dropout(失活概率0.5)防过拟合。

识别流程:

  1. 字符分割:用改进投影法(按字符宽高比0.3-0.71分割),将国药准字拆分为单个字符;
  2. 模型训练:用500张药瓶标签生成2000+字符样本(8:2分训练/测试集),MXNet框架训练,测试准确率98.7%;
  3. 推理预测:将训练好的模型封装为动态库,嵌入分拣系统,实时输出识别结果。

3.4 第四步:系统软件开发(人机界面+逻辑控制)

用MFC设计Windows界面,实现“登录→参数设置→分拣控制→结果显示”全流程交互:

3.4.1 核心界面功能

  1. 登录界面:支持账号注册/登录(MySQL存储用户信息),防止非授权使用;
  2. 主界面
    • 控制区:开机/启动/暂停/退出按钮,设置目标国药准字(如“H20063284”);
    • 显示区:实时显示相机采集的图像、分割结果、识别结果;
    • 统计区:显示总分拣数、正确数、错误数(错误药品自动计数);
  3. 调试区:支持本地加载图像,逐步骤测试算法(预处理→分割→识别),方便排查问题。

3.4.2 核心控制逻辑

// MFC界面按钮点击事件(启动分拣)
void CMedicineSortDlg::OnBnClickedBtnStart()
{
    // 1. 初始化相机(读取标定参数)
    CameraInit();
    // 2. 获取目标国药准字
    CString targetCode;
    m_editTargetCode.GetWindowText(targetCode);
    if (targetCode.IsEmpty()) {
        MessageBox(_T("请设置目标国药准字!"));
        return;
    }
    // 3. 循环采集+处理+分拣
    while (!m_bStop) {  // m_bStop为暂停/退出标志
        // 采集图像
        Mat frame = CameraCapture();
        // 图像预处理+分割+定位
        Mat approvalCode = LocateApprovalCode(frame);
        if (approvalCode.empty()) {
            continue;
        }
        // 字符分割+识别
        CString recognizedCode = RecognizeCode(approvalCode);
        // 对比目标准字,控制机器人
        if (recognizedCode != targetCode) {
            // 错误药品,控制机器人抓取
            RobotGrab();
            m_nErrorCount++;  // 错误计数+1
        } else {
            m_nCorrectCount++;  // 正确计数+1
        }
        // 更新统计显示
        UpdateStatistic();
        // 延时,控制分拣速度
        Sleep(500);
    }
}

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

3.5 第五步:系统测试与性能验证

在药厂搭建实验平台,用400瓶药品(分4组,每组含随机错误药品)测试,核心结果:

测试指标结果说明
分拣准确率测试指标结果说明
--------------------------------------------------------------------------------------------------------------------
分拣准确率97.75%400瓶药品中391瓶分拣正确,错误主要源于字母“Z”识别(样本量不足);
分拣效率800瓶/小时是人工分拣(200-300瓶/小时)的2.5-4倍,满足药厂日均分拣需求;
识别耗时2.4秒/瓶从图像采集到识别完成仅需2.4秒,无明显卡顿;
稳定性连续运行8小时无故障相机、机器人、算法均未出现异常,日志记录完整。

测试结论:系统满足“准确率≥95%、效率提升2倍以上”的设计目标,可投入实际使用。

四、毕业设计复盘:踩过的坑与经验

4.1 那些踩过的坑

  1. 药瓶柱面畸变:标签拉伸导致识别错误

    • 问题:圆形药瓶标签成像时边缘拉伸,字符变形,传统算法识别准确率仅85%;
    • 解决:提出柱面矫正算法,基于相似三角形原理拉伸矫正畸变区域,再用代数插值补全缺损像素,矫正后识别准确率提升至95%+。
  2. 算法实时性差:单瓶处理耗时超5秒

    • 问题:初始用传统LeNet-5+未优化的分割算法,单瓶处理需5.2秒,无法满足实时分拣;
    • 解决:① 取消LeNet-5的C5层,减少计算量;② 用改进鲸鱼算法替代暴力搜索阈值,分割时间从3.1秒缩短至0.17秒,最终单瓶耗时降至2.4秒。
  3. 相机标定不准:机器人抓取偏移

    • 问题:未做标定或标定图像不足10张,导致像素坐标与世界坐标映射偏差,机器人抓取药瓶时偏移5-10mm;
    • 解决:采集20张不同角度的标定板图像,用张正友标定法重新计算内外参,抓取偏移控制在±2mm内。

4.2 给学弟学妹的建议

  1. 硬件选型先做“最小验证”,再采购
    不要直接买昂贵的工业相机!先用普通USB相机+打印的标签做算法验证(比如测试边缘检测、识别效果),确认算法可行后,再根据需求选购工业级硬件——避免因算法不匹配导致硬件浪费。

  2. 算法优化要“针对性”,不盲目堆复杂度
    比如本文改进Canny时,仅将邻域从2×2改为3×3,就解决了噪声问题;改进LeNet-5时,取消一个卷积层就大幅提升速度。不要一开始就用复杂的YOLO、Transformer,先针对项目痛点做“小改进”,效率更高。

  3. 答辩重点展示“实物效果+数据对比”
    评委更关注“你的系统能不能用”:比如播放分拣实验视频(机器人精准抓取错误药品),对比“人工vs系统”的效率/准确率数据(用表格或柱状图),再展示核心算法的优化点(如分割效果对比图)——比纯讲理论更有说服力。

五、项目资源获取

完整项目包含:

  1. 算法代码:图像预处理、改进Canny、鲸鱼算法优化分割、改进LeNet-5识别(附注释);
  2. 软件源码:MFC人机界面源码、相机标定工具、机器人控制接口代码;
  3. 硬件资料:相机/镜头选型手册、分拣平台接线图、标定板模板;
  4. 答辩资料:PPT模板(含实验视频链接、核心数据图表)、测试报告、系统使用说明书。

如果本文对你的机器视觉实践、深度学习应用或毕业设计有帮助,欢迎点赞 + 收藏 + 关注,后续会分享更多工业场景实战案例!