仿真图

在移动机器人项目里,“跟着我走”看起来是一个很简单的需求:

摄像头看到一个人,机器人跟着他移动。

但真正拆开后,它至少涉及五个问题:

谁是我要跟的人?
        ↓
这个人现在在哪里?
        ↓
距离机器人有多远?
        ↓
机器人应该向哪边转?
        ↓
机器人应该以多快的速度前进?

Qualcomm qrb_ros_samples 中的 sample_followme 正好把这几个问题串成了一条完整的 ROS 2 闭环:

RGB Camera
    ↓
YOLOv8 Person Detection
    ↓
Re-ID 身份确认
    ↓
Depth Camera 距离估计
    ↓
目标方向计算
    ↓
双 PID 控制
    ↓
/cmd_vel
    ↓
AMR 底盘

本文就从工程实现角度把它完整跑一遍。


01 项目背景:这次到底要做什么

1.1 实验平台

本次主平台选择:

Qualcomm Dragonwing IQ-9075 EVK

软件环境采用:

项目本文配置
SoCQualcomm Dragonwing IQ-9075
AI 平台Hexagon / HTP
系统Qualcomm Ubuntu
ROSROS 2 Jazzy
目标检测YOLOv8
AI RuntimeQualcomm QNN
身份跟踪Person Re-ID
RGB-D CameraGazebo / Orbbec Gemini 335L
控制Dual PID
Robot InterfaceROS /cmd_vel

整个实战分成两部分:

A. Simulation
先证明算法链跑通

B. Real Robot
再接 RGB-D Camera + AMR 底盘

这样调试时不会把:

摄像头问题
AI 模型问题
Re-ID 问题
ROS 通信问题
底盘问题
PID 问题

全部混到一起。


1.2 本次最终目标

站在机器人正前方约 2 米的位置:

用户
 │
 │ 约 2m
 ▼
机器人

启动 Follow Me 后:

  1. YOLO 检测到 person
  2. 判断画面中心区域只有一个候选人
  3. 获取这个人的 Re-ID 特征
  4. 将其设定为跟随目标
  5. 后续即使画面中出现其他人,也尽量继续匹配原目标
  6. Depth Camera 持续计算人与机器人的距离
  7. 根据人物左右偏移计算方向角
  8. PID 计算线速度与角速度
  9. 发布 /cmd_vel
  10. 机器人保持约 2m 距离持续跟随

最终形成闭环:

看见人
  ↓
认出人
  ↓
测距离
  ↓
算方向
  ↓
控制底盘
  ↓
机器人移动
  ↓
重新看见人

1.3 BOM 清单

仿真版本

设备数量用途
IQ-9075 EVK1YOLO、Re-ID、Follow Me
Ubuntu PC1Gazebo Simulation
千兆网线1ROS 2 DDS
RGB Camera虚拟Gazebo
Depth Camera虚拟Gazebo
AMR虚拟Gazebo

实机版本

设备数量用途
IQ-9075 EVK1主计算平台
Orbbec Gemini 335L1RGB + Depth
AMR 差速底盘1机器人运动
USB 3.0 Cable1RGB-D Camera
电源1EVK + Camera
急停按钮1实机安全
调试 PC1SSH / RViz / ROS 调试

1.4 配图建议

文章这一节不建议一上来放代码。

可以放一张 Qualcomm Robotics 平台演进相关截图:

RB5 / RB6 Robotics Platform
            ↓
Dragonwing IQ 系列
            ↓
Edge AI + Robotics

RB5/RB6 可以作为 Qualcomm 机器人生态的背景素材。

但本文真正运行 Sample 的硬件写 IQ-9075,不要把 RB5/RB6 写成本文实验设备。


02 Follow Me 到底是什么

Follow Me 与普通 YOLO 最大的区别是:

普通 YOLO:

画面里有没有人?

Follow Me:

我要跟的是哪一个人?
他现在在哪里?
距离是多少?
机器人应该怎么动?

因此:

YOLO ≠ Follow Me

YOLO 只负责:

Person Detection

Re-ID 负责:

Is this the same person?

Depth 负责:

How far?

PID 负责:

How should the robot move?

03 系统架构:RGB + Depth + YOLO + Re-ID + PID

整个系统可以画成下面这张图。

机器人移动后重新观测

RGB Camera

Depth Camera

YOLOv8
Person Detection

Person BBox

Person Re-ID

Target Template

Depth Median
目标距离

BBox Center
目标方向

Dual PID

/cmd_vel

AMR Base

这才是 Follow Me 的完整闭环。


3.1 核心 ROS Topic

整个工程建议首先记住下面几个 Topic:

TopicType用途
/camera/color/image_rawImageRGB 图像
/camera/depth/image_rawImage深度图
/camera/color/camera_infoCameraInfo相机内参
/yolo_detect_resultDetection2DArrayYOLO 检测结果
/target_visualizationImage跟随可视化
/cmd_velTwist/TwistStamped机器人速度控制

另外还有两个关键 Re-ID Service:

/extract_feature

负责:

Person Crop
    ↓
Re-ID Network
    ↓
Feature Vector

以及:

/compute_similarity

负责比较:

Feature A
vs
Feature B

