Skip to content

SDK运行与同步

Lin Yicheng edited this page Sep 25, 2025 · 40 revisions

1. 通信方式的选择:串口 or 网口

同步板支持串口和网口两种通信方式,实现

  1. 在SDK中获取当前系统的基准时间并且与同步板进行实时的软件时间对齐
  2. 实时获取IO脉冲触发的时刻,以及IMU的反馈数据。

其中,

  • Type-C的串口同时支持供电和通信,使用简单方法。但是受限于串口通信的波特率,软件同步精度较低,并且目前最高以30Hz频率实时获取IO触发时刻。
  • 网口不支持供电,需要用串口供电或者12V电源供电,但是通信速度高、时间同步精度高,最高测试100Hz的频率获取IO触发时刻。

在之前的文档中,考虑到测试的便捷性,我们全都默认使用串口进行同步。但是在硬件支持的情况下,我们强烈建议使用网口作为通信方式,获取最高的精度和运行效率

注意 !!!!:一旦同步板在硬件上接了网口,串口的功能将会被禁用,直到断电重启。因此如果同时接入网口和串口,只会使用网口,并且只能使用网口对应的SDK。

1.2 网口的配置方法

当网线插入同步板后,同步板重新上电后会自动使用网口传输信息. 如果同步板IP配置为192.168.1.188.则可以通过ping查看网口配置是否成功或者同步板是否在线.

 ping 192.168.1.188

用户可以使用nc指令查看上传的数据,Linux中nc命令是一个功能强大的网络工具,全称是netcatz.

sudo apt-get install netcat #下载

打开任意终端,并输入以下指令:

# 使用前修改主机IP为192.168.1.xxx。(同一网段内即可)
# 192.168.1.188 8888 表示同步板的IP地址和端口
echo “Hello InfiniteSense” | nc -u 192.168.1.188 8888 

image

2. SDK 下载安装

sudo apt-get install libzmq3-dev #默认版本4.3.4
git clone git@github.com:InfiniteSenseLab/SimpleSensorSync.git -b main
cd SimpleSync
mkdir build && cd build
cmake ..
make -j

如果git clone 命令报错,无法下载。可以直接在本项目 页面中的Code/Download ZIP 按键下载。

3. 使用示例

由于不同的传感器同步组合较多,我们提供一些常用的示例参考。满足如下传感器同步需求的可以直接跳转到对应部分,我们也会不断更新,以提供更多的解决方案示例。

使用示例

  1. 多个USB免驱相机与同步板的IMU构成VIO系统
  2. MID360和多个USB免驱相机同步
  3. MID360和多个USB免驱相机同步——FAST-LIVO 方式
  4. 多个工业相机与同步板的IMU构成VIO系统
  5. MID360和多个工业相机同步
  6. MID360和多个工业相机同步——FAST-LIVO 方式
  7. 多个工业相机和脉冲触发的外部IMU同步

TODO 8. 速腾雷达和多个工业相机同步 9. 速腾雷达和多个USB免驱相机同步

对于在使用示例之外的同步需求,都是能够满足的。这是由于我们的SDK是开源的、支持二次开发的,因此需要用户具有一定的开发能力。如果有需要可以联系对应的技术支持,我们也会在不断满足用户的需求的同时扩展我们的示例库。

同步示例

3.1 多个USB免驱相机与同步板的IMU构成VIO系统

如果使用ROS1操作系统,打开 example/VideoCam/main_ros.cpp 文件,若使用ROS2操作系统则打开 example/VideoCam/main_ros2.cpp文件。

修改文件中的配置参数部分如下

struct video_config {
    bool onboard_imu = true;// 使用内部IMU
    bool extern_imu = false;// 使用外部IMU,TODO 
    std::string imu_name = "imu_1";

    // 相机名称 + 设备ID
    std::pair<std::string, int> CAM3_Video = {"left_cam",  0}; // 0 代表设备为 /dev/video0 
    std::pair<std::string, int> CAM4_Video = {"right_cam", -1};
    std::pair<std::string, int> CAM1_Video = {"front_cam", -1};
    std::pair<std::string, int> CAM2_Video = {"rear_cam", -1}; // -1 代表没有

