简介:这份资源是第二十五届中国机器人及人工智能大赛智慧药房组的完整参赛作品包,面向备战同类机器人竞赛的高校学生与指导教师,尤其适合需要参考赛题实现思路、搭建仿真与实机调试环境的中高级参赛者。压缩包共150个文件,约38.19MB,以39个yaml配置、23个Python脚本、11个launch启动文件、10个srv服务定义及若干cpp、msg、xml为主,构成ROS工程骨架;同时包含21张jpg与3张pgm图像、4份pdf设计文档、4份md说明、3段mp3语音及3mf模型等素材,覆盖感知、导航、任务调度与文档记录多个环节。资源已有825人学习下载,可帮助读者获取智慧药房赛项的完整方案参考,包括节点通信结构、目标点下发逻辑、视觉检测脚本与设计文档,便于对照复现、拆解模块并排查调试问题。
1. 智慧药房赛项资源包:从文件清单反推一套可复现的参赛系统
拆开这个压缩包,第一眼看到的不是整齐的工程目录,而是一堆“混搭”文件:main.cpp、send_goals_node.cpp是典型的 ROS 节点源码,yolox.bin是训练好的检测模型权重,reminder_basic.py.bak和launch_watcher.bash像是调试期留下的脚本备份,还有Start_Point1.JPG、test_image.jpg这类现场标定和测试图,以及一个珍贵的顶盖.gcode.3mf——这明显是打印件模型,大概率是药房机构里的某个结构件。这套资源对应的是大学生竞赛里“智慧药房”方向的完整参赛作品,包含设计文档和源码,适合正在备赛、想找一套能跑通的参考实现、或者需要对照自己方案查漏补缺的队伍。它解决的核心问题是:把“药房场景下的机器人取药、送药、识别、调度”这条链路,从文档到代码到模型权重一次性给全,让你不用从零搭框架,而是站在一个已经跑过赛的工程上做二次开发。如果你正卡在“知道要做什么但不知道怎么组织工程”的阶段,这份东西的参考价值比单纯看论文大得多。
2. 工程结构拆解:从文件后缀判断模块归属与依赖关系
2.1 按后缀给文件分类,先建一张依赖地图
拿到一个没有 README 的压缩包,最忌讳直接双击main.cpp开始读。我一般先按后缀和命名把文件归到几个功能域里,再决定阅读顺序。这套资源里的文件可以分成四类:
| 文件 | 类型 | 推测功能 | 阅读优先级 |
|---|---|---|---|
main.cpp | C++ 源码 | 主控节点,负责整体状态机或任务调度 | 高 |
send_goals_node.cpp | C++ 源码 | 向导航栈发送目标点,控制机器人移动 | 高 |
yolox.bin | 模型权重 | YOLOX 检测模型,用于药盒/药品识别 | 中 |
reminder_basic.py.bak | Python 备份 | 提醒逻辑或状态监控脚本 | 低 |
launch_watcher.bash | Shell 脚本 | 启动监控,可能负责拉起多个节点 | 中 |
Start_Point1.JPG/Start_Point2.JPG | 图片 | 起点标定参考图 | 低 |
test_image.jpg | 图片 | 检测模型测试输入 | 低 |
珍贵的顶盖.gcode.3mf | 3D 打印文件 | 药房机构顶盖结构件 | 低 |
.gitignore | 配置文件 | 版本控制忽略规则 | 低 |
这张表的价值在于:你不需要通读所有文件,先看main.cpp和send_goals_node.cpp就能抓住控制流,yolox.bin决定感知能力上限,其余是辅助。常见做法是先把两个 cpp 文件的#include和ros::init部分扫一遍,确认 ROS 版本和依赖包。
2.2 从main.cpp和send_goals_node.cpp还原控制流
send_goals_node.cpp这个名字已经暴露了它的职责:它大概率是一个 action client,向move_base或类似导航包发送move_base_msgs::MoveBaseGoal。我一般会先找goal.target_pose.pose.position和orientation的赋值位置,那里就是目标点的坐标来源。如果坐标是硬编码的,说明这套代码的导航点是提前标定好的;如果是从参数服务器或文件读取的,那就有配置入口。
main.cpp更可能是顶层状态机。重点看它有没有switch或状态枚举,以及它如何调用检测结果和导航目标。一个典型的智慧药房流程是:等待订单 → 导航到药架 → 检测药盒 → 抓取/确认 → 导航到取药口 → 完成。你需要在main.cpp里找到这个循环的入口和退出条件。
// 典型 send_goals_node.cpp 片段(根据常见 ROS 导航写法还原) #include <ros/ros.h> #include <move_base_msgs/MoveBaseAction.h> #include <actionlib/client/simple_action_client.h> typedef actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction> MoveBaseClient; int main(int argc, char** argv){ ros::init(argc, argv, "send_goals_node"); MoveBaseClient ac("move_base", true); // 等待导航服务器启动,超时 60 秒 while(!ac.waitForServer(ros::Duration(60.0))){ ROS_INFO("Waiting for move_base action server..."); } move_base_msgs::MoveBaseGoal goal; goal.target_pose.header.frame_id = "map"; goal.target_pose.header.stamp = ros::Time::now(); // 以下坐标通常来自标定,需按实际场地修改 goal.target_pose.pose.position.x = 1.2; goal.target_pose.pose.position.y = -0.8; goal.target_pose.pose.orientation.w = 1.0; ac.sendGoal(goal); ac.waitForResult(); return 0; }这段代码的逻辑说明:MoveBaseClient连接名为move_base的 action server,waitForServer确保导航栈已经就绪,goal.target_pose里的frame_id必须是map,坐标和朝向需要根据你的场地重新标定。参数说明:position.x/y是目标点在 map 坐标系下的位置,单位米;orientation.w是四元数,只给 w 表示朝向角为 0。如果你直接拿这套坐标去跑,机器人大概率会撞墙,因为场地尺寸不同。
2.3yolox.bin的加载方式与输入输出约定
yolox.bin是二进制权重,不是可以直接cat看的文本。你需要知道它对应哪个推理框架。常见做法是:如果工程里有 Python 推理脚本,那大概率是 PyTorch 导出的 ONNX 或 TensorRT 引擎;如果只有 C++,可能是 OpenVINO 或 NCNN 的模型文件。从文件后缀.bin看,OpenVINO 的 IR 模型通常是.xml+.bin成对出现,这里只有.bin,那更可能是 NCNN 的.param+.bin组合,但缺少.param文件。另一种可能是 TensorRT 序列化后的 engine 文件被重命名了。
你需要在工程里搜索yolox关键字,看哪个文件引用了它。如果找不到,那这个权重可能只是备份,实际推理用的是别的路径。我一般会先用file yolox.bin看文件类型,再用strings yolox.bin | head看有没有框架特征字符串。如果确认是 NCNN,你需要补一个.param文件才能加载;如果是 TensorRT,需要知道生成时的 CUDA 和 TensorRT 版本,版本不匹配会直接报错。
提示:模型权重和推理代码必须版本对齐,拿到的
.bin如果缺少配套的.param或.onnx,不要硬猜,先找文档里有没有记录导出命令。
3. 从源码到可运行:环境搭建与节点启动的实操步骤
3.1 ROS 工作空间初始化与依赖安装
这套代码用了ros::init和 actionlib,基本可以确定是 ROS1 环境。常见做法是建一个 catkin 工作空间,把源码放进src下,然后catkin_make。但直接编译大概率会报缺包,因为move_base_msgs、actionlib这些不是默认全装的。
# 创建 ROS 工作空间 mkdir -p ~/smart_pharmacy_ws/src cd ~/smart_pharmacy_ws/src # 把 main.cpp 和 send_goals_node.cpp 放到一个包目录下,例如 pharmacy_ctrl/src/ # 假设包名为 pharmacy_ctrl,需要先创建 package.xml 和 CMakeLists.txt catkin_create_pkg pharmacy_ctrl roscpp move_base_msgs actionlib std_msgs cd ~/smart_pharmacy_ws catkin_make source devel/setup.bash逻辑说明:catkin_create_pkg会自动生成package.xml和CMakeLists.txt,并把roscpp、move_base_msgs、actionlib、std_msgs列为依赖。参数说明:包名pharmacy_ctrl可以改成你自己的,但要和CMakeLists.txt里的add_executable对应。如果编译时报Could not find move_base_msgs,说明 ROS 安装时没装导航包,需要sudo apt install ros-<distro>-move-base-msgs,<distro>换成你的 ROS 版本,比如noetic。
3.2 启动顺序:先导航栈,再感知,最后主控
ROS 节点的启动顺序很关键。如果send_goals_node先起来,move_base还没启动,waitForServer会一直等,虽然不会崩,但会卡住。我一般按这个顺序:
- 启动机器人底盘驱动和传感器(激光雷达、相机)。
- 启动
move_base导航栈,加载地图和代价地图参数。 - 启动感知节点,加载
yolox.bin做检测。 - 启动
main.cpp对应的主控节点。 - 最后启动
send_goals_node或由主控节点内部调用。
launch_watcher.bash很可能就是干这个的。你可以先cat launch_watcher.bash看它rosrun或roslaunch了哪些东西,然后照着它的顺序手动跑一遍,这样出问题能定位到具体节点。
# 查看 launch_watcher.bash 内容,了解启动顺序 cat launch_watcher.bash # 典型内容可能类似: # roslaunch robot_bringup bringup.launch & # sleep 5 # rosrun move_base move_base & # sleep 3 # rosrun pharmacy_ctrl main_node & # rosrun pharmacy_ctrl send_goals_node &逻辑说明:&表示后台运行,sleep给前一个节点留出初始化时间。参数说明:sleep的秒数不是固定的,取决于你的硬件启动速度,激光雷达通常需要 5 到 10 秒才能出数据。如果move_base还没准备好就发目标点,机器人会原地不动或报Goal rejected。
3.3 用test_image.jpg验证检测链路是否通
test_image.jpg是现成的测试输入,你可以用它先单独跑一遍检测,确认yolox.bin能正常加载并输出框。如果工程里没有现成的推理脚本,常见做法是写一个最小的 Python 脚本,用 OpenCV 读图,用 ONNXRuntime 或 NCNN 加载模型,打印输出维度。
import cv2 import numpy as np # 假设模型是 ONNX 格式,输入尺寸 640x640 net = cv2.dnn.readNetFromONNX("yolox.onnx") # 如果只有 .bin,需要先转成 ONNX img = cv2.imread("test_image.jpg") blob = cv2.dnn.blobFromImage(img, 1/255.0, (640, 640), swapRB=True) net.setInput(blob) outs = net.forward() print("输出维度:", outs.shape) # 期望 (1, 8400, 85) 或类似逻辑说明:blobFromImage把图片缩放到 640x640 并归一化,swapRB=True是因为 OpenCV 默认 BGR 而模型训练常用 RGB。参数说明:1/255.0是归一化系数,(640,640)必须和训练时输入尺寸一致,否则检测框会错位。如果outs.shape不是预期的检测头维度,说明模型文件不对或输入尺寸不匹配。这一步能跑通,再往 ROS 里集成就有底了。
4. 避坑与排查:五个让参赛队翻车的典型问题
4.1 现象:catkin_make报undefined reference to ros::init
原因:CMakeLists.txt里没有正确链接roscpp,或者find_package(catkin REQUIRED COMPONENTS roscpp ...)写了但target_link_libraries漏了${catkin_LIBRARIES}。解决:打开CMakeLists.txt,确认add_executable之后有target_link_libraries(你的节点名 ${catkin_LIBRARIES}),并且include_directories包含了${catkin_INCLUDE_DIRS}。
4.2 现象:机器人收到目标点后原地转圈或不动
原因:move_base的全局规划器没有收到有效地图,或者目标点在障碍物里。Start_Point1.JPG和Start_Point2.JPG是起点标定图,如果你没按图上的位置摆放机器人,里程计初始位姿就是错的。解决:先确认map坐标系下的机器人初始位姿和标定图一致,再用rviz的2D Pose Estimate手动给一个初始位姿,看激光点云和地图是否重合。
4.3 现象:yolox.bin加载时报Unsupported model format
原因:.bin文件缺少配套的.param(NCNN)或.xml(OpenVINO),或者文件本身是 TensorRT engine 但当前环境没有对应版本的 TensorRT。解决:先用file yolox.bin确认文件类型,如果是data且strings里出现ncnn字样,去补.param;如果出现TensorRT字样,检查 CUDA 和 TensorRT 版本是否和生成时一致。不一致就重新导出 ONNX,再用当前环境转一次。
4.4 现象:reminder_basic.py.bak直接运行报语法错误
原因:.bak是备份文件,可能对应的是旧版本代码,里面的 API 已经和当前 ROS 消息定义不匹配。解决:不要直接跑.bak,先看有没有同名的.py文件。如果没有,把.bak复制成.py,然后检查import的包是否还在,消息类型是否和main.cpp里用的一致。常见的是std_msgs/String和自定义消息混用导致反序列化失败。
4.5 现象:珍贵的顶盖.gcode.3mf打印出来装不上
原因:.3mf文件里包含的模型可能不是最终版,或者打印材料收缩率没补偿。解决:先用切片软件打开.3mf,看模型尺寸和你的机构设计图是否一致。如果差 0.2 到 0.5 毫米,在切片软件里做 XY 补偿。另外,.gcode.3mf后缀说明它可能已经切片过,直接打印会沿用原来的参数,换机器前要重新切片。
注意:避坑的核心是“先验证单点,再串联全链路”。不要一上来就
roslaunch全部节点,那样报错信息会混在一起,根本定位不到是谁的问题。
5. 进阶用法:把send_goals_node改成参数化目标点,并用test_image做回归验证
这套资源里最值得动手改的是send_goals_node.cpp的硬编码坐标。比赛场地每年可能变,硬编码意味着每次都要重新编译。我一般会把它改成从 ROS 参数服务器读取目标点列表,这样只需要改 YAML 文件,不用动代码。
// 改造后的 send_goals_node.cpp 片段:从参数服务器读取多个目标点 #include <ros/ros.h> #include <move_base_msgs/MoveBaseAction.h> #include <actionlib/client/simple_action_client.h> #include <vector> typedef actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction> MoveBaseClient; int main(int argc, char** argv){ ros::init(argc, argv, "send_goals_node"); ros::NodeHandle nh("~"); MoveBaseClient ac("move_base", true); while(!ac.waitForServer(ros::Duration(60.0))){ ROS_INFO("Waiting for move_base..."); } XmlRpc::XmlRpcValue goals; nh.getParam("goal_list", goals); // 从参数服务器读取列表 for(int i = 0; i < goals.size(); ++i){ move_base_msgs::MoveBaseGoal goal; goal.target_pose.header.frame_id = "map"; goal.target_pose.header.stamp = ros::Time::now(); goal.target_pose.pose.position.x = (double)goals[i]["x"]; goal.target_pose.pose.position.y = (double)goals[i]["y"]; goal.target_pose.pose.orientation.w = (double)goals[i]["w"]; ac.sendGoal(goal); ac.waitForResult(); ROS_INFO("Reached goal %d", i); } return 0; }逻辑说明:ros::NodeHandle nh("~")让节点读取私有命名空间下的参数,nh.getParam("goal_list", goals)把 YAML 里的列表读进XmlRpcValue。参数说明:goals[i]["x"]对应 YAML 里的x字段,需要显式转成double。对应的 YAML 文件可以这样写:
# goals.yaml goal_list: - {x: 1.2, y: -0.8, w: 1.0} - {x: 2.5, y: 0.3, w: 0.707} - {x: 0.5, y: 1.1, w: 0.0}启动时用rosparam load goals.yaml加载,再rosrun pharmacy_ctrl send_goals_node。这样改完,场地变了只需要改 YAML,不用重新编译。
另一个进阶技巧是用test_image.jpg做回归验证。每次改完检测相关代码,先跑一遍test_image.jpg,把输出的框画出来存成图片,和上一次的结果对比。如果框的位置偏移超过几个像素,说明预处理或后处理被改动了。我一般会写一个verify_detection.py,固定随机种子,固定输入,输出框的坐标和置信度,存成 JSON。下次跑的时候 diff 一下 JSON,就能快速发现回归。
import cv2, json, numpy as np net = cv2.dnn.readNetFromONNX("yolox.onnx") img = cv2.imread("test_image.jpg") blob = cv2.dnn.blobFromImage(img, 1/255.0, (640,640), swapRB=True) net.setInput(blob) outs = net.forward() # 只保留置信度大于 0.5 的框,存成 JSON boxes = [] for det in outs[0]: conf = det[4] if conf > 0.5: boxes.append({"x": float(det[0]), "y": float(det[1]), "w": float(det[2]), "h": float(det[3]), "conf": float(conf)}) with open("detection_baseline.json", "w") as f: json.dump(boxes, f, indent=2) print(f"保存 {len(boxes)} 个检测框")逻辑说明:outs[0]是检测头输出,det[4]是置信度,det[0:4]是框的中心点和宽高。参数说明:置信度阈值0.5可以按场景调,药盒检测一般可以设到0.6减少误检。把结果存成 JSON 后,下次改动代码再跑一遍,用diff对比,就能知道有没有引入回归。
从那以后我每次拿到没有 README 的参赛资源包,都强制先做三件事:按后缀分类建依赖地图、用一张测试图验证感知链路、把硬编码参数改成可配置。这三步走完,后面无论怎么改都不会迷路。希望帮到你。
本文还有配套的精品资源,点击获取