3.2 整条 ROS 数据流

/camera/color/image_raw

/camera/depth/image_raw

/camera/color/camera_info

YOLO

/yolo_detect_result

person_tracker_node

/extract_feature

/compute_similarity

Dual PID

/cmd_vel

这里还有一个非常重要的设计:

RGB
Depth
YOLO Result

并不是三个独立 Callback 各算各的。

Tracker 使用 message_filters 将三路消息做时间同步后再处理。

原因很简单。

如果:

RGB = t0

Depth = t0 + 500ms

YOLO = t0 - 300ms

这三份数据对应的根本不是同一个人物位置。


3.3 Follow Me 状态机

项目提供四个控制状态:

START
PAUSE
RESUME
FINISH

可以画成:

START

PAUSE

RESUME

FINISH

FINISH

IDLE

RUNNING

PAUSED

其中:

PAUSE

机器人立即:

linear = 0
angular = 0

但目标状态仍可继续恢复。

FINISH

除了停车,还会:

清除 Re-ID Template
+
清除当前 Target

下一次 START 就需要重新初始化一个人物。


3.4 目标初始化逻辑

这个设计值得仔细理解。

并不是画面里随便出现一个人就开始跟。

当前逻辑要求:

检测到 Person
       ↓
人物位于画面中央区域
       ↓
距离 1.5m ~ 3.0m
       ↓
符合条件的人恰好只有 1 个
       ↓
Extract Re-ID Feature
       ↓
保存 Initial Template
       ↓
目标初始化成功

流程:

YOLO Person

在画面中心区域?

距离 1.5~3.0m?

Candidate

Candidate 数量 = 1?

Extract Re-ID Feature

保存 Initial Template

开始 Follow

忽略

等待下一帧

这也是为什么第一次测试时最好:

只让一个人站在机器人正前方。


3.5 为什么还需要 Re-ID

考虑这个场景:

        Person B

Person A      Robot
  ↑
原始跟随目标

如果只有 YOLO:

person
person

两个 Bounding Box 在类别上完全一样。

机器人根本不知道:

哪个 person 才是原来的目标。

Re-ID 做的事情就是:

Person Crop
     ↓
Embedding
     ↓
与 Initial Template 比较
     ↓
是不是同一个人

3.6 双模板抗漂移机制

项目并不是只保存一个 Re-ID Feature。

可以理解成:

Initial Template
       +
Latest Template

Initial Template:

第一次锁定目标
长期保留

Latest Template:

目标当前较新的外观
动态更新

于是:

当前人物
   │
   ├── 和 Initial 比较
   │
   └── 和 Latest 比较

既能适应:

人物姿态变化
人物转身
人物远近变化

又可以通过 Initial Template 避免长期跟着跟着漂到另一个人身上。


3.7 Depth 是如何算距离的

首先获得 Person Bounding Box:

(x1, y1)
     ┌─────────┐
     │ Person  │
     │         │
     └─────────┘
              (x2, y2)

然后映射到 Depth Image。

代码并不是只读中心一个像素,而是读取 Bounding Box 内的有效深度。

例如:

2.03
1.98
2.01
8.40  ← 错误深度
1.99
2.02

最终采用:

Median

而不是 Average。

这样能明显降低离群值影响。

公式可以写成:

d_target = Median(D(x,y)), (x,y) ∈ PersonBBox

3.8 如何算人物位于左边还是右边

首先利用 CameraInfo 得到水平 FOV:

FOVx = 2 × atan(W / 2fx)

其中:

W  = 图像宽度
fx = 相机水平焦距

目标框中心:

u_target

图像中心:

u_center = W / 2

横向偏移:

offset = u_target - u_center

最后近似:

angle =
offset / u_center × FOVx / 2

于是:

angle < 0
→ 人在左边

angle ≈ 0
→ 人在中间

angle > 0
→ 人在右边

3.9 PID 控制机器人

Follow Me 中实际上存在两个控制环。

线速度 PID

控制:

离人太远还是太近

距离误差:

e_distance =
current_distance - target_distance

默认:

target_distance = 2.0m

例如:

当前距离 = 3m

error = 3 - 2
      = +1m

机器人应该向前。


角速度 PID

控制:

人物是否位于图像中心
e_angle = target_angle

然后:

linear_velocity
=
PID_distance(e_distance)

angular_velocity
=
PID_angle(-e_angle)

为什么有负号?

ROS 中:

angular.z > 0
→ 左转

angular.z < 0
→ 右转

而图像中目标位于右侧时:

angle_error > 0

所以需要:

angular.z < 0

04 项目部署:先仿真,再实机

这一部分开始正式动手。

建议按照下面这个顺序:

Step 1  Gazebo RGB-D
Step 2  ROS 2 DDS
Step 3  YOLO
Step 4  Re-ID
Step 5  Follow Me Tracker
Step 6  cmd_vel
Step 7  Real Robot

不要改变顺序。


4.1 固定代码版本

当前仓库 main 属于持续开发分支。

工程项目里第一件事情不是:

colcon build

而是:

git rev-parse HEAD

把 Commit 保存下来。

例如:

git rev-parse HEAD > BUILD_COMMIT.txt

以后出现:

昨天能跑
今天不能跑

