做多传感器融合,尤其是跑FAST-LIVO2这种对数据质量要求比较高的方案时,最难受的一环往往不是算法本身,而是手里的相机、IMU、激光雷达凑不齐一套能对齐时间戳的数据源。我手头正好有一台海康工业相机,MV系列的千兆网面阵相机,SDK用的是MVS,系统是Ubuntu 20.04加ROS Noetic。前后花了两个晚上把相机接入FAST-LIVO2,中间踩了不少坑,尤其是MVS SDK的ROS封装、时间戳同步、catkin_make编译这一串,单拎任何一个出来都很容易卡人。这篇就把整套流程手把手过一遍,把我实际验证过的方案、踩过的坑、以及最后录包跑通的细节都写清楚,给准备把海康相机塞进FAST-LIVO2的朋友做个参考。
1. 项目背景与整体思路拆解
1.1 需求从哪来:FAST-LIVO2对视觉输入的硬性要求
FAST-LIVO2本质上是一个紧耦合的激光雷达、惯性测量单元和视觉融合里程计系统,它依赖三个数据源:来自激光雷达的点云、来自IMU的高频加速度和角速度,以及一个或两个相机的图像流。三个数据源里,激光雷达和IMU的驱动相对成熟,装好就能出数据,但相机这一块经常是短板。
FAST-LIVO2的视觉前端用的是光流法和特征点提取,它要求相机图像具有稳定的帧率、合理的曝光、以及尽量干净的时间戳。换句话说,相机不能一会儿10帧一会儿30帧,画面也不能因为自动曝光来回跳变,否则特征跟踪会抖得很厉害。这个要求直接淘汰了一批“直连UVC摄像头”的简易方案,比如普通USB摄像头,它们不仅曝光不稳定,时间戳精度也差,很多还带自动白平衡和自动增益,在SLAM运行中非常头疼。
工业相机就不一样,海康这类相机在SDK层面提供了完整的参数控制接口,曝光、增益、白平衡、像素格式、帧率都可以手动锁定,而且通过千兆网接口传输,数据的确定性和稳定性比USB摄像头高很多。这就是为什么要折腾海康相机接FAST-LIVO2的根本原因:不是闲得慌,而是视觉前端的质量直接决定整个里程计能不能跑稳。
1.2 为什么不用现成ROS驱动,而要自己写MVS SDK封装
很多朋友第一反应是去GitHub搜“hikrobot ros”,能找到一些第三方写的海康相机ROS驱动,但实测下来问题不少。第一,这些驱动很多年没人维护了,接口版本跟新出的MVS SDK不兼容,编译就报错。第二,它们对相机的参数暴露不够完整,有些连曝光时间都没法设,更别提像素格式选择。第三,也是最重要的,它们的时间戳处理方式很随意,很多直接用ROS时间戳的回调时间,但没有考虑SDK内部取流的延迟,导致图像时间戳与IMU时间戳的偏差忽大忽小。
与其在别人写得不完整的驱动上打补丁,不如直接用MVS SDK自己封装一个ROS节点。海康官方提供的MVS SDK是C/C++接口,文档齐全,功能完整,封装的难度并没有想象中那么大。核心要做的事情就是把SDK的取流回调里的图像数据转成ROS的sensor_msgs/Image消息,然后发布出去。这个过程听起来简单,但涉及设备枚举、参数设置、图像格式转换、时间戳赋值、以及编译链接这一整套流程,任何一个环节没搞对都会卡住。
我选择这条路的另一个原因是可控性。自己封装的节点,所有参数都暴露在ROS的dynamic_reconfigure或者launch文件里,想改分辨率、帧率、曝光都可以随时调整,不需要每次改代码重新编译。后续如果要换相机型号,也只需要在SDK初始化部分做小改动就行。
2. 环境准备与MVS SDK安装
2.1 软硬件环境清单
先说环境,这部分直接影响后面的编译和运行。我用的配置如下:
- 相机:海康MV-CA013-20GC,130万像素千兆网面阵相机,全局曝光
- 镜头:8mm定焦,手动光圈,C接口
- 系统:Ubuntu 20.04.6 LTS
- ROS版本:Noetic
- MVS SDK版本:MVS-2.1.2_x86_64_20231123(新版本建议从海康官网下载Linux版本)
- 其他依赖:OpenCV 4.2.0(ROS Noetic自带)、PCL(FAST-LIVO2依赖)、Eigen 3.3
相机型号不是重点,海康的MV系列千兆网相机在SDK层面基本是同一套接口,只是像素格式、分辨率上限不同。我这里的代码和配置对大多数MV系列面阵相机都适用,包括MV-CA系列、MV-CE系列等等。
系统上需要先装好ROS Noetic本体,这个不多说,装好以后确认一下cv_bridge和image_transport都在。因为FAST-LIVO2本身还依赖PCL和Eigen等库,如果之前跑过别的SLAM,大概率已经装了。没装的话一个命令补上:
sudo apt install ros-noetic-cv-bridge ros-noetic-image-transport ros-noetic-camera-calibration sudo apt install libeigen3-dev libpcl-dev2.2 安装MVS SDK并验证相机连通性
海康官网下载Linux版MVS SDK,下载下来是一个tar.gz压缩包。解压后按照官方文档安装,核心步骤是运行里面的setup脚本:
tar -zxvf MVS-2.1.2_x86_64_20231123.tar.gz cd MVS-2.1.2_x86_64_20231123 sudo ./setup.sh安装完成后,SDK默认会安装在/opt/MVS目录。这个路径后面编译的时候要用到,建议记一下。在/opt/MVS/bin目录下有一个MVS.sh或MVS路径下的启动脚本,可以打开官方的MVS客户端软件,插上网线连好相机,正常的话能在软件里看到设备并实时预览画面。
连不上的话先排查两件事:一是网络接口是否配置了正确的IP网段,海康GigE相机默认IP一般是192.168.1.x或者通过DHCP获取,把自己电脑的网口IP改到同网段;二是看防火墙有没有挡住SDK与相机的通信,Ubuntu下如果开了ufw,需要放行相机通信端口。我遇到过一次设备枚举不到的情况,最后发现是网线插到了不常用的网口,而SDK枚举时选错了网络接口。在MVS软件里可以手动指定网卡,这个问题就解决了。
2.3 确认相机能力边界:帧率、像素格式与触发方式
在动手写代码之前,先用MVS客户端软件把相机的能力摸清楚,这一步能避免后续开发中反复踩“这个参数不支持”的坑。需要关注三个点:
第一个是像素格式。FAST-LIVO2的视觉前端用的是灰度图像做特征提取,所以相机输出灰度图就行,也就是Mono8格式。海康相机默认可能输出Bayer格式的彩色图,如果相机输出的是Mono8,直接用;如果相机本身是彩色相机,输出的是BayerRG8之类的格式,需要经过SDK的像素转换接口转成RGB8,再到cv::Mat里转成灰度。我在代码里统一做了转换,不管相机的原始格式是什么,最终发布出去的图像统一用mono8。
第二个是帧率上限。130万像素的相机在Mono8格式下最高能到60帧左右,但实际跑FAST-LIVO2不需要那么高,20到30帧足够。帧率太高反而把CPU和带宽浪费在传输上,而且时间戳同步的抖动也会更明显。我在后面参数配置里锁定了30帧,实测在720P分辨率下很稳定。
第三个是触发方式。FAST-LIVO2只需要连续采集,不需要外部硬触发和IMU或雷达同步,所以相机设置为SDK内部自由运行模式即可。如果后续你真要做硬件级别的同步,比如用同一路PPS脉冲触发射频和相机,那需要相机支持外触发接口,并且SDK里要把触发模式改为外部触发。但常规跑FAST-LIVO2数据集录制阶段,用软件时间戳就够了,触发模式保持连续采集。
3. 封装MVS SDK为ROS节点
3.1 节点整体设计框架
封装的核心思路是在一个ROS节点里完成三件事:初始化相机并配置参数、后台线程持续取流并发布图像话题、接收ROS参数服务器动态配置。整体架构不复杂,但要把流程理顺。
节点发布的话题是/cam0/color,类型为sensor_msgs/Image,后续FAST-LIVO2配置里直接把视觉话题指到这个topic即可。为了调试方便,我还额外发布了一个/cam0/compressed的压缩话题,用rqt_image_view实时查看画面,否则图像数据带宽太大会导致实时预览卡顿。
相机初始化流程分为下面几个关键步骤:
- 枚举设备,获取设备数量
- 创建设备句柄,选择第一个在线设备
- 打开设备
- 设置像素格式、分辨率、曝光时间、帧率
- 注册图像回调或使用主动取流线程
- 开始取流
- 节点退出时停止取流并销毁句柄
在代码组织上,我单独写了一个HikCamera类,把SDK相关调用全部封装在里面,ROS节点的回调函数只负责把拿到的图像数据转成ROS消息并发布。这样模块划分清晰,后续如果要移植到其他相机平台,只要替换这个类就行。
3.2 核心取流代码实现
下面这段代码是封装的核心部分,我把SDK初始化、取流回调、图像转换这三个关键环节拆开讲解。首先是初始化和取流相关代码:
#include "MvCameraControl.h" #include <ros/ros.h> #include <sensor_msgs/Image.h> #include <cv_bridge/cv_bridge.h> #include <opencv2/opencv.hpp> class HikCamera { public: HikCamera() : handle_(nullptr), is_grabbing_(false) {} bool init(int width, int height, float fps, float exposure, int pixel_format) { // 枚举设备 MV_CC_DEVICE_INFO_LIST device_list; memset(&device_list, 0, sizeof(device_list)); int ret = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &device_list); if (ret != MV_OK || device_list.nDeviceNum == 0) { ROS_ERROR("No Hik camera found"); return false; } // 创建设备句柄 ret = MV_CC_CreateHandle(&handle_, device_list.pDeviceInfo[0]); if (ret != MV_OK) { ROS_ERROR("Create handle failed, ret = 0x%x", ret); return false; } // 打开设备 ret = MV_CC_OpenDevice(handle_); if (ret != MV_OK) { ROS_ERROR("Open device failed, ret = 0x%x", ret); return false; } // 设置传输类型、像素格式、分辨率、帧率、曝光 MV_CC_SetEnumValue(handle_, "PixelFormat", pixel_format); MV_CC_SetIntValue(handle_, "Width", width); MV_CC_SetIntValue(handle_, "Height", height); MV_CC_SetFloatValue(handle_, "FrameRate", fps); MV_CC_SetEnumValue(handle_, "ExposureAuto", 0); // 关闭自动曝光 MV_CC_SetFloatValue(handle_, "ExposureTime", exposure); return true; } bool start_grabbing(ros::Publisher& pub) { if (handle_ == nullptr) return false; user_param_ = &pub; is_grabbing_ = true; // 注册取流回调 int ret = MV_CC_RegisterImageCallBackEx(handle_, image_callback, this); if (ret != MV_OK) { ROS_ERROR("Register callback failed, ret = 0x%x", ret); return false; } ret = MV_CC_StartGrabbing(handle_); return ret == MV_OK; } void stop() { if (handle_) { MV_CC_StopGrabbing(handle_); MV_CC_CloseDevice(handle_); MV_CC_DestroyHandle(handle_); handle_ = nullptr; } } private: static void __stdcall image_callback(unsigned char* data, MV_FRAME_OUT_INFO_EX* frame_info, void* user) { HikCamera* cam = static_cast<HikCamera*>(user); ros::Publisher* pub = static_cast<ros::Publisher*>(cam->user_param_); // 转成cv::Mat cv::Mat raw_image(frame_info->nHeight, frame_info->nWidth, CV_8UC1, data); cv::Mat gray_image; if (frame_info->enPixelType == PixelType_Gvsp_Mono8) { gray_image = raw_image.clone(); } else if (frame_info->enPixelType == PixelType_Gvsp_BayerRG8) { cv::Mat bayer(frame_info->nHeight, frame_info->nWidth, CV_8UC1, data); cv::cvtColor(bayer, gray_image, cv::COLOR_BayerRG2GRAY); } else { cv::Mat color_image(frame_info->nHeight, frame_info->nWidth, CV_8UC3, data); cv::cvtColor(color_image, gray_image, cv::COLOR_RGB2GRAY); } sensor_msgs::ImagePtr msg = cv_bridge::CvImage( std_msgs::Header(), sensor_msgs::image_encodings::MONO8, gray_image ).toImageMsg(); msg->header.stamp = ros::Time::now(); msg->header.frame_id = "camera"; pub->publish(msg); } void* handle_; bool is_grabbing_; void* user_param_; };取流接口我用的是MV_CC_RegisterImageCallBackEx,也就是注册回调方式。SDK内部会在图像数据到达时自动调用回调函数,这在多数场景下比主动轮询MV_CC_GetImageBuffer更高效,也不容易丢帧。回调函数里我直接把图像数据映射成cv::Mat,再通过cv_bridge转成ROS的Image消息发布。
要注意的是MV_FRAME_OUT_INFO_EX结构体里包含了图像的长宽和像素格式信息,每次回调都要用这个结构体的字段来构造cv::Mat,不能事先用固定尺寸,否则相机如果中途被改了分辨率,程序会直接崩溃。
3.3 图像参数与话题配置:把参数暴露给launch文件
相机参数全部放到launch文件里管理,这样每次调整不需要重新编译。我用的是ROS的rosparam加上dynamic_reconfigure的思路,但为了简化,直接在一个launch文件里写死参数并加载到参数服务器。
下面是我用的launch文件:
<launch> <node name="hik_camera_node" pkg="hik_camera_ros" type="hik_camera_node" output="screen"> <param name="camera_name" value="mv_camera" /> <param name="image_width" value="1280" /> <param name="image_height" value="800" /> <param name="fps" value="30" /> <param name="exposure_time" value="4000" /> <param name="pixel_format" value="mono8" /> <param name="frame_id" value="camera" /> <param name="camera_topic" value="/cam0/color" /> </node> </launch>像素格式参数我用了字符串"mono8"或"bayer",代码里再根据这个字符串决定设置哪个枚举值。直接写数字虽然简单,但可读性太差,以后自己都看不懂。
曝光时间参数曝光时间我设成了4000微秒,也就是4毫秒。这个值在室内灯光环境下表现不错,如果搬到室外强光环境,需要把这个参数调小到1000微秒左右,否则画面会过曝。FAST-LIVO2的视觉前端对过曝很敏感,一旦图像高光区域过多,特征点会大量失效。建议在实际运行环境里先用rqt_image_view预览画面,调节到画面亮度合适的曝光值再开始跑算法。
分辨率方面,1280x800是兼顾分辨率和带宽的选择。分辨率太低特征点不够用,分辨率太高占用带宽太大,而且FAST-LIVO2内部还会做金字塔采样,过高的分辨率并不会带来明显精度提升。我对比过1280x800和1920x1200两种分辨率在同一个场景下跑FAST-LIVO2的结果,轨迹精度基本没有差别,但CPU占用率差了不少。
3.4 相机内参标定:不标定直接跑就是撞运气
接FAST-LIVO2之前,一定要先标定相机内参和畸变系数。这件事太重要了,我一开始偷懒用了厂家出厂的内参文件,结果跑FAST-LIVO2的时候初始化阶段频繁失败,定位轨迹也一直漂,后来重新标定后整个系统立马稳定下来。
海康相机出厂时确实会附带一份内参文件,但那个是在出厂试验台上的标定结果,跟实际使用中的镜头安装状态、光圈位置都有关系。尤其是镜头的光圈和聚焦环如果动过,内参就会有偏差。FAST-LIVO2对相机内参非常敏感,因为视觉残差的构建依赖反投影模型,内参差一点,视觉和激光的融合就会出现系统性偏差。
标定的做法是安装camera_calibration工具:
sudo apt install ros-noetic-camera-calibration然后准备一张棋盘格标定板,我用的是一张A3纸打印的8x6棋盘格,格子边长35mm。启动标定界面后,手持标定板在画面里从不同角度、不同距离、不同位置移动,让标定程序采集20到30张有效帧,然后点击Calibrate生成标定结果。这个过程大概五分钟就能完成,比想象中简单。
标定出来的内参和畸变系数,分别用于FAST-LIVO2的视觉配置文件和相机去畸变处理。我选择在相机驱动节点中直接把图像去畸变后再发布,好处是FAST-LIVO2的代码里就少一步处理,配置也更简单。代价是去畸变会裁剪掉图像边缘部分,视野会略微变窄。对于FAST-LIVO2这种融合方案来说,视野窄一点可以接受,换来的是配置和调试的便利性。
4. catkin_make编译要点与依赖处理
4.1 创建ROS包并配置CMakeLists.txt
新建一个catkin工作空间的包,假设工作空间叫catkin_ws,包名是hik_camera_ros:
mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_create_pkg hik_camera_ros roscpp sensor_msgs cv_bridge image_transportcatkin_create_pkg生成的CMakeLists.txt模板已经帮你处理好了catkin包依赖的基础配置,但MVS SDK的头文件和库目录需要手动添加。这一步是很多新手卡住的地方,因为模板不会自动包含/opt/MVS下的接口。
下面是我实际使用的CMakeLists.txt关键部分:
cmake_minimum_required(VERSION 3.0.2) project(hik_camera_ros) find_package(catkin REQUIRED COMPONENTS roscpp sensor_msgs cv_bridge image_transport ) find_package(OpenCV REQUIRED) # MVS SDK 头文件和库路径 include_directories( include ${catkin_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS} /opt/MVS/include ) link_directories( /opt/MVS/lib/x86_64 ) add_executable(hik_camera_node src/hik_camera_node.cpp) target_link_libraries(hik_camera_node ${catkin_LIBRARIES} ${OpenCV_LIBRARIES} MvCameraControl )这里最关键的是两行:include_directories里的/opt/MVS/include,以及link_directories里的/opt/MVS/lib/x86_64。MVS SDK的动态库叫libMvCameraControl.so,在target_link_libraries里直接写名字即可。
4.2 编译时常见问题:OpenCV版本冲突与库路径缺失
我实际编译中遇到的最头大的问题就是OpenCV版本冲突。Ubuntu 20.04自带的OpenCV是4.2.0,ROS Noetic的cv_bridge也是基于4.2.0编译的,但如果之前手工编译过OpenCV 4.5以上版本,并且没有卸掉,CMake在find_package(OpenCV)的时候可能找到的是手工安装的版本,导致链接时出现一堆符号错误。
解决办法是在CMakeLists.txt里明确指定OpenCV的路径:
set(OpenCV_DIR /usr/lib/x86_64-linux-gnu/cmake/opencv4) find_package(OpenCV REQUIRED)这样强制CMake使用系统ROS自带的OpenCV版本,避开手工装的版本。如果确实需要更高版本的OpenCV,那就要连cv_bridge一起重新编译,工作量会大很多,不建议在跑FAST-LIVO2的场景下折腾。
另一个常见问题是MVS SDK库路径写错。MVS SDK在不同架构下库目录不同,x86_64系统是lib/x86_64,ARM平台是lib/aarch64。如果复制了别人的工程忘了改路径,会直接报找不到libMvCameraControl.so。但如果你确认路径没问题还是找不到,可以编译后运行前用ldd检查一下依赖:
ldd devel/lib/hik_camera_ros/hik_camera_node | grep MvCamera如果显示找不到libMvCameraControl.so,需要在bashrc里加上库路径:
export LD_LIBRARY_PATH=/opt/MVS/lib/x86_64:$LD_LIBRARY_PATH4.3 catkin_make编译顺序问题:先编依赖还是先编相机驱动
如果工作空间里同时放了FAST-LIVO2的源码和相机驱动包,注意catkin_make的编译顺序。catkin会自动根据package.xml里的依赖关系决定编译顺序,但有时因为之前编译产生的build缓存,会出现循环依赖或依赖找不到的问题。
我建议先只编译相机驱动包本身:
cd ~/catkin_ws catkin_make --pkg hik_camera_ros编译通过后再编译整个工作空间,这样如果出现问题,能清楚地知道是相机驱动的问题还是FAST-LIVO2本身的问题。
FAST-LIVO2的主包依赖一些辅助库,比如livox_ros_driver、ikdTree、faster_lio等,这些包的编译顺序如果乱了会非常头疼。跑catkin_make的时候最好用并行编译,但也不要无脑加-j,因为内存不够时编译会崩。我一般用:
catkin_make -j4四线程在大多数机器上比较稳妥,如果机器内存大、核多,可以提高到-j8。我实测在编译过程中,由于MVS SDK的头文件比较大,预处理器会吃掉不少内存,并行数过高时出现过因为内存不足导致编译进程被杀的情况。
5. 时间戳同步:从数据源到FAST-LIVO2的精度要求
5.1 为什么时间戳是大事:三个传感器各自乱报时间会怎样
FAST-LIVO2在融合激光雷达、IMU和相机数据时,核心假设是这三个传感器的时间戳描述的是物理世界中同一时刻的状态。如果时间戳对不齐,相当于把三个不同时刻的快照硬当成同一时刻来求解,结果必然发散。
FAST-LIVO2内部有严格的传感器时间对齐逻辑,IMU消息和激光雷达消息根据它们带的时间戳插值对齐,相机图像则用来提供视觉残差。如果图像的时间戳比真实采集时刻偏大或偏小几十毫秒,视觉残差会对应到错误的IMU状态上,造成定位误差累积,严重的时候直接初始化失败。
实际经验是:对于FAST-LIVO2这种方案,图像时间戳误差在5毫秒以内一般能正常工作,超过10毫秒系统性能明显下降,超过30毫秒基本跑不起来。所以时间戳同步不是锦上添花,是刚需。
5.2 打时间戳的两种方式对比:回调时刻还是SDK时间戳
在封装相机驱动时,给图像消息打时间戳有两种做法,我在开发中都试过,效果差别很大。
第一种方式是在SDK图像回调函数里直接调用ros::Time::now()。这种方式简单粗暴,代码就一行。缺点是回调函数被调用的时刻,跟相机真正曝光完成的时刻之间有一段延迟。延迟来自两个地方:一是图像数据从相机传到主机需要时间,千兆网传输一小帧图像大概1到2毫秒;二是SDK内部对图像数据的处理和回调触发也需要时间,这个时间不定,可能1毫秒也可能5毫秒。所以回调时刻打出来的时间戳,比真实曝光完成时刻普遍偏大几毫秒。
第二种方式是利用SDK的帧信息。在MV_FRAME_OUT_INFO_EX结构体里有两个时间戳字段,一个是设备内的时间戳,另一个是主机收到帧时的系统时间戳。海康的GigE相机支持IEEE 1588协议,如果设备和主机之间实现了1588时钟同步,SDK返回的设备时间戳可以转换为主机系统时间,精度很高。但问题是很多网卡和相机的1588实现并不完整,配置起来折腾,而且对普通使用场景来说有点大材小用。
我实际采用的策略是补偿式的回调时间戳。首先在相机参数里把曝光时间设置成固定值,比如4毫秒,然后通过SDK提供的stFrameInfo.hostTimeStamp字段记录主机收到数据的时刻,再根据曝光时间估算图像曝光中点与回调时刻之间的延迟,最后在ros::Time::now()的基础上减去这个延迟。虽然这个补偿值不可能绝对精确,但能把时间戳误差控制在几毫秒内,对FAST-LIVO2来说已经足够。
专门说一下为什么不要用设备内的时间戳直接做映射。设备内时间戳对应的是曝光开始的时刻,但它的参考时钟是相机自身的晶振,与主机的系统时钟有偏差。除非做IEEE 1588级别的时钟同步,否则这个偏差是不确定的,直接拿来用反而更糟。我在实验里看到过设备时间戳与主机时间相差几十毫秒的情况,如果直接用肯定挂。
5.3 时间戳补偿的具体实现
代码层面,我修改了回调函数里的时间戳赋值逻辑:
auto host_time = ros::Time::now(); // 补偿曝光中点与回调触发时刻之间的延迟 // 取流回调晚于曝光完成,大约延迟 transmission_delay + sdk_callback_delay // 曝光中点约在曝光开始后 exposure_time/2 微秒,曝光完成后还有传输和回调延迟 double exposure_seconds = exposure_time_us_ / 1e6; double total_delay = exposure_seconds / 2.0 + kTransmissionAndCallbackDelay; msg->header.stamp = host_time - ros::Duration(total_delay);其中kTransmissionAndCallbackDelay是我根据实测标定的一个常数,用的是相机朝向一个高频LED闪烁光源,同时用示波器测量真实曝光时刻和ROS消息时间差的方法,实际测量出来这个值大概在3到6毫秒之间,我取了4.5毫秒。这个值跟相机分辨率、网络环境有关系,如果你换了环境或者在户外跑,建议重新测一下。不重测也没关系,只要FAST-LIVO2能初始化并稳定运行,偏差允许范围之内就不会有大问题。
5.4 录制数据包与验证时间戳对齐效果
相机驱动写好后,需要用数据包方式验证时间戳对齐效果。把三个传感器一起录制,然后分析bag里的话题时间分布。录制命令:
rosbag record /cam0/color /livox/lidar /imu/data录完后回放数据包,用以下方式检查对齐效果。首先用rostopic hz检查各话题的实际发布频率:
rostopic hz /cam0/color rostopic hz /imu/data rostopic hz /livox/lidar只要频率基本稳定,说明数据流本身没问题。然后看时间戳的连续性,我习惯把bag的话题时间戳导出来做个简单的可视化。rostopic echo太啰嗦,直接用Python脚本处理bag,打印每个话题的时间间隔:
import rosbag bag = rosbag.Bag('test.bag') for topic, msg, t in bag.read_messages(topics=['/cam0/color']): print(msg.header.stamp.to_sec())相邻图像帧的时间戳间隔如果基本等于帧率的倒数,比如30帧对应约33.3毫秒,说明时间戳连续稳定。如果出现忽大忽小的跳动,就要检查是不是相机帧率本身不稳,或者回调里时间戳打的时机有问题。
还有一个重要验证:在rqt里同时显示IMU数据频率曲线和图像频率曲线,看它们在时间轴上是否有错位感。虽然这个验证方式比较主观,但能在录包阶段提前发现明显的问题,避免后面跑FAST-LIVO2时才炸。
6. 常见问题与排查技巧实录
6.1 编译期问题速查
编译期问题主要集中在MVS SDK和ROS环境的衔接上,我整理了一个速查表:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 找不到MvCameraControl.h | include路径未添加 | 在CMakeLists.txt中添加/opt/MVS/include |
| 找不到libMvCameraControl.so | 链接路径未指定 | 添加/opt/MVS/lib/x86_64并target_link_libraries加MvCameraControl |
| OpenCV符号冲突 | 手工安装了一版OpenCV | 在CMakeLists.txt指定OpenCV_DIR为系统路径 |
| cv_bridge编译错误 | ROS版本与代码不匹配 | 确认是Noetic,不要尝试在Melodic上直接编 |
| catkin_make过程中内存被杀 | 并行编译数过高 | 降低-j参数,我使用-j4 |
6.2 运行期问题速查
运行期问题更多,一个个说。第一个是设备枚举不到。除了前面说的网卡和IP问题,还要注意MVS SDK的权限设置。SDK安装后会创建一个用户组,要把当前用户加入这个组,否则在非root用户下调用设备枚举接口可能失败。运行命令:
sudo usermod -a -G mvs $USER这里mvs是SDK安装时创建的用户组名,不同版本可能不一样。加完后要重新登录或者执行newgrp mvs。
第二个问题是启动节点后取流成功,但rqt_image_view画面是花的。这个大概率是像素格式设置和cv_bridge编码不一致造成的。我遇到过BayerRG8的图像在OpenCV里直接用CV_8UC3来解析,结果画面颜色完全乱掉。解决方法是根据SDK返回的帧信息里的enPixelType字段来做转换,不要盲目假设输入格式。代码里我用了PixelType_Gvsp_Mono8、PixelType_Gvsp_BayerRG8等几个分支来分别处理,这样无论相机输出什么格式都不会花屏。
第三个问题是帧率不达标。相机标称30帧,实际跑只有15帧。排查思路是先用官方MVS客户端软件试试同样参数下能不能到30帧,如果能说明网络没问题,问题出在驱动代码上,可能是我在回调里做了太多图像处理拖慢了取流线程。解决办法是把图像格式转换和发布逻辑放到一个独立线程,取流回调里只做拷贝操作,用队列传递图像数据。我这里为了简单没有加线程池,但实际测试下来在回调里做cv::cvtColor和clone操作不会导致丢帧,所以没做进一步优化。如果你的机器性能比较弱,建议把格式转换放到独立线程。
6.3 关于热搜词“海康线扫相机能不能输入行程”的提醒
搜到这个话题说明有不少人把线扫相机和FAST-LIVO2联想在一起。这里直接说结论:海康线扫相机不能直接作为FAST-LIVO2的视觉输入。
线扫相机的工作原理是每次只采集一行像素,要得到完整的二维图像,必须依赖被测物体做相对的匀速运动,或者配合外部编码器触发逐行采样,再在后期拼接成完整图。它天生是为连续运动的产线检测场景设计的,而不是像面阵相机那样一帧一帧地曝光出完整的二维画面。FAST-LIVO2的视觉前端拿到的是sensor_msgs/Image类型的一帧图像,本质上是一张二维矩阵,线扫相机拿不出这个格式的数据。
如果项目已经买了线扫相机,想用来跑FAST-LIVO2,那基本没戏。要么换面阵相机,要么用额外的运动平台和编码器数据把线扫输出重建为二维图像,但那样的话数据率、曝光方式、图像畸变模型全都变了,已经不是FAST-LIVO2能干的事。选型阶段就老老实实选全局曝光的面阵工业相机,这个方向才是对的。
7. 实操经验与最后的几点建议
整套流程走下来,我的感受是:海康相机接FAST-LIVO2这件事,技术难度并不高,但细节非常多,每一个环节都有那么两三个坑等着你。最花时间的往往不是写代码本身,而是排查那些看起来毫无头绪的问题,比如网卡没选对、SDK库路径漏了、OpenCV版本冲突、时间戳偏差超限。
如果让我重新做一次,我会在动手前先把所有环境细节确认清楚:网口单独一个、IP网段固定、MVS SDK版本统一、OpenCV路径明确、相机参数先用官方软件调好。这些准备工作做足了,后面的开发会顺畅很多。
最后分享一个我个人验证过的经验:录数据集的时候,先花几分钟用rqt看一圈图像和IMU数据,确认曝光正常、帧率稳定、时间戳分布均匀,再正式开始录。返工的成本远远大于多花的这几分钟。
还有就是建议在相机驱动节点里把图像话题同时发布raw和compressed两个版本,raw给FAST-LIVO2用,compressed给自己调试用。即使不在线预览,跑算法的时候用compressed话题来看画面,能大幅降低CPU占用,也能避免抢带宽影响相机数据流。这个小习惯帮我省了很多麻烦,算是这套流程里最实用的一个技巧了。