本篇目标:使用 ROS2 自定义服务接口,把一张图片发送给服务端,服务端用 face_recognition 识别人脸并返回人脸数量、耗时和矩形框坐标,客户端再用 OpenCV 把检测结果画出来。

最终运行效果:

  1. 服务端 face_detect_node 常驻运行,提供 /face_detect 服务。
  2. 客户端 face_detect_client_node 读取图片,调用服务。
  3. 客户端收到坐标后,用 cv2.rectangle() 画出人脸框,并弹出图片窗口。

一、创建自定义服务接口

1.1 创建接口包

进入第 4 章工作空间的 src 目录:

cd ~/ros2_ws/chapt4_ws/src
ros2 pkg create chapt4_interface --dependencies sensor_msgs rosidl_default_generators --license Apache-2.0

这里创建的是接口包,专门放自定义 msgsrvaction

1.2 创建服务文件

新建目录和文件:

mkdir -p ~/ros2_ws/chapt4_ws/src/chapt4_interface/srv
nano ~/ros2_ws/chapt4_ws/src/chapt4_interface/srv/FaceDetector.srv

写入:

sensor_msgs/Image image
---
int16 number
float32 use_time
int32[] top
int32[] right
int32[] bottom
int32[] left

含义:

  • 请求部分:客户端发送一张 sensor_msgs/Image 图片。
  • 响应部分:服务端返回人脸数量、识别耗时,以及每个人脸框的 top/right/bottom/left 坐标。

1.3 修改 CMakeLists.txt

打开:

nano ~/ros2_ws/chapt4_ws/src/chapt4_interface/CMakeLists.txt

添加:

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/FaceDetector.srv"
  DEPENDENCIES sensor_msgs
)

1.4 修改 package.xml

打开:

nano ~/ros2_ws/chapt4_ws/src/chapt4_interface/package.xml

确认包含:

<depend>sensor_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

如果 ros2 pkg create 已经生成了部分依赖,不要重复写多份。

1.5 编译接口包

cd ~/ros2_ws/chapt4_ws
colcon build --packages-select chapt4_interface
source install/setup.bash

验证接口:

ros2 interface show chapt4_interface/srv/FaceDetector

能看到 FaceDetector.srv 的字段,说明接口生成成功。


二、创建 Python 服务包

2.1 安装依赖

安装 OpenCV 和 cv_bridge

sudo apt update
sudo apt install ros-humble-cv-bridge python3-opencv -y

安装人脸识别库:

pip3 install face_recognition -i https://pypi.tuna.tsinghua.edu.cn/simple

如果 face_recognition 安装失败,通常是 dlib 编译依赖问题,需要先安装编译工具:

sudo apt install build-essential cmake python3-dev -y

然后重新执行 pip3 install face_recognition ...

2.2 创建 Python 包

cd ~/ros2_ws/chapt4_ws/src
ros2 pkg create demo_python_service --build-type ament_python --dependencies rclpy chapt4_interface sensor_msgs cv_bridge ament_index_python --license Apache-2.0

2.3 准备测试图片

创建资源目录:

mkdir -p ~/ros2_ws/chapt4_ws/src/demo_python_service/resource

下载两张测试图:

cd ~/ros2_ws/chapt4_ws/src/demo_python_service/resource
wget https://ultralytics.com/images/zidane.jpg -O default.jpg
wget https://ultralytics.com/images/bus.jpg -O bus.jpg

default.jpg 用于服务端默认测试,bus.jpg 用于客户端请求测试。

2.4 配置图片安装路径

打开:

nano ~/ros2_ws/chapt4_ws/src/demo_python_service/setup.py

data_files 中加入图片资源:

data_files=[
    ('share/ament_index/resource_index/packages',
        ['resource/' + package_name]),
    ('share/' + package_name, ['package.xml']),
    ('share/' + package_name + '/resource', [
        'resource/default.jpg',
        'resource/bus.jpg',
    ]),
],

这样 colcon build 后,图片会被安装到包的 share 目录,代码可以通过 get_package_share_directory() 找到。


三、单机版人脸检测测试

先不接 ROS2 服务,单独验证 OpenCV 和 face_recognition 能正常工作。

新建文件:

nano ~/ros2_ws/chapt4_ws/src/demo_python_service/demo_python_service/learn_face_detect.py

写入:

import cv2
import face_recognition
from ament_index_python.packages import get_package_share_directory