至少知道软件到底发生了什么变化。


4.2 ROS 2 基础环境

两端都检查:

source /opt/ros/jazzy/setup.bash

echo $ROS_DISTRO

期望:

jazzy

本文统一:

export ROS_DOMAIN_ID=78

Host 与 IQ-9075 都必须一样。


4.3 Host 启动 Follow Me Simulation

创建 Workspace:

mkdir -p ~/qrb_ws/src
cd ~/qrb_ws/src

拉取:

git clone https://github.com/qualcomm-qrb-ros/qrb_ros_simulation.git

git clone https://github.com/qualcomm-qrb-ros/qrb_ros_samples.git

安装依赖:

cd ~/qrb_ws

source /opt/ros/jazzy/setup.bash

rosdep install \
    --from-paths src \
    --ignore-src \
    -r \
    -y

编译:

colcon build --symlink-install

加载:

source install/setup.bash

export ROS_DOMAIN_ID=78

进入 Simulation Follow Me 配置目录:

cd ~/qrb_ws/src/qrb_ros_samples/robotics/simulation_follow_me

启动:

ros2 launch qrb_ros_sim_gazebo \
    gazebo_robot_base_mini.launch.py \
    world_model:=warehouse_followme_path2 \
    rgb_camera_config_file:=$(pwd)/followme_rgb_camera_params.yaml \
    enable_laser:=false \
    enable_imu:=false

4.4 先别启动 AI,检查传感器

执行:

ros2 topic list | grep camera

重点确认:

/camera/color/image_raw
/camera/depth/image_raw
/camera/color/camera_info

RGB:

ros2 topic hz /camera/color/image_raw

Depth:

ros2 topic hz /camera/depth/image_raw

Camera Info:

ros2 topic echo \
    /camera/color/camera_info \
    --once

只要这一层失败:

不要继续调 YOLO。


4.5 检查 PC → IQ-9075 DDS

IQ-9075:

export ROS_DOMAIN_ID=78

ros2 topic list

如果能够看到:

/camera/color/image_raw
/camera/depth/image_raw

再执行:

ros2 topic hz \
    /camera/color/image_raw

以及:

ros2 topic hz \
    /camera/depth/image_raw

到这里说明:

Gazebo
   ↓
Ethernet
   ↓
DDS
   ↓
IQ-9075

链路已经成立。


4.6 准备 YOLOv8 QNN 模型

在支持 Qualcomm AI Hub Models 的环境导出 IQ-9075 QNN Context Binary。

思路:

YOLOv8
   ↓
QAI Hub
   ↓
QNN Context Binary
   ↓
IQ-9075 HTP

模型最终放置:

/opt/model/
├── yolov8_det_qcs9075.bin
└── coco8.yaml

例如:

sudo mkdir -p /opt/model

sudo cp yolov8_det_qcs9075.bin \
    /opt/model/

sudo cp coco8.yaml \
    /opt/model/

确认:

ls -lh /opt/model/

4.7 不要直接机械照抄 Orbbec YOLO Launch

这里是当前工程非常容易踩的第一个大坑。

Follow Me 必须要:

RGB
+
Depth
+
YOLO Detection

但是当前 Object Detection 示例中的 Orbbec Launch 配置会关闭 Depth。

因此对于 Follow Me,更推荐将:

Camera Source

与:

YOLO Pipeline

拆开。

也就是:

Gazebo / Orbbec
      │
      ▼
/camera/color/image_raw
      │
      ▼
YOLO Pipeline

而不是让 YOLO Launch 顺便负责启动 Camera。


4.8 创建 External Camera YOLO Launch

可以从:

launch_with_orbbec_camera.py

复制一份:

launch_external_camera.py

保留:

Preprocess
QNN Inference
YOLO Postprocess
Overlay

删除:

Include Orbbec Camera Launch

核心结构可以写成:

from launch import LaunchDescription
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode


def generate_launch_description():

    preprocess = ComposableNode(
        package="qrb_ros_cv_tensor_common_process",
        plugin=(
            "qrb_ros::cv_tensor_common_process::"
            "CvTensorCommonProcessNode"
        ),
        name="yolo_preprocess",
        parameters=[{
            "target_res": "640x640",
            "normalize": True,
            "tensor_fmt": "nhwc",
            "data_type": "float32",
        }],
        remappings=[
            (
                "input_image",
                "/camera/color/image_raw"
            ),
            (
                "encoded_image",
                "/qrb_inference_input_tensor"
            ),
        ],
    )

    inference = ComposableNode(
        package="qrb_ros_nn_inference",
        plugin=(
            "qrb_ros::nn_inference::"
            "QrbRosInferenceNode"
        ),
        name="yolo_inference",
        parameters=[{
            "backend_option":
                "/usr/lib/libQnnHtp.so",
            "model_path":
                "/opt/model/yolov8_det_qcs9075.bin",
        }],
        remappings=[
            (
                "qrb_inference_output_tensor",
                "/yolo_detect_tensor_output"
            )
        ],
    )

    postprocess = ComposableNode(
        package="qrb_ros_yolo_process",
        plugin=(
            "qrb_ros::yolo_process::"
            "YoloDetPostProcessNode"
        ),
        name="yolo_postprocess",
        parameters=[{
            "label_file":
                "/opt/model/coco8.yaml",
            "score_thres": 0.4,
            "iou_thres": 0.5,
        }],
    )

    container = ComposableNodeContainer(
        name="followme_yolo_container",
        namespace="",
        package="rclcpp_components",
        executable="component_container",
        composable_node_descriptions=[
            preprocess,
            inference,
            postprocess,
        ],
        output="screen",
    )

    return LaunchDescription([
        container
    ])