    // 通信方式
    int type = 0; // 0 --> 串口通信  1 --> 网口通信
    std::string uart_dev = "/dev/ttyACM0";
    std::string net_ip = "192.168.1.188";
    int net_port = 8888;
};

根据需要选择

  1. 通信方式和参数
  2. 连接的相机的位置,设备名称

修改之后,重新编译并运行SDK

# 设备权限(选择串口同步才需要)
sudo chmod 777 /dev/ttyACM*
# 进入编译目录
cd SimpleSensorSync/build
# 编译
make -j
# 运行ROS1 SDK 
./example/VideoCam/video_cam_ros
# 运行ROS2 SDK 
./example/VideoCam/video_cam_ros2

同步结果验证

3.2 MID360和多个USB免驱相机同步

首先参考,3.1 多个USB免驱相机与同步板的IMU构成VIO系统,完成相机的同步功能。

唯一不同的在于配置参数中将内部IMU禁用。 也就是bool onboard_imu = true;// 使用内部IMU 修改为 false。我们建议使用MID360内部的IMU传感器而不是板载的IMU。

运行SDK后,按照之前的教程验证雷达是否正确同步即可。

之后正常运行对应的雷达驱动即可。

这种同步方式对雷达的同步满足大多数的同步应用需求,其时间戳信号来源是一致的,但是并不是在同一时刻采集的。 而特殊的FAST-LIVO同步方式将在下面描述。

3.3 MID360和多个USB免驱相机同步——FAST-LIVO 方式

FAST-LIVO 的同步方式是特殊的,其要求时间戳信号来源是一致的,并且相机和雷达在同一个时刻采集的数据进行关联。因此,雷达和相机的时间戳会是一模一样的对应上的。

准备工作

  1. 正确接线并且设置的相机触发频率为10Hz,并且用上位机软件观察,确保相机正确触发
  2. 正确接线,运行同步板提供的SDK, 打开雷达上位机软件后,出现GPS同步的标识

雷达驱动运行

官方的Livox驱动不能直接使用,需要使用原作者修改之后的Livox驱动

SDK修改

如果使用ROS1操作系统,打开 example/VideoCam_LVI/main_ros.cpp 文件,若使用ROS2操作系统则打开 example/VideoCam2_LVI/main_ros2.cpp文件。

参考 3.2 MID360和多个USB免驱相机同步 修改配置参数。

重新编译SDK并运行

# 设备权限(选择串口同步才需要)
sudo chmod 777 /dev/ttyACM*
# 进入编译目录
cd SimpleSensorSync/build
# 编译
make -j
# 运行ROS1 SDK 
./example/VideoCam_LVI/video_cam_ros
# 运行ROS2 SDK 
./example/VideoCam_LVI/video_cam_ros2

同步结果验证

3.4 多个工业相机与同步板的IMU构成VIO系统

3.5 MID360和多个工业相机同步

3.6 MID360和多个工业相机同步——FAST-LIVO 方式

3.7 多个工业相机和脉冲触发的外部IMU同步

基础例程

运行精简版本Demo

cd build/examples/NetCam
./net_cam

image

内部话题监听节点(可选)

运行内部话题监听,可以帮助检查是否有数据接收,数据频率是否正常等问题,其中monitor也可显示SDK内部Topic的名称,帮助快速找到对应话题。

cd build/tools
./monitor

monitor

其中,cam_1_trigger (num: 20) 表示图像触发信号,cam_1 (num: 20) 表示图像帧数据。num表示一秒内接收的图像帧数,即频率为20Hz。

ROS2例程

创建空间并运行

# 创建工作空间目录
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
# 下载代码
git clone git@github.com:InfiniteSenseLab/SimpleSync.git
# 返回工作空间根目录
cd ~/ros2_ws
# 编译工作空间
colcon build
# 加载环境变量
source install/setup.bash
# 找到运行节点
cd install/infinite_sense/lib/net_cam
# 启动ROS2节点
./net_cam_ros2

