You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于OpenCV复现Google Cartographer内部坐标系地图绘制

用OpenCV替代Cairo复现Cartographer子图坐标转换与地图重建

背景与坐标实体说明

我在研究Google Cartographer SLAM技术时,处理无文档说明的库API子图模块,遇到以下核心坐标实体:

  • 轨迹:关联子图的传感器位姿序列
  • 全局位姿:子图在全局坐标系中的位姿(单位:米)
  • 局部位姿:子图内部的局部位姿
  • 切片位姿:2D子图在原始地图中的局部像素位姿

原Cairo实现逻辑

Cartographer原本用Cairo实现子图绘制,核心代码如下:

void CairoPaintSubmapSlices(
    const double scale,
    const std::map<::cartographer::mapping::SubmapId, SubmapSlice>& submaps,
    cairo_t* cr, std::function<void(const SubmapSlice&)> draw_callback) {
  cairo_scale(cr, scale, scale);

  for (auto& pair : submaps) {
    const auto& submap_slice = pair.second;
    if (submap_slice.surface == nullptr) {
      return;
    }
    const Eigen::Matrix4d homo =
        ToEigen(submap_slice.pose * submap_slice.slice_pose).matrix();

    cairo_save(cr);
    cairo_matrix_t matrix;
    cairo_matrix_init(&matrix, homo(1, 0), homo(0, 0), -homo(1, 1), -homo(0, 1),
                      homo(0, 3), -homo(1, 3));
    cairo_transform(cr, &matrix);

    const double submap_resolution = submap_slice.resolution;
    cairo_scale(cr, submap_resolution, submap_resolution);

    // 调用回调在全局坐标系中处理切片数据(如计算边界框、绘制切片)
    draw_callback(submap_slice);
    cairo_restore(cr);
  }
}

其中ToEigen函数负责将Cartographer的3D刚体姿态(四元数表示)转换为Eigen齐次矩阵:

Eigen::Affine3d ToEigen(const ::cartographer::transform::Rigid3d& rigid3) {
  return Eigen::Translation3d(rigid3.translation()) * rigid3.rotation();
}

核心逻辑是:将子图的全局位姿与切片位姿复合,转换为齐次变换矩阵,再适配Cairo的坐标系做轴映射,最后缩放子图分辨率完成绘制。

OpenCV替代实现步骤

OpenCV的坐标系为X向右、Y向下,与Cartographer的X向前、Y向左坐标系存在轴映射差异,需对应复现变换逻辑:

1. 核心变换矩阵推导

原Cairo的变换本质是将Cartographer的3D姿态投影到2D平面,并适配画布坐标系。我们需要将Eigen齐次矩阵转换为OpenCV的2D仿射变换矩阵(cv::Mat类型,2x3):

  • Cartographer的2D平面变换:X向前,Y向左
  • OpenCV的画布变换:X向右,Y向下
  • 对应轴映射:OpenCV的X = Cartographer的Y,OpenCV的Y = -Cartographer的X

基于此,从Eigen的4x4齐次矩阵homo提取2D变换参数,构造OpenCV的仿射矩阵:

// 从复合姿态的齐次矩阵中提取2D变换参数
const Eigen::Matrix3d homo_2d = homo.block<3,3>(0,0); // 取前3x3子矩阵(忽略Z轴)

// 构造OpenCV的2x3仿射变换矩阵
cv::Mat cv_transform = cv::Mat::eye(2, 3, CV_64F);
// 旋转缩放部分:适配轴映射
cv_transform.at<double>(0, 0) = homo_2d(1, 1);
cv_transform.at<double>(0, 1) = -homo_2d(1, 0);
cv_transform.at<double>(1, 0) = homo_2d(0, 1);
cv_transform.at<double>(1, 1) = -homo_2d(0, 0);
// 平移部分:适配轴映射与缩放
cv_transform.at<double>(0, 2) = homo_2d(0, 2) * scale;
cv_transform.at<double>(1, 2) = -homo_2d(1, 2) * scale;

2. 子图分辨率缩放

原代码中最后对Cairo画布缩放submap_resolution(子图分辨率,单位:米/像素),对应到OpenCV中,我们需要在变换矩阵中整合分辨率缩放:

// 将分辨率缩放整合到变换矩阵
cv_transform.at<double>(0, 0) *= submap_resolution;
cv_transform.at<double>(0, 1) *= submap_resolution;
cv_transform.at<double>(1, 0) *= submap_resolution;
cv_transform.at<double>(1, 1) *= submap_resolution;

3. 绘制子图到全局画布

使用OpenCV的cv::warpAffine将子图切片绘制到全局地图画布上,需要先计算子图在全局画布中的边界,确保画布足够大:

// 假设全局地图画布为global_map,子图切片为submap_image(CV_8UC1或CV_8UC3)
cv::Size global_size = global_map.size();
// 执行仿射变换,将子图绘制到全局画布
cv::warpAffine(submap_image, global_map, cv_transform, global_size,
               cv::INTER_LINEAR, cv::BORDER_TRANSPARENT);

完整替代函数示例

void OpenCVPaintSubmapSlices(
    const double scale,
    const std::map<::cartographer::mapping::SubmapId, SubmapSlice>& submaps,
    cv::Mat& global_map, std::function<void(const SubmapSlice&, cv::Mat&)> draw_callback) {
  // 全局画布初始化(按需设置尺寸,这里假设已预先初始化)
  if (global_map.empty()) {
    // 可根据子图的全局边界计算合适的画布尺寸
    global_map = cv::Mat(cv::Size(10000, 10000), CV_8UC1, cv::Scalar(0));
  }

  for (const auto& pair : submaps) {
    const auto& submap_slice = pair.second;
    if (submap_slice.surface == nullptr) {
      continue; // 跳过无效子图
    }

    // 将Cairo Surface转换为OpenCV Mat(需根据实际Surface格式调整转换逻辑)
    cv::Mat submap_image;
    // 示例转换逻辑(以RGBA格式Surface为例)
    unsigned char* data = cairo_image_surface_get_data(submap_slice.surface);
    int width = cairo_image_surface_get_width(submap_slice.surface);
    int height = cairo_image_surface_get_height(submap_slice.surface);
    submap_image = cv::Mat(height, width, CV_8UC4, data).clone();
    // 若为灰度图可转成单通道:cv::cvtColor(submap_image, submap_image, cv::COLOR_RGBA2GRAY);

    // 计算复合姿态的齐次矩阵
    const Eigen::Matrix4d homo = ToEigen(submap_slice.pose * submap_slice.slice_pose).matrix();
    const Eigen::Matrix3d homo_2d = homo.block<3,3>(0,0);

    // 构造OpenCV仿射变换矩阵
    cv::Mat cv_transform = cv::Mat::eye(2, 3, CV_64F);
    // 轴映射与旋转缩放
    cv_transform.at<double>(0, 0) = homo_2d(1, 1);
    cv_transform.at<double>(0, 1) = -homo_2d(1, 0);
    cv_transform.at<double>(1, 0) = homo_2d(0, 1);
    cv_transform.at<double>(1, 1) = -homo_2d(0, 0);
    // 平移与全局缩放
    cv_transform.at<double>(0, 2) = homo_2d(0, 2) * scale;
    cv_transform.at<double>(1, 2) = -homo_2d(1, 2) * scale;
    // 子图分辨率缩放
    const double submap_resolution = submap_slice.resolution;
    cv_transform.colRange(0,2) *= submap_resolution;

    // 执行变换绘制子图
    cv::warpAffine(submap_image, global_map, cv_transform, global_map.size(),
                   cv::INTER_LINEAR, cv::BORDER_TRANSPARENT);

    // 调用自定义回调处理子图(如标记边界、添加额外信息)
    draw_callback(submap_slice, global_map);
  }
}

关键注意事项

  • 坐标系映射:必须处理Cartographer与OpenCV的轴方向差异,否则子图会出现旋转或翻转错误
  • 数据格式转换:需将Cairo的Surface正确转换为OpenCV的cv::Mat,注意通道数(RGBA转灰度或RGB)
  • 画布尺寸:全局画布需足够大以容纳所有子图,可通过遍历所有子图的全局位姿计算边界后初始化

内容的提问来源于stack exchange,提问作者Andrei Vukolov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 10:45:47