这样:

Simulation

和:

Real Orbbec

都可以复用同一个 YOLO Pipeline。


4.9 验证 YOLO

第一层:

ros2 topic hz \
    /camera/color/image_raw

第二层:

ros2 topic hz \
    /qrb_inference_input_tensor

第三层:

ros2 topic hz \
    /yolo_detect_tensor_output

第四层:

ros2 topic echo \
    /yolo_detect_result

最终应该能够看到:

class_id: person

或者对应:

class_id: 0

4.10 检查 Re-ID 服务

Follow Me 依赖外部 Re-ID Package。

先不要猜。

执行:

ros2 pkg prefix \
    qrb_ros_people_reid

存在后再启动:

ros2 launch \
    qrb_ros_people_reid \
    qrb_sample_people_reid.launch.py \
    reid_model_path:=/opt/model/osnet.bin

检查:

ros2 service list | grep feature

以及:

ros2 service list | grep similarity

期望能够看到:

/extract_feature
/compute_similarity

如果:

ros2 pkg prefix qrb_ros_people_reid

直接报 Package not found:

不要凭空找一个同名 GitHub 项目替代。

需要从对应 Qualcomm Robotics/QIRP 软件发布环境中获取与当前版本匹配的 Re-ID Package。


4.11 当前 main 分支源码需要注意 CMake

这里是第二个重要坑。

虽然:

src/person_tracker_node.cpp

存在完整代码,

但是当前 CMakeLists.txt 中 Tracker Target 的编译段处于注释状态。

所以如果你:

colcon build

然后发现:

ros2 run follow_me \
    person_tracker_node

找不到 executable,

不是你 ROS 学坏了。


4.12 源码构建修正

确保已经安装匹配的:

qrb_ros_people_reid

后,CMake 需要恢复类似:

find_package(qrb_ros_people_reid REQUIRED)

add_executable(person_tracker_node
    src/person_tracker_node.cpp
    src/template_pool.cpp
    src/pid_controller.cpp
    src/depth_processor.cpp
    src/state_machine.cpp
)

ament_target_dependencies(
    person_tracker_node

    rclcpp
    std_msgs
    sensor_msgs
    geometry_msgs
    vision_msgs
    cv_bridge
    image_transport
    message_filters
    qrb_ros_people_reid
)

target_link_libraries(
    person_tracker_node
    ${OpenCV_LIBRARIES}
)

rosidl_get_typesupport_target(
    cpp_typesupport_target
    ${PROJECT_NAME}
    rosidl_typesupport_cpp
)

target_link_libraries(
    person_tracker_node
    "${cpp_typesupport_target}"
)

install(
    TARGETS person_tracker_node
    DESTINATION lib/${PROJECT_NAME}
)

然后重新:

cd ~/qrb_ws

rm -rf \
    build/follow_me \
    install/follow_me

colcon build \
    --packages-select follow_me \
    --symlink-install

加载:

source install/setup.bash

检查:

ros2 pkg executables follow_me

应该能够看到:

follow_me person_tracker_node

4.13 启动 Tracker

Simulation:

ros2 launch follow_me \
    person_tracking.launch.py \
    use_sim_time:=true

真机:

ros2 launch follow_me \
    person_tracking.launch.py \
    use_sim_time:=false

4.14 启动 Follow Me

START:

ros2 service call \
    /follow_me/state_control \
    follow_me/srv/StateControl \
    "{set_state: 1}"

Pause:

ros2 service call \
    /follow_me/state_control \
    follow_me/srv/StateControl \
    "{set_state: 2}"

Resume:

ros2 service call \
    /follow_me/state_control \
    follow_me/srv/StateControl \
    "{set_state: 3}"

Finish:

ros2 service call \
    /follow_me/state_control \
    follow_me/srv/StateControl \
    "{set_state: 4}"

4.15 查看 Target Visualization

ros2 run rqt_image_view \
    rqt_image_view \
    /target_visualization

建议这里放文章截图:

![Follow Me Target Visualization](./assets/followme-target.png)

05 关键代码:Follow Me 到底是怎么跟人的

5.1 三路数据同步

Follow Me 同时依赖:

RGB
Depth
YOLO Detection

因此典型结构是:

using SyncPolicy =
    message_filters::sync_policies::ApproximateTime<
        sensor_msgs::msg::Image,
        sensor_msgs::msg::Image,
        vision_msgs::msg::Detection2DArray
    >;

然后:

RGB ──────┐
          │
Depth ────┼── Synchronizer
          │
YOLO ─────┘
              ↓
          Tracking

5.2 Person Filter

YOLO 输出可能包含:

person
chair
cup
laptop
car
...

Tracker 首先只保留:

person