image

打印原始 IMU 数据

ros2 topic echo /imu_1

查看姿态(基于 ICM42688P 航姿解算)

时间同步板搭载 ICM42688P 6轴 IMU,支持姿态解算,适用于非紧耦合方案应用,可在 rviz2 中查看。

  sudo apt-get install ros-${ROS_DISTRO}-imu-tools
  # 启动rivz2
  rviz2
  # rviz2 中订阅话题 /imu_1

传感器同步效果验证

在正确连接传感器以及正确运行SDK的状态下,传感器将会被正确同步,可以通过如下一些方法进行验证

1. 多相机之间的同步验证

方法1:

  1. 设置相机的频率为低频,例如1Hz, 然后多个相机平行放置
  2. 使用相机软件打开多路相机
  3. 在镜头前晃动物体,观察采集得到的多张图像中运动物体的位置是否一致。

Screenshot from 2025-06-22 13-10-45

方法2:

  1. 设置相机的频率为低频,例如1Hz, 然后多个相机平行放置
  2. 使用相机软件打开多路相机
  3. 用手机或者其他设备显示一个快速变化的时钟,观察多个相机拍到的时间是否是一致的

2. 雷达同步验证

  1. 连接雷达和同步板的线
  2. 运行SDK
  3. 打开雷达的软件或者上位机查看同步状态

这里以MID360为例,正确同步的状态如下

图片

3. 相机和雷达时间同步

这里必须要引入ROS等时间统计工具,这里以ROS1为例。

  1. 录制相机和雷达时间戳
rosbag record --use-header-time -O lidar_camera /相机topic /雷达topic  
  1. 查看时间戳
rqt_bag lidar_camera.bag
  1. 雷达和相机的时间是一致且对齐的

类似下图: image

自定义相机

为了更好的适配不同的相机,我们支持自定义相机,这里以opencv 读取 video 设备为例,解读自定义相机在ros2下的写法。

1. 准备工作

在 example 文件夹下新建 VideoCam 文件夹,包括文件

  • CMakeLists.txt
  • main_ros2.cpp
  • video_cam.h
  • video_cam.cpp

四个文件

往 example 文件夹下的CMakeLists.txt 文件添加对应路径

cmake_minimum_required(VERSION 3.10)
project(example)

add_subdirectory(NetCam)
add_subdirectory(VideoCam)
add_subdirectory(CustomCam) # 添加的路径

2. 参考示例完善代码

在video_cam.h 文件中包含代码如下,其中重载函数就是我们需要实现的部分

#pragma once
#include "infinite_sense.h"
#include <opencv2/opencv.hpp>
#include <atomic>
#include <thread>
#include <vector>
#include <mutex>
namespace infinite_sense {
// 继承自 Sensor 类,会自动调用初始化,开始,停止等函数 
class VideoCam final : public Sensor {
 public:
  ~VideoCam() override;

  bool Initialization() override; // 相机初始化函数
  void Stop() override; // 相机停止采集函数
  void Start() override;// 相机参数函数

 private:
  // 具体读取相机的实现
  void Receive(void* handle, const std::string&) override;
  // opencv 读取 video 设备的函数
  cv::VideoCapture cap_;
  // 是否初始化的标识
  std::atomic<bool> is_initialized_{false};

  // 读取图像设备的多线程,一个线程读取一个相机
  std::mutex cap_mutex_;
  std::vector<std::thread> cam_threads;
};
}  // namespace infinite_sense

在video_cam.cpp 文件中写入如下代码

