ROS2系列 (18) : Python服务通信实例------Client端(人脸检测客户端)
在上一篇中,我们实现了人脸检测服务的服务端(Server),本文将基于同一功能包demo_python_service,开发对应的客户端(Client),完成图像发送、服务调用及结果解析的全流程,并详解ROS2异步服务调用中future的两种处理方法。
一、客户端功能与技术要点
客户端功能 :读取本地测试图像,发送给服务端/face_detect服务,接收并解析人脸检测结果(数量、位置、耗时)。
核心技术点:
- 客户端创建:通过
rclpy的create_client绑定服务端。 - 图像格式转换:使用
cv_bridge将OpenCV图像转为ROS消息格式。 - 异步服务调用:通过
future对象处理服务响应,支持两种结果获取方式。
二、文件组织与前置准备
2.1 功能包目录结构
客户端代码将放入上一篇创建的demo_python_service功能包中,目录结构如下:
demo_python_service/
├── demo_python_service/
│ ├── __init__.py
│ ├── face_detection_server.py # 已有的服务端代码
│ └── face_detection_client.py # 新增的客户端代码
├── resource/
│ ├── default.jpg # 服务端默认图像
│ └── test1.jpg # 客户端测试图像
├── setup.py
├── package.xml
└── LICENSE
2.2 准备测试图像
在resource目录下添加客户端专用测试图像:
bash
# 进入资源目录
cd demo_python_service/demo_python_service/resource
# 下载测试图像(或自行放置一张图片命名为test1.jpg)
wget https://img.freepik.com/free-photo/portrait-group-people-standing-together_23-2149111110.jpg -O test1.jpg
三、编写客户端代码
在demo_python_service/demo_python_service/目录下创建face_detection_client.py,删除不必要的参数相关代码:
python
import rclpy
from rclpy.node import Node
from chapt4_interfaces.srv import FaceDetector
from sensor_msgs.msg import Image
from ament_index_python.packages import get_package_share_directory
import cv2
from cv_bridge import CvBridge
class FaceDetectorClient(Node):
def __init__(self):
super().__init__('face_detect_client')
# 创建客户端:绑定服务名/face_detect(需与服务端一致)
self.client = self.create_client(FaceDetector, '/face_detect')
# 初始化图像格式转换器
self.bridge = CvBridge()
# 加载测试图像路径
self.test1_image_path = get_package_share_directory(
'demo_python_service') + '/resource/test1.jpg'
# 读取测试图像(OpenCV格式)
self.image = cv2.imread(self.test1_image_path)
self.get_logger().info('人脸检测客户端已启动,准备发送请求')
def send_request(self):
"""方法1:使用spin_until_future_complete阻塞等待响应"""
# 1. 等待服务端上线(超时1秒循环检测)
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('服务端未上线,等待中...')
# 2. 构造服务请求:将OpenCV图像转为ROS Image消息
request = FaceDetector.Request()
request.image = self.bridge.cv2_to_imgmsg(self.image)
# 3. 异步发送请求,返回future对象(用于获取结果)
self.future = self.client.call_async(request)
# 4. 阻塞等待future完成(服务端返回响应)
rclpy.spin_until_future_complete(self, self.future)
# 5. 解析响应结果
response = self.future.result()
self.get_logger().info(
f'服务响应:检测到{response.number}张人脸,耗时{response.use_time:.2f}秒')
self.show_face_locations(response) # 可视化检测结果
return response
def send_request_with_callback(self):
"""方法2:使用add_done_callback异步处理响应"""
# 1. 等待服务端上线
if not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().error('服务端未上线,无法发送请求')
return
# 2. 构造请求(同方法1)
request = FaceDetector.Request()
request.image = self.bridge.cv2_to_imgmsg(self.image)
# 3. 异步发送请求,为future绑定回调函数
future = self.client.call_async(request)
future.add_done_callback(self.handle_response) # 响应到达时自动调用handle_response
def handle_response(self, future):
"""回调函数:处理服务响应(当future完成时调用)"""
try:
response = future.result()
self.get_logger().info(
f'回调处理:检测到{response.number}张人脸,耗时{response.use_time:.2f}秒')
self.show_face_locations(response)
except Exception as e:
self.get_logger().error(f'服务调用失败:{str(e)}')
def show_face_locations(self, response):
"""可视化人脸检测结果(在图像上绘制边界框)"""
# 复制原图避免修改源数据
img_with_boxes = self.image.copy()
for i in range(response.number):
# 提取人脸边界框坐标
top = response.top[i]
right = response.right[i]
bottom = response.bottom[i]
left = response.left[i]
# 绘制矩形框(蓝色,线宽2)
cv2.rectangle(img_with_boxes, (left, top), (right, bottom), (255, 0, 0), 2)
# 显示结果图像(按任意键关闭)
cv2.imshow('Face Detection Result', img_with_boxes)
cv2.waitKey(0)
cv2.destroyAllWindows()
def main(args=None):
rclpy.init(args=args)
client = FaceDetectorClient()
# 演示两种请求方式(任选其一或都执行)
client.get_logger().info('===== 使用spin_until_future_complete发送请求 =====')
client.send_request()
client.get_logger().info('===== 使用add_done_callback发送请求 =====')
client.send_request_with_callback()
rclpy.spin(client) # 需保持节点运行以等待回调执行
rclpy.shutdown()
四、修改setup.py(添加客户端入口)
更新demo_python_service/setup.py的entry_points,添加客户端节点命令:
python
entry_points={
'console_scripts': [
'face_detection_server = demo_python_service.face_detection_server:main',
'face_detection_client = demo_python_service.face_detection_client:main', # 新增客户端入口
],
},
五、构建与运行流程
5.1 构建功能包
bash
cd ~/chapt4/chapt4_ws
colcon build --packages-select demo_python_service
source install/setup.bash
5.2 运行服务端与客户端(两个终端)
终端1:启动服务端
bash
source install/setup.bash
ros2 run demo_python_service face_detection_server
终端2:启动客户端
bash
source install/setup.bash
ros2 run demo_python_service face_detection_client
预期效果:
- 客户端终端输出服务响应日志;
- 弹出图像窗口,显示带蓝色边界框的人脸检测结果。
六、future处理方法详解
在ROS2异步服务调用中,client.call_async(request)返回future对象,用于追踪服务调用状态,有两种结果处理方式:
6.1 方法1:rclpy.spin_until_future_complete(阻塞等待)
python
future = self.client.call_async(request)
rclpy.spin_until_future_complete(self, future) # 阻塞直到响应返回
response = future.result()
原理:
- 调用
spin_until_future_complete后,节点会进入事件循环,专门等待该future完成; - 响应返回后,函数退出阻塞,可通过
future.result()获取结果; - 适用于同步场景,需要等待结果后再执行后续逻辑。
6.2 方法2:add_done_callback(异步回调)
python
future = self.client.call_async(request)
future.add_done_callback(self.handle_response) # 绑定回调函数
rclpy.spin(client) # 保持节点运行以接收回调
原理:
add_done_callback为future注册一个回调函数(如handle_response);- 当服务端返回响应时,
future状态变为"完成",回调函数自动执行; - 节点需通过
rclpy.spin保持运行,否则可能错过回调; - 适用于异步场景,无需阻塞等待,可同时处理其他任务。
七、总结
本文实现了人脸检测服务的客户端,核心要点包括:
- 客户端创建:通过
create_client绑定服务端,确保服务名一致; - 图像处理:使用
cv_bridge将OpenCV图像转为ROS消息格式; future处理:两种方式实现服务响应获取(阻塞等待与异步回调),适应不同场景;- 结果可视化:通过OpenCV绘制人脸边界框,直观展示检测效果。
至此,我们完成了ROS2 Python服务通信的"服务端-客户端"闭环,掌握了自定义服务接口、异步调用及结果处理的核心流程,可扩展应用于机器人控制、传感器数据处理等场景。