逻辑可以抽象为:

for (const auto& detection : detections) {

    if (!isPerson(detection)) {
        continue;
    }

    person_boxes.push_back(
        toRgbCoordinate(detection.bbox)
    );
}

这里还存在一个分辨率转换:

YOLO Input
640 × 640

        ↓ Scale

Camera Image
640 × 360
或
640 × 480

所以 Bounding Box 需要重新映射回 Camera Image。


5.3 初始 Target 选择

我们自己把核心逻辑写得更容易读:

std::vector<Candidate> candidates;

for (const auto& bbox : person_boxes) {

    auto distance =
        getMedianDepth(bbox);

    if (!distance.has_value()) {
        continue;
    }

    if (*distance < 1.5 ||
        *distance > 3.0) {
        continue;
    }

    if (!insideCenterRegion(bbox)) {
        continue;
    }

    candidates.push_back({
        bbox,
        *distance
    });
}

if (candidates.size() != 1) {

    // 无法唯一确定目标
    return;
}

auto feature =
    extractReIdFeature(
        cropPersonImage(
            candidates[0].bbox
        )
    );

templatePool.initialize(feature);

这很好地解释了为什么:

画面中央同时站两个人

机器人反而不会初始化。

这是有意设计的。


5.4 Re-ID 匹配

进入 Tracking 后,每帧重新获得所有:

Person Detection

例如:

P1
P2
P3

可以先根据:

上一帧 Target BBox

计算 IoU。

优先判断空间位置最接近上一帧的人。

工程逻辑:

YOLO Person Candidates
        ↓
与上一帧 BBox 算 IoU
        ↓
按 IoU 从高到低排序
        ↓
依次 Extract Feature
        ↓
与 Re-ID Template 比较
        ↓
找到目标

这样避免:

每一帧
所有人
全部跑一次 Re-ID

降低计算量。


5.5 Re-ID Template 更新

可以抽象为:

if score <= update_threshold:
    latest_template = current_feature

匹配:

matched = (
    score_initial <= match_threshold
    or
    score_latest <= match_threshold
)

漂移:

if score_initial > drift_threshold:
    reject()

默认配置:

template_update_threshold: 0.1

match_threshold: 0.3

drift_threshold: 0.6

这里有一个非常值得提醒的地方:

当前代码把“数值越小”当成越接近。

所以虽然接口名字叫:

similarity

工程行为更接近:

distance score

如果以后自己把 Re-ID Service 改成真正的:

Cosine Similarity

通常:

数值越大越相似

那就不能直接继续使用:

score <= threshold

否则逻辑全部反了。


5.6 深度计算

推荐理解成:

std::vector<float> values;

for (pixel : person_bbox) {

    float d = readDepth(pixel);

    if (d > 0.1 &&
        d < 10.0) {

        values.push_back(d);
    }
}

float distance =
    median(values);

为什么不用 BBox 中心单点?

因为真实深度相机经常出现:

0
0
2010
0
1994
2003
8000

Median 通常会比单点稳定得多。


5.7 双 PID

标准 PID:

u(t)
=
Kp · e(t)
+
Ki · ∫e(t)dt
+
Kd · de(t)/dt

项目默认参数:

linear_kp: 0.5
linear_ki: 0.0
linear_kd: 0.1

angular_kp: 2.0
angular_ki: 0.0
angular_kd: 0.2

max_linear_speed: 0.5
max_angular_speed: 0.5

两个 PID:

Current Distance

Target = 2m

Distance Error

Linear PID

linear.x

Target Angle

Angle Error

Angular PID

angular.z


5.8 建议增加控制死区

原始 PID 靠近目标时容易出现:

前一点
后一点
前一点
后一点

实际机器人建议增加 Dead Zone:

if (std::abs(distance_error) < 0.08) {
    distance_error = 0.0;
}

if (std::abs(angle_error) <
    3.0 * M_PI / 180.0) {

    angle_error = 0.0;
}

意思:

距离误差 < 8cm
→ 不再前后调整

角度误差 < 3°
→ 不再左右调整

对真实机器人体验帮助通常很明显。


5.9 PID 调参思路

现象优先调整
机器人跟不上人增大 Linear Kp
前后反复振荡降低 Linear Kp / 增大 Kd
转向太慢增大 Angular Kp
左右疯狂摆动降低 Angular Kp
转向容易过冲增大 Angular Kd
靠近 2m 仍抖动加 Dead Zone
启动突然冲出去降低 max_linear_speed

实机第一次建议不要直接:

max_linear_speed: 0.5

可以先:

max_linear_speed: 0.15

max_angular_speed: 0.30

确认方向全部正确以后再逐渐增加。


06 实际效果与工程验证

这里建议插入官方仓库中的:

simulation-followme.gif

文章资源目录:

![Simulation Follow Me](./assets/simulation-followme.gif)

6.1 不要只看“机器人动了”

验收建议拆成:

Level内容检查方法
L1CameraRGB/Depth 有数据
L2YOLO能检测 person
L3Re-ID两个 Service 正常
L4Tracker能初始化 Target
L5Control能输出速度
L6Identity多人时保持目标
L7E2E人移动后机器人跟随

6.2 L1 Camera