#include "video_cam.h"
#include "infinite_sense.h"
namespace infinite_sense {

// 调用停止读取函数
VideoCam::~VideoCam() { Stop(); }

// 初始化摄像头
bool VideoCam::Initialization() {
  std::lock_guard<std::mutex> lock(cap_mutex_);
  if (!cap_.open(0)) {  // 打开默认摄像头
    // 打开失败报错
    std::cerr << "[OpenCVCam] Failed to open camera!" << std::endl;
    return false;
  }
  // 设置相机帧率,注意这里的帧率需要是 1. 摄像头支持的帧率 2. 尽量大于触发帧率,这样才能保证时间戳对上
  cap_.set(cv::CAP_PROP_FPS, 30);
  is_initialized_ = true;// 初始化标识
  std::cout << "[OpenCVCam] Camera initialized." << std::endl;
  return true;
}

// 停止读取函数 
void VideoCam::Stop() {
  is_running = false;// 初始化标识
  {
    std::lock_guard<std::mutex> lock(cap_mutex_);
    if (cap_.isOpened()) {
      cap_.release();// 释放摄像头设备
      std::cout << "[OpenCVCam] Camera released." << std::endl;
    }
  }
  std::this_thread::sleep_for(std::chrono::milliseconds(100));
  for (auto& t : cam_threads) {
    if (t.joinable()) {
      t.join();
    }
  }
  // 关闭线程
  cam_threads.clear();
  cam_threads.shrink_to_fit();
}

// 相机读取函数 
void VideoCam::Receive(void *handle, const std::string &name) {
  // 自定义的相机类,包含1. 图像数据 2. 时间戳 3. 相机名称 
  CamData cam_data;
  // 统一的消息管理类
  Messenger &messenger = Messenger::GetInstance();

  // 初始化完成,运行的状态中
  while (is_running) {
    // 1. 读取相机中的一帧图像
    cv::Mat frame_bgr;
    {
      std::lock_guard<std::mutex> lock(cap_mutex_);
      if (!cap_.isOpened() || !cap_.read(frame_bgr)) {
        std::cerr << "[OpenCVCam] Failed to read frame!" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
        continue;
      }
    }

 
    // 这里的time_stamp_us是相机触发时间,调用 GET_LAST_TRIGGER_STATUS(params[name], time)) 获取name 对应设备最新触发时间
    if (params.find(name) == params.end()) {
      LOG(ERROR) << "cam: " << name << " not found!";
    } else {
      if (uint64_t time; GET_LAST_TRIGGER_STATUS(params[name], time)) {
        cam_data.time_stamp_us = time;
      } else {
        LOG(ERROR) << "Trigger cam: " << name << " not found!";
      }
    }

    // 相机的名称
    cam_data.name = name;
    // 相机的数据,直接把原始数据发送
    cam_data.image = GMat(frame_bgr.rows, frame_bgr.cols,
                          GMatType<uint8_t, 3>::Type, frame_bgr.data).Clone();
    messenger.PubStruct(name, &cam_data, sizeof(cam_data));
    std::this_thread::sleep_for(std::chrono::milliseconds{2});
  }
}

// 开始读取函数
void VideoCam::Start() {
  if (!is_initialized_) {
    std::cerr << "[OpenCVCam] Start failed: not initialized." << std::endl;
    return;
  }
  // 运行标识位
  is_running = true;
  // 开启一个线程并告诉其相机名称
  std::string name = "video_cam";
  cam_threads.emplace_back(&VideoCam::Receive, this, nullptr, name);
  std::cout << "[OpenCVCam] Camera capture started." << std::endl;
}
}  // namespace infinite_sense

之后调用所提供的video_camera 类

#include "infinite_sense.h"
#include "video_cam.h"

#include "rclcpp/rclcpp.hpp"

#include <image_transport/image_transport.hpp>
#include <cv_bridge/cv_bridge.h>

#include <sensor_msgs/msg/imu.hpp>

