1. 唠唠叨叨
最近要在ROS里面启动摄像头,并采集图像,将得到的图像发布给图像接收节点,整个逻辑十分的简单清晰。
节点1:充当图像发布节点,并启动相机、采集图像,并发布图像话题 /camera/image
节点2:充当图像订阅节点,功能为订阅图像话题,显示相机图像
ROS官方教程对此提供了相应的教程,相关链接列举如下:
1. Writing a Simple Image Publisher (C++)
2. Writing a Simple Image Subscriber (C++)
3. Running the Simple Image Publisher and Subscriber with Different Transports
这里介绍下本文的整体框架:在第二节介绍整个功能包的部署和使用;在第三节介绍图像发布节点;第四节介绍图像订阅节点;第五节介绍在使用官方源码包时遇到的一些问题和自己开发过程的一点困难。
本文的实验平台:
- Ubuntu16.0
- ROS kinetic
- TX2
2. 功能包部署
这一步是我个人比较纠结的一块,自己对于ROS的功能包创建不是很熟悉,对于CmakeLists.txt和package.xml文件的编写方式和相关依赖项不是很熟悉。
# 创建工作空间文件夹,并进入
$ mkdir -p ~/robot_ws/src
$ cd robot_ws/src
# 克隆代码
$ git clone https://github.com/zhouyumeng/camera_driver.git
# 返回上一层目录
$ cd ..
# 编译源码
$ catkin_make
# 加载环境变量
$ source devel/setup.bash
# 对于加载环境变量这一步操作,通常直接将环境变量直接写入~/.bashrc文件下
# 即在~/.bashrc文件下添加“ source ~/robot_ws/devel/setup.bash”
camera_driver文件夹下的整体文件部署如下:
.
├── CMakeLists.txt
├── package.xml
├── README.md
├── src
├── camera_node.cpp
└── cam_test_subscriber.cpp
下面分析下ROS功能包的CmakeLists.txt文件和package.xml文件。
# CmakeLists.txt文件分析
cmake_minimum_required(VERSION 3.5)
project(camera_driver)
SET(OpenCV_DIR /usr/local/share/OpenCV)
find_package(OpenCV REQUIRED)
find_package(catkin REQUIRED COMPONENTS
cv_bridge
image_transport
message_generation
message_runtime
roscpp
sensor_msgs
std_msgs
)
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
catkin_package(CATKIN_DEPENDS cv_bridge image_transport message_generation roscpp sensor_msgs std_msgs
)
include_directories(
include
${catkin_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
)
add_executable(camera_node src/camera_node.cpp)
add_dependencies(camera_node ${catkin_EXPORTED_TARGETS} ${${PROJECT_NAME}_EXPORTED_TARGETS})
target_link_libraries(camera_node ${catkin_LIBRARIES} ${OpenCV_LIBS})
add_executable(cam_test_subscriber src/cam_test_subscriber.cpp)
add_dependencies(cam_test_subscriber ${catkin_EXPORTED_TARGETS} ${${PROJECT_NAME}_EXPORTED_TARGETS})
target_link_libraries(cam_test_subscriber ${catkin_LIBRARIES} ${OpenCV_LIBS})
# package.xml文件分析
<?xml version="1.0"?>
<package format="2">
<name>camera_driver</name>
<version>0.0.0</version>
<description>The camera_driver package</description>
<maintainer email="nvidia@todo.todo">nvidia</maintainer>
<license>TODO</license>
<buildtool_depend>catkin</buildtool_depend>
<build_depend>cv_bridge</build_depend>
<build_depend>image_transport</build_depend>
<build_depend>message_generation</build_depend>
<build_depend>roscpp</build_depend>
<build_depend>sensor_msgs</build_depend>
<build_depend>std_msgs</build_depend>
<build_depend>opencv2</build_depend>
<build_export_depend>cv_bridge</build_export_depend>
<build_export_depend>image_transport</build_export_depend>
<build_export_depend>message_generation</build_export_depend>
<build_export_depend>roscpp</build_export_depend>
<build_export_depend>sensor_msgs</build_export_depend>
<build_export_depend>std_msgs</build_export_depend>
<exec_depend>opencv2</exec_depend>
<exec_depend>cv_bridge</exec_depend>
<exec_depend>image_transport</exec_depend>
<exec_depend>message_runtime</exec_depend>
<exec_depend>roscpp</exec_depend>
<exec_depend>sensor_msgs</exec_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>opencv2</exec_depend>
<export>
</export>
</package>
3. 图像发布节点:camera_node.cpp
我个人的项目比较特殊,特殊在相机平台特殊,需要借助V4L2驱动来开启和采集图片(目前项目尚在开发,以后项目开发完了,再进行介绍),导致相机的启动不能通过OpenCV的相关函数直接启动,为了简单期间,本文使用S-YUE晟悦的WX051摄像头,是一款USB摄像头,免驱即插即用。
//camera_node.cpp:位于功能包的src文件夹下
//重点在于将OpenCV和ROS联合使用,用OpenCV来开启摄像头,ROS自带的功能包进行图像传递
#include <ros/ros.h> //ros.h包含大部分通用的ROS头文件
#include <opencv2/opencv.hpp> //opencv.hppp包含大部分通用的OpenCV头文件
#include <opencv2/highgui.hpp> //opencvGUI组件
#include <opencv2/imgproc.hpp> //引入cvtColor()
#include <image_transport/image_transport.h> //image_transport实现图像传输
#include <cv_bridge/cv_bridge.h> //cv_bridge提供ROS对OpenCV格式的接口功能
using namespace cv;
using namespace std;
int main(int argc, char **argv)
{
//ROS节点初始化,节点名:image_publisher
ros::init(argc, argv, "image_publisher");
//创建一个节点句柄,方便节点资源使用和管理
ros::NodeHandle nh;
//图像发送接口句柄,用于管理图像接口资源
image_transport::ImageTransport it(nh);
//创建一个图像信息发布器,发布名为“camera/image”的topic,消息类型为sensor_msgs::ImagePtr
//发布图像消息队列的长度只能是1
image_transport::Publisher pub = it.advertise("camera/image", 1);
//获取摄像机接口
VideoCapture cam("/dev/video0"); //启动相机的端口号,一般默认相机为video0,可以通过`ls /dev/video*`来查看
//检测接口是否打开
if (!cam.isOpened())
{
exit(0);
}
printf("摄像头开启正常\n");
//声明图像帧
Mat srcframe;
//设置循环频率
ros::Rate loop_rate(10); //单位为Hz,10Hz不会让人察觉图像的停顿
while (nh.ok())
{
//从摄像头读取图像
cam >> srcframe;
//将OpenCV的Mat转变为sensor_msgs
sensor_msgs::ImagePtr msg = cv_bridge::CvImage(std_msgs::Header(), "bgr8", srcframe).toImageMsg();
//发布封装完毕的消息
pub.publish(msg);
//循环等待回调函数
ros::spinOnce();
//按照循环频率延时
loop_rate.sleep();
printf("发布信息\n");
}
}
4.图像订阅节点:cam_test_subscriber.cpp
//如标题表明的那样,该节点只是为了测试图像的传递功能
//cam_test_subscriber.cpp:位于功能包的src文件夹下
//订阅图像话题,并显示图像
#include <ros/ros.h> //ros.h包含大部分通用的ROS头文件
#include <opencv2/highgui.hpp> //opencvGUI组件
#include <opencv2/opencv.hpp> //opencv.hppp包含大部分通用的OpenCV头文件
#include <image_transport/image_transport.h> //image_transport实现图像传输
#include <cv_bridge/cv_bridge.h> //cv_bridge提供ROS对OpenCV格式的接口功能
using namespace cv;
using namespace std;
//接收到订阅的消息后,会进入消息回调函数
void imageCallback(const sensor_msgs::ImageConstPtr &msg)
{
try
{
//显示图像
imshow("view", cv_bridge::toCvShare(msg, "bgr8")->image);
//等待30ms
waitKey(5);
}
catch (cv_bridge::Exception &e)
{
ROS_ERROR("Could not convert from '%s' to 'bgr8'.", msg->encoding.c_str());
}
}
int main(int argc, char **argv)
{
//初始化ROS节点,节点名:image_listener
ros::init(argc, argv, "image_shower");
//创建节点句柄
ros::NodeHandle nh;
//定义窗口
namedWindow("view", CV_WINDOW_NORMAL);
//图像消息句柄
image_transport::ImageTransport it(nh);
//创建一个Subscribe, 订阅名为“camera/image”的画图,注册回调函数imageCallback
image_transport::Subscriber sub = it.subscribe("camera/image", 1, imageCallback);
//循环等待回调函数
ros::spin();
// 销毁显示窗口
cv::destroyWindow("view");
}
5. 我所遇到的那些别人也同样遇到过的奇葩们
- cv::startWindowThread()函数带来麻烦
对于cv::startWindowThread()
函数,官方文档并没有给出合理的解释,而且据Github上面的issues提示,该函数将会被移除,该函数的功能目前可以总结为“及时刷新窗口”,且该函数的优先级高于cv::waitKey()
。
常见用法如下:
cv::namedWindow("view");
cv::startWindowThread();
cv::imshow("view", img);
cv::destroyWindow("view");
在cv::imshow
之后无需搭配cv::waitKey(10)
来刷新图像。
在ROS的图像传递过程中,禁止将cv::startWindowThread()
和cv::waitKey(10)
混合使用,否则会出现线程锁报错。
建议用法:
在摄像头或者视频流数据的读取过程中,如果使用了函数cv::startWindowThread()
请勿使用cv::waitKey()
,如果是常见的图片显示的话,无需使用cv::startWindowThread()
函数。由于该函数将在不久移除,建议采用传统方式显示图像,可能在刷新速度上要慢一点,但是足够满足开发需求,即:
cv::namedWindow("view", CV_WINDOW_NORMAL);
cv::imshow("view", img);
cv::waitKey(10);
cv::destroyWindow("view");
- 在
sudo
权限下开启ROS节点
- 在
该问题是使用的相机平台导致的,在此将它记录下来。产生的原因是该相机平台涉及到GPIO总线的调用,而GPIO总线的调用需要在sudo
命令的超级权限下才能开启,我这里采用了一个比较简单,但是不怎么方便的方法,好在问题解决了,特此记录:
# 进入ROS功能包的/devel/lib/package/路径下
# 对相关节点产生的可执行文件执行下述三条指令,添加权限即可
chown root:root my_node
chmod a+rx my_node
chmod u+s my_node
- CmakeLists.txt文件中,有一个坑
我使用的OpenCV是自己编译的版本,所以需要这一步:
SET(OpenCV_DIR /usr/local/share/OpenCV)
可以直接注释掉,find_package(OpenCV REQUIRED)
会自动检索系统中ROS自带的OpenCV版本,如果实在不知道如何实现,可以参考官方项目包,祝你好运
- 相机的启动端口号和图像节点的发布频率设置
详情请见程序注释
网友评论