ros2 topic hz \
    /camera/color/image_raw
ros2 topic hz \
    /camera/depth/image_raw

6.3 L2 YOLO

ros2 topic echo \
    /yolo_detect_result

必须稳定出现:

person

6.4 L3 Re-ID

ros2 service list | grep -E \
    "extract_feature|compute_similarity"

至少:

/extract_feature
/compute_similarity

6.5 L4 Tracker

ros2 topic hz \
    /target_visualization

然后:

rqt_image_view

观察锁定框。


6.6 L5 cmd_vel

ros2 topic echo \
    /cmd_vel

测试人物站在右侧:

Robot
   \
    \
    Person

应该看到机器人产生:

右转方向 Angular Command

人离机器人 3 米:

linear > 0

约 2 米:

linear ≈ 0

6.7 推荐测试矩阵

Case场景期望
T01单人在中央 2m成功初始化
T02单人在中央 4m不初始化
T03两人在中央不初始化
T04Target 向左走Robot 左转
T05Target 向右走Robot 右转
T06Target 远离Robot 前进
T07Target 靠近Robot 后退/减速
T08第二个人进入仍追原 Target
T09Target 短暂遮挡等待重新匹配
T10Target 完全丢失Robot 停止
T11Pause立即停止
T12Resume恢复
T13Finish停车并清 Target

6.8 ROS Topic 图

运行:

rqt_graph

理想情况下可以看到:

camera
 ├─ color ──→ YOLO
 │              │
 │              ↓
 │        yolo_detect_result
 │              │
 └──────────────┼──→ follow_me
                │
depth ──────────┘

Re-ID Service
      ↑
      │
 follow_me
      │
      ▼
  /cmd_vel
      │
      ▼
 Robot Base

这里非常适合在文章中放一张真实的 rqt_graph 截图。


6.9 故障定位决策树

机器人不 Follow

RGB 有数据?

修 Camera

Depth 有数据?

修 Depth

YOLO 有 person?

检查 Model / QNN / Threshold

Re-ID Service 存在?

检查 qrb_ros_people_reid

Target 初始化?

检查中央区域 / 1.5~3m / 人数

有 cmd_vel?

检查 Sync / PID / State

底盘运动?

检查 Topic Type / Robot Base

Follow Me PASS


07 实机应用:从 Gazebo 换成真实 RGB-D Camera

仿真跑通以后,真正迁移实机反而比较简单。

原来:

Gazebo RGB
Gazebo Depth

替换成:

Orbbec RGB
Orbbec Depth

后面的:

YOLO
Re-ID
Tracker
PID

保持一致。


7.1 Orbbec Gemini 335L

启动 Camera:

ros2 launch \
    orbbec_camera \
    gemini_330_series.launch.py \
    enable_depth:=true \
    depth_registration:=true

这里建议:

depth_registration = true

非常重要。


7.2 为什么 Depth Registration 非常关键

当前 Follow Me 的深度实现,本质上是:

RGB BBox
   ↓
按照分辨率比例
   ↓
Depth BBox

即:

x_depth =
x_rgb × depth_width / rgb_width

它没有在 Tracker 中自行完成完整的:

RGB Camera Intrinsic
+
Depth Camera Intrinsic
+
Extrinsic
+
3D Reprojection

因此如果 RGB 和 Depth 没有对齐:

RGB:

     ┌ Person ┐
     │        │
     └────────┘


Depth:

             ┌ Person ┐
             │        │
             └────────┘

你取到的可能根本不是人物深度。

所以真机:

优先使用 Registered/Aligned Depth。


7.3 实机不要一次全部启动

推荐调试顺序:

① Camera

确认:

RGB
Depth
CameraInfo

然后:

② YOLO

确认:

person

然后:

③ Re-ID

确认:

ExtractFeature
ComputeSimilarity

然后:

④ Tracker

此时:

先不要让轮子着地。

只观察:

/cmd_vel

最后:

⑤ Base

才真正让机器人运动。


7.4 一个当前必须注意的 cmd_vel 接口差异

当前 Follow Me 源码发布:

geometry_msgs/TwistStamped

而不少 ROS AMR 底盘,包括当前 QRB Robot Base 接口,使用:

geometry_msgs/Twist

先检查:

ros2 topic type /cmd_vel

不要等到:

Follow Me 明明有速度
机器人就是不走

才发现消息类型不匹配。


7.5 推荐使用一个 Adapter,而不是直接乱改上游代码

例如让 Follow Me 输出:

/follow_me/cmd_vel_stamped

再增加转换器:

#!/usr/bin/env python3

import rclpy

from rclpy.node import Node

from geometry_msgs.msg import (
    Twist,
    TwistStamped,
)


class CmdVelAdapter(Node):

    def __init__(self):

        super().__init__(
            "follow_me_cmd_vel_adapter"
        )

        self.pub = self.create_publisher(
            Twist,
            "/cmd_vel",
            10,
        )

        self.sub = self.create_subscription(
            TwistStamped,
            "/follow_me/cmd_vel_stamped",
            self.callback,
            10,
        )

    def callback(self, msg):

        out = Twist()

        out.linear.x = msg.twist.linear.x
        out.linear.y = msg.twist.linear.y
        out.linear.z = msg.twist.linear.z

        out.angular.x = msg.twist.angular.x
        out.angular.y = msg.twist.angular.y
        out.angular.z = msg.twist.angular.z

        self.pub.publish(out)


