一、项目背景:为什么需要机器视觉药品分拣?
传统药品分拣靠人工,存在两大核心问题:
- 效率低:医院/药厂日均分拣 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/C | CCD传感器(成像比CMOS更清晰)、千兆网接口(传输距离100m,稳定无延迟); |
| 工业镜头 | AFT-1214MP | C口镜头(与相机匹配),焦距12mm,适合20-50cm距离拍摄药瓶标签; |
| 光源 | 环形LED光源 | 均匀打光,避免药瓶反光导致标签模糊; |
| 分拣机器人 | 协作机器人(重复精度±0.02mm) | 负载1kg,足够抓取药瓶,运动灵活不卡顿; |
3.1.2 相机标定(张正友标定法)
相机存在畸变(如桶形畸变),需通过标定获取内外参数,确保“像素坐标→世界坐标”精准转换:
- 准备标定板:打印棋盘格标定板(如11×8格,格子大小10mm);
- 采集标定图像:用相机从不同角度拍摄15-20张标定板图像;
- 计算参数:用OpenCV的
calibrateCamera函数,输出内参(焦距、主点坐标)和外参(旋转矩阵、平移向量); - 矫正畸变:用
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 图像预处理(灰度化→平滑→增强)
- 灰度化:用加权平均值法(兼顾人眼对RGB的敏感度),公式:
gray = 0.299*R + 0.587*G + 0.114*B,避免彩色信息干扰; - 平滑去噪:3×3高斯滤波,核矩阵为
[[1,2,1],[2,4,2],[1,2,1]]/16,保留边缘同时去除噪声; - 图像增强:分段线性增强,将标签灰度区间
[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 改进鲸鱼算法优化二维最大熵(分割国药准字)
传统二维最大熵易陷入局部最优,用改进鲸鱼算法(加自适应权重ω)优化阈值选择,分割国药准字区域:
- 鲸鱼算法改进:引入
ω = 0.6 + 0.4*(1 - iter/T)(iter为当前迭代,T为最大迭代),前期扩大搜索范围,后期精细寻优; - 二维最大熵:以“像素灰度+邻域平均灰度”建立二维直方图,计算目标区与背景区的总熵,熵最大时的
(s,t)即为最优阈值; - 分割效果:对比大津法、传统二维最大熵,改进算法能精准分割出国药准字,背景噪声减少60%。
3.3 第三步:国药准字定位与字符识别
分割出药瓶区域后,需定位“国药准字”标签并识别字符,确保分拣正确:
3.3.1 数学形态学+行扫描定位
- 形态学处理:先膨胀(填充字符间隙)再腐蚀(去除细小噪声),将国药准字字符连成一个连通域;
- 行扫描验证:统计每行像素跳变次数(字符区跳变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点改进:
- 输入层:改为32×20像素,避免字符拉伸;
- 激活函数:用ReLU替代Sigmoid,解决梯度消失问题;
- 网络结构:取消C5卷积层,减少4万+参数,加Dropout(失活概率0.5)防过拟合。
识别流程:
- 字符分割:用改进投影法(按字符宽高比0.3-0.71分割),将国药准字拆分为单个字符;
- 模型训练:用500张药瓶标签生成2000+字符样本(8:2分训练/测试集),MXNet框架训练,测试准确率98.7%;
- 推理预测:将训练好的模型封装为动态库,嵌入分拣系统,实时输出识别结果。
3.4 第四步:系统软件开发(人机界面+逻辑控制)
用MFC设计Windows界面,实现“登录→参数设置→分拣控制→结果显示”全流程交互:
3.4.1 核心界面功能
- 登录界面:支持账号注册/登录(MySQL存储用户信息),防止非授权使用;
- 主界面:
- 控制区:开机/启动/暂停/退出按钮,设置目标国药准字(如“H20063284”);
- 显示区:实时显示相机采集的图像、分割结果、识别结果;
- 统计区:显示总分拣数、正确数、错误数(错误药品自动计数);
- 调试区:支持本地加载图像,逐步骤测试算法(预处理→分割→识别),方便排查问题。
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 那些踩过的坑
-
药瓶柱面畸变:标签拉伸导致识别错误
- 问题:圆形药瓶标签成像时边缘拉伸,字符变形,传统算法识别准确率仅85%;
- 解决:提出柱面矫正算法,基于相似三角形原理拉伸矫正畸变区域,再用代数插值补全缺损像素,矫正后识别准确率提升至95%+。
-
算法实时性差:单瓶处理耗时超5秒
- 问题:初始用传统LeNet-5+未优化的分割算法,单瓶处理需5.2秒,无法满足实时分拣;
- 解决:① 取消LeNet-5的C5层,减少计算量;② 用改进鲸鱼算法替代暴力搜索阈值,分割时间从3.1秒缩短至0.17秒,最终单瓶耗时降至2.4秒。
-
相机标定不准:机器人抓取偏移
- 问题:未做标定或标定图像不足10张,导致像素坐标与世界坐标映射偏差,机器人抓取药瓶时偏移5-10mm;
- 解决:采集20张不同角度的标定板图像,用张正友标定法重新计算内外参,抓取偏移控制在±2mm内。
4.2 给学弟学妹的建议
-
硬件选型先做“最小验证”,再采购
不要直接买昂贵的工业相机!先用普通USB相机+打印的标签做算法验证(比如测试边缘检测、识别效果),确认算法可行后,再根据需求选购工业级硬件——避免因算法不匹配导致硬件浪费。 -
算法优化要“针对性”,不盲目堆复杂度
比如本文改进Canny时,仅将邻域从2×2改为3×3,就解决了噪声问题;改进LeNet-5时,取消一个卷积层就大幅提升速度。不要一开始就用复杂的YOLO、Transformer,先针对项目痛点做“小改进”,效率更高。 -
答辩重点展示“实物效果+数据对比”
评委更关注“你的系统能不能用”:比如播放分拣实验视频(机器人精准抓取错误药品),对比“人工vs系统”的效率/准确率数据(用表格或柱状图),再展示核心算法的优化点(如分割效果对比图)——比纯讲理论更有说服力。
五、项目资源获取
完整项目包含:
- 算法代码:图像预处理、改进Canny、鲸鱼算法优化分割、改进LeNet-5识别(附注释);
- 软件源码:MFC人机界面源码、相机标定工具、机器人控制接口代码;
- 硬件资料:相机/镜头选型手册、分拣平台接线图、标定板模板;
- 答辩资料:PPT模板(含实验视频链接、核心数据图表)、测试报告、系统使用说明书。
如果本文对你的机器视觉实践、深度学习应用或毕业设计有帮助,欢迎点赞 + 收藏 + 关注,后续会分享更多工业场景实战案例!