// 继承自 rclcpp::Nod
class CamDriver final : public rclcpp::Node {
 public:
  CamDriver()
      : Node("ros2_cam_driver"), node_handle_(std::shared_ptr<CamDriver>(this, [](auto *) {})), transport_(node_handle_) {
    // 相机名称,注意和video_cam.cpp 中的相机名称保持一致
    std::string camera_name_1 = "video_cam";
    // IMU 设备名称,自定义即可
    const std::string imu_name = "imu_1";
    // 使用串口设备进行同步,这里可以选择不同的设备
    synchronizer_.SetUsbLink("/dev/ttyACM0", 921600);
    // synchronizer_.SetNetLink("192.168.1.188", 8888);
    // 构建 VideoCam 类
    const auto video_cam = std::make_shared<infinite_sense::VideoCam>();
    // 重要!!!注册相机名称video_cam, 并且绑定 CAM_1 接口,也就是 video_cam对应的设备必须要接到 CAM_1
    video_cam->SetParams({{camera_name_1, infinite_sense::CAM_1}
    });
    // 同步类设置添加相机类 
    synchronizer_.UseSensor(video_cam);

    // 逐个调用开始函数 
    synchronizer_.Start();

    // IMU 和 图像信息的发布函数 
    imu_pub_ = this->create_publisher<sensor_msgs::msg::Imu>(imu_name, 10);
    img1_pub_ = transport_.advertise(camera_name_1, 10);
    // SDK 获取IMU和图像信息之后的回调函数 
    {
      using namespace std::placeholders;
      infinite_sense::Messenger::GetInstance().SubStruct(imu_name, std::bind(&CamDriver::ImuCallback, this, _1, _2));
      infinite_sense::Messenger::GetInstance().SubStruct(camera_name_1,
                                                         std::bind(&CamDriver::ImageCallback1, this, _1, _2));
    }
  }

  // SDK 获取IMU信息后的回调函数 
  void ImuCallback(const void *msg, size_t) const {
    // 按照ROS2的格式发布的IMU数据
    const auto *imu_data = static_cast<const infinite_sense::ImuData *>(msg);
    sensor_msgs::msg::Imu imu_msg;
    // 得到的时间单位us,需要转换为ms
    imu_msg.header.stamp = rclcpp::Time(imu_data->time_stamp_us * 1000);
    imu_msg.header.frame_id = "map";
    imu_msg.linear_acceleration.x = imu_data->a[0];
    imu_msg.linear_acceleration.y = imu_data->a[1];
    imu_msg.linear_acceleration.z = imu_data->a[2];
    imu_msg.angular_velocity.x = imu_data->g[0];
    imu_msg.angular_velocity.y = imu_data->g[1];
    imu_msg.angular_velocity.z = imu_data->g[2];
    imu_msg.orientation.w = imu_data->q[0];
    imu_msg.orientation.x = imu_data->q[1];
    imu_msg.orientation.y = imu_data->q[2];
    imu_msg.orientation.z = imu_data->q[3];
    imu_pub_->publish(imu_msg);
  }

  // SDK 获取图像信息后的回调函数 
  void ImageCallback1(const void *msg, size_t) const {
    // 原始的图像数据
    const auto *cam_data = static_cast<const infinite_sense::CamData *>(msg);
    std_msgs::msg::Header header;
    // 得到的时间单位us,需要转换为ms
    header.stamp = rclcpp::Time(cam_data->time_stamp_us * 1000);
    header.frame_id = "map";
    // 构造opencv图像,这里是彩色图片因此是 CV_8UC3 和  bgr8
    const cv::Mat image_mat(cam_data->image.rows, cam_data->image.cols, CV_8UC3, cam_data->image.data);
    const sensor_msgs::msg::Image::SharedPtr image_msg = cv_bridge::CvImage(header, "bgr8", image_mat).toImageMsg();
    img1_pub_.publish(image_msg);
  }


 private:
  infinite_sense::Synchronizer synchronizer_;
  SharedPtr node_handle_;
  image_transport::ImageTransport transport_;
  image_transport::Publisher img1_pub_,img2_pub_;
  rclcpp::Publisher<sensor_msgs::msg::Imu>::SharedPtr imu_pub_;
};

// main 函数 
int main(const int argc, char *argv[]) {
  rclcpp::init(argc, argv);
  // 构造 CamDriver 类,用于读取相机、IMU等信息
  rclcpp::spin(std::make_shared<CamDriver>());
  return 0;
}

