cartographer实时显示三维点云地图

2024-04-02 22:18

本文主要是介绍cartographer实时显示三维点云地图,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

cartographer不能实时显示三维点云地图, 这是大家公认的cartographer 3d建图的弊端.

这篇文章就带着大家将cartographer三维地图显示出来.

这部分的代码已经开源在
https://github.com/xiangli0608/cartographer_detailed_comments_ws

1 需要的数据

既然想要显示三维点云地图, 就必须要有每帧数据的点云, 以及这帧点云经过后端优化后的坐标, 这2个数据.

通过阅读代码可知, cartographer中前端是不保存点云的, 前端也不会有后端优化后的坐标.

所以, 我们想要找到这2个数据就要在后端的代码中找.

1.1 PoseGraphData


struct PoseGraphData {// Submaps get assigned an ID and state as soon as they are seen, even// before they take part in the background computations.// submap_data_ 里面,包含了所有的submapMapById<SubmapId, InternalSubmapData> submap_data;// Global submap poses currently used for displaying data.// submap 在 global 坐标系下的坐标MapById<SubmapId, optimization::SubmapSpec2D> global_submap_poses_2d;MapById<SubmapId, optimization::SubmapSpec3D> global_submap_poses_3d;// Data that are currently being shown.// 所有的轨迹节点的id与 节点的在global坐标系下的坐标, 在local map 下的坐标与时间MapById<NodeId, TrajectoryNode> trajectory_nodes;// Global landmark poses with all observations.std::map<std::string /* landmark ID */, PoseGraphInterface::LandmarkNode>landmark_nodes;// How our various trajectories are related.TrajectoryConnectivityState trajectory_connectivity_state;// 节点的个数int num_trajectory_nodes = 0;// 轨迹与轨迹的状态std::map<int, InternalTrajectoryState> trajectories_state;// Set of all initial trajectory poses.std::map<int, PoseGraph::InitialTrajectoryPose> initial_trajectory_poses;// 所有的约束数据std::vector<PoseGraphInterface::Constraint> constraints;
};

这是后端代码中保存所有数据的数据结构, 其中有个 MapById<NodeId, TrajectoryNode> trajectory_nodes;

1.2 TrajectoryNode

struct TrajectoryNode {struct Data {common::Time time;// Transform to approximately gravity align the tracking frame as// determined by local SLAM.Eigen::Quaterniond gravity_alignment;// Used for loop closure in 2D: voxel filtered returns in the// 'gravity_alignment' frame.sensor::PointCloud filtered_gravity_aligned_point_cloud;// Used for loop closure in 3D.sensor::PointCloud high_resolution_point_cloud;sensor::PointCloud low_resolution_point_cloud;Eigen::VectorXf rotational_scan_matcher_histogram;// The node pose in the local SLAM frame.transform::Rigid3d local_pose;};common::Time time() const { return constant_data->time; }// This must be a shared_ptr. If the data is used for visualization while the// node is being trimmed, it must survive until all use finishes.std::shared_ptr<const Data> constant_data;// The node pose in the global SLAM frame.transform::Rigid3d global_pose;
};

可以看到, TrajectoryNode这个数据结构即保存了点云, 又保存了前端与后端的位姿, 其中后端的位姿global_pose正是我们需要的.

1.3 PoseGraph3D::GetTrajectoryNodes()

现在有了数据了, 如果能有接口就更好了. 通过寻找, 发现还真有, 就是PoseGraph3D::GetTrajectoryNodes(), 这个函数直接返回TrajectoryNodes.

所以, 我们可以不用修改cartographer的代码, 只修改cartographer_ros的代码就可以了, 因为cartographer的代码存在接口满足我们的要求.

2 修改代码

2.1 MapBuilderBridge

cartographer_ros中从后端位姿图中拿数据的只有 MapBuilderBridge, 所以在 这个类中要添加获取TrajectoryNodes的接口.

由于cartographer_ros 中进行可视化操作的代码都在Node类中, 我们在MapBuilderBridge类这只需要添加获取TrajectoryNodes的接口就行了, 具体的可视化要在Node类中进行.

2.1.1 map_builder_bridge.h

首先, 在 cartographer_ros/cartographer_ros/map_builder_bridge.h 文件中, 添加public的函数, 如下所示.
std::shared_ptr<MapById<NodeId, TrajectoryNode>> GetTrajectoryNodes();

之后需要在 cartographer_ros 的命名空间下使用using引用如下变量. 这里还要添加一下头文件, 具体添加啥我不记得了, 如果感兴趣的就去看我的代码吧.

namespace cartographer_ros {// lx add
using ::cartographer::mapping::NodeId;
using ::cartographer::mapping::MapById;
using ::cartographer::mapping::TrajectoryNode;
2.1.2 map_builder_bridge.cc

然后, 在 cartographer_ros/cartographer_ros/map_builder_bridge.cc 中的最下面, 添加如下函数

std::shared_ptr<MapById<NodeId, TrajectoryNode>> MapBuilderBridge::GetTrajectoryNodes() {std::shared_ptr<MapById<NodeId, TrajectoryNode>> trajectory_nodes =std::make_shared<MapById<NodeId, TrajectoryNode>>(map_builder_->pose_graph()->GetTrajectoryNodes());return trajectory_nodes;
}

这段函数的作用就是, 生成一个指向 MapById<NodeId, TrajectoryNode> 的共享智能指针, 然后将这个智能指针返回.

2.2 Node

2.2.1 node.h

我们通过MapBuilderBridge生成了一个获取TrajectoryNode数据的接口, 所以在Node类中, 使用这个接口进行三维点云地图的可视化就可以了.

首先, 在 node.h 中添加如下私有的成员变量

