单元 7 · 手势识别与交互机器人视觉实战课程

单元 7

7. 手势识别与交互

本章使用MediaPipe Hand Landmarker识别手部的21个关键点,根据手指弯曲状态判断手势,并通过ROS2速度话题控制机器人底盘。用户可以先体验基础手势控制,再通过修改Python程序增加原地旋转和点赞识别。

5个学习部分

7.0 · 手势识别与交互

课程导读

20~30分钟 · 入门

本章使用MediaPipe Hand Landmarker识别手部的21个关键点,根据手指弯曲状态判断手势,并通过ROS2速度话题控制机器人底盘。用户可以先体验基础手势控制,再通过修改Python程序增加原地旋转和点赞识别。

7.0.1 本章能完成什么

完成本章学习后,用户可以:

  • 使用一条ROS2 Launch命令启动底盘、相机和手势控制节点。
  • 在画面中查看左右手、21个手部关键点和手势名称。
  • 使用openclose手势控制机器人前进、后退。
  • 了解手势识别结果如何映射为ROS2 Twist速度指令。
  • 修改基础版程序,为ok增加原地旋转动作。
  • 选择性学习如何增加thumb_up点赞识别。

7.0.2 实验列表

课程内容 难度 预计时间 前置内容 说明
7.1 实验1:基础手势控制 入门 20~30分钟 7.0课程导读 使用open前进、close后退。
7.2 实验2:增加ok原地旋转 入门 20~30分钟 建议完成7.1 修改基础版程序,增加原地旋转。
7.3 进阶拓展:增加thumb_up识别 进阶/可选 20~30分钟 建议完成7.1 增加带方向限制的点赞识别。

7.0.4 通用启动与退出方法

  1. 启动机器人,并使用NoMachine连接机器人远程桌面。关于连接方法,可参考 1 教程资料/1 机器人快速使用指南/ROSOrin Pro使用手册/7.2直连模式连接步骤

  2. 在远程桌面中打开命令行终端。输入命令时需要严格区分大小写,可使用“Tab”键补齐关键词。

  3. 按照所选实验的操作步骤启动对应Launch文件。基础版、入门版和进阶版的启动命令不同,请勿混用或同时运行。

  4. 实验结束后,在运行Launch文件的终端中按“Ctrl+C”停止程序。若实验过程中关闭了APP自启服务,请执行以下命令恢复服务:

Terminal
sudo systemctl restart start_app_node.service

7.0.5 实验学习路径

  • 基础体验路线: 7.1 基础手势控制。
  • 入门实践路线: 7.1 基础手势控制 → 7.2 增加ok原地旋转。
  • 进阶拓展路线: 7.1 基础手势控制 → 7.3 增加thumb_up识别。

7.1 · 手势识别与交互

实验1:基础手势控制

20~30分钟 · 入门
预计时间 难度 前置内容
20~30分钟 入门 7.0 课程导读

本实验先体验完整的手势识别与底盘控制流程,并了解程序的基本结构和安全停车机制。

7.1.1 你将完成什么

启动基础版程序后,将手掌完整放入摄像头视野。程序会绘制手部关键点和连线,并在每只手旁显示即时识别结果,在画面右上角显示连续稳定识别后的控制手势。

当程序稳定识别到open时,机器人前进;稳定识别到close时,机器人后退;手离开画面、手势不稳定或出现其他手势时,机器人停止。

图:程序检测手部关键点并识别手势。

7.1.2 实验目标

  • 正确启动基础版手势控制程序。
  • 使用openclose手势控制机器人前进、后退。
  • 观察手边即时识别结果与右上角稳定控制手势之间的区别。
  • 理解无手势、图像中断和程序退出时的停车保护。
  • 找到入门版中新增ok原地旋转功能的代码位置。

7.1.3 实验准备

开始实验前,请确认:

  • 机器人已正常启动,深度相机连接正常。
  • 电脑已安装NoMachine,并能够连接机器人远程桌面。
  • ROS2工作空间和本节实验程序已安装。
  • 摄像头前方光线充足,手掌无遮挡且能够完整进入画面。
  • 机器人周围具有足够的前进、后退和旋转空间。
  • 旧的指尖轨迹程序hand_gesture_control_node.launch.py没有运行。