def main():
    default_image_path = (
        get_package_share_directory('demo_python_service')
        + '/resource/default.jpg'
    )

    print(f'图片真实路径:{default_image_path}')

    image = cv2.imread(default_image_path)
    if image is None:
        raise RuntimeError(f'图片读取失败:{default_image_path}')

    face_locations = face_recognition.face_locations(
        image,
        number_of_times_to_upsample=1,
        model='hog'
    )

    print(f'识别到的人脸个数:{len(face_locations)}')

    for top, right, bottom, left in face_locations:
        cv2.rectangle(image, (left, top), (right, bottom), (0, 0, 255), 2)

    cv2.imshow('image', image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

配置入口:

entry_points={
    'console_scripts': [
        'learn_face_detect = demo_python_service.learn_face_detect:main',
    ],
},

编译运行:

cd ~/ros2_ws/chapt4_ws
colcon build --packages-select demo_python_service
source install/setup.bash
ros2 run demo_python_service learn_face_detect

如果这里不能弹出图片窗口,先解决 WSL/OpenCV 图形显示问题,再继续服务通信。


四、编写人脸检测服务端

服务端负责提供 /face_detect 服务。收到图片后,服务端识别人脸并返回坐标。

新建文件:

nano ~/ros2_ws/chapt4_ws/src/demo_python_service/demo_python_service/face_detect_node.py

写入:

import os
import time

import cv2
import face_recognition
import rclpy
from ament_index_python.packages import get_package_share_directory
from chapt4_interface.srv import FaceDetector
from cv_bridge import CvBridge
from rclpy.node import Node


class FaceDetectNode(Node):
    def __init__(self):
        super().__init__('face_detect_node')

        self.service_ = self.create_service(
            FaceDetector,
            'face_detect',
            self.detect_face_callback
        )

        self.bridge = CvBridge()
        self.number_of_times_to_upsample = 1
        self.model = 'hog'
        self.default_image_path = os.path.join(
            get_package_share_directory('demo_python_service'),
            'resource',
            'default.jpg'
        )

        self.get_logger().info('人脸识别服务端初始化完成,等待客户端请求')

    def detect_face_callback(self, request, response):
        if request.image.encoding:
            cv_image = self.bridge.imgmsg_to_cv2(
                request.image,
                desired_encoding='bgr8'
            )
        else:
            cv_image = cv2.imread(self.default_image_path)
            self.get_logger().info('传入图像为空,使用默认图像')

        if cv_image is None:
            self.get_logger().error('图像读取失败,无法识别人脸')
            response.number = 0
            response.use_time = 0.0
            return response

        start_time = time.time()
        self.get_logger().info('加载图像完成,开始识别')

        face_locations = face_recognition.face_locations(
            cv_image,
            number_of_times_to_upsample=self.number_of_times_to_upsample,
            model=self.model
        )

        response.use_time = time.time() - start_time
        response.number = len(face_locations)

        for top, right, bottom, left in face_locations:
            response.top.append(top)
            response.right.append(right)
            response.bottom.append(bottom)
            response.left.append(left)

        self.get_logger().info(
            f'识别完成:{response.number} 张人脸,耗时 {response.use_time:.3f} 秒'
        )
        return response


def main():
    rclpy.init()
    node = FaceDetectNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

关键点:

  • node = FaceDetectNode() 必须有括号,表示创建节点实例。
  • rclpy.spin(node) 让服务端常驻运行,等待客户端请求。
  • 服务端不弹图片窗口。它只负责识别和返回结果。

五、编写人脸检测客户端

客户端负责读取图片、调用 /face_detect 服务、接收坐标并显示图片。

新建文件:

nano ~/ros2_ws/chapt4_ws/src/demo_python_service/demo_python_service/face_detect_client_node.py

写入:

import os

import cv2
import rclpy
from ament_index_python.packages import get_package_share_directory
from chapt4_interface.srv import FaceDetector
from cv_bridge import CvBridge
from rclpy.node import Node


class FaceDetectClientNode(Node):
    def __init__(self):
        super().__init__('face_detect_client_node')

        self.bridge = CvBridge()
        self.default_image_path = os.path.join(
            get_package_share_directory('demo_python_service'),
            'resource',
            'bus.jpg'
        )
        self.image = cv2.imread(self.default_image_path)
        if self.image is None:
            raise RuntimeError(f'图片读取失败:{self.default_image_path}')

        self.client = self.create_client(FaceDetector, 'face_detect')
        self.get_logger().info('人脸检测客户端初始化完成')

    def send_request(self):
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('等待服务端 /face_detect ...')

        request = FaceDetector.Request()
        request.image = self.bridge.cv2_to_imgmsg(
            self.image,
            encoding='bgr8'
        )

        future = self.client.call_async(request)
        rclpy.spin_until_future_complete(self, future)

        response = future.result()
        if response is None:
            self.get_logger().error('服务调用失败,没有返回结果')
            return

        self.get_logger().info(
            f'识别到的人脸个数:{response.number},耗时 {response.use_time:.3f} 秒'
        )
        self.show_response(response)

    def show_response(self, response):
        for i in range(response.number):
            top = response.top[i]
            right = response.right[i]
            bottom = response.bottom[i]
            left = response.left[i]
            cv2.rectangle(self.image, (left, top), (right, bottom), (0, 0, 255), 2)

        cv2.imshow('face detect result', self.image)
        cv2.waitKey(0)
        cv2.destroyAllWindows()


def main():
    rclpy.init()
    node = FaceDetectClientNode()
    node.send_request()
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

关键点:

  • 客户端不需要 rclpy.spin(node) 常驻。
  • 客户端要主动调用 node.send_request()
  • rclpy.spin_until_future_complete(self, future) 的第一个参数是当前节点,第二个参数是服务调用返回的 future
  • 画框时必须同时取出 top/right/bottom/left 四个坐标。

六、配置 setup.py

打开:

nano ~/ros2_ws/chapt4_ws/src/demo_python_service/setup.py

确认入口完整:

entry_points={
    'console_scripts': [
        'learn_face_detect = demo_python_service.learn_face_detect:main',
        'face_detect_node = demo_python_service.face_detect_node:main',
        'face_detect_client_node = demo_python_service.face_detect_client_node:main',
    ],
},

七、编译与运行

7.1 编译

cd ~/ros2_ws/chapt4_ws
colcon build
source install/setup.bash

如果只改了 Python 服务包:

colcon build --packages-select demo_python_service
source install/setup.bash

7.2 运行服务端

终端 1:

cd ~/ros2_ws/chapt4_ws
source install/setup.bash
ros2 run demo_python_service face_detect_node

正常输出类似:

人脸识别服务端初始化完成,等待客户端请求

7.3 运行客户端

终端 2:

cd ~/ros2_ws/chapt4_ws
source install/setup.bash
ros2 run demo_python_service face_detect_client_node

客户端会:

  1. 读取 bus.jpg
  2. 调用 /face_detect 服务。
  3. 打印识别数量和耗时。
  4. 弹出图片窗口并画出人脸框。

八、命令行测试服务

如果只想测试服务是否存在:

ros2 service list

查看服务类型:

ros2 service type /face_detect

查看接口:

ros2 interface show chapt4_interface/srv/FaceDetector

直接用命令行调用空请求:

ros2 service call /face_detect chapt4_interface/srv/FaceDetector

因为请求里的 image 为空,服务端会使用默认图片 default.jpg


九、常见错误与修复

9.1 TypeError: 'property' object is not iterable

错误写法:

node = FaceDetectNode
rclpy.spin(node)

正确写法:

node = FaceDetectNode()
rclpy.spin(node)

原因:FaceDetectNode 是类,FaceDetectNode() 才是节点实例。rclpy.spin() 需要节点实例。

9.2 AttributeError: module 'posixpath' has no attribute 'pardirjoin'

错误写法:

os.path.pardirjoin(...)

正确写法:

os.path.join(...)

9.3 客户端没有图片窗口

检查客户端 main() 是否调用了:

node.send_request()

如果写成:

rclpy.spin(node)

客户端不会主动调用服务,cv2.imshow() 不会执行。

9.4 spin_until_future_complete() missing 1 required positional argument: 'future'

错误写法:

rclpy.spin_until_future_complete(future)

正确写法:

rclpy.spin_until_future_complete(self, future)

9.5 self.cilent 拼写错误

错误写法:

future = self.cilent.call_async(request)

正确写法:

future = self.client.call_async(request)

9.6 OpenCV 窗口不显示

先测试 WSL / OpenCV 图形环境:

python3 -c "import cv2; import numpy as np; img=np.zeros((300,500,3), dtype=np.uint8); cv2.imshow('test', img); cv2.waitKey(0); cv2.destroyAllWindows()"

如果测试窗口也不出现,问题在 WSL GUI 或 OpenCV 显示环境,不在 ROS2 服务代码。


十、学习重点总结

这部分要掌握的不是“人脸识别库”本身,而是 ROS2 服务通信的完整结构:

  1. .srv 定义请求和响应。
  2. rosidl_generate_interfaces() 生成 Python 可导入接口。
  3. 服务端用 create_service() 提供服务。
  4. 客户端用 create_client() 调用服务。
  5. 图像在 ROS2 和 OpenCV 之间通过 cv_bridge 转换。
  6. 服务端返回坐标,客户端负责显示图片。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