知乎:从零开始做自动驾驶定位; 注释详解(一)

消息发布与订阅


目录

  1. 消息发布与订阅
  2. 缓冲区机制
  3. CMakeLists文件规划*
  4. 注释
  5. 参考链接

1. 消息发布与订阅

  消息的订阅和发布,这是每个ROS工程都必备的东西,我们常见的使用方式是在main函数中定义subscriber和publisher,每个subscriber会有一个callback函数与之对应。

  这种使用方式会带来一些问题,那就是如果订阅的topic比较多,那这个node文件就会充斥大量的callback函数,而且如果有些信息需要在callback内部做比较多的解析和处理,那这个node文件的代码长度会很长,这会影响程序的清晰度。

  针对这个问题,作者把每一类信息的订阅和发布封装成一个类 ,它的callback做为 类内函数存在 ,这样我们在node文件中想要订阅这个消息的时候只需要在初始化的时候定义一个类的对象,就可以在正常使用过程中从类内部直接取它的数据了。

  以订阅GNSS信息为例子,代码中,它的头文件是gnss_subscriber.hpp,源文件是gnss_subscriber.cpp。在头文件中,类的声明如下:

/** @Description: * @Author: Ren Qian* @Date: 2019-03-31 12:58:10*/
#ifndef LIDAR_LOCALIZATION_SUBSCRIBER_GNSS_SUBSCRIBER_HPP_
#define LIDAR_LOCALIZATION_SUBSCRIBER_GNSS_SUBSCRIBER_HPP_#include <deque>
#include <ros/ros.h>
#include "sensor_msgs/NavSatFix.h"
#include "lidar_localization/sensor_data/gnss_data.hpp" //这里是包含gnss信息类型的头文件namespace lidar_localization { //namespace
class GNSSSubscriber {public:GNSSSubscriber(ros::NodeHandle& nh, std::string topic_name, size_t buff_size);//带参构造函数GNSSSubscriber() = default; // 注释1.1/*函数ParseData就是实现从类里取数据的功能*/void ParseData(std::deque<GNSSData>& deque_gnss_data);private:/*回调函数,也就是接收和处理信息的地方*/void msg_callback(const sensor_msgs::NavSatFixConstPtr& nav_sat_fix_ptr);private:ros::NodeHandle nh_;ros::Subscriber subscriber_;/*注释1.2*/std::deque<GNSSData> new_gnss_data_; //这里是定义GNSSData类型的对象
};
}
#endif


注释1.1
注释1.2


2. 缓冲区机制

  这种机制完全是由于ROS自身的缺陷导致的。

  这个问题和ROS订阅信息时缓冲区读取有关,ROS在每次循环时,会逐个遍历各个 subscriber 的缓冲区,并且把缓冲区中的数据读完,不管有多少。我们在subscriber的callback中 解析数据 的时候,一般都是把数据赋给一个变量,然后在融合的时候使用 最后更新的值作为输入。

  如果觉得不好理解,我们使用伪代码举一个小例子,假如目前有雷达和GNSS信息,我们要融合它。

gnss_callback {gnss 数据解析,赋给变量 gnss_data
}
lidar_callback {雷达数据解析,得到lidar_data融合(lidar_data, gnss_data)
}

  这样看好像没什么问题,问题在于当融合算法处理时间比较长,超出了传感器信息的发送周期的时候,未被接收的数据会被放在每个subscriber对应的缓冲区中,等当前融合步骤处理完之后,下次ros从缓冲区中读取数据的时候,会先把gnss的数据读完,然后再读lidar的数据,这就导致,我们再一次进入lidar_callback函数时,使用的gnss_data已经不是和这个lidar_data同一时刻的数据了,而是它后面时刻的数据。

  为了解决这一问题,办法也很简单,就是我们不用单个变量来存储数据,而是用容器。各位这时候可以去第一步看我们举的那个GNSS信息订阅类的例子,在它的msg_callback函数里,信息解析完之后是放在一个deque容器里的(这样后续可以在容器里面寻找对应当前clouddata时间戳的传感器数据,而不是只能用最新的数据)。