7.1.4 开始实验

本小节先运行基础版。基础版用于体验完整的手势检测和底盘控制流程,不包含ok原地旋转和thumb_up点赞识别。

  1. 启动机器人,并使用NoMachine连接机器人远程桌面。关于连接方法,可参考 1 教程资料/1 机器人快速使用指南/ROSOrin Pro使用手册/7.2直连模式连接步骤

  2. 点击系统桌面的图标,打开命令行终端。

  3. 输入以下命令,停止APP自启服务:

Terminal
sudo systemctl stop start_app_node.service
  1. 确认机器人驱动轮已架空,或机器人周围没有人员和障碍物。

  2. 输入以下命令,启动基础版程序:

Terminal
ros2 launch example hand_gesture_drive.launch.py

该Launch文件会自动完成以下操作:

  • 启动底盘控制节点。
  • 启动深度相机节点。
  • 等待3秒后启动手势控制节点hand_pose_drive
  1. 等待标题为hand_gesture_control的图像窗口打开,将手掌完整放入摄像头视野。

  2. 依次测试下表中的手势:

手势名称 手部姿态 机器人动作
open 五指张开 0.15 m/s前进
close 握拳 0.15 m/s后退
one 伸出食指 只识别,机器人停止
two 伸出食指和中指 只识别,机器人停止
rock 伸出拇指、食指和小指 只识别,机器人停止
ok 收起拇指和食指,伸出中指、无名指和小指 只识别,机器人停止
none 未检测到稳定手势 机器人停止

7.1.5 查看实验结果

程序正常运行后,检查以下现象:

  • 图像窗口能够显示摄像头实时画面。
  • 每只手上能够显示21个关键点和关键点连线。
  • 手边文字能够显示当前即时识别结果。
  • 右上角深色背景区域能够显示Control: openControl: closeControl: none
  • openclose连续稳定出现5帧后,机器人执行对应动作。
  • 松开手、改变为其他手势或将手移出画面后,机器人立即停止。
  • 相机图像中断超过0.5秒后,程序发布停车指令。

图:程序识别手部关键点和手势。

7.1.6 结束实验

  1. 返回运行Launch文件的命令行终端,按“Ctrl+C”停止程序。

7.1.7 完成标准

满足以下条件即表示完成本节实验:

  • 能够使用一条Launch命令启动基础版程序。
  • 能够区分手边即时手势和右上角稳定控制手势。
  • 使用open时机器人前进,使用close时机器人后退。
  • 使用其他手势或移开手后,机器人能够停止。
  • 能够按“Ctrl+C”结束实验。

7.1.8 基础版程序简要分析

本节课程程序统一位于:

TEXT
~/ros2_ws/src/example/example/hand_gesture_course

目录结构如下:

TEXT
hand_gesture_course/
├── __init__.py
├── fps.py
├── hand_gesture_control.py
├── hand_gesture_ok_control.py
├── hand_gesture_thumb_up.py
├── hand_gesture_drive.launch.py
├── hand_gesture_ok_drive.launch.py
├── hand_gesture_thumb_up_drive.launch.py
└── model/
    └── hand_landmarker.task

三个版本相互独立,学习某个版本时可以直接打开对应Python文件进行查看,不需要同时运行其他版本。

7.1.8.1 基础版启动关系

基础版的启动关系如下:

TEXT
hand_gesture_drive.launch.py
        ↓
ROS2可执行入口 hand_pose_drive
        ↓
hand_gesture_control.py中的main()

Launch文件同时包含底盘和相机节点,并使用TimerAction延迟3秒启动手势控制节点,避免底盘和相机尚未完成初始化时就开始处理手势。

7.1.8.2 初始化Hand Landmarker

程序使用MediaPipe Tasks的HandLandmarker,以视频模式最多检测两只手:

PYTHON
current_dir = os.path.dirname(os.path.realpath(__file__))
model_path = os.path.join(current_dir, 'model', 'hand_landmarker.task')
options = vision.HandLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=model_path),
    running_mode=vision.RunningMode.VIDEO,
    num_hands=2,
    min_hand_detection_confidence=0.75,
    min_hand_presence_confidence=0.75,
    min_tracking_confidence=0.75,
)
self.detector = vision.HandLandmarker.create_from_options(options)

