cartographer 源码解析 (五)
相关链接:
cartographer 源码解析(一)
cartographer 源码解析(二)
cartographer 源码解析(三)
cartographer 源码解析(四)
cartographer 源码解析(五)
cartographer 源码解析(六)
怎么进行配准呢,首先找到一段激光A,一段激光B, 然后找到他们的R和T然后放到一块去。重复的地方放到一起,不重复的地方就是地图的扩展,激光的地图匹配都是以帧为单位。
直接开始了。
上回书说道。2D激光的局部轨迹构建器添加测距数据。什么?忘了啊,那直接上代码吧。
local_trajectory_builder_2d.cc
std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>
LocalTrajectoryBuilder2D::AddRangeData(const std::string& sensor_id,const sensor::TimedPointCloudData& unsynchronized_data) {auto synchronized_data =range_data_collator_.AddRangeData(sensor_id, unsynchronized_data);...}
我们往局部轨迹构建器中添加测距数据,如果是激光数据的话就是激光测距数据。进来之后首先对激光数据进行同步,代码如下:
auto synchronized_data =range_data_collator_.AddRangeData(sensor_id, unsynchronized_data);
- 情况1: 你有两个激光雷达,两个激光雷达的名字重复了,比如雷达1 叫
scan1
雷达2也叫scan1
.但是两帧激光雷达的数据有重叠的部分。如下图所示
其中橙色的是雷达1 的一帧激光数据,红色是雷达2的一帧激光数据,蓝色是两帧激光雷达数据重复的部分。
看上面的代码我们知道,我们有一个range_data_collator_
, 之前我们有一个collator_trajectory_builder
里面有一个collator
, 我们的局部轨迹构建器中也有个collator
。他们共同作用就是收集加同步的作用。这个range_data_collator_
收集齐了所有的测距数据,比如scan1、scan2、scan3
然后再对这些数据进行同步,我们会在这个基础上,找到时间重复的,把重复的那一部分去掉。只保留不重复的部分。 - 情况2: 还有一种情况就是有多个雷达,他们分别是
scan1、scan2、scan3
。当这些scan具有相同部分的时候,不是去掉时间同步的部分,而是将重复部分的点以时间的顺序重新排序,排序后生成一个新的scan,我们暂时起名叫做scan_merge
吧。
其中红色是scan1
、紫色是scan2
、最下面是scan1和scan2
结合后的一整帧点云数据。中间重复部分以时间顺序排列好,点云会特别的密。
好了,我们正式开始,虽然还没有涉及到scan-match
,也就是激光匹配。不过我们还是数据预处理的模块。主要就是外推器对激光数据的畸变矫正处理。
我们从头到尾的讲一下这个代码来捋一捋。
首先是对激光雷达的数据做一个同步
local_trajectory_builder_2d.cc
const sensor::TimedPointCloudData& unsynchronized_data) {auto synchronized_data =range_data_collator_.AddRangeData(sensor_id, unsynchronized_data);)) {LOG(INFO) << "Range data collator filling buffer.";return nullptr;}
const common::Time& time = synchronized_data.time;
if (!options_.use_imu_data()) {InitializeExtrapolator(time);}
void LocalTrajectoryBuilder2D::InitializeExtrapolator(const common::Time time) {if (extrapolator_ != nullptr) {return;}CHECK(!options_.pose_extrapolator_options().use_imu_based());// TODO(gaschler): Consider using InitializeWithImu as 3D does.extrapolator_ = absl::make_unique<PoseExtrapolator>(::cartographer::common::FromSeconds(options_.pose_extrapolator_options().constant_velocity().pose_queue_duration()),options_.pose_extrapolator_options()2015-03-08.constant_velocity().imu_gravity_time_constant());extrapolator_->AddPose(time, transform::Rigid3d::Identity());
}
如果我们使用imu数据的话,我们会在添加imu数据的时候添加外推器。
void LocalTrajectoryBuilder2D::AddImuData(const sensor::ImuData& imu_data) {CHECK(options_.use_imu_data()) << "An unexpected IMU packet was added.";InitializeExtrapolator(imu_data.time);extrapolator_->AddImuData(imu_data);
}
岔开话题了哈,我们接着刚才的没有使用imu数据,自己创建外推器的代码开始讲。
if (extrapolator_ == nullptr) {// Until we've initialized the extrapolator with our first IMU message, we// cannot compute the orientation of the rangefinder.LOG(INFO) << "Extrapolator not yet initialized.";return nullptr;}
CHECK(!synchronized_data.ranges.empty());
CHECK_LE(synchronized_data.ranges.back().point_time.time, 0.f);
下面又是一个公式,就是第一个点的绝对时间戳等于相对时间戳+第一个点的绝对时间戳
const common::Time time_first_point =time +common::FromSeconds(synchronized_data.ranges.front().point_time.time);
if (time_first_point < extrapolator_->GetLastPoseTime()) {LOG(INFO) << "Extrapolator is still initializing.";return nullptr;}
std::vector<transform::Rigid3f> range_data_poses;range_data_poses.reserve(synchronized_data.ranges.size());bool warned = false;for (const auto& range : synchronized_data.ranges) {common::Time time_point = time + common::FromSeconds(range.point_time.time);if (time_point < extrapolator_->GetLastExtrapolatedTime()) {if (!warned) {LOG(ERROR)<< "Timestamp of individual range data point jumps backwards from "<< extrapolator_->GetLastExtrapolatedTime() << " to " << time_point;warned = true;}time_point = extrapolator_->GetLastExtrapolatedTime();}range_data_poses.push_back(extrapolator_->ExtrapolatePose(time_point).cast<float>());}
std::vector<transform::Rigid3f> range_data_poses;
首先这一行就是定义一个容器存放每个点被发射的时候的雷达的位姿。
range_data_poses.reserve(synchronized_data.ranges.size());
bool warned = false;for (const auto& range : synchronized_data.ranges) {common::Time time_point = time + common::FromSeconds(range.point_time.time);if (time_point < extrapolator_->GetLastExtrapolatedTime()) {if (!warned) {LOG(ERROR)<< "Timestamp of individual range data point jumps backwards from "<< extrapolator_->GetLastExtrapolatedTime() << " to " << time_point;warned = true;}time_point = extrapolator_->GetLastExtrapolatedTime();}range_data_poses.push_back(extrapolator_->ExtrapolatePose(time_point).cast<float>());}
bool warned = false;
首先是设置一个布尔值类型的判断,用来负责断定是否要发出警告。
接着是一个for
循环,从循环的执行条件可以看出是遍历所有的点。
for (const auto& range : synchronized_data.ranges) {......
}
common::Time time_point = time + common::FromSeconds(range.point_time.time);
这个time_point
是什么呢?他是当前一帧点云的时间戳time
+ 当前点的相对时间戳,结果就等于当前点的绝对时间戳。
if (time_point < extrapolator_->GetLastExtrapolatedTime()) {if (!warned) {LOG(ERROR)<< "Timestamp of individual range data point jumps backwards from "<< extrapolator_->GetLastExtrapolatedTime() << " to " << time_point;warned = true;}time_point = extrapolator_->GetLastExtrapolatedTime();}
time_point = extrapolator_->GetLastExtrapolatedTime();
这时间就算不增长,也不能外推一个比之前外推时间要早的时间戳。干脆还有上一次的外推时间戳。
range_data_poses.push_back(extrapolator_->ExtrapolatePose(time_point).cast<float>());
我们先不看外推器外推的原理,我们先把这个逻辑走完。
我们接着往下看
if (num_accumulated_ == 0) {// 'accumulated_range_data_.origin' is uninitialized until the last// accumulation.accumulated_range_data_ = sensor::RangeData{{}, {}, {}};}
// Drop any returns below the minimum range and convert returns beyond the// maximum range into misses.for (size_t i = 0; i < synchronized_data.ranges.size(); ++i) {const sensor::TimedRangefinderPoint& hit =synchronized_data.ranges[i].point_time;const Eigen::Vector3f origin_in_local =range_data_poses[i] *synchronized_data.origins.at(synchronized_data.ranges[i].origin_index);sensor::RangefinderPoint hit_in_local =range_data_poses[i] * sensor::ToRangefinderPoint(hit);const Eigen::Vector3f delta = hit_in_local.position - origin_in_local;const float range = delta.norm();if (range >= options_.min_range()) {if (range <= options_.max_range()) {accumulated_range_data_.returns.push_back(hit_in_local);} else {hit_in_local.position =origin_in_local +options_.missing_data_ray_length() / range * delta;accumulated_range_data_.misses.push_back(hit_in_local);}}}
代码上完了,是不是有点上头,我们一点点的说,先从for循环开始吧。我们回溯一下,弄明白这到底是怎么一回事。
for (size_t i = 0; i < synchronized_data.ranges.size(); ++i) {......}
auto synchronized_data =range_data_collator_.AddRangeData(sensor_id, unsynchronized_data);)) {LOG(INFO) << "Range data collator filling buffer.";return nullptr;}
这里定义的是auto
,那么紧接着我们看看函数返回的类型就知道它是定义的什么类型了。
range_data_collator.cc
sensor::TimedPointCloudOriginData RangeDataCollator::AddRangeData(const std::string& sensor_id,sensor::TimedPointCloudData timed_point_cloud_data) {......}
struct TimedPointCloudOriginData {struct RangeMeasurement {TimedRangefinderPoint point_time;float intensity;size_t origin_index;};common::Time time;std::vector<Eigen::Vector3f> origins;std::vector<RangeMeasurement> ranges;
};
其中包含了一个RangeMeasurement
的结构体,然后是时间time
,origins
是雷达位置的可变数组,ranges
是相关的点的可变数组。
timed_point_cloud_data.h
struct RangeMeasurement {TimedRangefinderPoint point_time;float intensity;size_t origin_index;};
首先是TimedRangefinderPoint
这个数据类型。具体是什么呢?我们看看
struct TimedRangefinderPoint {Eigen::Vector3f position;float time;
};
struct TimedPointCloudData {common::Time time;Eigen::Vector3f origin;TimedPointCloud ranges;// 'intensities' has to be same size as 'ranges', or empty.std::vector<float> intensities;
};
void AddSensorData(const std::string& sensor_id,const sensor::TimedPointCloudData& timed_point_cloud_data) override {CHECK(local_trajectory_builder_)<< "Cannot add TimedPointCloudData without a LocalTrajectoryBuilder.";std::unique_ptr<typename LocalTrajectoryBuilder::MatchingResult>matching_result = local_trajectory_builder_->AddRangeData(sensor_id, timed_point_cloud_data);......}
void SensorBridge::HandleLaserScanMessage(const std::string& sensor_id, const sensor_msgs::LaserScan::ConstPtr& msg) {carto::sensor::PointCloudWithIntensities point_cloud;carto::common::Time time;std::tie(point_cloud, time) = ToPointCloudWithIntensities(*msg);HandleLaserScan(sensor_id, time, msg->header.frame_id, point_cloud);
}
struct PointCloudWithIntensities {TimedPointCloud points;std::vector<float> intensities;
};
using TimedPointCloud = std::vector<TimedRangefinderPoint>;
发现它是一个TimedRangefinderPoint类型的数组,但具体我们看看这个数组的数据类型是什么呢?
struct TimedRangefinderPoint {Eigen::Vector3f position;float time;
};
HandleLaserScan(sensor_id, time, msg->header.frame_id, point_cloud);
void SensorBridge::HandleLaserScan(const std::string& sensor_id, const carto::common::Time time,const std::string& frame_id,const carto::sensor::PointCloudWithIntensities& points) {if (points.points.empty()) {return;}CHECK_LE(points.points.back().time, 0.f);// TODO(gaschler): Use per-point time instead of subdivisions.for (int i = 0; i != num_subdivisions_per_laser_scan_; ++i) {const size_t start_index =points.points.size() * i / num_subdivisions_per_laser_scan_;const size_t end_index =points.points.size() * (i + 1) / num_subdivisions_per_laser_scan_;carto::sensor::TimedPointCloud subdivision(points.points.begin() + start_index, points.points.begin() + end_index);if (start_index == end_index) {continue;}const double time_to_subdivision_end = subdivision.back().time;// `subdivision_time` is the end of the measurement so sensor::Collator will// send all other sensor data first.const carto::common::Time subdivision_time =time + carto::common::FromSeconds(time_to_subdivision_end);auto it = sensor_to_previous_subdivision_time_.find(sensor_id);if (it != sensor_to_previous_subdivision_time_.end() &&it->second >= subdivision_time) {LOG(WARNING) << "Ignored subdivision of a LaserScan message from sensor "<< sensor_id << " because previous subdivision time "<< it->second << " is not before current subdivision time "<< subdivision_time;continue;}sensor_to_previous_subdivision_time_[sensor_id] = subdivision_time;for (auto& point : subdivision) {point.time -= time_to_subdivision_end;}CHECK_EQ(subdivision.back().time, 0.f);HandleRangefinder(sensor_id, subdivision_time, frame_id, subdivision);}
}
由于之前讲过,这里就不细讲了,主要是将points
转化成一个个的subdivision
。
void SensorBridge::HandleRangefinder(const std::string& sensor_id, const carto::common::Time time,const std::string& frame_id, const carto::sensor::TimedPointCloud& ranges) {if (!ranges.empty()) {CHECK_LE(ranges.back().time, 0.f);}const auto sensor_to_tracking =tf_bridge_.LookupToTracking(time, CheckNoLeadingSlash(frame_id));if (sensor_to_tracking != nullptr) {trajectory_builder_->AddSensorData(sensor_id, carto::sensor::TimedPointCloudData{time, sensor_to_tracking->translation().cast<float>(),carto::sensor::TransformTimedPointCloud(ranges, sensor_to_tracking->cast<float>())});}
}
const auto sensor_to_tracking =tf_bridge_.LookupToTracking(time, CheckNoLeadingSlash(frame_id));
首先我们经过tf_bridge
的查找器进行查找,tf_bridge
是用来进行坐标转换的,记录坐标系之间的转换关系的工具,查找的是sensor_to_tracking。
我又要开始变色了。
to is in !
to is at !
举个例子来说明情况
首先是一个激光点P
而这个sensor_to_tracking
其实就是sensor
in
tracking
, 这个sensor
在这里是雷达,其实就是激光雷达的外参。
接下来我们做什么呢?
void SensorBridge::HandleRangefinder(const std::string& sensor_id, const carto::common::Time time,const std::string& frame_id, const carto::sensor::TimedPointCloud& ranges) {......}
struct TimedPointCloudData {common::Time time;Eigen::Vector3f origin;TimedPointCloud ranges;// 'intensities' has to be same size as 'ranges', or empty.std::vector<float> intensities;
};
sensor_id, carto::sensor::TimedPointCloudData{time, sensor_to_tracking->translation().cast<float>(),carto::sensor::TransformTimedPointCloud(ranges, sensor_to_tracking->cast<float>())}
carto::sensor::TransformTimedPointCloud(ranges, sensor_to_tracking->cast<float>())
for (size_t i = 0; i < synchronized_data.ranges.size(); ++i) {const sensor::TimedRangefinderPoint& hit =synchronized_data.ranges[i].point_time;const Eigen::Vector3f origin_in_local =range_data_poses[i] *synchronized_data.origins.at(synchronized_data.ranges[i].origin_index);sensor::RangefinderPoint hit_in_local =range_data_poses[i] * sensor::ToRangefinderPoint(hit);const Eigen::Vector3f delta = hit_in_local.position - origin_in_local;const float range = delta.norm();if (range >= options_.min_range()) {if (range <= options_.max_range()) {accumulated_range_data_.returns.push_back(hit_in_local);} else {hit_in_local.position =origin_in_local +options_.missing_data_ray_length() / range * delta;accumulated_range_data_.misses.push_back(hit_in_local);}}}
我们看到了hit
经过了同步之后所有点中的一帧点云synchronized_data.ranges[i]
,
struct TimedPointCloudOriginData {struct RangeMeasurement {TimedRangefinderPoint point_time;float intensity;size_t origin_index;};common::Time time;std::vector<Eigen::Vector3f> origins;std::vector<RangeMeasurement> ranges;
};
首先把ranges
的point_time
拿出来,也就是带着时间戳的点的坐标拿到。
origin我们先跳过,我们先看看hit主要用来干什么。
sensor::RangefinderPoint hit_in_local =range_data_poses[i] * sensor::ToRangefinderPoint(hit);
然后我们看看ToRangefinderPoint
做了什么?这个hit
就是一帧激光里的相对坐标
rangefinder_point.h
inline RangefinderPoint ToRangefinderPoint(const TimedRangefinderPoint& timed_rangefinder_point) {return {timed_rangefinder_point.position};
}
const Eigen::Vector3f origin_in_local =range_data_poses[i] *synchronized_data.origins.at(synchronized_data.ranges[i].origin_index);
const Eigen::Vector3f delta = hit_in_local.position - origin_in_local;
所以hit_in_local.position - origin_in_local.norm()
就是测距长度。
if (range >= options_.min_range()) {...}
虽然长,但有一句话我们需要记住,将来我们跟别人拉开差距的地方,其实就是细节。
if (range <= options_.max_range()) {accumulated_range_data_.returns.push_back(hit_in_local);} else {hit_in_local.position =origin_in_local +options_.missing_data_ray_length() / range * delta;accumulated_range_data_.misses.push_back(hit_in_local);}
if (num_accumulated_ >= options_.num_accumulated_range_data()) {......}
其中options_.num_accumulated_range_data()
是我们设置的收集雷达的帧数,比如我们设置为2或者3,就是等到收集了2帧或者3帧数据了。
const common::Time current_sensor_time = synchronized_data.time;absl::optional<common::Duration> sensor_duration;if (last_sensor_time_.has_value()) {sensor_duration = current_sensor_time - last_sensor_time_.value();}
首先计算一下,上一次传感器传到这里的时间,然后获取当前的时间,二者相减获得差值。后面会有用到。
last_sensor_time_ = current_sensor_time;num_accumulated_ = 0;
更新last_sensor_time_
,同时统计收集雷达帧数的变量为0,重新开始收集。
const transform::Rigid3d gravity_alignment = transform::Rigid3d::Rotation(extrapolator_->EstimateGravityOrientation(time));
accumulated_range_data_.origin = range_data_poses.back().translation();
我们用多个激光雷达建图的时候,会有多个origin
。上面代码表示,用最后的外推位姿的发射原点作为整个一帧大激光,也即是合并之后的激光,作为它的原点。
return AddAccumulatedRangeData(time,TransformToGravityAlignedFrameAndFilter(gravity_alignment.cast<float>() * range_data_poses.back().inverse(),accumulated_range_data_),gravity_alignment, sensor_duration);
LocalTrajectoryBuilder2D::AddRangeData(const std::string& sensor_id,const sensor::TimedPointCloudData& unsynchronized_data) {......return AddAccumulatedRangeData(time,TransformToGravityAlignedFrameAndFilter(gravity_alignment.cast<float>() * range_data_poses.back().inverse(),accumulated_range_data_),gravity_alignment, sensor_duration);}return nullptr;}
sensor::RangeData
LocalTrajectoryBuilder2D::TransformToGravityAlignedFrameAndFilter(const transform::Rigid3f& transform_to_gravity_aligned_frame,const sensor::RangeData& range_data) const {const sensor::RangeData cropped =sensor::CropRangeData(sensor::TransformRangeData(range_data, transform_to_gravity_aligned_frame),options_.min_z(), options_.max_z());return sensor::RangeData{cropped.origin,sensor::VoxelFilter(cropped.returns, options_.voxel_filter_size()),sensor::VoxelFilter(cropped.misses, options_.voxel_filter_size())};
}
最后总结一下,就是做激光矫正的时候,都把每个点的位置坐标转化到local坐标系下,转换完了之后呢,再转化到统一的同一个激光雷达坐标系下,变成一帧比较独立的激光。
cartographer 源码解析 (五)相关推荐
- (Nacos源码解析五)Nacos服务事件变动源码解析
Nacos源码解析系列目录 Nacos 源码编译运行 (Nacos源码解析一)Nacos 注册实例源码解析 (Nacos源码解析二)Nacos 服务发现源码解析 (Nacos源码解析三)Nacos 心 ...
- 【cartographer源码解析--外推器】
cartographer源码解析–外推器 文章目录 cartographer源码解析--外推器 前言 一.cartographer中的PoseExtrapolator类 二.接受数据,并分析处理逻辑 ...
- Tomcat源码解析五:Tomcat请求处理过程
前面已经分析完了Tomcat的启动和关闭过程,本篇就来接着分析一下Tomcat中请求的处理过程. 在开始本文之前,咋们首先来看看一个Http请求处理的过程,一般情况下是浏览器发送http请求-> ...
- Spring源码解析(五)-Bean的实例化流程(上)
在前面已经完成了对需要实例化bean的收集并封装成BeanDefinition,并且将BeanPostProcess等组件进行了提前实例化.接下来就到了容器启动的最后一步,也是最复杂的一步-实例化be ...
- spring源码解析五
2019独角兽企业重金招聘Python工程师标准>>> 1.创建用于承载属性的BeanDefinition 这是一个接口,在spring中存在三种实现:RootBeanDefinit ...
- cartographer源码解析(二)node_main.cc文件详解
描述 cartographer学习笔记(二) 这一篇主要分析node_main.cc的代码 主函数main() 主函数 int main(int argc, char** argv) {google: ...
- Cartographer源码解析——位姿推测器PoseExtrapolator
这一章主要了解外推器,上一章讲到激光点云畸变的矫正,其中最关键的是推测估计出一帧点云中每一个激光点所对应的机器人的位姿.这里就用到了外推器去推测每个发射点的位置. 具体代码调用如下: // Step: ...
- mybatis源码解析(五) --- typehandler注册和处理的查询结果对象的类型转换
上一次分析了mapper接口动态代理调用的处理过程,在查询结果后,然后转换的java所对应的类型,这篇文章将对这个类型转换处理详细介绍,在Configuration中TypeHandlerRegist ...
- QT源码解析(一) QT创建窗口程序、消息循环和WinMain函数
版权声明 请尊重原创作品.转载请保持文章完整性,并以超链接形式注明原始作者"tingsking18"和主站点地址,方便其他朋友提问和指正. QT源码解析(一) QT创建窗口程序.消 ...
最新文章
- HP-UX 六大虚拟化技术之“分区”
- Angular应用input和div标签页的动态创建场景
- 自动化运维平台中的统一认证接入与单点登录实现
- 【ZOJ - 3212 】K-Nice (构造)
- 计算机网络技术实验,计算机网络技术实验
- 【金融工程实验】【matlab】使用candle函数画日均k线图
- Zend Framework 简介
- Android Studio|简单记事本开发
- php ZipArchive 压缩整个文件夹 - 自带ZipArchive类 - PHP递归创建目录压缩包
- 安装JDK并配置环境变量(详细图文介绍)
- 如何利用计算机换算16进制,16进制怎么转换10进制?计算机进制转换方法汇总
- 输入一系列整数,建立二叉排序树,并进行前序,中序,后序遍历。
- U盘引导盘制作工具---Rufus
- python+selenium之元素、下拉列表的定位
- 通过mtd读写flash_Linux下读写FLASH驱动——MTD设备分析
- 使用webpack-cli或vue-cli 解决ie兼容性与报错问题
- python多窗口传递信息_PyQT5 中两个界面之间数据传递
- SD-WAN行业经常说CPE、uCPE、vCPE是什么意思,各自有什么区别和应用场景?
- 本题要求实现一个函数,可统计任一整数中某个位数出现的次数。例如-21252中,2出现了3次,则该函数应该返回3
- 万字长文深度复盘:创业一年半,一败涂地,我到底犯了什么错?
热门文章
- Mac Grapher(图形软件)
- 马自达推出首个混合动力车型
- for(Map.EntryString, String entry:params.entrySet())
- hdu 5873 Football Games 模拟、兰道定理Landau's Theorem
- 【夏目鬼鬼分享】springboot搭建阿里Druid数据源监控
- unity打包安卓(anroid)APK及安卓环境设置
- 计算机组成原理研究生试题三,计算机组成原理研究生入学试题.doc
- 利用Python进行数据分析(Ⅴ)
- 使用selenium实现前程无忧简历自动刷新
- 树莓派ssh远程连接