void GNSSSubscriber::msg_callback(const sensor_msgs::NavSatFixConstPtr& nav_sat_fix_ptr) {GNSSData gnss_data;gnss_data.time = nav_sat_fix_ptr->header.stamp.toSec();gnss_data.latitude = nav_sat_fix_ptr->latitude;gnss_data.longitude = nav_sat_fix_ptr->longitude;gnss_data.altitude = nav_sat_fix_ptr->longitude;gnss_data.status = nav_sat_fix_ptr->status.status;gnss_data.service = nav_sat_fix_ptr->status.service;new_gnss_data_.push_back(gnss_data); //这个是队列/*  std::deque<GNSSData> new_gnss_data_;  */
}

  这样算法再使用数据的时候,应该从容器中去找。只不过找的时候要注意,多个传感器产生了多个容器,往算法模块里输入的时候,应该按照各容器第一个数据的时间戳,把最早的那个输入送进去,循环这个过程,直到所有容器数据送完为止。


  经过这样的改造,我们在node文件中使用发布和订阅的时候,只需要完成类对象定义和取数据两步(订阅对象subscriber_会在定义对象的时候,自动调用构造函数,然后在构造函数里面给定消息名称和回调函数):

// 定义类对象指针
std::shared_ptr<GNSSSubscriber> gnss_sub_ptr = std::make_shared<GNSSSubscriber>(nh, "/kitti/oxts/gps/fix", 1000000);ros::Rate rate(100);
while (ros::ok()) {ros::spinOnce();//取数据,存储进队列gnss_sub_ptr->ParseData(gnss_data_buff);rate.sleep();
}

这样node文件中代码量就会大大减少,使程序更清晰。

3. CMakeLists文件规划


注释:

注释1.1
注释1.1:对于 GNSSSubscriber() = default;的注释:
  /** 在C++中约定如果一个类中自定义了带参数的构造函数,那么编译器就不会再自动生成默认构造函数* 也就是说该类将不能默认创建对象,只能携带参数进行创建一个对象;* 但有时候需要创建一个默认的对象但是类中编译器又没有自动生成一个默认构造函数,* 那么为了让编译器生成这个默认构造函数就需要default这个属性。*     * 无论是显式的默认构造函数(=default),还是隐式合成的默认构造函数(编译器生成),* 都是用来控制默认初始化过程的。它按照如下规则初始化类的数据成员:* 1.如果存在类内的初始值,用它来初始化成员。* 2.如果不存在类内的初始值,默认初始化该成员。*/GNSSSubscriber() = default;

返回原阅读位置


注释1.2
注释1.2:对于 std::deque<GNSSData> new_gnss_data_;我们在这里定义了GNSSData类型的队列对象,是因为,对于每一种传感器信息,作者都专门专门封装了对应的数据结构,在sensor_data文件夹下,目前有imu_data.hpp、gnss_data.hpp、cloud_data.hpp分别对应IMU数据、GNSS数据、点云数据。

这种封装就是为了适应一开始提到的接口功能,同时也可以配合第一步封装的订阅类和发布类使用,把订阅的数据直接封装好再供主程序取,这样封闭性更强。

以gnss_data 为例,gnss_data.hpp如下:

/** @Description: * @Author: * @Date: 2019-07-17 18:25:13*/
#ifndef LIDAR_LOCALIZATION_SENSOR_DATA_GNSS_DATA_HPP_
#define LIDAR_LOCALIZATION_SENSOR_DATA_GNSS_DATA_HPP_#include <deque>#include "Geocentric/LocalCartesian.hpp"namespace lidar_localization{
class GNSSData {
public:double time = 0.0;double longitude = 0.0;double latitude = 0.0;double altitude = 0.0;double local_E = 0.0;double local_N = 0.0;double local_U = 0.0;int status = 0;int service = 0;private:static GeographicLib::LocalCartesian geo_converter;//查一下static bool origin_position_inited;public: void InitOriginPosition();void UpdateXYZ();static bool SyncData(std::deque<GNSSData>& UnsyncedData, std::deque<GNSSData>& SyncedData, double sync_time);};
}

基本同样的形式,点云数据的为:

namespace lidar_localization {
class CloudData {public:using POINT = pcl::PointXYZ; //点using CLOUD = pcl::PointCloud<POINT>; //由点构成的点云using CLOUD_PTR = CLOUD::Ptr;  //点云指针public:CloudData()     //构造函数:cloud_ptr(new CLOUD()) {    //初始化点云指针}public:double time = 0.0;  //时间戳?CLOUD_PTR cloud_ptr;
};
}

所有传感器的数据,我们都会封装成一个类作为接口,来方便使用。除了点云数据,每个传感器的类中都会有一个SyncData()公有函数,这个函数主要用来同步数据,以IMU数据为例:

bool IMUData::SyncData(std::deque<IMUData>& UnsyncedData, std::deque<IMUData>& SyncedData, double sync_time) 
{// 传感器数据按时间序列排列,在传感器数据中为 需要同步的时间 点找到合适的时间位置// 即找到与 需要同步的时间 相邻的左右两个数据// 需要注意的是,如果左右相邻数据有一个离同步时间差值比较大,则说明数据有丢失,时间离得太远不适合做差值while (UnsyncedData.size() >= 2) {//这个判断条件,保证UnsyncedData容器里大于两个元素//T& front() :返回容器中第一个元素的引用if (UnsyncedData.front().time > sync_time) //如果第一个元素的时间比同步时间大(即在需要同步的时间后面),即插入时刻的前面没有数据,那么就无从插入,直接退出return false;if (UnsyncedData.at(1).time < sync_time) {//上一个语句,没有ruturn说明第一个数据比插入时刻早,此句判断若第二个数据[UnsyncedData.at(1)]也比插入时刻早,那么第一个时刻的数据是没意义的,应该接着往下找,并删除第一个数据UnsyncedData.pop_front();//pop_front(): 删除容器开头元素 ,这样容器内元素整体向前移动一个continue;}if (sync_time - UnsyncedData.front().time > 0.2) { //0.2s内//如果雷达采集时刻已经处在前两个数据的中间了,但是第一个数据时刻与雷达采集时刻时间差过大,那么中间肯定丢数据了,退出UnsyncedData.pop_front();return false;}if (UnsyncedData.at(1).time - sync_time > 0.2) {//同样,如果第二个数据时刻与雷达采集时刻时间差过大,那么也是丢数据了,也退出UnsyncedData.pop_front();//这里面应该是把(0)删除了,(1)留下了吧return false;}break;}/*上面这段就是索引所需要的四个步骤,核心思想是让容器第一个数据时间比插入时刻早,第二个数据时间比插入时刻晚:1)如果第一个数据时间比雷达时间还要靠后,即插入时刻的前面没有数据,那么就无从插入,直接退出2)如果第一个数据比插入时刻早,第二个数据也比插入时刻早,那么第一个时刻的数据是没意义的,应该接着往下找,并删除第一个数据3)如果雷达采集时刻已经处在前两个数据的中间了,但是第一个数据时刻与雷达采集时刻时间差过大,那么中间肯定丢数据了,退出4)同样,如果第二个数据时刻与雷达采集时刻时间差过大,那么也是丢数据了,也退出以上四个限制条件如果都通过了,那么就算是找到对应位置了。*/if (UnsyncedData.size() < 2)return false;IMUData front_data = UnsyncedData.at(0); IMUData back_data = UnsyncedData.at(1);   IMUData synced_data;
/* 线性插值大家都懂,有两个数据a和b,时刻分别是0和1,那么时间t(0<t<1)时刻的插值就是a*(1-t)+b*t。第一个点: (0,a) ;第二个点(1,b);中间点(t,y),根据线性插值:(y-a)/(t-0) = (b-a)/(1-0) -> y-a=(b-a)*t -> y= (b-a)*t +a = a*(1-t)+b*t*/double front_scale = (back_data.time - sync_time) / (back_data.time - front_data.time);//(1-t)double back_scale = (sync_time - front_data.time) / (back_data.time - front_data.time);//tsynced_data.time = sync_time;//a*(1-t)+b*t//front_data.linear_acceleration.x :a//back_data.linear_acceleration.x: bsynced_data.linear_acceleration.x = front_data.linear_acceleration.x * front_scale + back_data.linear_acceleration.x * back_scale;synced_data.linear_acceleration.y = front_data.linear_acceleration.y * front_scale + back_data.linear_acceleration.y * back_scale;synced_data.linear_acceleration.z = front_data.linear_acceleration.z * front_scale + back_data.linear_acceleration.z * back_scale;synced_data.angular_velocity.x = front_data.angular_velocity.x * front_scale + back_data.angular_velocity.x * back_scale;synced_data.angular_velocity.y = front_data.angular_velocity.y * front_scale + back_data.angular_velocity.y * back_scale;synced_data.angular_velocity.z = front_data.angular_velocity.z * front_scale + back_data.angular_velocity.z * back_scale;// 四元数插值有线性插值和球面插值,球面插值更准确,但是两个四元数差别不大是,二者精度相当// 由于是对相邻两时刻姿态插值,姿态差比较小,所以可以用线性插值// 旋转角度用的四元数: q = w + xi + yj + zksynced_data.orientation.x = front_data.orientation.x * front_scale + back_data.orientation.x * back_scale;synced_data.orientation.y = front_data.orientation.y * front_scale + back_data.orientation.y * back_scale;synced_data.orientation.z = front_data.orientation.z * front_scale + back_data.orientation.z * back_scale;synced_data.orientation.w = front_data.orientation.w * front_scale + back_data.orientation.w * back_scale;// 线性插值之后要归一化;四元数归一化synced_data.orientation.Normlize();SyncedData.push_back(synced_data);return true;
}