VIDEO模式会利用连续帧之间的跟踪结果。调用detect_for_video()时,程序为每一帧提供单调递增的毫秒时间戳。

7.1.8.3 从关键点判断手势

程序先把21个归一化关键点转换为像素坐标,再计算五根手指的弯曲角度:

PYTHON
pixels = np.asarray([
    (lm.x * width, lm.y * height) for lm in landmarks
], dtype=np.float32)
gesture = classify_gesture(hand_angle(pixels), pixels)

classify_gesture()根据手指弯曲角度返回closeonetworockokopennone

左右手标签只用于画面显示。连续帧缓存使用hand_0hand_1作为键,避免模型的左右手结果短暂波动时重置稳定帧计数:

PYTHON
hand_key = 'hand_%d' % index
stable = self.update_stable_gesture(hand_key, gesture)

只有同一个手势连续稳定出现5帧后,才会进入底盘控制逻辑。

7.1.8.4 基础版动作映射

基础版动作映射位于:

TEXT
hand_gesture_course/hand_gesture_control.py

handle_gesture()只允许openclose控制底盘:

PYTHON
def handle_gesture(self, gesture):
    if gesture != self.last_control_gesture:
        self.get_logger().info('Stable gesture: %s' % gesture)
        self.last_control_gesture = gesture

    if gesture == 'open':
        desired_speed = MOVE_SPEED
    elif gesture == 'close':
        desired_speed = -MOVE_SPEED
    else:
        desired_speed = 0.0
    self.set_linear_speed(desired_speed)

其中:

PYTHON
MOVE_SPEED = 0.15

Twist.linear.x为正值时前进,为负值时后退,为0时停止。

7.1.8.5 安全停车

程序除了在其他手势和无手势时停车,还通过定时器检查图像是否持续更新:

PYTHON
IMAGE_TIMEOUT_SECONDS = 0.5

def motion_watchdog(self):
    if self.active_linear_speed == 0.0:
        return
    if (self.last_processed_image_time is None or
            time.monotonic() - self.last_processed_image_time
            > IMAGE_TIMEOUT_SECONDS):
        self.get_logger().warn('Image timeout: stopping chassis')
        self.stop_chassis()

如果相机图像中断超过0.5秒,程序会主动发布零速度。按“Ctrl+C”退出程序时,也会在销毁节点前调用stop_chassis()

7.2 · 手势识别与交互

实验2:增加ok原地旋转

20~30分钟 · 入门
预计时间 难度 前置内容
20~30分钟 入门 建议完成7.1

完成基础版实验后,本小节将在基础版程序上增加ok原地旋转。请先了解目标效果,再按照步骤修改程序;完成修改和测试后,最后再运行课程提供的参考答案程序。

7.2.1 要实现的效果

手势 机器人动作 功能来源
open 前进 基础版原有
close 后退 基础版原有
ok 原地旋转 本实践新增
其他手势或无手势 停止 基础版原有

保持ok手势时,机器人以0.5 rad/s的角速度原地旋转;松开手、换成其他手势或识别结果不稳定时,机器人停止。

ok原地旋转不需要修改MediaPipe模型。基础版已经能够输出ok,本实践只需要在手势结果与ROS2 Twist速度指令之间增加一条动作映射。

7.2.2 备份并打开基础版程序

  1. 确认基础版Launch已经停止。如果程序仍在运行,请在对应终端按“Ctrl+C”。

  2. 输入以下命令进入课程程序目录:

Terminal
cd ~/ros2_ws/src/example/example/hand_gesture_course
  1. 修改前先备份基础版程序:
Terminal
cp hand_gesture_control.py hand_gesture_control_backup.py
  1. 打开基础版程序:
Terminal
gedit hand_gesture_control.py

本实践需要修改的文件为:

TEXT
~/ros2_ws/src/example/example/hand_gesture_course/hand_gesture_control.py

7.2.3 参考修改过程

1. 增加转向速度

找到MOVE_SPEED,在其下方增加TURN_SPEED

PYTHON
MOVE_SPEED = 0.15
TURN_SPEED = 0.5

