本文提出了一种用于生成高分辨率毫米波雷达点云的方法:RadarHD,端到端的神经网络,用于从低分辨率雷达构建类似激光雷达的点云。本文通过在大量原始雷达数据上训练 RadarHD 模型,同时这些雷达数据有对应配对的激光雷达点云数据。本文的实验表明,即使是在未遇见过的场景以及存在严重烟雾遮挡的情况下,RadarHD也能生成丰富的点云数据。同时,这些生成的点云也能应用于现有的激光雷达里程计和建图方法中。
项目链接:https://akarsh-prabhakara.github.io/research/radarhd/
1. RadarHD System Design
首先讲一下RadarHD系统设计,RadarHD的核心目标是提高雷达信号的分辨率,使它们类似激光雷达。首先,可以考虑简单地插值方法并对雷达图像进行放大,就像相机图像上的超分辨率。但与相机图像不同,低分辨率雷达图像的邻域缺乏相似性。本文提出的RadarHD方法超越了传统的邻域方法,探索机器学习方法来获得全局图像理解。
ML 设计选择:机器学习方法的选择上,通常可以考虑一个简单的CNN模型,依次使用卷积层并构建输入图像的全局视图。然而,由于本文最终对超分辨率任务感兴趣,需要结合考虑全局图像理解和上采样。因此,RadarHD 建立在基于 U-Net 的编码器-解码器架构之上,该架构通常用于图像分割任务,使用它来解决雷达超分辨率任务。U-Net的编码器可以用来理解各种雷达伪影并获得真实世界目标的语义准确表征,解码器则使用这种表征来执行超分辨率。
设计挑战:本节的剩下章节描述了RadarHD模型的关键设计,帮助U-Net有效地学习。(1) 有效地表示、预处理和输入雷达I/Q数据。(2) 设计 U-Net 网络消除虚假伪影,同时保留场景中真实对象的数据。(3) 设计损失函数以确保保留预期输出中的尖锐线条等特征(如图 2所示)。
雷达数据表示:
- 本文选择的毫米波雷达装置可以提供原始I/Q数据流,根据需要进行进一步处理。典型的雷达处理包括空间傅立叶变换,输出具有反射雷达信号在距离和方位上的强度热力图。热力图被阈值化为“点云”。CFAR就是常用的阈值检测器,图2展示了CFAR处理后的雷达图像。本文中RadarHD对处理后得到的热力图应用非常低的阈值,以保留主要的反射信号许多伪影信号,将其留给ML模型来学习和过滤伪影。 在图2中,CFAR处理后雷达图像包含110个非零像素,RadarHD处理后包含1606个非零像素。
- 坐标表示:雷达本质上是径向测量的,为了捕捉这些径向和方位角的变化,需要进行径向/方位角处理。但机器学习中卷积层的主要学习元素是一个滤波器,它在输入的高度和宽度上执行2D卷积运算。为了利用这一点,本文选择了极坐标表示,这样当滤波器分别处理高度和宽度时,它们就会自然地沿着距离和方位角移动。因此,本文处理后的热力图为0-10米的图像,角度为-90°-90°。由于雷达方位分辨率较差,雷达图像大小为 64×256,激光雷达图像大小为 512×256。
神经网络架构:
- 本文选择的网络架构为 U-Net网络 (图 3所示),它的编码器进行去噪处理并获得准确的语义场景理解,解码器部分来执行超分辨率。RadarHD的设计与传统的U-Net有所不同,对于超分辨率任务,U-Net是不对称的(5个下采样和8个上采样)。
- 为了消除伪影,本文选择历史帧作为输入,同时在当前帧上执行超分辨率。实验发现使用 40 个历史帧(过去2 秒时间)作为输入效果最好,即使在静态情况下,使用历史帧也会保持输出平滑和更少的抖动。
神经网络训练方法:
- 损失函数的选择上,主要是保留激光雷达图像中出现的尖锐线条等特征。为了比较两幅图像,一个是真值,一个是网络输出,首先考虑最标准的损失函数——pixel wise loss。本文选择的真值标签是二分类激光雷达图像。将此二值图像与网络sigmoid输出层进行比较。在所有像素上使用平均二元交叉熵 (BCE)损失。这种像素级损失的目标是强制每个像素匹配预期输出,图4展示了使用BCE损失得到的输出结果。
- 为了促进输出图像中的清晰锐利线条,本文还使用Dice loss:
D
=
1
−
2
∑
i
=
1
N
o
i
g
i
∑
i
=
1
N
o
i
2
+
∑
i
=
1
N
g
i
2
D=1-\frac{2 \sum_{i=1}^{N} o_{i} g_{i}}{\sum_{i=1}^{N} o_{i}^{2}+\sum_{i=1}^{N} g_{i}^{2}}
D=1−∑i=1Noi2+∑i=1Ngi22∑i=1Noigi,图4分析了 BCE 损失 和 Dice 损失之间不同权重的输出结果。Dice 损失权重越大,反而会导致消除某些重要特征。
2. Implementation
下面介绍本文的具体实现:
- 硬件上,RadarHD是使用TI毫米波雷达AWR1843实现的,一种最先进的单芯片雷达,理论距离分辨率为3.75 cm,方位角分辨率为15°。RadarHD的目标是提高方位角分辨率,使用 AWR1843 和 DCA1000EVM 来收集原始 I/Q 数据流。
- 本文的试验台由雷达、激光雷达和相机组成,相机用于调试——所有传感器时间上进行了同步。测试平台安装在移动试验台上。整个数据存储库由大约 200k 雷达 I/Q - 激光雷达对组成,在整个 5147平方米区域内收集。
- 真值设备,使用 Ouster OS 0 - 64 线激光雷达作为真值。激光雷达配置为0.35°方位角分辨率。只使用前置激光雷达点云进行超分辨率任务,还将激光雷达的仰角视场限制在+/-30厘米以内。
- Baseline上,使用具有不同的阈值(1dB、3dB、5dB、8dB)的CFAR算法作为我们的基线。
- 训练和测试数据:在包含家具、电子产品、墙壁、会议室和立方体在内的大型办公空间中训练RadarHD模型。在三个环境中进行了测试: (1) 相同环境中的未使用过的数据,在不同的办公空间中收集不同的轨迹,以及 (2) 相似的环境但不同的空间结果,以及 (3) 不同的环境,包括建筑游艇和室外环境。
3. Results
下面介绍本文实验结果,首先是点云比较。为了与激光雷达点云进行比较,首先转换距离-方位输出图像,得到一个点列表及其(x,y)位置。然后使用两个常用的点云相似度度量来比较点云误差:(1) Chamfer距离,找到一个点云中的每个点到另一个点云的最近点,取所有这些距离的平均值,得到每个点云对的误差。(2) Modified Hausdorff 距离,找到最近的点并获得距离中值。
与CFAR基线的比较:在这里,本文展示了在 19 个不同的轨迹上在不同 CFAR 阈值下的的性能。如图 5 所示,RadarHD获得了0.24m修改后的 Hausdorff 中值误差和 0.36m Chamfer中值误差。另一方面,CFAR 因阈值而异,像 1dB 阈值这样的低阈值创建了比激光雷达更密集的点云(5倍),而像 8dB 这样的高阈值将只有激光雷达捕获的点数的10%。尽管密度不同,但它们都没有与激光雷达点云有任何结构相似性。所以从1dB到8dB,随着阈值的增加和密度的降低,两个点云误差度量累积分布函数向左移动。然而,由于最高阈值生成的点云非常稀疏,并且与地面实况不相似,CDF 不会随着阈值的增加而向左移动更多。事实上超过 8dB,点数少于激光雷达的 1%。生成的点云不仅将 CFAR 提高了 3.5 倍(mod-Hausdorff)和 2.7 倍(Chamfer),而且在结构上更类似于地面实况,如图 8 所示。
泛化性:图 6 显示了新环境中性能的变化。相似环境的中值误差为 0.75 米 (mod-Hausdorff) 和 0.8 米 (Chamelfer),不同环境的中值误差为 0.94 米 (mod-Hausdorff) 和 1.03 米 (Chamelfer)。可能会感觉这些中位数与图5中一些CFAR CDF的中位数相似。然而,这里想指出RadarHD的点云仍然更好的三个重要原因。首先,CFAR CDF 从 x 轴0.4m处开始;相比之下,即使对于相似和不同的环境,可以看到RadarHD CDF 从 0.08 m 开始。这表明本文提出的系统可以准确地推断出点云中的很大一部分。其次,由于Chamfer距离和修改后的Hausdorff距离都具有最近邻点关联,因此不能完全捕获结构相似性。然而,可以从图8中定性地看出,本文提出的系统确实生成了有意义的点。第三,为了定量地显示提高精度的影响,本文将RadarHD与CFAR在两个关键应用中进行了比较——里程计和建图。
烟雾:为了研究遮挡的影响,在测试装置周围构建了一个烟雾室。雷达保持静态,在没有烟雾和不同程度的烟雾的情况下捕获相同的场景。由于收集到的雷达信号保持不变,预计性能与没有烟雾相似。图7通过显示RadarHD的性能不会下降来验证这一点,无论是低烟雾密度还是高烟雾密度,雷达性能都没有很大变化。
里程计和建图比较:里程计中本文使用绝对轨迹误差ATE来进行评估,图10中在所有情况下(包括不同的环境),无论阈值设为何值,RadarHD里程计精度都优于CFAR。定性地说,可以清楚地看到图9中RadarHD和CFAR之间的差异。RadarHD的性能可与Radar+IMU等方法媲美(0.8m)。
通过识别现实世界中相同物理特征的关键点(如房间的角落)对建图性能进行基准测试,然后计算RadarHD和真值之间相应关键点的欧氏距离误差。图9显示了一个轨迹生成的地图的定性比较。很明显,CFAR没有提供任何有意义的特征来提取关键点,而RadarHD与激光雷达相比实现了结构相似的地图。图10显示了不同环境下关键点之间的欧氏距离误差。只有当RadarHD生成无伪影、有意义的点云时,里程计或建图的良好性能才是可能的。
4. Model Code
U-Net参考代码如下:
class UNet1(nn.Module):
def __init__(self, n_channels, n_classes, bilinear=True):
super(UNet1, self).__init__()
self.n_channels = n_channels
self.n_classes = n_classes
self.bilinear = bilinear
self.inc = DoubleConv(n_channels, 64)
self.down1 = Down(64, 128)
self.down2 = Down(128, 256)
self.down3 = Down(256, 512)
factor = 2 if bilinear else 1
self.down4 = Down(512, 1024 // factor)
self.up1 = Up(1024, 512 // factor, bilinear)
self.up2 = Up(512, 256 // factor, bilinear)
self.up3 = Up(256, 128 // factor, bilinear)
self.up4 = Up(128, 64, bilinear)
self.up5 = Up_nocat(64, 64, bilinear)
self.up6 = Up_nocat(64, 64, bilinear)
self.up7 = Up_nocat(64, 64, bilinear)
self.outc = OutConv(64, n_classes)
self.final_sigmoid = nn.Sigmoid()
def forward(self, x):
x1 = self.inc(x)
x2 = self.down1(x1)
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)
x = self.up1(x5, x4)
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)
x = self.up5(x)
x = self.up6(x)
x = self.up7(x)
conv_out = self.outc(x)
logits = self.final_sigmoid(conv_out)
return logits
U-Net各模块参考代码如下:
# Core components of U-net
# Adapted from: https://github.com/milesial/Pytorch-UNet/blob/master/unet/unet_parts.py
import torch
import torch.nn as nn
import torch.nn.functional as F
class DoubleConv(nn.Module):
"""(convolution => [BN] => ReLU) * 2"""
def __init__(self, in_channels, out_channels, mid_channels=None):
super().__init__()
if not mid_channels:
mid_channels = out_channels
self.double_conv = nn.Sequential(
nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),
nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.double_conv(x)
class Down(nn.Module):
"""Downscaling with maxpool then double conv"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.maxpool_conv = nn.Sequential(
nn.MaxPool2d(2),
DoubleConv(in_channels, out_channels)
)
def forward(self, x):
return self.maxpool_conv(x)
class Up(nn.Module):
"""Upscaling then double conv"""
def __init__(self, in_channels, out_channels, bilinear=True):
super().__init__()
# if bilinear, use the normal convolutions to reduce the number of channels
if bilinear:
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
self.conv = DoubleConv(in_channels, out_channels, in_channels // 2)
else:
self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
self.conv = DoubleConv(in_channels, out_channels)
def forward(self, x1, x2):
x1 = self.up(x1)
# input is CHW
diffY = x2.size()[2] - x1.size()[2]
diffX = x2.size()[3] - x1.size()[3]
x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
diffY // 2, diffY - diffY // 2])
# if you have padding issues, see
# https://github.com/HaiyongJiang/U-Net-Pytorch-Unstructured-Buggy/commit/0e854509c2cea854e247a9c615f175f76fbb2e3a
# https://github.com/xiaopeng-liao/Pytorch-UNet/commit/8ebac70e633bac59fc22bb5195e513d5832fb3bd
x = torch.cat([x2, x1], dim=1)
return self.conv(x)
class Up_nocat(nn.Module):
"""Upscaling then double conv"""
def __init__(self, in_channels, out_channels, bilinear=True):
super().__init__()
# if bilinear, use the normal convolutions to reduce the number of channels
if bilinear:
self.up = nn.Upsample(scale_factor=(1,2), mode='bilinear', align_corners=True)
self.conv = DoubleConv(in_channels, out_channels, in_channels)
def forward(self, x1):
x1 = self.up(x1)
return self.conv(x1)
class Up_nocat_sym(nn.Module):
"""Upscaling then double conv"""
def __init__(self, in_channels, out_channels, bilinear=True):
super().__init__()
# if bilinear, use the normal convolutions to reduce the number of channels
if bilinear:
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
self.conv = DoubleConv(in_channels, out_channels, in_channels)
def forward(self, x1):
x1 = self.up(x1)
return self.conv(x1)
class OutConv(nn.Module):
def __init__(self, in_channels, out_channels):
super(OutConv, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)
def forward(self, x):
return self.conv(x)