最后,整体的CMakeLists.txt文件如下

cmake_minimum_required(VERSION 3.16)

project(video_cam VERSION 1.0)
message(STATUS "System Processor: ${CMAKE_SYSTEM_PROCESSOR}")

find_package(Threads REQUIRED)
find_package(OpenCV QUIET)

if(OpenCV_FOUND)
message(STATUS "Found OpenCV version: ${OpenCV_VERSION}")
message(STATUS "OpenCV include dirs: ${OpenCV_INCLUDE_DIRS}")
message(STATUS "OpenCV libraries: ${OpenCV_LIBS}")

# **************************编译普通节点*****************************
add_executable(${PROJECT_NAME}
        main.cpp
        video_cam.cpp
        video_cam.h
)
target_link_directories(${PROJECT_NAME} PRIVATE )
target_link_libraries(${PROJECT_NAME} PRIVATE
        infinite_sense_core
        ${OpenCV_LIBS}
        Threads::Threads
)
target_include_directories(${PROJECT_NAME} PRIVATE ${OpenCV_INCLUDE_DIRS})
set_target_properties(${PROJECT_NAME} PROPERTIES INSTALL_RPATH "$ORIGIN")

# **************************编译ROS2节点*****************************
find_package(rclcpp QUIET)
if (NOT rclcpp_FOUND)
    message(STATUS "rclcpp not found. ROS2 demo will not be built")
else ()
    message(STATUS "rclcpp found. ROS2 demo will be built")
    find_package(ament_cmake REQUIRED)
    find_package(std_msgs REQUIRED)
    find_package(sensor_msgs REQUIRED)
    find_package(image_transport REQUIRED)
    find_package(OpenCV REQUIRED)
    find_package(cv_bridge REQUIRED)
    add_executable(${PROJECT_NAME}_ros2
            main_ros2.cpp
            video_cam.cpp
            video_cam.h
    )
    target_link_directories(${PROJECT_NAME}_ros2 PRIVATE ${OpenCV_LIBS})
    target_include_directories(${PROJECT_NAME}_ros2 PUBLIC ${OpenCV_INCLUDE_DIRS})
    target_link_libraries(${PROJECT_NAME}_ros2
            infinite_sense_core
            ${OpenCV_LIBRARIES}
    )
    set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
    ament_target_dependencies(${PROJECT_NAME}_ros2
            rclcpp
            std_msgs
            sensor_msgs
            image_transport
            OpenCV
            cv_bridge
    )
    install(TARGETS ${PROJECT_NAME}_ros2
            DESTINATION lib/${PROJECT_NAME}
    )
    install(TARGETS infinite_sense_core
            DESTINATION lib/${PROJECT_NAME}
    )
    ament_package()
endif ()

# **************************编译ROS1节点*****************************
find_package(catkin QUIET)
if (NOT catkin_FOUND)
    message(STATUS "catkin not found, ROS1 demo will not be built")
else ()
    message(STATUS "catkin found. ROS1 demo will be built")
    find_package(catkin REQUIRED COMPONENTS
            roscpp
            sensor_msgs
            cv_bridge
            image_transport
            geometry_msgs
    )
    file(GLOB_RECURSE SOURCE_FILES
            main_ros.cpp
            video_cam.cpp
            video_cam.h
    )
    add_executable(${PROJECT_NAME}_ros ${SOURCE_FILES})
    target_link_directories(${PROJECT_NAME}_ros PRIVATE ${OpenCV_LIBS})
    target_link_libraries(${PROJECT_NAME}_ros
            infinite_sense_core
            Threads::Threads
            ${catkin_LIBRARIES}
    )
    target_include_directories(${PROJECT_NAME}_ros PRIVATE
            ${catkin_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS}
    )
    target_compile_definitions(${PROJECT_NAME}_ros PUBLIC -DROS_PLATFORM)
endif ()
endif()

重新编译整个工程即可生成针对特定相机型号的可执行文件,执行改文件即可。

Clone this wiki locally