TURN_SPEED对应Twist.angular.z,单位为rad/s

2. 记录当前角速度

HandGestureNode.__init__()中找到self.active_linear_speed,在其下方增加角速度状态:

PYTHON
self.active_linear_speed = 0.0
self.active_angular_speed = 0.0

程序同时记录线速度和角速度,避免重复发布相同指令,并使图像断流保护能够判断机器人是否正在旋转。

3. 修改动作映射

找到handle_gesture(),将该函数整体替换为:

PYTHON
def handle_gesture(self, gesture):
    if gesture != self.last_control_gesture:
        self.get_logger().info('Stable gesture: %s' % gesture)
        self.last_control_gesture = gesture

    if gesture == 'open':
        linear_speed = MOVE_SPEED
        angular_speed = 0.0
    elif gesture == 'close':
        linear_speed = -MOVE_SPEED
        angular_speed = 0.0
    elif gesture == 'ok':
        linear_speed = 0.0
        angular_speed = TURN_SPEED
    else:
        linear_speed = 0.0
        angular_speed = 0.0
    self.set_velocity(linear_speed, angular_speed)

其中,openclose只设置线速度,ok只设置角速度,其他情况同时将两个速度设置为0。

4. 发布线速度和角速度

找到基础版的set_linear_speed(),将该函数整体替换为:

PYTHON
def set_velocity(self, linear_speed, angular_speed, force=False):
    linear_speed = float(linear_speed)
    angular_speed = float(angular_speed)
    if (not force and linear_speed == self.active_linear_speed and
            angular_speed == self.active_angular_speed):
        return

    message = Twist()
    message.linear.x = linear_speed
    message.angular.z = angular_speed
    self.cmd_vel_pub.publish(message)
    self.active_linear_speed = linear_speed
    self.active_angular_speed = angular_speed

接着找到stop_chassis(),将其修改为同时清零线速度和角速度:

PYTHON
def stop_chassis(self):
    self.set_velocity(0.0, 0.0, force=True)

5. 更新图像断流保护

找到motion_watchdog()开头的运动状态判断,将只检查线速度修改为同时检查线速度和角速度:

PYTHON
def motion_watchdog(self):
    if self.active_linear_speed == 0.0 and self.active_angular_speed == 0.0:
        return
    if (self.last_processed_image_time is None or
            time.monotonic() - self.last_processed_image_time
            > IMAGE_TIMEOUT_SECONDS):
        self.get_logger().warn('Image timeout: stopping chassis')
        self.stop_chassis()

如果机器人正在原地旋转时相机图像中断,更新后的看门狗也能够发布停车指令。

7.2.4 保存并测试修改结果

  1. 保存hand_gesture_control.py并关闭编辑器。

  2. 返回终端,直接运行修改后的基础版:

Terminal
ros2 launch example hand_gesture_drive.launch.py
  1. 依次检查以下效果:
  • 使用open时,机器人前进。
  • 使用close时,机器人后退。
  • 使用ok时,机器人原地旋转。
  • 换成其他手势或将手移出画面时,机器人停止。
  • 原地旋转期间中断相机图像时,机器人能够自动停止。

测试结束后,在Launch终端按“Ctrl+C”。

7.2.5 完成标准

满足以下条件即表示完成本节实验:

  • openclose仍能正常控制机器人前进、后退。
  • ok连续稳定出现5帧后,机器人能够原地旋转。
  • 改变为其他手势、移开手或相机图像中断时,机器人能够停止。
  • 能够找到并说明TURN_SPEEDhandle_gesture()set_velocity()的作用。
  • 能够使用备份文件恢复修改前的基础版程序。

7.2.6 参考实现程序

点击展开参考实现

完成上述修改后,可以对照课程提供的入门版参考实现:

~/ros2_ws/src/example/example/hand_gesture_course/hand_gesture_ok_control.py

参考实现使用独立的ROS2可执行入口和Launch文件。运行前,请确认修改后的基础版已经停止,然后输入:

ros2 launch example hand_gesture_ok_drive.launch.py

参考实现不会覆盖用户修改的hand_gesture_control.py。可以同时打开两个Python文件,对比TURN_SPEEDhandle_gesture()set_velocity()stop_chassis()motion_watchdog()的差异。

