资讯动态

小贩机器人赛项全解析:从底盘导航到视觉交互的工程实践

发布时间:2026/10/22 3:18:11 来源:尧图企业网站定制
简介这份资源是2021年挑战杯国赛优秀作品「小贩机器人」的完整参赛资料包来自成都越凡创新科技有限公司适合关注人工智能与机器人方向的参赛学生、指导教师及技术爱好者用于了解国赛级项目的研发思路与竞赛准备路径。压缩包共70个文件约20.37MB以48张jpeg与7张png图片为主辅以12个xml及3个rels等Office文档结构文件图片多用于呈现机器人外观、交互界面与场景演示xml等则承载PPT的元数据与版式信息。资源内含项目PPT与演示视频PPT系统梳理了项目背景、自动驾驶与计算机视觉等技术概述、产品设计、功能特性、市场分析、商业模型、项目实施、团队介绍及未来规划视频则直观展示机器人在零售场景中的实际运行与用户互动过程。目前已有91人学习可为复盘国赛作品结构、提炼创新点与撰写竞赛材料提供参考。1. 小贩机器人赛项拆解从一台会卖货的移动平台说起第一次看到“小贩机器人”这个赛项标题很多人会以为重点是“卖货”其实真正决定成败的是底盘能不能在人群里稳定穿行、识别能不能在逆光和遮挡下把商品认对、交互流程能不能在几十秒内完成一次闭环。这个赛项面向的是移动机器人方向的从业者和学生团队核心考察自主导航、视觉识别、人机交互和任务调度四块能力的整合。它不需要你从零造一台机器人常见做法是基于现成的移动底盘加装货架、屏幕和深度相机再用一套上层调度软件把“招手即停、选品、支付、取货”串起来。适合有 ROS 基础、做过巡线或 SLAM 小项目、想往服务机器人方向深入的人。如果你只调过仿真没碰过实车这个赛项会逼你把传感器标定、坐标变换和异常恢复全部走一遍血泪经验基本都藏在这些环节里。2. 赛项任务流与硬件选型先想清楚机器人要干什么2.1 从“招手—选品—交付”拆出四个可独立验证的模块赛项的任务描述通常是一段连续动作但直接照着做会陷入“整条链路一起调、出错不知道哪一层”的困境。我一般会把它拆成四个可独立验证的模块感知模块负责检测行人手势和商品位置决策模块负责判断“是否停车、停在哪、推荐什么”运动模块负责把底盘开到目标点并保持朝向交互模块负责屏幕显示、语音提示和取货舱门控制。拆开之后每个模块都能单独写测试用例比如让机器人静止只跑手势检测看帧率和误检再让机器人空载跑固定路线看定位漂移。这样出问题时能快速定位是感知误判还是控制超调。拆分的另一个好处是并行开发。一个三人小队可以一人负责视觉、一人负责导航、一人负责交互和状态机接口用 ROS 话题或自定义消息固定下来。常见做法是定义一个/task_state话题用整数表示“待机、接近、等待选品、交付中、异常”所有模块只读写这个状态避免互相直接调用。这个约定看起来简单但能省掉后期大量联调扯皮。2.2 底盘、相机、算力的选型边界与参数底线硬件选型直接决定后面能调多细。底盘方面差速驱动是赛项里最稳的方案转弯半径小、控制模型简单麦克纳姆轮虽然能横移但对地面平整度敏感赛场地毯接缝处容易打滑。底盘最大速度建议限制在 0.6 米每秒以内加速度不超过 0.5 米每二次方秒否则急停时货架上的商品会倒。深度相机选型看两点水平视场角至少 70 度才能覆盖机器人正前方和两侧行人的手势近处盲区小于 0.3 米否则人伸手选品时手会消失在画面里。算力平台是容易被低估的一环。如果视觉模型跑在 CPU 上手势检测很难稳定到 15 帧每秒交互就会卡顿。常见做法是选带入门级 GPU 的嵌入式计算平台或者用轻量模型加推理加速库。内存建议不低于 4GB因为同时跑导航、视觉和交互进程时内存峰值会比你想象的高。电源方面底盘电池和计算平台最好分开供电避免电机启动瞬间把计算平台拉重启这个坑我在早期测试时踩过机器人跑到一半直接黑屏排查了半天才发现是共地干扰。2.3 用 URDF 和 TF 树把传感器坐标一次标对坐标变换是移动机器人里最玄学也最不能省的一步。相机装在货架顶部它的坐标系和底盘坐标系之间差一个平移加旋转如果不标定检测到的商品位置投影到地面就会偏出几十厘米。标准做法是写一个 URDF 文件描述机器人各部件关系然后用静态变换发布器把相机、激光雷达、底盘的相对位置固定下来。!-- 机器人URDF片段定义底盘、相机、货架的相对位姿 -- robot namevendor_bot !-- 底盘基座作为根坐标系 -- link namebase_link/ !-- 深度相机安装在货架顶部向前倾斜15度 -- joint namecamera_joint typefixed parent linkbase_link/ child linkcamera_link/ !-- 平移前方0.35米上方1.2米 -- origin xyz0.35 0 1.2 rpy0 0.26 0/ /joint link namecamera_link/ !-- 取货舱口在底盘正前方用于计算交付点 -- joint nametray_joint typefixed parent linkbase_link/ child linktray_link/ origin xyz0.4 0 0.6 rpy0 0 0/ /joint link nametray_link/ /robot这段 URDF 的关键参数是origin里的xyz和rpy。xyz单位是米表示子坐标系原点在父坐标系下的位置rpy是绕固定轴的滚转、俯仰、偏航角单位弧度。相机向前倾斜 15 度对应约 0.26 弧度这个角度决定了它主要看人脸高度还是看地面商品。标定完之后用rosrun tf view_frames生成 TF 树图确认从base_link到camera_link的链路完整没有断开的节点。如果后面发现检测框在地面上的投影总是偏一侧优先检查rpy的偏航角是不是写成了 0 但实际安装有偏转。3. 感知与交互落地让机器人认出手和商品3.1 手势检测用轻量模型加 ROI 裁剪别一上来就上大网络赛项里“招手即停”是第一个交互触发点检测目标就是“人举起手并左右摆动”。直接跑通用人体姿态估计模型在嵌入式计算平台上帧率不够常见做法是先用一个轻量的人体检测器框出上半身再在裁剪区域里跑手部关键点或简单分类。这样计算量能降一半以上而且背景干扰也少了。# 手势触发检测先检测人再在ROI内判断举手 import cv2 import numpy as np def detect_waving(frame, person_detector, hand_classifier): # 第一步检测画面中的人返回上半身框 person_boxes person_detector.detect(frame) if len(person_boxes) 0: return False, None for box in person_boxes: x1, y1, x2, y2 box # 只取上半身区域减少手部搜索范围 upper frame[y1:y1 (y2 - y1) // 2, x1:x2] if upper.size 0: continue # 第二步在裁剪区域判断是否举手 is_waving hand_classifier.predict(upper) if is_waving: # 返回触发标志和人的位置供决策模块计算停车点 return True, (x1, y1, x2, y2) return False, None这段代码的逻辑是“先粗后精”person_detector可以用轻量 YOLO 或 MobileNet 系列输入分辨率降到 320 乘 320 就够用hand_classifier是一个二分类小网络输入是上半身裁剪图输出“是否举手”。参数上person_boxes的置信度阈值建议设 0.5太低会把货架误检成人手部分类的阈值设 0.7因为误触发一次停车比漏触发更影响体验。如果现场逆光严重可以在预处理里加直方图均衡化但要注意别把噪声也放大。3.2 商品识别用模板匹配还是分类网络看商品数量决定商品识别有两种常见路线商品种类少于 10 种且外观差异大时用颜色加形状的模板匹配就够速度快、不需要训练数据种类多或者外观相近时才需要训练分类网络。赛项里通常商品数量有限我一般先用模板匹配跑通流程再根据误检情况决定要不要换网络。模板匹配的关键是给每个商品定义稳定的特征区域。比如饮料瓶取瓶身标签区域零食袋取正面 logo 区域。代码上可以用 OpenCV 的matchTemplate但要注意光照变化会让灰度值整体偏移所以匹配前先做归一化。# 商品模板匹配对每个商品在货架区域做归一化匹配 import cv2 def match_products(shelf_img, templates, threshold0.75): results [] # 转灰度并做直方图均衡降低光照影响 gray cv2.cvtColor(shelf_img, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) for name, tmpl in templates.items(): tmpl_gray cv2.cvtColor(tmpl, cv2.COLOR_BGR2GRAY) tmpl_gray cv2.equalizeHist(tmpl_gray) # 使用归一化相关系数匹配对亮度变化更鲁棒 res cv2.matchTemplate(gray, tmpl_gray, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(res) if max_val threshold: results.append((name, max_val, max_loc)) # 按匹配分数排序取最高分作为识别结果 results.sort(keylambda x: x[1], reverseTrue) return resultsTM_CCOEFF_NORMED是归一化相关系数匹配返回值在负一到一之间越接近一越匹配。阈值 0.75 是经验值低于这个值容易把相似包装认错高于 0.85 又会在商品轻微移动时漏检。max_loc是匹配区域左上角坐标可以换算成商品在货架上的位置供机械结构或提示灯定位。如果发现同一商品在不同光照下分数波动大优先检查模板图是不是在均匀光下拍的模板质量比算法选择更影响结果。3.3 交互状态机把“等待”和“超时”当成一等公民交互流程最容易翻车的地方不是识别不准而是状态卡死。比如机器人停下后等人选品人选了半天没选机器人一直等后面任务全堵住。所以状态机里必须给每个等待状态设超时超时后回到待机或播报提示。# 简化交互状态机每个等待状态都有超时保护 import time class TaskState: IDLE 0 APPROACH 1 WAIT_SELECT 2 DELIVER 3 TIMEOUT 4 class StateMachine: def __init__(self, timeout_sec20): self.state TaskState.IDLE self.state_start time.time() self.timeout timeout_sec def update(self, events): elapsed time.time() - self.state_start # 任何等待状态超时都回到待机并播报 if self.state in (TaskState.WAIT_SELECT, TaskState.DELIVER) and elapsed self.timeout: self._transition(TaskState.TIMEOUT) return if self.state TaskState.IDLE and events.get(wave_detected): self._transition(TaskState.APPROACH) elif self.state TaskState.APPROACH and events.get(arrived): self._transition(TaskState.WAIT_SELECT) elif self.state TaskState.WAIT_SELECT and events.get(product_selected): self._transition(TaskState.DELIVER) elif self.state TaskState.DELIVER and events.get(taken): self._transition(TaskState.IDLE) def _transition(self, new_state): self.state new_state self.state_start time.time()这段状态机的核心是state_start和timeout。每次状态切换都重置计时等待超过 20 秒就进入超时分支播报“请重新招手”并回到待机。参数timeout_sec可以根据赛项节奏调整一般选品等待给 20 到 30 秒交付等待给 15 秒。注意events字典的键要和感知模块输出对齐比如wave_detected来自手势检测arrived来自导航模块taken来自舱门红外或重量传感器。如果发现状态偶尔跳变先检查事件是不是在同一帧里同时为真必要时加去抖。4. 导航与运动控制在人群里停得稳比走得快重要4.1 用代价地图加膨胀层处理行人别把障碍物当静态点赛项场地里行人走动频繁如果导航只按静态地图规划机器人会频繁急停。常见做法是在局部代价地图里把检测到的人标记为高代价区域并做膨胀处理让规划器提前绕开而不是贴脸急停。膨胀半径建议设 0.4 到 0.5 米相当于给行人留出半步空间。# 局部代价地图配置片段行人膨胀参数 local_costmap: plugins: - {name: obstacle_layer, type: costmap_2d::ObstacleLayer} - {name: inflation_layer, type: costmap_2d::InflationLayer} obstacle_layer: observation_sources: scan camera scan: {data_type: LaserScan, topic: /scan, marking: true, clearing: true} camera: {data_type: PointCloud2, topic: /person_cloud, marking: true, clearing: false} inflation_layer: inflation_radius: 0.45 # 膨胀半径覆盖行人周围安全距离 cost_scaling_factor: 3.0 # 代价衰减速度越大越倾向于远离障碍inflation_radius是膨胀层从障碍物边缘向外扩展的距离0.45 米大约是一个人肩宽的一半。cost_scaling_factor控制代价随距离衰减的快慢值越大规划器越早开始避让。camera源只标记不清除是因为行人点云更新频率低如果让它清除障碍人走过去后代价地图会残留空白反而危险。这个配置的坑在于膨胀半径设太大窄通道会直接规划失败所以赛前要按场地最窄处反推上限。4.2 停车点计算让机器人正对用户而不是斜着停“招手即停”不是随便停而是停在用户正前方约 1 米处并且车头朝向用户。常见做法是拿到人的位置后在机器人坐标系下算一个目标点沿机器人到人的方向后退 1 米同时把目标朝向设成面向人。# 根据检测到的人位置计算停车目标点 import math def compute_stop_pose(robot_pose, person_pose, standoff1.0): # robot_pose: (x, y, yaw)person_pose: (x, y) dx person_pose[0] - robot_pose[0] dy person_pose[1] - robot_pose[1] dist math.hypot(dx, dy) if dist 0.1: return None # 太近忽略 # 目标点从人往机器人方向退 standoff 米 ratio max(0.0, (dist - standoff)) / dist goal_x robot_pose[0] dx * ratio goal_y robot_pose[1] dy * ratio # 目标朝向面向人 goal_yaw math.atan2(dy, dx) return (goal_x, goal_y, goal_yaw)standoff是停车距离设 1 米是为了让用户伸手能够到货架同时相机还能拍到全身。ratio的计算保证目标点落在机器人和人之间不会越过人。goal_yaw用atan2算的是从机器人指向人的方向作为车头朝向。如果发现机器人停稳后车头偏了优先检查导航模块是不是把goal_yaw当成了可选参数有些配置里朝向容差设得太大机器人会斜着停。4.3 速度平滑用梯形加减速替代阶跃指令底盘速度指令如果直接给阶跃机器人会猛冲猛停货架上的商品容易倒轮子也容易打滑。常见做法是在速度指令和底盘之间加一个平滑器用梯形或 S 形加减速限制加速度。# 速度平滑限制加速度避免阶跃指令 class VelocitySmoother: def __init__(self, max_accel0.5, dt0.05): self.max_accel max_accel # 米每二次方秒 self.dt dt self.current_v 0.0 self.current_w 0.0 def smooth(self, target_v, target_w): # 线速度按最大加速度逼近目标 dv target_v - self.current_v max_dv self.max_accel * self.dt self.current_v max(-max_dv, min(max_dv, dv)) # 角速度同样限制避免原地猛转 dw target_w - self.current_w max_dw self.max_accel * 2 * self.dt self.current_w max(-max_dw, min(max_dw, dw)) return self.current_v, self.current_wmax_accel设 0.5 米每二次方秒是保守值实际可以按底盘扭矩和负载调但赛项里稳比快重要。dt要和底盘控制周期一致常见是 0.05 秒即 20 赫兹。角速度的加速度上限设成线速度的两倍是因为转向需要更快响应但也不能无限大。这个平滑器的坑在于如果目标速度频繁跳变平滑后的速度会一直追不上表现为机器人“肉”这时候要检查上游决策是不是在抖动。5. 避坑与排查那些让机器人当场罢工的细节5.1 现象机器人走到一半突然重启原因底盘电机启动瞬间电流大和计算平台共电源时把电压拉低计算平台欠压重启。解决计算平台单独供电或者加一个大电容缓冲电源线分开走避免电机线缆和信号线捆在一起。5.2 现象手势检测在逆光下完全失效原因相机自动曝光把画面压暗人变成剪影颜色和纹理特征丢失。解决关闭相机自动曝光手动设固定曝光值或者在预处理里加自适应直方图均衡但要注意别把噪声放大。更稳的做法是加一个补光灯主动补光比后期调参可靠。5.3 现象商品识别偶尔把 A 认成 B原因两个商品包装颜色相近模板匹配的归一化相关系数都超过阈值排序后取了错误的高分。解决给每个商品加一个辅助特征比如标签上的文字区域做 OCR 校验或者用两个不同角度的模板取平均分。阈值不要一刀切外观相近的商品单独设更高阈值。5.4 现象导航规划频繁失败机器人原地转圈原因膨胀半径设太大窄通道两侧障碍物的代价地图连成一片规划器找不到可行路径。解决按场地最窄处反推膨胀半径上限或者对静态地图和动态障碍用不同的膨胀参数。动态障碍膨胀大一点静态地图膨胀小一点兼顾安全和通行。5.5 现象状态机卡在等待选品后面任务全堵原因等待状态没有超时保护或者超时后没有正确重置事件标志。解决每个等待状态都设超时超时后强制回到待机并清空事件队列事件标志用后即清避免上一轮的事件触发下一轮状态。6. 从能跑到跑稳用日志回放和边界测试收尾赛项现场只有一次机会但调试不能只靠现场。我一般会在机器人上开一个日志节点把关键话题按时间戳录下来包括相机帧、检测结果、状态切换、速度指令。出问题时用回放工具重放比在现场反复跑省时间。日志里最值得看的是状态切换的时间戳如果两个状态之间隔了异常长的时间说明中间有阻塞。# 录制关键话题方便赛后回放排查 rosbag record /camera/image_raw /detection/result /task_state /cmd_vel -O debug.bag # 回放时用倍速播放快速定位异常时间段 rosbag play debug.bag --clock -r 2.0-O指定输出文件名--clock让回放时使用 bag 里的时间-r 2.0是两倍速。回放时配合可视化工具看检测框和状态能很快发现是感知漏检还是状态机没响应。边界测试是另一个容易被忽略的环节。我会专门测几种极端情况人站在机器人正后方招手、商品被放到货架边缘、两个人同时招手、网络延迟导致状态更新滞后。这些场景在赛项里不一定出现但一旦出现就是翻车。测试时把机器人速度降到最低先保证逻辑正确再逐步提速。最后一个具体技巧给状态机加一个“心跳”话题每秒发布一次当前状态和运行时间。如果主控发现心跳停了直接切到安全停车。这个机制在调试时能帮你快速判断是程序崩了还是逻辑卡了比看终端输出直观。我自己的习惯是每次改完状态机先跑十分钟空载循环看心跳有没有断再上商品。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