def main():

    rclpy.init()

    node = CmdVelAdapter()

    rclpy.spin(node)

    node.destroy_node()

    rclpy.shutdown()


if __name__ == "__main__":
    main()

这样系统边界很清楚:

Follow Me
TwistStamped
      ↓
Adapter
      ↓
Twist
      ↓
Robot Base

7.6 实机安全

Follow Me Demo 直接产生速度控制。

它本身不是完整的安全导航系统。

也就是说:

Person
   ↑
   │
Robot ───────→ cmd_vel

并不等价于:

Obstacle Avoidance
+
Collision Monitor
+
Local Planner
+
Safety Controller

因此真实测试必须至少做到:

低速
+
空旷场地
+
急停
+
旁边有人看护

第一次运行:

max_linear_speed: 0.15

max_angular_speed: 0.30

通常比直接 0.5 更合适。


08 当前源码最容易踩的 10 个坑

坑 1:直接 colcon build 后没有 person_tracker_node

原因:

当前 main 的 CMake Target 被注释

解决:

恢复 CMake
+
准备 Re-ID Dependency
+
重新编译

坑 2:找不到 qrb_ros_people_reid

Follow Me 本身不是一个完全孤立的仓库。

它依赖外部 Re-ID Service。

先:

ros2 pkg prefix qrb_ros_people_reid

找不到就先解决依赖。


坑 3:照着 Object Detection Launch 启动后没有 Depth

检查:

ros2 topic list | grep depth

Follow Me 没有:

/camera/depth/image_raw

一定跑不完整。


坑 4:真机人物距离完全不对

优先检查:

RGB / Depth Registration

而不是先修改 PID。

因为:

错误 Depth
→ 错误 Distance
→ 错误 PID

PID 再好也救不了错误传感器数据。


坑 5:RGB、Depth、YOLO 都有,但是 Tracker 没反应

检查 Header Timestamp:

ros2 topic echo \
    /camera/color/image_raw \
    --field header

ros2 topic echo \
    /camera/depth/image_raw \
    --field header

ros2 topic echo \
    /yolo_detect_result \
    --field header

当前同步窗口非常严格。

源码实际配置接近:

10ms

如果 YOLO Pipeline 带来明显延迟,三路消息可能永远匹配不到。

调试阶段可以尝试:

sync_->setMaxIntervalDuration(
    rclcpp::Duration::from_seconds(
        0.05
    )
);

即:

50ms

但窗口也不是越大越好。

窗口太大会导致:

旧 YOLO BBox
+
新 Depth

被错误配到一起。


坑 6:日志和实际 Synchronizer 参数不一致

看代码时不要只相信:

RCLCPP_INFO(...)

当前代码中同步器实际配置和某些日志文字存在差异。

工程调试应该看:

真正构造函数参数

而不是只看打印信息。


坑 7:Re-ID “similarity” 方向理解反了

项目逻辑:

Score 越小
→ 越相似

如果换了自己的 Re-ID:

Cosine Similarity 越大越相似

需要同步修改:

match
update
drift

所有比较逻辑。


坑 8:画面两个人时无法初始化

这是设计行为。

第一次目标初始化要求:

中央有效 Candidate = 1

所以:

两个同事一起站机器人前面

然后说:

怎么不跟?

其实代码没坏。


坑 9:Follow Me 有 cmd_vel,但底盘不动

执行:

ros2 topic info /cmd_vel -v

重点检查:

Message Type
Publisher
Subscriber

尤其:

Twist
vs
TwistStamped

坑 10:把 Follow Me 当 Nav2

Follow Me 目前核心思路:

视觉
↓
PID
↓
cmd_vel

它并不是:

视觉
↓
目标坐标
↓
Nav2
↓
Obstacle Avoidance
↓
cmd_vel

两种架构安全边界完全不一样。


09 实机应用案例应该如何写

这里可以放你规划的:

Qualcomm Symposium Follow-Me Robot 视频

但是正文措辞建议写成:

“同类 Follow-Me 机器人应用展示”

不要写:

“这就是本文 sample_followme 的官方实机效果。”

如果公开 Symposium Demo 使用的是:

Pose Estimation
+
Stereo Depth

而本文实现是:

YOLO
+
Re-ID
+
Depth
+
PID

二者的产品目标类似,但技术实现并不完全相同。

工程文章必须把:

案例展示

和:

源码实现

分开。


10 扩展思路:从 Follow Me Demo 走向真正机器人能力

你给出的四个方向其实都非常合适:

Gesture
VLM
Voice
Navigation

我建议最后画成:

RGB-D Camera

YOLO

Re-ID

Pose / Gesture

VLM

ASR / Voice

Target Manager

Follow Tracker

Nav2

Collision Monitor

Robot Base


10.1 Gesture:抬手告诉机器人“跟我”

当前初始化条件是:

中央只有一个人
+
距离合适

可以升级成:

检测到多人
        ↓
Pose Estimation
        ↓
哪个人在举手
        ↓
