ROS(Robot Operating System)在机器人领域是非常有名的,中文教程里古月的一系列教程较为有名。最近我学习了 ROS 的基本使用并且实现了一个基于 ROS 的摄像头图像实时处理代码。
认识 ROS
Robot Operating System (ROS or ros) is an open source robotics middleware suite. Although ROS is not an operating system but a collection of software frameworks for robot software development, it provides services designed for a heterogeneous computer cluster such as hardware abstraction, low-level device control, implementation of commonly used functionality, message-passing between processes, and package management.
Wikipedia 的这段介绍言简意赅。ROS 是一个主要安装运行在 Linux 操作系统(主要为 Ubuntu 发行版)上的软件集,提供了一个标准化、社区化、跨语言的开发运行环境。ROS 提供的工具和语言支持帮助开发者使用一个统一的标准组织自己的代码,实现不同功能的松耦合和数据通讯,在 ROS 社区中发布或使用代码,降低机器人控制系统的开发难度。 ROS 目前已经发布了 ROS2 的 LTS 版本,但是由于 ROS2 相对于 ROS1 变动非常大,并且相对来说要新很多(第一个 ROS2 LTS 发布于 Ubuntu 18.04 LTS),所以队内目前使用的是 ROS1。
安装与配置
使用 apt 安装
Ubuntu 是 ROS 主要支持的发行版,我使用的 Debian 可以算是次要支持,两者都使用 apt 作为包管理器,在这两个发行版上 apt 也是 ROS 的包管理器。安装的第一个坑是 ROS 版本与发行版版本强关联,比如我安装的 ROS Noetic 支持的版本为 Ubuntu Focal 和 Debian Buster,至于为什么有强制要求我没有去深究。
安装过程是非常常见的添加软件源,导入 gpg key,更新列表,安装包组。
使用 CLion 开发
ROS 目前使用的 catkin 编译系统基于 cmake 和 python 执行代码编译,而 CLion 对两者的支持也很好,所以使用 CLion 进行 ROS 开发的难点在于如何让 CLion 正确识别项目结构。
第一个坑在环境变量,在 ROS 安装后需要在终端执行source /opt/ros/noetic/setup.bash
以设置环境变量,即使将这条命令写入.bashrc
或.zshrc
也无法对 CLion 的编译工具生效,导致编译时找不到 catkin 库。对于这一点最简单粗暴的方法是在终端中启动 CLion,这样 CLion 就可以获得所有在终端中设置好的环境变量。另一种方法是在每个项目的配置中手动增加环境变量(比如 CMAKE_PREFIX_PATH)。
第二个坑在 CMakeLists 和 CLion 的编译配置。catkin 只会帮你完成 CMakeLists 的初始化,你还需要根据自己的需要调整 CMakeLists 才能正常编译,CLion 在项目初始化后会识别可用的编译配置,此时会识别到大量 catkin 组件的编译配置,并且不一定会识别到项目的编译配置,所以这里也需要注意不要无脑点编译运行。
hatchery 插件提供了更多的 ROS 支持。
第一个 ROS 项目
功能包与通信
在 ROS 中,功能包(package)是一个功能单元,实现了某个特定功能,主要用于解耦;工作空间(workspace)是一个编译单元,可以存放多个功能包,虽然单个功能包可能可以单独编译,但 catkin 倾向于编译整个工作空间;不同功能包之间在编译时可以相互引用变量,在运行时可以通过 ROS 的话题(topic)和服务(service)两种机制通信。 topic 与 MQTT 相似,是发布者-订阅者间的异步通信;service 与 HTTP 相似,是服务端-客户端间的同步通信。
第一个功能包
在一个空文件夹中执行catkin_make
即可生成一个新的工作空间,这个命令也是编译整个工作空间的命令。在工作空间的src
文件夹中执行catkin_create_pkg
并提供适当的参数即可生成一个新的功能包。在新功能包中,如果不需要发布此功能包就可以不配置package.xml
,CMakeLists.txt
在编译前配置好即可。源代码放置于功能包中的 src 文件夹,如果像本例一样功能包需要作为节点(node)运行,默认的文件名为[package name]_node.cpp
,使用其他名称需要修改CMakeLists.txt
为对应的名称。
使用的库及代码实现
<ros/ros.h>
ROS 标准库。这里用到的都是一些很基础又很重要的功能
int main(int argc, char** argv)
{
// 初始化节点
ros::init(argc, argv, "cam_process");
// 构造结构体,由于处理程序由话题驱动,所以在这里不需要调用函数
ImageConverter obj;
// 循环监听,开始监听订阅
ros::spin();
}
<dynamic_reconfigure/server.h>
使用这个库需要除引入包之外的 CMakeLists.txt 配置
由于本例需要能够动态调整图像处理的方式,所以需要实现动态调参功能。dynamic_reconfigure 库提供了一个可以从节点外获取参数表并修改值的方式:需要动态调参的节点开启一个服务端,其他节点可以作为客户端向该节点提交新参数。参数表的定义用到了 python
#! /usr/bin/env python
# coding=utf-8
PACKAGE = "cam_process"
#初始化ROS,并导入参数生成器
from dynamic_reconfigure.parameter_generator_catkin import *
# 创建一个参数生成器
gen = ParameterGenerator()
# 添加参数说明,便于后续生成界面
# 参数名 类型 等级 参数描述 默认值 最小值 最大值
gen.add("blur_kernel", int_t, 0, "int", 0, 0, 30)
gen.add("shold", bool_t, 0, "bool", True)
# 调用生成器生成config配置文件
# 包名 节点名称 生成文件名
exit(gen.generate(PACKAGE, PACKAGE, "param"))
这个文件放置于功能包内src/cfg
文件夹,它为动态调参的服务端和客户端定义了参数结构、或者说定义了接口。
catkin 会根据这个文件生成头文件供 cpp 代码使用。在服务端的回调函数中可以获得客户端发送的参数,这些参数可以立即使用或保存在外部变量中供其他函数使用。
// 功能包名 文件名+Config
#include <cam_process/paramConfig.h>
dynamic_reconfigure::Server<cam_process::paramConfig> server;
dynamic_reconfigure::Server<cam_process::paramConfig>::CallbackType callBackType;
ImageConverter()
:it_(nh_) //构造函数
{
// ...
// 绑定回调函数
callBackType = boost::bind(&ImageConverter::paramCallback, this, _1, _2);
server.setCallback(callBackType);
}
<image_transport/image_transport.h>
用于在话题中发布和订阅图像消息,在这里定义了一个接收器和一个发布器用于获取摄像头图像并发出已处理图像
image_transport::ImageTransport it_; //定义一个 image_transport 实例
image_transport::Subscriber image_sub_; //声明接收器
image_transport::Publisher image_pub_; //声明发布器
ImageConverter()
:it_(nh_) // 构造函数
{
// 两个函数的第一个参数都是话题名
// convert_calback 是用于处理图像的成员函数
image_sub_ = it_.subscribe("/usb_cam/image_raw", 1, &ImageConverter::convert_callback, this);
image_pub_ = it_.advertise("/image_converter/output", 1);
}
<cv_bridge/cv_bridge.h>
ROS 中的标准图像消息为sensor_msgs::Image
,OpenCV 中的图片为cv::Mat
,cv_bridge
实现了两者的转换
// 上个小节中提供给接收器的回调函数
void convert_callback(const sensor_msgs::ImageConstPtr& msg)
{
cv_bridge::CvImagePtr cv_ptr;
try {
cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::RGB8);
}
catch(cv_bridge::Exception& e) {
ROS_ERROR("cv_bridge exception: %s", e.what());
return;
}
// 得到了 cv::Mat 类型的图象,将结果传送给处理函数
image_process(cv_ptr->image);
}
void image_process(cv::Mat src)
{
Mat dst;
// ...
// 将 cv::Mat 图像转换为 sensor_msgs::Image 发出
sensor_msgs::ImagePtr msg;
msg = cv_bridge::CvImage(std_msgs::Header(), "rgb8", dst).toImageMsg();
image_pub_.publish(msg);
}
<opencv2/core.hpp>
OpenCV 标准库,事实上cv_bridge
已经包含了这个头文件。
文件结构总览
catkin_workspace/
build/
devel/
src/
CMakeLists.txt
cam_process/
cfg/
param.cfg
src/
cam_process_node_cpp
CMakeLists.txt
package.xml