DynamicX 视觉组学习笔记 - 初识 ros

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.xmlCMakeLists.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::Matcv_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

评论区