资讯详情

Python智慧药房DrugDeliverer源码:从视觉识别到机械臂抓取的全流程实现

📅 2026/10/31 1:11:53 | 华诺云谱 👁 阅读
Python智慧药房DrugDeliverer源码:从视觉识别到机械臂抓取的全流程实现
简介面向机器人竞赛与智慧医疗自动化开发者的参赛作品源码基于 Python 与 ROS 构建药房药物自动配送系统旨在减少人工干预、提升药品分发效率与准确性。压缩包共163个文件约55.36MB包含23个Python脚本、39个YAML配置、11个launch与10个srv文件覆盖核心逻辑、参数设置与ROS通信另有图片、PDF、TXT等文档及Shell部署脚本便于理解与二次开发。包内还提供mp3操作录屏、可直接运行的依赖包如playsound、imutils以及3D打印模型便于在不同环境中快速复现赛题。项目完整覆盖智慧药房场景下的建图、导航、抓取与配送流程涉及多传感器融合、路径规划与机械臂控制等关键环节是学习机器人竞赛项目架构、ROS节点通信及Python工程实践的优质样例。已有804人学习参考适合参赛队伍、科研人员及机器人爱好者深入研读。1. 智慧药房 DrugDeliverer一套能跑通“识别—导航—抓取—放置”全流程的 Python 源码做机器人及人工智能大赛智慧药房组的人应该都有同感赛题文档写得再细真正动手时最耗时间的还是“药房场景建模、药盒识别、机械臂抓取”这一整条链路的代码从零拼装。DrugDeliverer 这套基于 Python 的智慧药房设计源码核心价值就是把药物递送系统里最烦琐的视觉定位、路径规划、抓取控制、任务调度四件事串成了一条可复现的主线而不是零散挂在网上的算法片段。适合两类人一是准备第二十五届中国机器人及人工智能大赛的参赛队想拿一套结构清楚的代码做底子再二次开发二是课程设计做到“智慧药房”“药物配送”题目的学生需要的是能跑、能改、能写进报告的东西。源码解决了什么实际问题简单说你给它一个目标药盒编号它能完成从识别药盒坐标、规划抓取路径到机械臂动作输出的完整闭环不是只给你一个模型或一段算法。2. 拆解代码架构为什么 Python 能撑起整个药物递送主流程2.1 模块划分与调度逻辑先看主线再碰细节DrugDeliverer 源码拿到手第一件事不是去读算法而是先把目录结构摸清楚。常见做法是分成 vision、navigation、manipulation、scheduler、utils 五个包外加一个 main.py 做总入口。vision 负责“看到药盒在哪”navigation 负责“怎么移动过去”manipulation 负责“怎么抓起来”scheduler 负责“先干哪个任务”utils 里放的则是串口通信、日志、坐标系转换这类公共工具。scheduler 是整个系统的调度中枢。一个典型的递送任务输入是药品订单列表比如 [(Aspirin, 2), (Insulin, 1)]输出是一串执行动作序列。scheduler 内部维护了一个任务队列按优先级和当前位置计算执行顺序然后把每一步分解成“导航目标点 抓取目标坐标 放置目标坐标”的元组下发到对应模块。class Task: def __init__(self, drug_name, quantity, priority0): self.drug_name drug_name self.quantity quantity self.priority priority self.status pending # pending / navigating / grasping / placing / done class Scheduler: def __init__(self, robot_state): self.robot_state robot_state # 维护机器人当前位置、机械臂状态 self.task_queue [] def plan_routes(self): # 按优先级排序同一优先级按当前距离近者先执行 self.task_queue.sort(keylambda t: (-t.priority, self.robot_state.distance_to(t.drug_name))) return self.task_queue这段代码的逻辑不复杂但它是整个系统的“决策层”。priority字段对应竞赛规则里的紧急药品优先机制distance_to方法则是从导航模块的代价地图里取曼哈顿距离或欧氏距离用来决定先送哪个货架。实际比赛中如果把紧急药优先级设成 1、普通药设成 0队列排序就能自动把急救药先送出去不需要你手动干预。注意sort的 key 用了负的 priority是为了让数值大的任务排前面。2.2 坐标系转换与数据流串起摄像头、底盘与机械臂的关键整套代码里最容易让人绕晕的就是坐标系。摄像头给的是像素坐标导航模块要的是地图坐标机械臂要的是基座标系下的三维坐标三个坐标系如果不做统一视觉识别精度再高也白搭。DrugDeliverer 的做法是在 utils 里封装了一个坐标转换类维护着一张“摄像头 → 机械臂基座”的变换矩阵。import numpy as np class CoordinateTransformer: def __init__(self, intrinsic_matrix, extrinsic_matrix): # intrinsic_matrix: 相机内参 3x3extrinsic_matrix: 外参 4x4 self.intrinsic intrinsic_matrix self.extrinsic extrinsic_matrix self.extrinsic_inv np.linalg.inv(extrinsic_matrix) def pixel_to_world(self, px, py, depth): # 像素坐标 深度值 - 相机坐标系下的三维点 fx self.intrinsic[0, 0] fy self.intrinsic[1, 1] cx self.intrinsic[0, 2] cy self.intrinsic[1, 2] x (px - cx) * depth / fx y (py - cy) * depth / fy z depth cam_point np.array([x, y, z, 1.0]) # 相机坐标系 - 机械臂基座坐标系 world_point self.extrinsic_inv cam_point return world_point[:3]参数说明fx、fy是焦距单位是像素cx、cy是光心偏移这几个值来自相机标定结果。depth是深度相机测得的距离如果没有深度相机也可以用已知高度的平面做单应性变换来估算。extrinsic_inv左乘相机坐标是把点从相机系变换到机械臂基座系注意顺序不能反过来矩阵乘的顺序错了结果是完全不确定的。很多队伍在这块翻车就是没意识到 OpenCV 的标定结果是相机到标定板的外参机械臂抓取时还需要再乘一个“标定板到机械臂基座”的偏移量。3. 视觉识别与药盒定位从粗检测到精定位的具体实现3.1 为什么选 ARUco 码加 QR 码的组合方案智慧药房赛题里的药盒识别网上有两条路线一条是训练 YOLO 目标检测模型一条是用视觉标记定位。DrugDeliverer 的做法是后者——药盒侧壁贴 ARUco 码用来定位上方贴 QR 码用来读取药品名。这个选型是合理的因为竞赛环境的药盒种类有限但摆放角度随机YOLO 需要你收集大量不同角度的标注数据而 ARUco 码天然具备角度鲁棒性只要码不破损就能解算出精确的位姿。ARUco 码解算出来的不是一个矩形框而是四个角点的像素坐标加上旋转向量。拿到这些数据后可以算出药盒中心点在地图上的位置以及药盒的朝向角这对后续机械臂抓取姿态的调整很有用。3.2 药盒识别与坐标解算直接可跑的 OpenCV 代码import cv2 from cv2 import aruco def locate_drug_boxes(frame, camera_matrix, dist_coeffs): # 加载预定义字典4x4_50 适用于药盒小场景 aruco_dict aruco.Dictionary_get(aruco.DICT_4X4_50) parameters aruco.DetectorParameters_create() corners, ids, _ aruco.detectMarkers( frame, aruco_dict, parametersparameters, cameraMatrixcamera_matrix, distCoeffdist_coeffs ) results [] if ids is not None: for i, corner in enumerate(corners): # corner 是 1x4x2 的数组 reshape 后为四个角点 pts corner.reshape(4, 2) center_px pts.mean(axis0) # 像素中心 # 用 solvePnP 求药盒相对相机的位姿 obj_points np.float32([ [-half_w, -half_h, 0], [half_w, -half_h, 0], [half_w, half_h, 0], [-half_w, half_h, 0] ]) success, rvec, tvec cv2.solvePnP( obj_points, pts, camera_matrix, dist_coeffs) if success: results.append({ aruco_id: int(ids[i]), center_px: center_px, tvec: tvec.flatten(), # 相机坐标系下的三维坐标 rvec: rvec.flatten() }) return results这段代码的坑在两处。一是half_w和half_h必须是二维码贴纸的实际物理尺寸一半单位是毫米不是像素很多队伍在这里填错导致 solvePnP 解出来的距离偏得离谱。二是 OpenCV 版本不同DICT_4X4_50的访问方式也不同新版本推荐用cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)老版本才用Dictionary_get。检测出来tvec的 z 值就是你到药盒的距离把它喂给上一章的pixel_to_world就能得到机械臂基座坐标系下的抓取点。3.3 QR 码读药名别让中文编码卡住整条流水线药盒识别只解决了“哪里抓”的问题“抓哪盒药”要靠 QR 码内容。这里有个实际工程问题竞赛药盒上的药品名通常是英文代码比如 ASPIRIN-500但课程设计里可能就是中文。OpenCV 的 QRCodeDetector 对中文支持不友好识别出来是乱码。我一般会绕一步用 pyzbar 读 QR 码把结果统一转成 UTF-8from pyzbar.pyzbar import decode import cv2 def read_drug_name(roi): # roi 是裁剪出的 QR 码区域 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 先做一次二值化增强QR 码在光线不均时容易读失败 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) barcodes decode(binary) drug_name None for barcode in barcodes: raw barcode.data try: drug_name raw.decode(utf-8) except UnicodeDecodeError: # 有些扫码枪输出是 GBK 编码做一次兜底 drug_name raw.decode(gbk) break return drug_name参数说明THRESH_OTSU是自适应阈值能解决一部分光照不均的问题但解决不了阴影后面避坑章再说。decode返回的对象里还有rect字段可以定位 QR 码在帧里的位置用来算药盒的精确抓取点。实际比赛中QR 码贴歪了会导致读取角度偏大pyzbar 的容错率不如 OpenCV 自带检测器但胜在编码处理灵活。建议在代码里同时保留两套方案QRCodeDetector 读不出来的场景自动切 pyzbar。4. 路径规划与机械臂抓取从栅格地图到执行指令的完整链路4.1 导航模块为什么用 A* 而不是 RRT 或 DWA智慧药房场地是结构化环境货架是规矩排列的矩形通道笔直这种场景下栅格地图加 A* 是最稳的组合。RRT 适合狭窄不规则的障碍环境优势是不需要建栅格但规划出的路径是折线不平滑DWA 是局部速度采样适合动态避障但在静态货架场景里会绕冤枉路。DrugDeliverer 用的是 A* 做全局路径规划再配上简单的平滑处理把曲折的搜索路径转换成直线段。栅格地图生成这块常见的做法是把场地俯视图按分辨率网格化货架占据的格子标记为障碍药盒位置标记为目标点。地图分辨率一般取每格 2 厘米既能保证路径精度又不会让搜索空间过大导致计算超时。import heapq def astar(grid, start, goal): # grid: 二维数组0 可通行1 障碍 rows, cols len(grid), len(grid[0]) open_heap [] heapq.heappush(open_heap, (0, start)) came_from {} g_score {start: 0} while open_heap: cur_cost, cur heapq.heappop(open_heap) if cur goal: path [] while cur in came_from: path.append(cur) cur came_from[cur] path.append(start) return path[::-1] for dx, dy in [(1,0),(-1,0),(0,1),(0,-1)]: nxt (cur[0]dx, cur[1]dy) if not (0 nxt[0] rows and 0 nxt[1] cols): continue if grid[nxt[0]][nxt[1]] 1: continue tentative g_score[cur] 1 if tentative g_score.get(nxt, float(inf)): came_from[nxt] cur g_score[nxt] tentative # 启发函数用曼哈顿距离药房场景比欧氏距离更贴近实际走法 h abs(nxt[0]-goal[0]) abs(nxt[1]-goal[1]) heapq.heappush(open_heap, (tentative h, nxt)) return None参数说明1是每一步的代价值对应栅格地图上 2 厘米的实际距离如果想让机器人优先走直线减少转弯可以把转弯的代价值调成 2 或 3但这需要扩展搜索逻辑当前代码里没有处理。启发函数用曼哈顿距离是因为药房通道只能横平竖直地走用欧氏距离会高估可达性规划出的路径可能贴障碍物太近。float(inf)是 g_score 字典的初始值表示未访问节点代价无穷大这个写法比初始化全图二维数组更省内存。4.2 机械臂抓取指令封装逆解算好别重复造轮子机械臂部分的核心不是逆解算法而是“把位姿转换成设备能执行的指令”这层封装。市面上的竞赛机械臂大多自带运动学库有的用 ROS 的 MoveIt有的用厂家提供的 Python SDKDrugDeliverer 源码里默认用的是串口 TCP 指令方式兼容性最好。def execute_grasp(robot_ip, port, pose, approach_height0.05): # pose: [x, y, z, rx, ry, rz]单位毫米和弧度 # approach_height: 抓取前抬升高度避免撞到药盒边缘 commands [ fMOVL {pose[0]} {pose[1]} {approach_height} 0 3.14 0 80.encode() # 先移动到药盒正上方保持末端朝下 ] for cmd in commands: send_tcp(robot_ip, port, cmd) wait_response(robot_ip, port, timeout5) # 等机械臂到位后再下移直接垂直抓取 grasp_cmd fMOVL {pose[0]} {pose[1]} {pose[2]} 0 3.14 0 40.encode() send_tcp(robot_ip, port, grasp_cmd) wait_response(robot_ip, port, timeout5) # 闭合夹爪这里的 40 是夹爪力度百分比 send_tcp(robot_ip, port, bGRIPPER CLOSE 40) wait_response(robot_ip, port, timeout3)参数说明MOVL是直线运动指令后面的数字依次是 x、y、z 坐标和 rx、ry、rz 姿态角最后是速度百分比。先移动到approach_height再下移是为了留出安全余量避免机械臂末端带着速度直接砸到药盒上。夹爪力度 40 是针对标准药盒纸箱的调试值如果药盒换成玻璃瓶或塑料瓶这个值要往下降到 15 到 20不然会夹碎。wait_response函数很重要机械臂没到位就发下一条指令会导致运动重叠实际设备上表现出来的症状是机械臂忽快忽慢地抽搐我调试时被这个问题坑过一整个下午。4.3 抓取失败重试机制纯算法解决不了的靠状态机很多队伍忽略了一个实际问题药盒位置有误差机械臂第一次抓取可能抓空。DrugDeliverer 里实现的抓取状态机很实用允许连续重试三次每次重试时通过视觉模块重新计算目标坐标而不是沿用第一次的结果。from enum import Enum class GraspStatus(Enum): SUCCESS success FAILED failed RETRYING retrying def grasp_with_retry(locate_func, grasp_executor, max_retries3): status GraspStatus.FAILED attempts 0 while attempts max_retries: attempts 1 # 每次重试都重新视觉定位消除机械臂移动带来的累积误差 pose locate_func() if pose is None: continue result grasp_executor(pose) if result is not None and result[gripped]: status GraspStatus.SUCCESS break else: # 抓空后机械臂退回到安全高度避免拖拽药盒 grasp_executor.reset_to_safe_pose() status GraspStatus.RETRYING return status这段逻辑看着简单但解决了三个实际问题视觉定位存在毫米级误差机械臂本身存在回差药盒在抓取过程中可能轻微位移。三次重试中如果每次都重新视觉定位成功率能从单次的八成提升到接近满八。reset_to_safe_pose是容易被忽略的一个环节抓空之后机械臂如果直接抬升夹爪可能会勾住药盒边缘把它带倒先平移到安全位置再抬升是更稳的顺序。5. 避坑指南DrugDeliverer 调试中五个高频翻车点5.1 视觉识别偶发失效药盒反光导致 ARUco 码漏检现象同一批药盒同一个机位有些帧能识别出 ARUco 码换一个角度就完全检测不到时好时坏。原因药盒表面的塑封膜有反光ARUco 码黑白边界在强光下过曝角点检测失效。解决在相机镜头前加偏光镜代码里再补一道预处理把图像转到 HSV 空间增强对比度后再送入检测器不要直接拿原始 BGR 图做输入。def preprocess_for_aruco(frame): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 提升 S 通道饱和度压暗反光区域的过曝 h, s, v cv2.split(hsv) s cv2.add(s, 30) v cv2.normalize(v, None, 0, 255, cv2.NORM_MINMAX) enhanced cv2.merge([h, s, v]) return cv2.cvtColor(enhanced, cv2.COLOR_HSV2BGR)5.2 机械臂抓取坐标偏移标定板没固定导致外参漂移现象第一天调试抓取很准第二天开机后所有抓取点整体往一个方向偏了 2 到 3 厘米。原因标定板贴在货架侧面搬运过程中被碰歪了外参矩阵还是旧值。解决标定完成后在代码里对变换矩阵做一次备份每次启动时校验当前画面中的标定板角点位置与备份值是否一致偏差超过 5 个像素就强制重新标定。5.3 导航路径穿过货架栅格地图和真实障碍对不上现象A* 规划的路径看着没问题但机器人走到一半撞到货架立柱。原因栅格地图是人工标注的货架立柱的实际位置和标注差了半格路径沿着“障碍边缘”走过去时刚好刮上。解决栅格膨胀处理把障碍物周围两格也标成不可通行别贪图路径短贴着障碍走。膨胀半径取多少要看机器人底盘尺寸一般取车体宽度的一半换算成格子数。5.4 串口指令丢失导致机械臂停在半空现象运行过程中机械臂偶尔停在某个位置不动过几秒又自己动了。原因TCP 串口通信没有做应答重发指令发出去了但机械臂控制器没收到。解决所有发送指令的函数统一走一个带重发的封装发出去后 1 秒内没收到 ACK 就重发连续三次没响应直接置为故障状态停止任务别默默往下执行。def send_with_ack(sock, payload, timeout1.0, retries3): for attempt in range(retries): sock.send(payload) try: sock.settimeout(timeout) resp sock.recv(64) if bOK in resp: return True except socket.timeout: continue return False5.5 多任务调度顺序错乱药盒位置更新没加锁现象两个任务同时命中同一个药盒第二个任务抓到的坐标是第一个任务执行前的位置抓空。原因任务队列里的坐标在调度时计算了一次之后没有更新。解决不要在调度阶段把坐标写死任务执行前实时从视觉模块拉取一次最新位置把“调度时定位”改成“执行前定位”。6. 调试与验证技巧用离线回放和坐标可视化把问题摁死在实验室里DrugDeliverer 给你的是源码但真正决定比赛成绩的是调试方法。我在调这套系统时养成了一套固定流程每次改完代码先不急着上真机而是把视觉识别结果、路径规划结果、机械臂指令全部记录到本地日志文件然后用一套离线回放脚本把执行过程可视化出来。日志格式我一般固定成 CSV每行记录时间戳、事件类型、x 坐标、y 坐标、附加信息。事件类型包括detect_drug、path_planned、grasp_attempt、grasp_success四种。回放时用 matplotlib 把药盒位置画成散点路径画成折线机械臂动作画成箭头这样一眼就能看出药盒定位是否抖动、路径是否贴障碍、抓取点是否偏移。import csv import matplotlib.pyplot as plt def replay_log(log_path, map_w, map_h): fig, ax plt.subplots(figsize(10, 6)) ax.set_xlim(0, map_w) ax.set_ylim(0, map_h) with open(log_path, r) as f: reader csv.DictReader(f) for row in reader: event row[event] x, y float(row[x]), float(row[y]) if event detect_drug: ax.scatter(x, y, cblue, markero) elif event path_planned: ax.plot([float(row[prev_x]), x], [float(row[prev_y]), y], cgray, linestyle--) elif event grasp_attempt: ax.scatter(x, y, corange, markerx) elif event grasp_success: ax.scatter(x, y, cgreen, marker*, s120)回放脚本跑完把图放大看三件事药盒定位散点是否聚成一团聚成一团说明视觉稳定灰色虚线路径是否始终离货架障碍有一个身位贴着走说明膨胀参数不够绿色星号和橙色叉叉是否重合偏了说明抓取点换算有问题。第二次调试我才会开真机但运行前会改一个参数把机械臂末端速度从 80 降到 20抓取流程走得慢一点方便肉眼观察每一步的动作是否合理。这个习惯让我躲过了不少险情最典型的一次是发现机械臂在靠近药盒时姿态角突然翻转了 180 度回放日志才发现是逆解模块对多组解的取舍逻辑有问题选了另一个等效但姿态异常的关节组合速度调低后这个问题暴露得特别明显。速度参数我在不同阶段用不同值写代码调试时用 20全流程联调用 40比赛演示前改回 80。每次改速度参数后都要重新跑一遍离线回放因为速度会影响机械臂的轨迹插补同样一组坐标在不同速度下实际走的路径曲线是有差别的尤其是转弯处会切弯。从那以后我每次拿到一套新的机器人源码都强制自己先写一个离线回放工具再碰真机日志先行、可视化兜底这套习惯帮我省下的调参时间比写工具的时间多出好几倍。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