若需要恢复修改前的基础版,可在所有手势程序停止后输入:

cd ~/ros2_ws/src/example/example/hand_gesture_course
cp hand_gesture_control_backup.py hand_gesture_control.py

7.3 · 手势识别与交互

进阶拓展(可选):增加thumb_up识别

20~30分钟 · 进阶/可选
预计时间 难度 前置内容
20~30分钟 进阶/可选 建议完成7.1

本小节以基础版为起点增加thumb_up点赞识别。请先了解目标效果,再按照步骤修改程序;完成修改和测试后,最后再展开课程提供的参考实现。

7.3.1 要实现的效果

手势 机器人动作 功能来源
open 前进 基础版原有
close 后退 基础版原有
thumb_up 只识别和显示,机器人停止 本拓展新增
其他手势或无手势 停止 基础版原有

正确的thumb_up姿态需要满足:拇指伸直并明显朝画面上方,食指、中指、无名指和小指保持弯曲。手势连续稳定出现5帧后,画面右上角显示Control: thumb_up,终端输出Stable gesture: thumb_up

本拓展只增加识别结果,不为thumb_up绑定底盘动作,因此识别到点赞时机器人保持停止。

7.3.2 备份并打开基础版程序

  1. 确认所有手势控制Launch已经停止。如果程序仍在运行,请在对应终端按“Ctrl+C”。

  2. 输入以下命令进入课程程序目录:

Terminal
cd ~/ros2_ws/src/example/example/hand_gesture_course
  1. 本拓展应以基础版为起点。如果刚刚完成了7.2ok原地旋转实践,请先使用备份恢复基础版:
Terminal
cp hand_gesture_control_backup.py hand_gesture_control.py

如果没有进行过7.2实践,则无需执行上面的恢复命令。

  1. 确认当前文件为基础版后,重新备份:
Terminal
cp hand_gesture_control.py hand_gesture_control_backup.py
  1. 打开基础版程序:
Terminal
gedit hand_gesture_control.py

本拓展需要修改的文件为:

TEXT
~/ros2_ws/src/example/example/hand_gesture_course/hand_gesture_control.py

7.3.3 参考修改过程

1. 增加手势优先级和最大倾角

在文件顶部的手势配置区域,将thumb_up加入优先级,并增加最大倾角:

PYTHON
GESTURE_PRIORITY = (
    'thumb_up', 'rock', 'one', 'two', 'ok', 'open', 'close')
THUMB_UP_MAX_TILT_DEGREES = 25.0

最大倾角表示拇指相对画面竖直向上的允许偏差:

  • 表示垂直向上。
  • 25°允许少量倾斜。
  • 接近90°表示拇指水平,不应识别为点赞。

若正常点赞不容易识别,可放宽到30.0;若斜放拇指仍会误识别,可收紧到20.0

2. 增加点赞方向判断函数

is_thumb_extended()之后、classify_gesture()之前增加:

PYTHON
def is_thumb_up(landmarks):
    """Return True when an extended thumb points clearly upward."""
    thumb_tip = landmarks[4]
    thumb_ip = landmarks[3]
    thumb_mcp = landmarks[2]
    palm_size = np.linalg.norm(landmarks[0] - landmarks[9])
    vertical_extension = thumb_mcp[1] - thumb_tip[1]
    horizontal_offset = abs(thumb_tip[0] - thumb_mcp[0])
    tilt_degrees = np.degrees(np.arctan2(
        horizontal_offset, max(vertical_extension, 1e-6)))

    return (
        is_thumb_extended(landmarks)
        and thumb_tip[1] < thumb_ip[1] < thumb_mcp[1]
        and vertical_extension > max(8.0, palm_size * 0.35)
        and tilt_degrees <= THUMB_UP_MAX_TILT_DEGREES
    )

该函数依次检查:

  1. 拇指两个主要关节是否伸直。
  2. 拇指指尖、指间关节和掌指关节是否从上到下排列。
  3. 拇指向上的长度是否相对于手掌足够明显。
  4. 拇指相对竖直方向的倾角是否不超过25°。

3. 在close之前增加thumb_up分类

