前言
最近看了一篇深蓝学院的推文介绍了如何将多线旋转式激光雷达的点云数据按线束进行数据处理,并探讨了这种线束分离对语义分割等算法的影响。我在本科毕设的时候也专门研究过KITTI点云数据的处理,这里把当初的一些想法写下来,算是对这篇推文做个补充。主要内容就是KITTI点云数据是如何保存的,这些数据的一些规律,以及如何将点云分到64条线中。代码已经放在github了。
1. KITTI点云数据结构
KITTI将64线点云数据保存成二进制bin文件,文件里的数据按Nx4的结构排列,其中N是点的个数,4是x,y,z,intensity(反射率)四个字段。我们在使用时,常常要将这N个点分到64条线中。比如LOAM里需要在每条线上提取线和面特征,RangeNet里需要将点云重新组织成Hx64的range image。
2. 规律
大家在使用Velodyne的64线激光雷达时,可以在driver里设置返回的数据包为Hx64,这样在读数据的时候直接一条线一条线的读就可以把线分好。KITTI的点云其实也是按这个规律保存数据的,下面以Odometry 00序列000000.bin为例说明这个问题。
我们先写个简单的脚本分析一下数据的规律。首先读取00序列的第一帧,并且对每个点计算点的方位角
α
\alpha
α,也就是
α
=
a
t
a
n
2
(
x
y
)
.
\alpha=\mathop{atan2}(\frac{x}{y}).
α=atan2(yx).
然后把每个点的方位角按顺序画出来。脚本如下:
#! -*-coding=utf-8-*-
import numpy as np
import matplotlib.pyplot as plt
data = np.fromfile('000000.bin',dtype=np.float32).reshape(-1,4) #读取bin文件reshape成Nx4
hori_angle = np.arctan2(data[:,0],data[:,1])/np.pi*180 #计算方位角
plt.plot(hori_angle)
plt.xlabel('Point Index')
plt.ylabel('Azimuthal Angle (degrees)')
plt.show()
上图展示了运行结果。可以看出方位角呈周期排布,仔细数一数的话可以发现一共有64个周期,正好对应了激光雷达的64线。由此可以猜测KITTI的激光数据是一条线一条线保存的。那么这64条线是按1,2,3,…,64这样按顺序排列还是随机排列的呢。我们将上面的脚本改成画高度角,
#! -*-coding=utf-8-*-
import numpy as np
import matplotlib.pyplot as plt
data = np.fromfile('000000.bin',dtype=np.float32).reshape(-1,4) #读取bin文件reshape成Nx4
elev_angle = np.arctan2(data[:,2],np.sqrt(data[:,1]**2+data[0,:]**2))/np.pi*180 #计算高度角
plt.plot(elev_angle)
plt.xlabel('Point Index')
plt.ylabel('Elevation Angle (degrees)')
plt.show()
结果如上所示。可以看到高度角从大到小变化,这意味着这64条线确实是按顺序排列的。值得注意的是上图每条线内的高度角的变化规律并不一样。理想情况下,每条线内的高度角应该是一个定值,反映在上图应该呈现64个阶梯。而实际上由于KITTI已经做了去运动畸变处理(详情见这里),所以导致了这些奇怪的形状。
现在的部分算法通常按照高度角来分线,根据上面的分析,高度角的分布其实并不那么有规律,相邻线之间的高度角会存在混叠,所以现在的分线方法效果并不好。那为什么方位角的分布会那么有规律呢?这显然是去运动畸变的时候点的x,y,z分量变化不同所导致的,有兴趣的同学可以自己做下数学分析,应该不难,我就懒得分析了哈哈。
3. 方法
根据上面观察,我们可以得出KITTI点云数据的两个结论,
- 数据是一条线一条线按线束顺序保存的;
- 方位角呈现周期规律,而高度角由于去运动畸变的原因不太规律。
由此,我们对点云的分线方法就显而易见了。首先读取整个bin文件,计算每个点的方位角,然后按方位角的周期分线,一个周期就是一条线。需要注意的问题是,KITTI在保存数据时,NAN点(如打到天空的点)是不保存的,导致每条线上的数量不恒定。甚至在一些空旷的场景(如01高速路),有些线全部打到天上去了,导致保存下来的数据线束小于64。我这儿本来想展示一张01序列测试的图,但是上次电脑存储不够把这个序列清理掉了,大家可以自己拿上面的脚本去测试一下。完整的C++处理代码可以在我的github上查看。下图展示了用我们方法处理后生成的range image(一半的视野),看上去还是比较平滑的。