  // lx add::ros::Publisher point_cloud_map_publisher_;absl::Mutex point_cloud_map_mutex_;bool load_state_ = false;size_t last_trajectory_nodes_size_ = 0;sensor_msgs::PointCloud2 ros_point_cloud_map_;

添加如下的共有的成员函数

  // lx addvoid PublishPointCloudMap(const ::ros::WallTimerEvent& timer_event);
2.2.2 node.cc

首先, 添加头文件的引入以及变量名的引入.

变量名的引入是将 using ::cartographer::mapping::NodeId; 等放在了 cartographer_ros 命名空间之下.

#include "cartographer/mapping/id.h"
#include "cartographer/mapping/trajectory_node.h"
#include "cartographer/sensor/point_cloud.h"
#include "pcl/point_cloud.h"
#include "pcl/point_types.h"
#include "pcl_conversions/pcl_conversions.h"
#include <pcl/io/pcd_io.h>namespace cartographer_ros {namespace carto = ::cartographer;using carto::transform::Rigid3d;
using TrajectoryState =::cartographer::mapping::PoseGraphInterface::TrajectoryState;// lx add
using ::cartographer::mapping::NodeId;
using ::cartographer::mapping::MapById;
using ::cartographer::mapping::TrajectoryNode;
using ::cartographer::sensor::RangefinderPoint;

之后, 在构造函数中, 添加如下代码. 当使用3d建图时, 初始化点云发布器.

  // lx addif (node_options_.map_builder_options.use_trajectory_builder_3d()) {point_cloud_map_publisher_ =node_handle_.advertise<sensor_msgs::PointCloud2>("point_cloud_map", kLatestOnlyPublisherQueueSize, true);}

在构造函数的最后, 为发布点云函数添加定时器, 设置为10秒种执行一次.

