(02)Cartographer源码无死角解析-(79) ROS服务→子图压缩与服务发送

讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下:
(02)Cartographer源码无死角解析- (00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/127350885
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

通过前几篇博客,了解到 ROS 是如何发布 Cartographer 计算出来的 3D点云地图、子图位姿、Landmark、2D点云数据、tf、机器人tracking frame轨迹发布等。但是却没有讲解2D栅格地图是如何发发布到 Rviz 进行展示的。

这是因为 2D栅格地图 并不是通过话题的方式进行发布的,而是有一个自己的服务。在 node.cc 构造函数中,可以看到其有创建很多的服务:

	// Step: 2 声明发布对应名字的ROS服务, 并将服务的发布器放入到vector容器中
	service_servers_.push_back(node_handle_.advertiseService(kSubmapQueryServiceName, &Node::HandleSubmapQuery, this))
......
	service_servers_.push_back(node_handle_.advertiseService(kReadMetricsServiceName, &Node::HandleReadMetrics, this));

这些服务实际上就是执行对应的回调函数,从命名可以知道,关于子图的服务对应的回调函数就是 Node::HandleSubmapQuery(),其他的服务这里就暂时不进行讲解了。

该函数主要完成子图的发送,但是很明显,这里都是子图,而不是全局地图,那么全局地图又在哪里获取呢?如本人运行的 src/cartographer_ros/cartographer_ros/launch/demo_backpack_2d.launch 文件,其包含了 backpack_2d.launch,而 backpack_2d.launch 存在如下内容:

  <node name="cartographer_occupancy_grid_node" pkg="cartographer_ros"
      type="cartographer_occupancy_grid_node" args="-resolution 0.05" />

可知,其运行了一个 cartographer_occupancy_grid_node 的可执行文件,且与栅格占用地图有关,其对应的源码位于 src/cartographer_ros/cartographer_ros/cartographer_ros/occupancy_grid_node_main.cc 文件中,后面会重点对该文件进行分析。当然,第一步是先分析 Node::HandleSubmapQuery()。

二、Node::HandleSubmapQuery()

/**
 * @brief 获取对应id轨迹的 索引为submap_index 的submap
 *
 * @param[in] request 获取submap的请求
 * @param[out] response 服务的回应
 * @return true: ROS的service只能返回true, 返回false程序会中断
 */
bool Node::HandleSubmapQuery(
    ::cartographer_ros_msgs::SubmapQuery::Request& request,
    ::cartographer_ros_msgs::SubmapQuery::Response& response) {
  absl::MutexLock lock(&mutex_);
  map_builder_bridge_.HandleSubmapQuery(request, response);
  return true;
}

该函数就不做过多解释了,其就是上锁然后调用 MapBuilderBridge::HandleSubmapQuery() 这个函数,其就是一个子图查询服务。

三、HandleSubmapQuery()-整体注释

代码不是很复杂,这里先给出整体注释:

/**
 * @brief 获取对应id轨迹的 索引为 submap_index 的地图的栅格值及其他信息
 * 
 * @param[in] request 轨迹id与submap的index
 * @param[in] response 是否成功
 */
void MapBuilderBridge::HandleSubmapQuery(
    cartographer_ros_msgs::SubmapQuery::Request& request,
    cartographer_ros_msgs::SubmapQuery::Response& response) {
  cartographer::mapping::proto::SubmapQuery::Response response_proto;
  cartographer::mapping::SubmapId submap_id{request.trajectory_id,
                                            request.submap_index};
  // 获取压缩后的地图数据
  const std::string error =
      map_builder_->SubmapToProto(submap_id, &response_proto);
  if (!error.empty()) {
    LOG(ERROR) << error;
    response.status.code = cartographer_ros_msgs::StatusCode::NOT_FOUND;
    response.status.message = error;
    return;
  }

  response.submap_version = response_proto.submap_version();

  // 将response_proto中的地图栅格值存入到response中
  for (const auto& texture_proto : response_proto.textures()) {
    response.textures.emplace_back();
    // 获取response中存储地图变量的引用
    auto& texture = response.textures.back();
    // 对引用的变量进行赋值
    texture.cells.insert(texture.cells.begin(), texture_proto.cells().begin(),
                         texture_proto.cells().end());
    texture.width = texture_proto.width();
    texture.height = texture_proto.height();
    texture.resolution = texture_proto.resolution();
    texture.slice_pose = ToGeometryMsgPose(
        cartographer::transform::ToRigid3(texture_proto.slice_pose()));
  }
  response.status.message = "Success.";
  response.status.code = cartographer_ros_msgs::StatusCode::OK;
}

四、MapBuilder::SubmapToProto()

该函数具体实现如下:

// 返回压缩后的地图数据
std::string MapBuilder::SubmapToProto(
    const SubmapId& submap_id, proto::SubmapQuery::Response* const response) {
  // 进行id的检查
  if (submap_id.trajectory_id < 0 ||
      submap_id.trajectory_id >= num_trajectory_builders()) {
    return "Requested submap from trajectory " +
           std::to_string(submap_id.trajectory_id) + " but there are only " +
           std::to_string(num_trajectory_builders()) + " trajectories.";
  }

  // 获取地图数据
  const auto submap_data = pose_graph_->GetSubmapData(submap_id);
  if (submap_data.submap == nullptr) {
    return "Requested submap " + std::to_string(submap_id.submap_index) +
           " from trajectory " + std::to_string(submap_id.trajectory_id) +
           " but it does not exist: maybe it has been trimmed.";
  }

  // 将压缩后的地图数据放入response
  submap_data.submap->ToResponseProto(submap_data.pose, response);
  return "";
}

首先检测输入的子图id是否正常,不正常则报错,其告知只能选择那些轨迹的子图。接着就是调用 pose_graph_->GetSubmapData() 从后端获取数据,子图数据存储于后端优化 PoseGraph2D::data::submap_data 这个变量之中。然后进行数据压缩,也就是调用 submap_data.submap->ToResponseProto() 这个函数,其会把压缩之后的数据存放在 response 之中。

五、ProbabilityGrid::DrawToSubmapTexture()-子图压缩

进入到 Submap2D::ToResponseProto 函数之后,可以看到其调用了函数代码 grid()->DrawToSubmapTexture(),该函数实现于 src/cartographer/cartographer/mapping/2d/probability_grid.cc 文件中,来看看该函数的实现。

首先子图在构建的时候会扩张,扩展的地图可能存在很多未知区域,也就是栅格值为 0.5,其是没有太大意义的,所以只需要根据 ProbabilityGrid::known_cells_box_ 把探索过的区域剪裁下来就可以了,然后再进行图像数据的压缩。先看一下该函数的的整体注释,然后再进行细节分析:

// 获取压缩后的地图栅格数据
bool ProbabilityGrid::DrawToSubmapTexture(
    proto::SubmapQuery::Response::SubmapTexture* const texture,
    transform::Rigid3d local_pose) const {
  Eigen::Array2i offset;
  CellLimits cell_limits;
  // 根据bounding_box对栅格地图进行裁剪
  ComputeCroppedLimits(&offset, &cell_limits);

  std::string cells;
  // 遍历地图, 将栅格数据存入cells
  for (const Eigen::Array2i& xy_index : XYIndexRangeIterator(cell_limits)) {
    if (!IsKnown(xy_index + offset)) {
      cells.push_back(0 /* unknown log odds value */);
      cells.push_back(0 /* alpha */);
      continue;
    }
    // We would like to add 'delta' but this is not possible using a value and
    // alpha. We use premultiplied alpha, so when 'delta' is positive we can
    // add it by setting 'alpha' to zero. If it is negative, we set 'value' to
    // zero, and use 'alpha' to subtract. This is only correct when the pixel
    // is currently white, so walls will look too gray. This should be hard to
    // detect visually for the user, though.
    // 我们想添加 'delta',但使用值和 alpha 是不可能的
    // 我们使用预乘 alpha,因此当 'delta' 为正时,我们可以通过将 'alpha' 设置为零来添加它。 
    // 如果它是负数,我们将 'value' 设置为零,并使用 'alpha' 进行减法。 这仅在像素当前为白色时才正确,因此墙壁看起来太灰。 
    // 但是,这对于用户来说应该很难在视觉上检测到。
    
    // delta处于[-127, 127]
    const int delta =
        128 - ProbabilityToLogOddsInteger(GetProbability(xy_index + offset));
    const uint8 alpha = delta > 0 ? 0 : -delta;
    const uint8 value = delta > 0 ? delta : 0;
    // 存数据时存了2个值, 一个是栅格值value, 另一个是alpha透明度
    cells.push_back(value);
    cells.push_back((value || alpha) ? alpha : 1);
  }

  // 保存地图栅格数据时进行压缩
  common::FastGzipString(cells, texture->mutable_cells());
  
  // 填充地图描述信息
  texture->set_width(cell_limits.num_x_cells);
  texture->set_height(cell_limits.num_y_cells);
  const double resolution = limits().resolution();
  texture->set_resolution(resolution);
  const double max_x = limits().max().x() - resolution * offset.y();
  const double max_y = limits().max().y() - resolution * offset.x();
  *texture->mutable_slice_pose() = transform::ToProto(
      local_pose.inverse() *
      transform::Rigid3d::Translation(Eigen::Vector3d(max_x, max_y, 0.)));

  return true;
}

( 1 ) \color{blue}(1) (1) 首先调用 ComputeCroppedLimits() 求得已知区域的 offset 平移与大小 cell_limits。

( 2 ) \color{blue}(2) (2) 创建一个 std::string cells 实例,用于存储剪切之后的地图,源码中对 XYIndexRangeIterator(cell_limits) 进行迭代,可以理解为 xy_index 就剪切之后(新子图)中像素坐标,xy_index + offset 就是未剪切之前子图(旧子图)中像素坐标。

( 3 ) \color{blue}(3) (3) 判断一下新子图像素对应与旧子图中的位置,是否被探索过,如果没有,则 log odds(栅格值) 与透明度都设置为 0,添加到新地图 cells中。

( 4 ) \color{blue}(4) (4) 如果被探索过,先把其被占用的概率赋值给缩放到 [-127, 127],赋值给 delta,该数值越大,说明被占用的几率越大。

( 5 ) \color{blue}(5) (5) 对于一个像素的描述,使用两个 uint8来描述,也就是 16 个字节。
第一个为像素值 value,第二个为透明度 alpha。总的来说,最后的效果如下:

1.当 delta 大于 0 时,表示需要添加一个正数值。此时,value 被设置为 delta,而 alpha 被设置为 0。也就是说,value 表示要添加的正数值,而 alpha 表示透明度为 0,即完全不透明。

2.当 delta 小于等于 0 时,表示需要减去一个负数值或者不进行任何操作。此时,value 被设置为 0,而 alpha 被设置为 -delta。也就是说,value 为 0 表示不进行任何操作,而 alpha 表示透明度为 -delta,即根据需要减去的负数值的大小确定透明度。

根据这些设定,value 和 alpha 的取值情况如下:

当 delta 大于 0 时,value 大于 0,alpha 为 0。
当 delta 小于等于 0 时,value 为 0,alpha 大于等于 0。

对于占用率比较高的,不透明。对于占用率低,约低则约透明。

( 6 ) \color{blue}(6) (6) 设置号像素值与透明度之后调用 common::FastGzipString() 进行压缩,其内部压缩核心操作为 boost::iostreams::gzip_compressor。

( 7 ) \color{blue}(7) (7) 对剪切之后的地图重新进行描述,如高宽的设置(像素为单位),分辨率,以及世界坐标系下x,y 轴的最大值。且为其设置了位姿,也就是下面这句代码:

  const double max_x = limits().max().x() - resolution * offset.y();
  const double max_y = limits().max().y() - resolution * offset.x();
  *texture->mutable_slice_pose() = transform::ToProto(
      local_pose.inverse() *
      transform::Rigid3d::Translation(Eigen::Vector3d(max_x, max_y, 0.)));

这个地方需要注意,首先子图的初始位姿时基于local系的,其与 max_x, max_y 都是世界物理单位。另外,local 系 +x 轴为机器人起始位置正前方,+y 轴为机器人起始位置正左方,本人绘制图像如下:
在这里插入图片描述
红色坐标系为local系,蓝色坐标系为submap系,①表示子图在local系下的位姿,也就是源码中的 local_pose, ②表示切片在local系下的位姿,切片的原点应该是对应于代码中的[resolutionoffset.y(), resolutionoffset.y()],本人也没有理解 *texture->mutable_slice_pose() 最终的结果是什么,从命名来看可能是切片相对于子图的位姿,但是什么求解过程涉及到 limits().max() 与 max_x、max_y。感觉最终获得的结果,不知道是个啥玩意。这里先记录一下,为 疑问 1 \color{red}疑问1 疑问1

六、结语

ProbabilityGrid::DrawToSubmapTexture() 函数会返回到 Submap2D::ToResponseProto() 再返回到 MapBuilder::SubmapToProto() 再到 MapBuilderBridge::HandleSubmapQuery()。回到该函数,可以知道这样就获得了地图压缩之后的结果 response_proto,对于 response_proto 来说其可以存储多个 texture 对象,不过这里只存储了一个。把 response_proto 根式的数据转换成 response 之久,该函数结束,然后 Node::HandleSubmapQuery() 且执行结束。

到这里,大致明白了子图压缩压缩过程,首先把子图中已经探索过的区域剪切下来,然后再进行压缩。但是遗留下了一个疑问。

疑问 1 : \color{red}疑问1: 疑问1 src/cartographer/cartographer/mapping/2d/probability_grid.cc 文件中的 ProbabilityGrid::DrawToSubmapTexture 函数的 proto::SubmapQuery::Response::SubmapTexture* 示例对象 texture 中texture->mutable_slice_pose() 的含义具体是什么。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/35362.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

机器学习——掌握决策树ID3算法的原理,通过增益熵实现手工推导的过程。

文章目录 决策树介绍优缺点ID3算法原理举例 决策树的构建1、特征选择&#xff08;1&#xff09;香农熵&#xff08;2&#xff09;信息增益 2、决策树的生成3、决策树的修剪 总结&#xff1a;参考文献 决策树 介绍 决策树(decision tree)是一种基本的分类与回归方法。ID3是其中…

Linux学习之分区和挂载磁盘配额

先分区然后格式化。 fdisk /dev/sdb开始分区。 输入p&#xff0c;然后按下Enter&#xff0c;可以查看当前设备的分区情况。 输入d&#xff0c;然后按下Enter&#xff0c;就可以删除上边的分区&#xff0c;要是有多个分区&#xff0c;会让你选择删除哪个分区。 输入n&…

mysql基础5——mysql主从

文章目录 一、基本了解二、主从原理三、主从复制3.1 从无到有3.1.1 服务器初始化3.1.2 配置主库3.1.3 配置从库3.1.4 效果验证 3.2 从有到无3.2.1 主库全备&#xff0c;并同步到从库3.2.2 配置主库3.2.3 配置从库3.2.4 效果验证 四、数据库运维4.1 几个参数4.2 查看进程列表 一…

MATLAB | 如何使用MATLAB获取顶刊《Nature》全部绘图(附带近3年全部图像)

我出了如何使用MATLAB获取期刊《Cell》全部绘图&#xff0c;立马就有粉丝问《Nature》、《Sience》、《PNAS》啥的会不会安排&#xff0c;这期就给大家安排《Nature》全部绘图获取&#xff0c;之后其他期刊也会慢慢安排&#xff0c;但是不会一次性全出完(毕竟不能抓住一个主题就…

【Java基础教程】(五)程序概念篇 · 下:夯实基础!全面解析Java程序的逻辑控制体:顺序、选择与循环结构~

Java基础教程之程序概念 下 本节学习目标1️⃣ 程序逻辑控制1.1 顺序结构1.2 分支结构1.2.1 if 选择结构1.2.2 switch 选择结构 1.3 循环结构1.3.1 while 循环1.3.2 for 循环1.3.3 循环控制 &#x1f33e; 总结 本节学习目标 掌握Java中分支结构、循环结构、循环控制语法的使…

Squid 缓存代理--反向代理

Squid 缓存代理–反向代理 反向代理&#xff1a;如果Squid反向代理服务器中缓存了该请求的资源&#xff0c;则将该请求的资源直接返回给客户端&#xff1a;否则反向代理服务器将向后台的WEB服务器请求资源&#xff0c;然后将请求的应答返回给客户端&#xff0c;同时也将应答缓…

Django框架-11

聚合查询 1.聚合函数 使用aggregate()过滤器调用聚合函数。聚合函数包括&#xff1a;Avg 平均&#xff0c;Count 数量&#xff0c;Max 最大&#xff0c;Min 最 小&#xff0c;Sum 求和&#xff0c;被定义在django.db.models中。 例&#xff1a;查询图书的总阅读量。 from mo…

如何确定活动隔断整体色调

确定活动的整体色调可以通过以下几个步骤&#xff1a; 1. 确定主题或目标&#xff1a;首先要明确活动的主题或目标&#xff0c;这将有助于确定活动需要传达的情感或氛围。 2. 考虑活动类型&#xff1a;根据活动的类型&#xff0c;例如婚礼、生日派对、企业活动等&#xff0c;可…

vue3+pinia用户信息持久缓存(token)的问题

vue3pinia用户信息持久缓存&#xff08;token)的问题 对博主来说&#xff0c;这是个相当复杂的问题。 当初在使用vue2vuex进行用户信息持久登录时&#xff0c;写了不下3篇博客&#xff0c;确实是解决了问题&#xff0c;博客链接如下 vue存储和使用后端传递过来的tokenvue中对…

gma 2 教程(二)数据操作:1. 相关模块组成

考虑到数据读写是地理空间数据分析和应用的基础&#xff0c;因此将本章作为正文第一部分&#xff0c;以便为后续章节应用提供基础支持。本章以gma栅格/矢量数据输入输出模块&#xff08;io&#xff09;栅格/矢量数据的读取、创建、变换等主要操作为基础&#xff0c;配合gma地理…

基于PyQt5的桌面图像调试仿真平台开发(13)图像边缘显示

系列文章目录 基于PyQt5的桌面图像调试仿真平台开发(1)环境搭建 基于PyQt5的桌面图像调试仿真平台开发(2)UI设计和控件绑定 基于PyQt5的桌面图像调试仿真平台开发(3)黑电平处理 基于PyQt5的桌面图像调试仿真平台开发(4)白平衡处理 基于PyQt5的桌面图像调试仿真平台开发(5)…

2023年第一届证券基金行业先进计算峰会在沪成功召开

2023年7月7日&#xff0c;在中国计算机学会集成电路设计专委会、中国通信学会金融科技发展促进中心、中国电子工业标准化技术协会新一代计算标准工作委员会和证券基金信息技术创新联盟WG1工作组的指导下&#xff0c;由中科驭数主办的2023年第一届证券基金行业先进计算峰会在上海…

用矩阵处理3D变换

Rotation 也可以把三个旋转矩阵合并为一个综合旋转矩阵: 平移和旋转结合 有时我们想要将平移和旋转结合起来&#xff0c;这样我们就可以在一次操作中同时进行两者&#xff0c;但是我们不能用3x3矩阵来做3D平移&#xff0c;只能用4x4矩阵来做&#xff0c;如下所定义&#xff1a…

iOS打包IPA教程

转载&#xff1a;xcode打包导出ipa 众所周知&#xff0c;在开发苹果应用时需要使用签名&#xff08;证书&#xff09;才能进行打包安装苹果 IPA&#xff0c;作为刚接触ios开发的同学&#xff0c;只是学习ios app开发内测&#xff0c;并没有上架appstore需求&#xff0c;对于苹…

UE4/5用贴图和GeneratedDynamicMeshActor曲面细分与贴图位移制作模型

目录 制作逻辑&#xff1a; ​编辑 曲面细分函数&#xff1a; 添加贴图逻辑&#xff1a; 代码&#xff1a; 制作逻辑&#xff1a; 在之前的文章中&#xff0c;我们使用了网格细分&#xff0c;而这一次我们将使用曲面细分函数&#xff0c;使用方法和之前是一样的&#xff1a…

2023年Web安全学习路线总结!430页Web安全学习笔记(附PDF)

关键词&#xff1a;网络安全入门、渗透测试学习、零基础学安全、网络安全学习路线、web安全攻防笔记、渗透测试路线图 网络安全的范畴很大&#xff0c;相较于二进制安全等方向的高门槛、高要求&#xff0c;Web安全体系比较成熟&#xff0c;在现阶段来看&#xff0c;但凡有自己…

浅析便捷生活的新选择——抖音本地服务

抖音是一款风靡全球的短视频分享平台&#xff0c;其本地服务功能的发展也逐渐引起了广泛关注。本地服务是指抖音平台上的用户可以通过平台直接查找并使用周边的各种服务&#xff0c;比如美食外卖、快递配送、家政服务等。本地服务的发展对用户和商家都带来了很多便利和机遇。 首…

Mockplus Cloud - June 2023crack

Mockplus Cloud - June 2023crack 添加便签以澄清情节提要上的任何设计概念。 新的流程图工具直接在情节提要上可视化任何设计流程和过程。 添加了在发布到Mockplus Cloud时删除RP页面的功能。 添加设计注释时包括图像和链接。 添加了一个新的提示&#xff0c;用于在断开互联网…

四、Docker镜像详情

学习参考&#xff1a;尚硅谷Docker实战教程、Docker官网、其他优秀博客(参考过的在文章最后列出) 目录 前言一、Docker镜像1.1 概念1.2 UnionFS&#xff08;联合文件系统&#xff09;1.3 Docker镜像加载原理1.4 重点理解 二、docker commit 命令2.1 是什么&#xff1f;2.2 命令…

element之el-table合并列功能

目标效果如下&#xff1a; 实现代码如下&#xff1a; html部分&#xff1a; <!--定义表格组件,用组件自带的span-method属性定义合并列的方法--> <el-table :data"tableData" :span-method"spanRow"><el-table-column prop"RegionNa…
最新文章