锁定该人的 BBox
        ↓
Extract Re-ID
        ↓
Follow

这样交互自然很多:

👋
机器人,跟我。

10.2 Voice:语音控制状态机

当前:

ros2 service call ...

显然不适合最终产品。

可以加入 ASR:

“跟着我”
    ↓
START

“先停一下”
    ↓
PAUSE

“继续”
    ↓
RESUME

“不用跟了”
    ↓
FINISH

底层仍然调用:

/follow_me/state_control

这样 Voice 并不会破坏原有确定性控制接口。


10.3 VLM:让机器人知道“我要跟谁”

Re-ID 擅长:

这个人
是不是刚刚那个人

但不擅长:

找到穿红色衣服的人。

VLM 可以负责高层理解:

“跟着戴黄色安全帽的人。”
            ↓
VLM / Visual Grounding
            ↓
Target BBox
            ↓
Re-ID 初始化
            ↓
后续高速跟踪

正确分工应该是:

VLM
低频目标理解

Re-ID
中高频身份保持

Depth + PID
高频运动控制

而不是每一帧都:

VLM → cmd_vel

那样实时性和可控性都不理想。


10.4 Navigation:真正解决绕障跟随

当前 PID Follow Me:

Target
  ↓
Angle + Distance
  ↓
cmd_vel

升级后:

RGB-D
   ↓
人物相对坐标
   ↓
TF Transform
   ↓
Person Pose in map
   ↓
生成 Follow Goal
   ↓
Nav2
   ↓
Local Planner
   ↓
Collision Monitor
   ↓
Robot

此时:

桌子
椅子
行人
墙面

都可以交给 Navigation Stack 参与处理。

这才更接近真正产品级:

Person Following Robot。


11 常见问题 FAQ

Q1:为什么不用普通目标跟踪算法,非要 Re-ID?

普通 Tracker 很适合:

连续画面
短时无遮挡

但人物:

被遮挡
离开画面
重新出现
与其他人交叉

后,ID 很容易切换。

Re-ID 能利用人物外观 Feature 做身份恢复。


Q2:一定要 Depth Camera 吗?

按照当前实现:

是。

因为控制器直接需要:

current_distance

没有 Depth 就无法直接得到人与机器人的实际米制距离。

当然也可以以后改成:

Monocular Depth
Stereo
LiDAR + Camera Fusion

但就不是当前 Sample 的原始实现了。


Q3:为什么目标距离默认 2m?

它位于默认初始化范围:

1.5 ~ 3.0m

中间,是一个比较容易演示和控制的距离。

真实产品应该根据:

Camera FOV
机器人尺寸
使用场景
刹车距离
人员速度

重新确定。


Q4:为什么不用 Nav2?

这个 Sample 重点验证的是:

实时人物跟踪
+
视觉闭环运动控制

而不是地图导航。

如果用于真实商用机器人,则非常建议进一步与:

Nav2
Collision Monitor

结合。


Q5:为什么 person_class_id 是 0?

COCO 数据集中的:

person = class 0

如果你换了自己的模型:

person = 3

则对应参数必须调整。


Q6:为什么一开始机器人不跟?

按顺序查:

State 是否 START
        ↓
YOLO 是否检测到 Person
        ↓
人物是否在中央区域
        ↓
距离是否 1.5~3m
        ↓
中央 Candidate 是否只有一个
        ↓
ExtractFeature 是否成功

不要第一反应就调 PID。


Q7:机器人左右抖动怎么办?

优先:

降低 angular_kp

其次:

适当增加 angular_kd

同时建议增加:

3° 左右 Dead Zone

Q8:机器人一直前后震荡怎么办?

降低:

linear_kp

增加适量:

linear_kd

并给:

distance_error

增加约:

±5~10cm

死区。


12 结论

Follow Me 看起来只是一个:

“机器人跟着人走”

的小 Demo。

但真正把源码拆开,会发现它已经包含一条相当完整的机器人 AI 闭环:

RGB
   ↓
YOLO Person Detection
   ↓
Person BBox
   ↓
Re-ID
   ↓
Target Identity
   ↓
Depth
   ↓
Target Distance
   ↓
Camera Geometry
   ↓
Target Angle
   ↓
Dual PID
   ↓
cmd_vel
   ↓
AMR Motion
   ↓
下一帧重新感知

如果进一步从系统层级抽象:

感知
Perception
    ↓
识别
Identification
    ↓
定位
Localization
    ↓
跟踪
Tracking
    ↓
控制
Control
    ↓
执行
Actuation

这也是这类 Sample 最值得学习的地方。

真正做产品时,再继续向上叠加:

Gesture
+
Voice
+
VLM
+
Navigation
+
Collision Avoidance

就可以从一个单纯的:

Follow Me Demo

逐步发展成:

“跟着我”
“跟着穿红衣服的人”
“先停一下”
“绕过前面的桌子继续跟”
“跟我去会议室”

这样的自然交互机器人,而高通端侧平台在这里承担的角色,不只是简单的跑一个 YOLO 模型,而是同时承载:Vision+Re-ID+Sensor Processing+ROS+Robot Control形成一个真正运行在机器人本体上的端侧 AI 闭环。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