返回原阅读位置


参考链接:

[1] https://www.zhihu.com/column/c_1114864226103037952

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【软件测试】Bug 篇

哈喽&#xff0c;哈喽&#xff0c;大家好~ 我是你们的老朋友&#xff1a;保护小周ღ 今天给大家带来的是 【软件测试】Bug 篇&#xff0c;首先了解, 什么是Bug, 如何定义一个Bug, 如何描述一个 Bug, Bug的级别, 和 Bug 的生命周期, 以及测试人员跟开发人员产生争执如何处理,…

【MYSQL】聚合查询、分组查询、联合查询

目录 聚合查询聚合函数count()sum()avg()max()和min()总结 分组查询group by 子句having 子句 联合查询笛卡尔积内连接外连接自连接子查询单行子查询多行子查询from子句使用子查询 合并查询 聚合查询 聚合查询就是针对表中行与行之间的查询。 聚合函数 count() count(列名)&a…

个人随想-代码生成工具v0+claude+cursor

cursor出来已经有一段时间了&#xff0c;不知道大家用了感觉怎么样。今天就以我个人为例&#xff0c;给大家介绍一下我是如何使用cursor搭建原型。 首先&#xff0c;我并不觉得cursor对于后端程序员带来了革命性改进&#xff0c;我们与很多团队沟&#xff0c;使用cursor80%以上…

spring中的容器接口的实现类和功能