classify_gesture()中,完成无效角度检查后、判断close之前增加:

PYTHON
if (is_thumb_up(landmarks) and
        a[1] > bent and
        a[2] > bent and
        a[3] > bent and
        a[4] > bent):
    return 'thumb_up'

该判断要求拇指伸直向上,同时食指、中指、无名指和小指保持弯曲。

thumb_up必须放在close之前判断。点赞时其他四指处于弯曲状态,如果先执行握拳判断,点赞可能被提前分类为close,从而错误触发后退动作。

4. 保持基础版动作映射不变

本拓展不需要修改handle_gesture()。基础版只为openclose设置速度,thumb_up会进入else分支并将速度设置为0:

PYTHON
if gesture == 'open':
    desired_speed = MOVE_SPEED
elif gesture == 'close':
    desired_speed = -MOVE_SPEED
else:
    desired_speed = 0.0

7.3.4 保存并测试修改结果

  1. 保存hand_gesture_control.py并关闭编辑器。

  2. 返回终端,直接运行修改后的基础版:

Terminal
ros2 launch example hand_gesture_drive.launch.py
  1. 依次检查以下效果:
  • 使用open时,机器人前进。
  • 使用close时,机器人后退。
  • 拇指竖直向上、其余四指收起时,程序识别为thumb_up,机器人保持停止。
  • 拇指接近水平时,不应识别为thumb_up
  • 换成其他手势或将手移出画面时,机器人停止。

测试结束后,在Launch终端按“Ctrl+C”。

7.3.5 完成标准

满足以下条件即表示完成本节拓展:

  • openclose仍能正常控制机器人前进、后退。
  • 拇指竖直向上且其余四指弯曲时,程序能够稳定识别thumb_up
  • 拇指接近水平时,不会被识别为thumb_up
  • 识别到thumb_up时机器人保持停止,不会误触发close后退动作。
  • 能够说明THUMB_UP_MAX_TILT_DEGREES对识别严格程度的影响。

7.3.6 参考实现程序

点击展开参考实现

完成上述修改后,可以对照课程提供的进阶版参考实现:

~/ros2_ws/src/example/example/hand_gesture_course/hand_gesture_thumb_up.py

参考实现使用独立的ROS2可执行入口和Launch文件。运行前,请确认修改后的基础版已经停止,然后输入:

ros2 launch example hand_gesture_thumb_up_drive.launch.py

参考实现不会覆盖用户修改的hand_gesture_control.py。可以同时打开两个Python文件,对比GESTURE_PRIORITYTHUMB_UP_MAX_TILT_DEGREESis_thumb_up()classify_gesture()的差异。

若需要恢复修改前的基础版,可在所有手势程序停止后输入:

cd ~/ros2_ws/src/example/example/hand_gesture_course
cp hand_gesture_control_backup.py hand_gesture_control.py

7.4 · 手势识别与交互

常见问题

课程内容
遇到问题?点击展开排查方法

7.4.1 画面有手部关键点,但机器人不运动

  • 查看右上角是否稳定显示Control: openControl: close
  • 手边文字是即时识别结果,右上角结果连续稳定5帧后才用于控制。
  • 确认没有同时运行其他会向/controller/cmd_vel发布速度的程序。
  • 使用以下命令检查速度话题是否同时具有发布者和订阅者:
ros2 topic info /controller/cmd_vel -v

7.4.2 手势始终显示none

  • 将完整手掌放入摄像头视野,不要裁切手腕或指尖。
  • 保持手掌正对摄像头,并减少手部旋转。
  • 调整手掌与摄像头之间的距离。
  • 增加环境光线,避免阴影和强烈逆光。

7.4.3 ok无法识别

本课程的ok不是传统OK手势。应收起拇指和食指,伸出中指、无名指和小指,并保持至少5帧。

7.4.4 水平拇指被识别为thumb_up

进阶版通过THUMB_UP_MAX_TILT_DEGREES限制点赞方向。可将默认值25.0调小为20.0,使判断更加严格。

7.4.5 左右手标签相反

程序在检测前对图像进行镜像翻转,并在显示时交换MediaPipe返回的LeftRight。若修改了图像翻转流程,应同时检查左右手标签交换逻辑。