资讯动态

智慧药房赛项实战:视觉识别、抓取补偿与任务调度避坑指南

发布时间:2026/10/22 5:22:24 来源:尧图企业网站定制
简介这份资源是第二十五届中国机器人及人工智能大赛智慧药房组的完整参赛作品包面向备战同类机器人竞赛的高校学生与指导教师尤其适合需要参考真实赛题方案、搭建ROS仿真与实物调试环境的中高级选手。压缩包共150个文件约38.19MB以39个yaml参数配置、23个py功能脚本、11个launch启动文件、10个srv服务定义为主辅以cpp节点源码、xml与msg通信定义、pgm地图、jpg/png图像素材及pdf设计文档覆盖感知、导航、任务调度等模块。资源内含设计文档与设计源码可帮助读者理解智慧药房场景下的任务流程、坐标点标定与节点通信逻辑并对照gcode与3mf模型文件复现机械结构。目前已有825人学习下载适合作为赛题复盘、代码移植与排错思路的参考。1. 智慧药房赛项到底在比什么从一份赛题压缩包说起如果你拿到的是“第二十五届中国机器人及人工智能大赛智慧药房组.zip”这类赛题包第一反应大概率是解压、翻文档、找例程然后被里面一堆任务描述、场地尺寸、评分表砸晕。我当年第一次接触智慧药房赛项时也这样以为核心是“让机械臂抓药盒”结果跑通最小闭环才发现真正拉开差距的是视觉识别稳定性、抓取位姿补偿、任务调度容错这三件事。智慧药房组模拟的是自动化药房场景机器人要从药架上识别指定药品、精准抓取、按处方配药还要应对药盒摆放偏差、光照变化、药品外观相似等现实问题。它适合有ROS基础、想验证视觉-抓取-调度全链路的人也适合作为多机协同与边缘部署的练手项目。这一章不堆概念只讲这个赛项在工程上到底要你交付什么以及为什么很多人卡在“能跑demo但拿不到分”。2. 从赛题包到可运行系统环境搭建与最小闭环2.1 先别急着写代码把赛题包里的坐标系对齐赛题包通常包含场地说明、药品清单、评分细则但不会给你现成的标定参数。我一般会先做三件事确认相机安装高度与俯仰角、测量药架格子实际尺寸、记录机械臂基座与药架原点的相对位置。这三组数据决定了后续所有坐标变换的基准。常见做法是用ArUco码或棋盘格做一次手眼标定把相机像素坐标映射到机械臂基座坐标系。如果你跳过这一步后面抓取会频繁出现“看着对准了但抓空”的玄学问题。# 手眼标定从像素坐标到机械臂基座坐标的简易映射 # 假设已通过标定得到单应性矩阵 H (3x3) import numpy as np H np.load(hand_eye_H.npy) # 标定得到的单应矩阵 def pixel_to_base(u, v): 输入像素坐标返回机械臂基座平面坐标 (x, y) pt np.array([u, v, 1.0]) base H pt base base / base[2] # 归一化 return base[0], base[1] # 参数说明 # u, v 为图像像素坐标H 由至少4组对应点求解 # 若药架有高度差需额外做深度补偿或使用RGB-D相机逻辑说明单应性矩阵适用于平面抓取场景如果药盒高度不一致需要引入深度信息或做多次平面标定。参数上标定点要覆盖药架四角与中心避免外推区域误差放大。2.2 用ROS搭最小任务节点识别→定位→抓取→放置最小闭环不需要复杂调度先让一个节点串起流程。我习惯用Python写快速验证因为调参直观。下面是一个简化版任务节点骨架实际比赛再拆成多个节点做并行。#!/usr/bin/env python3 # 最小任务节点订阅相机图像识别药盒计算抓取位姿调用机械臂 import rospy from sensor_msgs.msg import Image from geometry_msgs.msg import Pose from cv_bridge import CvBridge import cv2 import numpy as np class PharmacyTask: def __init__(self): self.bridge CvBridge() self.sub rospy.Subscriber(/camera/image_raw, Image, self.callback) self.pub_target rospy.Publisher(/arm/target_pose, Pose, queue_size1) self.H np.load(hand_eye_H.npy) def callback(self, msg): frame self.bridge.imgmsg_to_cv2(msg, bgr8) # 颜色阈值分割示例识别红色药盒 hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (0, 120, 70), (10, 255, 255)) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return c max(contours, keycv2.contourArea) M cv2.moments(c) if M[m00] 0: return u M[m10] / M[m00] v M[m01] / M[m00] x, y self.pixel_to_base(u, v) pose Pose() pose.position.x x pose.position.y y pose.position.z 0.15 # 抓取高度需实测 self.pub_target.publish(pose) def pixel_to_base(self, u, v): pt np.array([u, v, 1.0]) base self.H pt base base / base[2] return base[0], base[1] if __name__ __main__: rospy.init_node(pharmacy_task) PharmacyTask() rospy.spin()逻辑说明这个节点只做了一件事——把图像中最像目标药盒的轮廓中心转成机械臂目标点。参数上HSV阈值需要根据现场光照重新标定抓取高度z要结合药盒厚度和夹爪开口调整。常见翻车点是轮廓筛选太粗暴把相邻药盒连成一片导致中心点偏移。2.3 机械臂运动规划别直接发笛卡尔点先过MoveIt很多新手直接把目标位姿发给机械臂结果要么姿态突变撞药架要么逆解失败。稳妥做法是走MoveIt的规划组设置好场景碰撞体。下面是一个调用示例。# 通过MoveIt规划到目标位姿避免直接关节角跳变 import rospy import moveit_commander from geometry_msgs.msg import Pose moveit_commander.roscpp_initialize([]) group moveit_commander.MoveGroupCommander(arm_group) group.set_goal_position_tolerance(0.005) group.set_goal_orientation_tolerance(0.01) target Pose() target.position.x 0.25 target.position.y -0.10 target.position.z 0.15 target.orientation.w 1.0 # 先保持默认姿态实际需根据药盒朝向调整 group.set_pose_target(target) plan group.plan() if plan[0]: group.execute(plan[0], waitTrue) else: rospy.logwarn(规划失败检查目标点是否在可达空间内)逻辑说明set_pose_target会尝试逆解若失败会返回空规划。参数上位置容差和姿态容差要根据夹爪精度设太松会抓偏太紧会频繁规划失败。常见坑是药架被当作碰撞体但没加入规划场景导致机械臂“穿模”撞架。3. 视觉识别与抓取位姿补偿让机器人不靠运气抓药3.1 药盒识别颜色分割够用但别只靠颜色赛题药盒通常有固定颜色或标签颜色分割是最快上手的方案。但现场光照一变阈值就废。我一般会做两层先用颜色粗筛再用轮廓形状或模板匹配精筛。如果药盒上有文字或条码可以加一个轻量OCR或条码识别做校验。下面是一个结合轮廓面积和长宽比的筛选逻辑。# 在颜色mask基础上按面积和长宽比过滤候选药盒 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for c in contours: area cv2.contourArea(c) if area 800 or area 20000: # 根据实际像素调整 continue rect cv2.minAreaRect(c) w, h rect[1] if w 0 or h 0: continue ratio max(w, h) / min(w, h) if 1.2 ratio 3.5: # 药盒常见长宽比范围 candidates.append(c) # 取面积最大的作为目标 if candidates: target max(candidates, keycv2.contourArea)参数说明面积上下限和长宽比范围需要根据相机分辨率和药盒实际尺寸标定。常见错误是阈值设得太宽把药架边缘或反光区域也当成药盒。3.2 抓取位姿补偿为什么你的夹爪总是差几毫米视觉给出的中心点往往是药盒几何中心但夹爪需要的是抓取边或抓取面的中心。如果药盒摆放有旋转直接抓几何中心可能导致夹爪撞到相邻药盒。我一般会计算最小外接矩形的角度把抓取点沿短轴方向偏移一个夹爪厚度。下面是一个补偿示例。# 根据最小外接矩形计算抓取点偏移 rect cv2.minAreaRect(target) center, (w, h), angle rect # 假设夹爪沿短轴方向抓取偏移量为夹爪半宽 gripper_half 15 # 像素需根据标定换算 if w h: offset np.array([np.cos(np.radians(angle)), np.sin(np.radians(angle))]) * gripper_half else: offset np.array([-np.sin(np.radians(angle)), np.cos(np.radians(angle))]) * gripper_half grasp_u center[0] offset[0] grasp_v center[1] offset[1]逻辑说明偏移方向取决于夹爪安装方向和药盒朝向。参数gripper_half要换算到像素单位否则补偿量不对。这个步骤能明显减少“抓空”和“撞盒”。3.3 多药盒场景下的任务排序先抓哪个有讲究如果处方要求抓多个药盒顺序会影响节拍。我一般按“从远到近、从高到低”排序避免机械臂反复跨越已抓区域。可以用简单的贪心策略每次选距离当前位姿最近的目标。下面是一个排序片段。# 按距离当前末端位置排序减少空走 targets [(x1, y1), (x2, y2), (x3, y3)] # 从视觉获得的基座坐标 current (0.0, 0.0) # 当前末端xy targets_sorted sorted(targets, keylambda p: (p[0]-current[0])**2 (p[1]-current[1])**2)参数说明距离度量用欧氏距离即可若药架有高度差可加权z方向。常见坑是忽略机械臂关节限位排序后某些点根本不可达规划时才发现。4. 避坑与排查智慧药房赛项里最容易翻车的五件事4.1 标定当天有效第二天就飘了现象昨天抓得准今天同一位置抓空。原因相机支架被碰动、光照变化导致阈值偏移、机械臂零点漂移。解决每次上电先跑一次标定验证用固定标记点检查重投影误差超过阈值就重新标定。我习惯在药架上贴一个已知位置的ArUco码每次启动先看它的像素坐标是否在允许范围内。4.2 识别框看着对但抓取点偏到隔壁药盒现象可视化显示目标框正确但机械臂抓的是相邻药盒。原因轮廓筛选把两个药盒连在一起或者最小外接矩形角度计算错误。解决加形态学开运算分离粘连区域检查minAreaRect返回的角度定义OpenCV版本差异会导致角度范围不同。血泪经验是角度补偿前先打印实际角度值别凭感觉写。4.3 MoveIt规划成功但执行时撞架现象plan返回成功execute时机械臂撞到药架边缘。原因规划场景中没有加入药架碰撞体或者碰撞体尺寸比实际小。解决用RViz的Scene Robot或直接加载STL模型作为碰撞体尺寸留2-3厘米余量。注意MoveIt的碰撞检测默认可能不包含环境物体必须手动添加。4.4 多药盒任务中途失败整个流程卡死现象抓第三个药盒时识别失败程序一直等不跳过也不报错。原因状态机没有超时和重试机制。解决每个步骤加超时识别失败重试2次后跳过并记录继续下一个目标。比赛评分通常按完成数量算卡死等于零分。我一般会写一个简单的状态机用rospy.Timer做超时监控。4.5 现场光照导致颜色阈值全废现象实验室调好的HSV阈值到赛场完全失效。原因赛场灯光色温不同药盒反光。解决准备一套自适应阈值方案比如用OTSU分割或直方图均衡化预处理。更稳妥的是带一个可调LED补光灯现场微调。后悔药是提前在多种光照下录视频离线调参。5. 进阶技巧用状态机与日志回放把稳定性拉满如果你已经跑通最小闭环下一步不是加功能而是加可观测性。我一般会做两件事一是把任务流程写成显式状态机每个状态有进入条件、退出条件、超时和重试二是把所有传感器数据、识别结果、规划目标、执行结果打成日志支持离线回放。这样出问题时不用猜直接看日志定位。状态机可以用Python的transitions库或自己写一个简单字典。下面是一个极简状态机骨架展示如何管理“识别-抓取-放置”循环。# 极简状态机管理多药盒任务带超时和重试 import time class StateMachine: def __init__(self, targets): self.targets targets self.idx 0 self.state IDLE self.retry 0 self.max_retry 2 self.timeout 5.0 self.last_time time.time() def run(self): while self.idx len(self.targets): if self.state IDLE: self.state DETECT self.last_time time.time() elif self.state DETECT: ok self.detect(self.targets[self.idx]) if ok: self.state GRASP elif time.time() - self.last_time self.timeout: self.retry 1 if self.retry self.max_retry: self.idx 1 self.retry 0 self.state IDLE else: self.state DETECT self.last_time time.time() elif self.state GRASP: ok self.grasp() if ok: self.state PLACE else: self.state DETECT # 抓取失败重新识别 elif self.state PLACE: ok self.place() if ok: self.idx 1 self.retry 0 self.state IDLE else: self.state GRASP # 放置失败重新抓 time.sleep(0.1) def detect(self, target): # 实际调用视觉识别返回是否成功 return True def grasp(self): # 实际调用机械臂抓取 return True def place(self): # 实际调用放置 return True逻辑说明这个状态机把每个动作拆成独立状态失败时回退到上一步而不是整个流程重启。参数上超时和重试次数要根据节拍调整太短会频繁重试太长会浪费时间。日志回放方面我习惯用rosbag录下所有话题赛后用rviz和rqt_bag逐帧分析。验证方法很简单把录好的bag重新播放看状态机是否按预期跳转识别结果是否与当时一致。最后说一个我自己的习惯每次比赛前我会把整个流程在脑子里过三遍然后写一张“故障树”贴在工位上——从电源、相机、机械臂、网络到每个节点的超时设置逐项打勾。这个习惯帮我省过至少两次现场翻车。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