容器实现 BeanFactory 实现 这里我们就来一步步实现BeanFactory的功能。 首先创建我们需要的类 Configuration static class Config{Beanpublic Bean1 bean1(){return new Bean1();}Beanpublic Bean2 bean2(){return new Bean2();}}static class Bean1{private static fina…

【Linux】Shell 编程规范及检查工具推荐

本文内容均来自个人笔记并重新梳理&#xff0c;如有错误欢迎指正&#xff01; 如果对您有帮助&#xff0c;烦请点赞、关注、转发、订阅专栏&#xff01; 专栏订阅入口 | 精选文章 | Kubernetes | Docker | Linux | 羊毛资源 | 工具推荐 | 往期精彩文章 【Docker】&#xff08;全…

【RH124】解释Linux文件系统权限

RH124教材中控制对文件的访问一章中有一道解释Linux文件系统权限的测验题&#xff0c;可以一起来看看&#xff1a; 一、权限解释 这是通过 ls -l 命令查看的结果。它显示了文件或目录的权限、拥有者、所属组等信息。 1、长列表的第一个字符表示文件类型&#xff1a; -是常…

【C语言零基础入门篇 - 16】:栈和队列

文章目录 栈和队列栈栈功能的实现源代码 队列队列功能的实现源代码 栈和队列 栈 什么是栈&#xff1a;功能受限的线性数据结构 栈的特点&#xff1a;先进后出 。例如&#xff1a;仓库进货、出货。 栈只有一个开口&#xff0c;先进去的数据在栈底&#xff08;bottom&#xf…

STM32篇:STM32CubeMX的安装

一.介绍与安装 1.作用 通过界面的方式&#xff0c;快速生成工程文件。 2.下载 官网 https://www.st.com/zh/development-tools/stm32cubemx.html#overview 3.安装 一路下一步&#xff0c;建议不要安装在C盘 4.配置 更新固件包位置&#xff08;比较大&#xff0c;默认在…

LeetCode 257. 二叉树的所有路径(回溯详解)

文章目录 LeetCode 257. 二叉树的所有路径思路递归版本一:非常明确的回溯代码版本二&#xff1a;精简的回溯代码 LeetCode 257. 二叉树的所有路径 LeetCode 257. 二叉树的所有路径 给定一个二叉树&#xff0c;返回所有从根节点到叶子节点的路径。 说明: 叶子节点是指没有子节…

全网最适合入门的面向对象编程教程:51 Python函数方法与接口-使用Zope实现接口

全网最适合入门的面向对象编程教程&#xff1a;51 Python 函数方法与接口-使用 Zope 实现接口 摘要&#xff1a; 在 Python 中&#xff0c;Zope 提供了一种机制来定义和实现接口。Zope 的接口模块通常用于创建可重用的组件&#xff0c;并确保组件遵循特定的接口规范。 原文链…

力扣 209.长度最小的子数组

一、长度最小的子数组 二、解题思路 采用滑动窗口的思路&#xff0c;详细见代码。 三、代码 class Solution {public int minSubArrayLen(int target, int[] nums) {int n nums.length, left 0, right 0, sum 0;int ans n 1; for (right 0; right < n; right ) { …

【二等奖论文】2024年华为杯研赛D题成品论文(后续会更新)

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片&#xff0c;那是获取资料的入口&#xff01; 点击链接获取【2024华为杯研赛资料汇总】&#xff1a; https://qm.qq.com/q/jTIeGzwkSchttps://qm.qq.com/q/jTIeGzwkSc 题 目&#xff1a; 大数据驱动的…

一劳永逸:用脚本实现夸克网盘内容自动更新

系统环境&#xff1a;debian/ubuntu 、 安装了python3 原作者项目&#xff1a;https://github.com/Cp0204/quark-auto-save 感谢 缘起 我喜欢看电影追剧&#xff0c;会经常转存一些资源到夸克网盘&#xff0c;电影还好&#xff0c;如果是电视剧&#xff0c;麻烦就来了。 对于一…

深度学习-卷积神经网络(CNN)

文章目录 一、网络构造1. 卷积层&#xff08;Convolutional Layer&#xff09;&#xff08;1&#xff09;卷积&#xff08;2&#xff09;特征图计算公式&#xff08;3&#xff09;三通道卷积 2. 激活函数&#xff08;Activation Function&#xff09;3. 池化层&#xff08;Pool…

【JUC并发编程系列】深入理解Java并发机制:线程局部变量的奥秘与最佳实践(五、ThreadLocal原理、对象之间的引用)

文章目录 【JUC并发编程系列】深入理解Java并发机制&#xff1a;线程局部变量的奥秘与最佳实践(五、ThreadLocal原理、对象之间的引用)1. 基本 API 介绍2. 简单用法3. 应用场景4. Threadlocal与Synchronized区别5. 内存溢出和内存泄漏5.2 内存溢出 (Memory Overflow)5.2 内存泄…

全栈项目小组【算法赛】题目及解题

题目&#xff1a;全栈项目小组【算法赛】 题目&#xff1a; 解题思路 1.遍历简历信息&#xff1a;我们需要读取所有简历&#xff0c;根据期望薪资和岗位类型进行分类和统计。 2.分类统计&#xff1a;使用哈希表来存储每个薪资下的前端&#xff08;F&#xff09;和后端&#…

【线程】线程的同步

本文重点&#xff1a;理解条件变量和生产者消费者模型 同步是在保证数据安全的情况下&#xff0c;让我们的线程访问资源具有一定的顺序性 条件变量cond 当一个线程互斥地访问某个变量时&#xff0c;它可能发现在其它线程改变状态之前&#xff0c;它什么也做不了&#xff0c;…

window系统DockerDesktop 部署windows容器

目录 参考文献1、安装Docker Desktop1.1 下载安装包1.2 安装教程1.3 异常解决 2、安装windows容器2.1 先启动DockerDesktop 软件界面2.2 检查docker版本2.3 拉取windows镜像 参考文献 windows容器docker中文官网 Docker: windows下跑windows镜像 1、安装Docker Desktop 1.1 …

SSM框架VUE电影售票管理系统开发mysql数据库redis设计java编程计算机网页源码maven项目

一、源码特点 smm VUE电影售票管理系统是一套完善的完整信息管理类型系统&#xff0c;结合SSM框架和VUE、redis完成本系统&#xff0c;对理解vue java编程开发语言有帮助系统采用ssm框架&#xff08;MVC模式开发&#xff09;&#xff0c;系 统具有完整的源代码和数据库&#…

【C语言零基础入门篇 - 17】:排序算法

文章目录 排序算法排序的基本概念冒泡排序选择排序插入排序 排序算法 排序的基本概念 1、什么是排序&#xff1f; 排序是指把一组数据以某种关系&#xff08;递增或递减&#xff09;按顺序排列起来的一种算法。 例如&#xff1a;数列 8、3、5、6、2、9、1、0、4、7 递增排序…