启元 T1 Pro 的 SDK 里没有导航模块官方示例 carry-clothes 的检测器是一段返回空列表的占位代码。本文记录 color_seek 的完整开发过程用 HSV 颜色检测配上针孔测距与比例控制让机器人在狭窄场地里自己找到指定颜色的目标并停在 0.6m 外。全部逻辑先在 WSL 的 --sim 里验证1.20m → 0.68m 收敛218 帧零振荡再打包 .rpk 经 PrimeConsole 部署上板。实测数据来自 Ubuntu 22.04.5 ROS2 Humble 环境代码开源可复现。代码仓库地址color_seek:基于 ROS2 与 HSV 颜色检测的机器人目标搜索项目 - AtomGit目录一、环境配置与部署二、HSV 检测与现场标定三、视觉伺服的三个参数四、代码结构框架设计五、调试实录记录我遇到的两个坑六、上板从仿真到部署的三段路七、常见问题排查八、需要注意的限制结语 / 参考资料一、环境配置与部署在我写第一行控制代码之前我把环境链路整个过了一遍。这一步虽然似浪费时间小编也是通过查资料、取各个官网查询问题做了近2个小时后面各个开发都需要这个环境所以还是很重要的。实测环境组件版本说明Windows 11 WSL222H2镜像网络模式WSL 与机器人同网段Ubuntu22.04.5 LTS官方 SDK 指定版本ROS 2Humbledesktop安装T1 SDKv1.0.0.0colcon 编译4 个包OpenCV4.13.0源码编译SDK 内置依赖SDK 的编译是整条链路的地基。t1-sdk 在 WSL 里 colcon 编译4 个包aimdk_msgs、aimdk_opencv、ruckig_for_primebot、aimdk_examples_cpp一次跑完 5 分 49 秒。中间那个 ippicv 下载 SSL 报错只是警告编译没有被掐断最后 Summary 里 4 packages finished——消息包和服务定义就都齐了WSL 里 colcon build 的完整输出4 packages finishedSDK 与 aimdk_msgs 消息包就绪。模型侧的验证靠 t1-urdfRViz 里能看到完整的 T1 模型和关节列表Gazebo 里能把机器人 spawn 进世界。这两步过了说明 URDF 和仿真链路是通的Gazebo 中成功加载 T1 Pro 模型仿真世界就绪。--dry-run的输出表明机器人头部相机的 RTSP 流拉通了[rtsp] connected: rtsp://192.168.100.198:2554/live_head_stereo_left以及运行计划打印正常——模式、颜色、停车距离、限速、伺服频率一行齐全。官方文档 附近写了无线场景可以用rtsp://IP:2554/live_head_stereo_left访问相机但同一份文档也说了“暂不支持在 Windows/Mac 下进行开发”。我实际测下来的结论是WiFi 下相机流间歇可用PC 与机器人同网段时多数时候能拉到 640×480 的实时画面偶发握手失败强制 RTSP over TCP 后稳定得多接口层对应的处理是FFMPEG 环境变量锁死 TCP 传输、5 秒流超时外加三次重试——每次失败间隔 1 秒os.environ[OPENCV_FFMPEG_CAPTURE_OPTIONS] ( rtsp_transport;tcp|stimeout;5000000) for attempt in range(3): self._cap cv2.VideoCapture(self._rtsp_url) if self._cap.isOpened(): self.node.get_logger().info(f[rtsp] connected: {self._rtsp_url}) break self.node.get_logger().warn(f[rtsp] open failed ({attempt 1}/3)) time.sleep(1.0)三次都拉不开就放弃 RTSPcapture_image()会自动落到备选路径——走CaptureJpegImage服务单帧取图。两条取图路径在接口层做了透明封装策略层只管调capture_image()。二、HSV 检测与现场标定机器人“找到”一个东西前提是它先“看见”。我发现官方示例 carry-clothes 的vision.py里有一个 Detector 接口读一遍源码就会发现官方给的是占位实现——不管输入什么画面返回的都是空列表。伺服、测距、控制的骨架都在唯独“画面里到底有没有目标”这一段要自己写。官方的这段占位实现长这样官方示例 carry-clothes 的vision.pyDetector接口与默认的HeuristicDetector占位实现——detect()收到任何画面都返回空列表注释里写明“请接入真实检测模型后上机”。我们用最直接的办法HSV 颜色阈值。HSV_RANGES { red: [((0, 80, 80), (10, 255, 255)), ((170, 80, 80), (180, 255, 255))], green: [((35, 60, 60), (85, 255, 255))], blue: [((100, 80, 80), (130, 255, 255))], }红色跨越了色相的 0°需要拆成两段。选目标物时我们做过一轮测算把候选目标一块砖红色封面的笔记本的照片按 HSV 分布统计结果如下目标物颜色分布与 HSV 阈值对照色相 99% 落在 0~10°、饱和度 94% 高于 80、明度 97% 高于 80三项全部命中预设 red 阈值——这块封皮参数不用调整。换成颜色更杂的目标需要现场标定。为此我加了一个--detect-test模式取一帧、检测、画绿框、存图不发起运动指令。先在真机上完成这个等它框住了目标再让它走python3 main.py --detect-test --color red阈值分割 → 5×5 形态学开运算去噪点、闭运算补空洞 → 取最大连通域 → 输出它的中心偏移和像素高度。下面是完整实现class ColorDetector: def detect(self, jpeg_bytes): buf np.frombuffer(jpeg_bytes, dtypenp.uint8) frame cv2.imdecode(buf, cv2.IMREAD_COLOR) hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask None for low, high in self.ranges: part cv2.inRange(hsv, np.array(low), np.array(high)) mask part if mask is None else cv2.bitwise_or(mask, part) kernel np.ones((5, 5), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) best max(contours, keycv2.contourArea) if cv2.contourArea(best) self.min_area_ratio * w * h: return [] x, y, bw, bh cv2.boundingRect(best) cx (x bw / 2.0 - w / 2.0) / (w / 2.0) cy (h / 2.0 - (y bh / 2.0)) / (h / 2.0) return [Detection(self.color, confidence, cx, cy, float(bh))]几个设计点坐标归一化cx以画面中心为 0、右为正cy上为正。这样“偏了多少”和相机分辨率解耦640×480 和 1920×1080 的流走同一套控制参数置信度按面积给min(1.0, area / (0.25 * w * h))目标占画面四分之一就算满置信。它不参与判定只在日志里给“框得稳不稳”一个直观读数min_area_ratio 0.002面积小于画面千分之二的连通域直接丢弃。真机上远处的小色块、反光噪点全靠这一刀过滤没有它伺服循环会被噪声目标牵着走官方示例 carry-clothes 在仿真里是这么收敛的走到洗衣机前 0.60m 停下9.2 秒完成放衣服任务。我们要做的只是把它的空检测器换成真的官方示例 carry-clothes 的--sim运行实录[drive]序列里距离从 1.19m 一路收敛[servo] 已到位距离 0.60m 方位 -0.7°——官方伺服骨架的完整工作样貌。三、视觉伺服的三个参数目标在画面里了接下来是让它走过去。整个闭环如下图所示color_seek 视觉伺服闭环取图、检测、测距、控制、下发速度50Hz 一圈停车判据是距离误差和角度误差同时进容差。三个核心参数下面是介绍没有深度相机、没有激光测距用的是最朴素的针孔模型如何让机器人获取距离distance FOCAL_PX * target_height_m / height_px目标物的实际高度是非常重要的。我们默认按 A4 纸短边 0.21m 标定换成 213×290mm 立放的笔记本时传--target-height 0.29。如果这个参数写错比如换成一块 12cm 高的饮料盒和原物体也就是笔记本距离会被高估近一倍机器人会直接冲过停车点怼上去那就无法有效识别。那么机器人的速度给多少比例控制两个增益各管一个方向forward clip(k_forward * (distance - standoff), -max_forward, max_forward) yaw clip(k_yaw * radians(angle), -max_yaw, max_yaw)k_forward0.6、k_yaw1.2沿用官方示例的取值max_forward我们从官方的 0.35 压到 0.2——狭窄场地里慢就是快。落到代码上就是两个纯函数一个把检测框翻译成距离和方位角一个把误差翻译成速度def estimate(self, det): if det.height_px 1e-3: distance FOCAL_PX * self.cfg.target_height_m / det.height_px distance max(0.2, min(12.0, distance)) else: distance -1.0 angle det.cx * (FOV_DEG / 2.0) return distance, angle def control(self, distance, angle): c self.cfg derr distance - c.standoff_m forward max(-c.max_forward, min(c.max_forward, c.k_forward * derr)) yaw max(-c.max_yaw, min(c.max_yaw, math.radians(c.k_yaw * angle))) arrived (abs(derr) c.dist_tolerance_m and abs(angle) c.angle_tolerance_deg) return forward, yaw, arrived真正骨架是真机主循环——50Hz 绝对节拍丢目标原地慢转搜索丢失超 2 秒放弃while time.monotonic() - start timeout_s: tick time.monotonic() image self.bot.capture_image() dets detector.detect(image) if image else [] if not dets: if time.monotonic() - last_seen LOST_TIMEOUT_S: break self.bot.drive(0.0, 0.0, 0.25) else: last_seen time.monotonic() distance, angle self.estimate(dets[0]) forward, yaw, arrived self.control(distance, angle) self.bot.drive(forward, 0.0, yaw) if arrived: self.bot.stop() self.bot.say(f找到{self.color}目标啦) self.bot.play_emotion(EMOTION[great_joy]) self.bot.play_led(LED[twinkle]) self.bot.release_input_source() return True gap time.monotonic() - tick if gap dt: time.sleep(dt - gap)三个细节节拍用绝对时间tick dt不用“干完活睡固定时长”——检测耗时波动会累积成频率漂移走跑控制对 50Hz 的要求是持续、不是平均丢目标先原地慢转0.25 rad/s再谈放弃盲走只会离目标更远收尾必须 release_input_source需要把控制权还回去否则遥控和语音都进不来。那么什么时候停距离误差 ≤0.08m 且角度误差 ≤6° 判定到位。--sim的实测输出把整个过程写得很清晰--sim实测SimWorld 从 1.20m、偏 15° 起步218 帧收敛到 0.68m 停车。注意最后一行——停在 0.68 而不是 0.60因为 0.60.08 的容差内即判到位。把收敛过程画成曲线形态更直观收敛曲线按 --sim 实测数据重绘距离单调递减、无回摆角度红线同步归零。末段 0.71 → 0.68 两步收尾。有一个细节非常重要仿真里距离起点读数是 1.20m 还是 1.66m取决于--target-height与仿真世界参数是否一致。第一版代码里 SimDetector 的目标高度写死 0.21m命令行传 0.29 就会出现“实际 1.2 米、估出来 1.66 米”的偏差。改成跟随配置之后仿真读数才和真机标定值对得上——仿真的价值就在于提前暴露这类标定错误。四、代码结构框架设计color_seek/ ├── main.py # 入口参数解析、--dry-run/--sim/--detect-test/--record ├── manifest.json # .rpk 技能声明8 项原子能力 ├── pack_rpk.py # 打包脚本manifest checksums → .rpk ├── src/ │ ├── seek.py # 伺服任务SimWorld 仿真 / 真机循环 / CSV 记录 │ ├── iface.py # SDK 接口封装速度发布、动作切换、输入源、TTS/表情/灯带 │ └── detector.py # HSV 检测器 SimDetector仿真世界 └── cfg/pkg.yaml # PrimeConsole 读取的技能身份入口层把两种运行方式分开命令行调试走 argparse部署上板走固定入口run()def run(simFalse, **kwargs): if not rclpy.ok(): rclpy.init() node rclpy.create_node(color_seek) cfg ServoConfig(standoff_mkwargs.get(standoff_m, 0.6), max_forwardkwargs.get(max_forward, 0.2), target_height_mkwargs.get(target_height_m, 0.29)) task SeekTask(node, colorkwargs.get(color, red), cfgcfg, record_csvkwargs.get(record_csv, False), image_sourcekwargs.get(image_source, service)) task.bot.sim sim try: arrived task.run(simsim, timeout_skwargs.get(timeout_s, 60.0)) finally: task.close() if rclpy.ok(): rclpy.shutdown()注意部署入口默认image_sourceservice包跑在机器人板上板内回环取图走CaptureJpegImage服务比 RTSP 稳--target-height默认值也换成真机主目标 0.29m立放笔记本。iface.py里最难缠的是动作切换的状态等待——SetMcAction发出去只是受理状态要从 TRANSITION(200) 轮询到 RUNNING(100) 才算切完中途发速度指令全部无效def switch_action(self, action_id, desc, timeout_s30.0): gap time.monotonic() - self._last_set_action if gap 0.6: time.sleep(0.6 - gap) req SetMcAction.Request() req.command.action.value action_id req.command.action_desc desc resp self._call(self.cli_set_action, req, timeout_sec3.0) self._last_set_action time.monotonic() deadline time.monotonic() timeout_s while time.monotonic() deadline: q GetMcAction.Request() r self._call(self.cli_get_action, q, timeout_sec1.0, retries1) if r is not None and r.info.status.value ACTION_RUNNING: return True time.sleep(0.2) return False开头的 0.6 秒间隔护栏是给SetMcAction的调用限制留的——两次间隔小于 500ms 会返回 -10001接口层记下上次调用时间自动补齐策略层永远不用操心这个数。分层的原则是把“和 SDK 对话的部分”和“控制策略的部分”隔开iface.py里所有 topic 名、QoS、500ms 节流、零速补发这些脏活都在seek.py只写“取图→算误差→发速度”的策略。后来 color_seek 和 crosstalk 的部署包出问题时要改的只有 manifest 层这样可以不用修改策略。五、调试实录记录我遇到的两个坑坑一距离标定错仿真里可以看见真机上就是一次碰撞。针孔测距的误差方向很恶劣——目标被估矮了距离会算远提前停估高了距离算近冲过去。上面说的 1.66m 偏差就是在仿真里抓到的。如果没这层验证第一次真机跑就是拿机器人做碰撞测试。坑二包能校验、不能部署。color_seek 和 crosstalk 两个包第一次部署全部失败报 skillid unknown。拿部署成功的 touch_game 包做 diff唯一结构差异是cfg/pkg.yaml——PrimeConsole 的技能身份是从这个文件的package_name字段读的缺了它校验报告显示“通过”部署按钮点了没反应。补上之后一次通过。这个坑的教训是校验通过和部署成功之间还隔着一层身份识别。六、上板从仿真到部署的三段路整个开发路径可以切成三段每段有明确的产物第一段在 WSL 里完成逻辑验证第二段打包.rpk本质是 zip 加一份校验清单八份文件pack_rpk.py一键打包输出包大小与文件数。包内容清单manifest.json能力声明、cfg/pkg.yaml技能身份、assets/icon.png、META-INF/checksums.jsonsha256 校验加源码四件套。第三段在 PrimeConsole 里完成导入 .rpk → 七维校验files/errors/warnings/imports/sdk_calls/security/runtime→ 部署到机器人。color_seek 的验证报告skillid 正确解析为color_seek原子能力列表与验证记录双双“通过”部署成功。部署完成意味着 Skill 跑在机器人板上ROS2 通信走板内回环——WiFi 下的所有限制全部绕开。这也是我们把开发重心放在“仿真验证逻辑、部署验证链路”这条路线上的原因。下面是原子能力展示{ skillId: color_seek, name: color_seek, version: 1.0.0, robotModel: q1, description: 视觉寻物机器人扫描锁定指定颜色目标并走近播报方位狭窄场地实测限速 0.2m/s, atomicCapabilities: [ { id: motion.emotion, label: 表情播放, entry: prime.motion.emotion }, { id: motion.preset, label: 预设动作, entry: prime.motion.preset }, { id: tts.speak, label: 语音合成, entry: prime.tts.speak }, { id: led.control, label: LED 灯效, entry: prime.led.control } ] }本次二次开发已经发布skill“视觉寻物”欢迎大家体验七、常见问题排查现象原因处理ros2 topic list收不到 HAL 数据BMS 等 HAL 接口是 Best Effort订阅端默认 Reliable用qos_profile_sensor_data走跑指令发了机器人不动速度指令中断超 1s 脱锁停机保持 ≥50Hz 持续下发循环用绝对节拍SetMcAction返回 -10001两次调用间隔小于 500ms接口层记录上次调用时间自动补齐动作切换后指令被忽略状态还在 TRANSITION(200)轮询 GetMcAction 到 RUNNING(100) 再发速度控制权抢不回来SDK 输入源没释放任务结束调 release_input_sourceRTSP 间歇连不上默认 UDP 传输不稳定rtsp_transport;tcp 超时 重试三次目标找到了但停太远/冲过去--target-height与实际高度不符按目标物实际高度重新标定八、需要注意的限制HSV 阈值对光照敏感哑光材质、避免强逆光否则检测率明显下降针孔测距只在 1m 内可靠远了只适合粗定位没有避障逻辑也没有电量护栏——真机测试需要 1.5m 内清空、人随时待命结语color_seek 做的事可以一句话概括在没有导航模块的 SDK 上用相机和颜色检测把“走到目标前”这件事自己搭出来。从官方留空的检测器出发到 --sim 收敛、到 .rpk 上板每一步都有实测输出记录可以说这次二次开发是成功的。本文使用的 SDK 与消息包均来自启元机器人开发者社区——官方在 AtomGit 开源了 t1-sdk含 aimdk_msgs 消息包与 carry-clothes 示例技能本文的伺服骨架即改造自后者检测部分为需要自行开发实现。参考资料启元机器人开发者社区SDK/文档下载启元机器人开发者社区 · 一站式具身智能研发平台官方 t1-sdk 仓库含 aimdk_msgs 消息包t1-sdk:基于 ROS2 与 FastDDS 的机器人二次开发 SDK 项目 - AtomGit官方 carry-clothes 示例技能视觉伺服范式来源https://atomgit.com/primebot/carry-clothes本文配套仓库 color_seekcolor_seek:基于 ROS2 与 HSV 颜色检测的机器人目标搜索项目 - AtomGit
阅读完成 · 觉得有帮助?