  // lx addif (node_options_.map_builder_options.use_trajectory_builder_3d()) {wall_timers_.push_back(node_handle_.createWallTimer(::ros::WallDuration(10),  // 10s&Node::PublishPointCloudMap, this));}

之后, 就是将之前声明的 PublishPointCloudMap 这个函数进行实现了.

void Node::PublishPointCloudMap(const ::ros::WallTimerEvent& timer_event) {// 纯定位时不发布点云地图if (load_state_ || point_cloud_map_publisher_.getNumSubscribers() == 0) {return;}// 只发布轨迹id 0 的点云地图constexpr int trajectory_id = 0;// 获取优化后的节点位姿与节点的点云数据std::shared_ptr<MapById<NodeId, TrajectoryNode>> trajectory_nodes =map_builder_bridge_.GetTrajectoryNodes();// 如果个数没变就不进行地图发布size_t trajectory_nodes_size = trajectory_nodes->SizeOfTrajectoryOrZero(trajectory_id);if (last_trajectory_nodes_size_ == trajectory_nodes_size)return;last_trajectory_nodes_size_ = trajectory_nodes_size;absl::MutexLock lock(&point_cloud_map_mutex_);pcl::PointCloud<pcl::PointXYZ>::Ptr point_cloud_map(new pcl::PointCloud<pcl::PointXYZ>());pcl::PointCloud<pcl::PointXYZ>::Ptr node_point_cloud(new pcl::PointCloud<pcl::PointXYZ>());// 遍历轨迹0的所有优化后的节点auto node_it = trajectory_nodes->BeginOfTrajectory(trajectory_id);auto end_it = trajectory_nodes->EndOfTrajectory(trajectory_id);for (; node_it != end_it; ++node_it) {auto& trajectory_node = trajectory_nodes->at(node_it->id);auto& high_resolution_point_cloud = trajectory_node.constant_data->high_resolution_point_cloud;auto& global_pose = trajectory_node.global_pose;if (trajectory_node.constant_data != nullptr) {node_point_cloud->clear();node_point_cloud->resize(high_resolution_point_cloud.size());// 遍历点云的每一个点, 进行坐标变换for (const RangefinderPoint& point : high_resolution_point_cloud.points()) {RangefinderPoint range_finder_point = global_pose.cast<float>() * point;node_point_cloud->push_back(pcl::PointXYZ(range_finder_point.position.x(), range_finder_point.position.y(),range_finder_point.position.z()));}// 将每个节点的点云组合在一起*point_cloud_map += *node_point_cloud;}} // end forros_point_cloud_map_.data.clear();pcl::toROSMsg(*point_cloud_map, ros_point_cloud_map_);ros_point_cloud_map_.header.stamp = ros::Time::now();ros_point_cloud_map_.header.frame_id = node_options_.map_frame;LOG(INFO) << "publish point cloud map";point_cloud_map_publisher_.publish(ros_point_cloud_map_);
}

通过 std::shared_ptr<MapById<NodeId, TrajectoryNode>> trajectory_nodes = map_builder_bridge_.GetTrajectoryNodes(); 这条语句调用了之前我们在 map_builder_bridge 设置的接口, 获取了TrajectoryNode数据, 也就是点云与优化后的坐标.

之后, 将点云先进行坐标变换, 再添加到pcl中的点云里.

这里要说明一下, 为什么要进行坐标变换.
这是因为TrajectoryNode里存的点云, 都是以local坐标系为原点的点云, 是围绕着local坐标系的, 所以, 要根据优化出来的位姿, 将这帧点云变换到优化之后的位姿上去, 变换了之后才能进行拼接.

同时, 使用的点云是high_resolution_point_cloud, 没有使用低分辨率的.

最后, 通过ros的发布器发布出去, 就完成了三维点云地图的可视化.

还剩最后一个问题, 如何将点云地图直接保存成pcd格式的数据.

将HandleWriteState函数改写一下, 每次调用服务进行pbstream保存的时候, 同时也保存一下pcd格式的数据.

默认是不进行保存的, 需要将 save_pcd 这个变量改成 true 重新编译之后才能保存.

bool Node::HandleWriteState(::cartographer_ros_msgs::WriteState::Request& request,::cartographer_ros_msgs::WriteState::Response& response) {{absl::MutexLock lock(&mutex_);// 直接调用cartographer的map_builder_的SerializeStateToFile()函数进行文件的保存if (map_builder_bridge_.SerializeState(request.filename,request.include_unfinished_submaps)) {response.status.code = cartographer_ros_msgs::StatusCode::OK;response.status.message =absl::StrCat("State written to '", request.filename, "'.");} else {response.status.code = cartographer_ros_msgs::StatusCode::INVALID_ARGUMENT;response.status.message =absl::StrCat("Failed to write '", request.filename, "'.");}}// lx addconstexpr bool save_pcd = false;if (node_options_.map_builder_options.use_trajectory_builder_3d() &&save_pcd) {absl::MutexLock lock(&point_cloud_map_mutex_);const std::string suffix = ".pbstream";std::string prefix =request.filename.substr(0, request.filename.size() - suffix.size());LOG(INFO) << "Saving map to pcd files ...";pcl::PointCloud<pcl::PointXYZ>::Ptr pcl_point_cloud_map(new pcl::PointCloud<pcl::PointXYZ>());pcl::fromROSMsg(ros_point_cloud_map_, *pcl_point_cloud_map);pcl::io::savePCDFileASCII(prefix + ".pcd", *pcl_point_cloud_map);LOG(INFO) << "Pcd written to " << prefix << ".pcd";}return true;
}

这里, 如果添加了这里要在 CMakeLists.txt 中将 PCL 引入的库加一个io, 才能编译通过.

find_package(PCL REQUIRED COMPONENTS common io)

3 三维点云地图显示

3.1 运行

通过如下指令运行代码, 注意我这里是跑的bag.
roslaunch cartographer_ros lx_rs16_3d.launch

3.2 三维点云地图可视化

在rviz中订阅 /point_cloud_map 话题的 pointcloud2 格式的数据, 即可看到点云地图.

建图过程中
请添加图片描述
建图结束
请添加图片描述
请添加图片描述
可以看到, cartographer三维建的图, 墙是挺厚的, 效果应该没有lio-sam好.

到此为止, 就实现了cartographer三维点云地图的可视化, 以及保存pcd格式地图的操作.

具体的代码都开源在文章开头处的地址中, 感兴趣的同学可以好好看看.

这篇关于cartographer实时显示三维点云地图的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/871207

相关文章

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

hdu1240、hdu1253(三维搜索题)

1、从后往前输入,(x,y,z); 2、从下往上输入,(y , z, x); 3、从左往右输入,(z,x,y); hdu1240代码如下: #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#inc

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

hdu4826(三维DP)

这是一个百度之星的资格赛第四题 题目链接:http://acm.hdu.edu.cn/contests/contest_showproblem.php?pid=1004&cid=500 题意:从左上角的点到右上角的点,每个点只能走一遍,走的方向有三个:向上,向下,向右,求最大值。 咋一看像搜索题,先暴搜,TLE,然后剪枝,还是TLE.然后我就改方法,用DP来做,这题和普通dp相比,多个个向上

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

2、PF-Net点云补全

2、PF-Net 点云补全 PF-Net论文链接:PF-Net PF-Net (Point Fractal Network for 3D Point Cloud Completion)是一种专门为三维点云补全设计的深度学习模型。点云补全实际上和图片补全是一个逻辑,都是采用GAN模型的思想来进行补全,在图片补全中,将部分像素点删除并且标记,然后卷积特征提取预测、判别器判别,来训练模型,生成的像

lvgl8.3.6 控件垂直布局 label控件在image控件的下方显示

在使用 LVGL 8.3.6 创建一个垂直布局,其中 label 控件位于 image 控件下方,你可以使用 lv_obj_set_flex_flow 来设置布局为垂直,并确保 label 控件在 image 控件后添加。这里是如何步骤性地实现它的一个基本示例: 创建父容器:首先创建一个容器对象,该对象将作为布局的基础。设置容器为垂直布局:使用 lv_obj_set_flex_flow 设置容器

Vector3 三维向量

Vector3 三维向量 Struct Representation of 3D vectors and points. 表示3D的向量和点。 This structure is used throughout Unity to pass 3D positions and directions around. It also contains functions for doin

C# dateTimePicker 显示年月日,时分秒

dateTimePicker默认只显示日期,如果需要显示年月日,时分秒,只需要以下两步: 1.dateTimePicker1.Format = DateTimePickerFormat.Time 2.dateTimePicker1.CustomFormat = yyyy-MM-dd HH:mm:ss Tips:  a. dateTimePicker1.ShowUpDown = t