论文RODNet:A Real-Time Radar Object Detection Network Cross-Supervised by Camera-Radar Fused Object 3D
原论文地址:https://arxiv.org/abs/2102.05150
代码地址:https://github.com/yizhou-wang/RODNet
摘要
在目标检测中,雷达虽然在恶劣天气下有精准采集信号的优势,但缺乏语义信息,若对采集的雷达信号进行手工标注,则非常费时,因此可以同时采集RGB数据,通过对RGB图像进行目标识别,再把识别的结果与雷达信息同步,达到对雷达信号自动标注的目的。该算法将毫米波雷达信号与摄像头采集的RGB信号融合对模型进行监督训练,最终达到只输入雷达信号,就能检测出某位置有某物的效果。
基础知识回顾
调频连续波雷达(Frequency Modulated Continuous Wave Radar,简称FMCW雷达)是指发射频率受特定信号调制的连续播雷达,通过对信号分析,以测量距离、速度和角度。
由上半部分所示,波形的振幅(纵坐标)不变,周期逐渐变小,波形越来越密集;下半图表示该波形的频率随时间呈线性增长,周期T=1/f,频率越来越大,则周期越来越小,B表示带宽,Tc表示持续时间,斜率S=B/Tc,这种波形的信号称为chirp。
如上图所示,在雷达中,chirp由上图1合成器生成,由发射天线TX射出,打到目标以后反射回来,由接收天线RX接收,期间产生了一个时间延迟 τ (如下图),把两个chirp送入mixer混合器中,根据三角函数的计算法则,混合信号里有高频和低频(Tx和Rx的频率一样,混合以后既有频率相加的部分,也有频率相减的部分),使用低通滤波器(LPF)过滤高频信号,就得到了如下图下半部分所示的信号,两个不同时间发射的相同频率的信号,频率相减得到了一个频率常数f,以及时间延迟 τ 。
对采集到的信号做快速傅里叶(FFT)变换能够得到目标的距离,角度及速度,距离(Range)和角度(Angle)两个信息可以在极坐标系下形成BEV(鸟瞰图)视角的射频(RF)图像,每一个像素点的横纵坐标值相当于雷达信号中的距离和角度,使用CFAR对雷达信号做分析,能够得到预测目标的目标与角度,然后将其标注在图像里,如下图所示。
***值得注意的是,对于RGB图像来说,图像是三通道的,分别由红,绿,蓝组成,图像在计算机中可以等价为一个矩阵,每一个坐标的值表示了该点的像素值,如(255,10,20)是一个非常红的点。
**而在雷达的RF图像中,雷达信号本身并不是图像,因此其中每个点的值的横纵坐标仅代表距离和方位角,复数表示为a+jb。
*但是,在使用计算机读取RF图像(本质上是个矩阵)时,可以将其看成一张2个通道的图片,一个通道表示实数值,另一个通道表示虚部值,然后给横纵轴加一个笛卡尔坐标(0,1,2,3…),就可以把RF当作普通的双通道图像来处理了。然后在这张图像上做关键点检测就变得容易的多,假设在下面这张图(50,50)的位置检测出一个点,并识别为自行车,再将其转化为极坐标系,就得到了这个点的距离与方位角。
基于这个想法,RODNet算法把雷达信号转化为射频图像进行处理。
算法原理
简单说一下该算法的思想,对于深度学习,通俗简单的说就是输入一个数据,网络根据这个数据预测结果,然后告诉它正确结果,它就可以学习这类数据到结果的映射,最终预测的越来越准确。 对于刚才说的雷达数据来说,应该输入一个RF图像,网络预测图中哪些位置有目标,分别是什么,然后再把目标的坐标通过笛卡尔与极坐标的相互转化,就可以得到目标的角度和距离了。
此时,存在一个很大的问题,那就是雷达数据采集的时候会生成很多信号,深度学习也需要很多数据,但是在训练的时候,网络需要提前知道图片里哪些位置有东西,是什么,也就是说要提前告诉网络正确答案才行。雷达采集的信号有非常多的杂波,使用CFAR能够对信号进行检测,并且能够检测出来哪些地方有东西,但是,CFAR的检测并不准确,并且经常出现错误(如上图的“teacher”部分中的Peak Dectection的图片中检测出了很多个点)。有一个很好的方法就是手工标注,一张一张的看雷达图,然后用鼠标进行标注哪些地方有人,汽车等等,但是人看着射频图像也不见得能准确分类。
该作者在使用雷达采集数据的时候,同时配一个摄像头采集数据,两个设备同步采集相同场景的数据。首先对雷达产生的RF图像进行CFAR检测,在一张图上检测出了很多个点,有些是真的,有些是虚报。然后,使用3D目标检测算法对图像进行检测,对图像检测的精度会远高于使用CFAR对RF图的检测结果,然后会在图像中预测出哪些位置有人,自行车或者汽车。最后,将CFAR在RF图上的检测结果与使用3D检测算法在RGB图像上的结果进行匹配取交集,就得到了相对更加正确的答案,并将其作为标签,如上图的“teacher”部分中最右边的结果。
文章的主干网络名为RODNet,把一张RF图像输入进RODNet,输出是一张置信度地图。置信度图是一个矩阵,每一行一列都有一个数值,数值的大小在0-1之间,表示这点有目标的概率。假设RF图像的大小为100x100,置信度图(50,50)的位置的值为(0.74,person),那就表示在RF图像的正中心的位置,有74%的概率是个行人。 那么相对的,也要提前给网络设置好正确答案,即一张真正的置信度地图,也就是把上一段提到的标签转化为置信度地图。
标签,置信度,损失函数与极大值抑制
把标签转为置信度地图也有一个问题,假设在一张射频图像中(50,50)的位置上有一辆车,现在网络预测的结果是(49,49)的位置上有辆车,这个答案对还是不对? 当然是对的,因此在设置置信度地图的时候,不能只在一个点上设置值,否则标签就会告诉网络(49,49)的位置没有车,预测错误,这样模型就会很难收敛,找不到梯度下降的方向。 在设置置信度图的时候,会在该点产生一个高斯分布,(50,50)的置信度是1,(49,49)的置信度是0.99,(35,35)的置信度也许是0.6,这样模型就知道自己该往哪个方向走了,允许预测有点偏差。然后就是类别,标签会对每一类单独做一个置信度图,比如一张图里有一个人,一辆车,一辆自行车,那么就会同时生成三张置信度图,每个图代表一类的概率分布,然后三张图拼在一起。如果图片只有一个类别,那么两外两个图就全为0,这样就也是三张图叠在一起,这么做是为了数据的统一,以防有时候数据是一个通道,有时候多个通道。
然后该文章对人,自行车和汽车,分别设置了不同的方差(高斯分布里的参数,方差的大小影响分布的范围,方差设置的小,离得远的坐标的置信度就会很低),个人理解就是这三个物体的形状大小是不一样的,所允许的误差范围也不一样。对于汽车而言,如果预测的点离真实的点比较远,那也认为是对的,毕竟汽车很大,因此给了较大的方差,这里的方差可以理解为允许误差。
同时,该算法与目标检测算法一样,有时候一个物体会预测出很多个点,这时候就要进行抑制,文章使用的是L-NMS(位置极大值抑制),非常类似Yolo算法里的NMS,yolo里的NMS是通过box的IOU进行抑制,这里是通过距离之间的OLS来抑制,一个道理,不过这里对每个类别分别设置了一个值,与上述的方差类似,也是基于不同大小设置的允许误差。具体可先了解NMS,再看L-NMS。损失函数使用自己提出的OLS,用来衡量预测点和真实点之间的差距,通过回归来修正预测分布。
预测点的位置使用的损失函数为OLS,人体关键点检测算法Openpose非常相似,Openpose使用的损失函数为OKS,与这里的OLS也基本一致,分类函数使用的交叉熵。
这就是作者提到的雷视融合,其实际上整个teacher部分只解决了数据标注的问题,并且该算法没有给出teacher部分的代码,只给了处理好的数据,没有真正的将融合数据放到算法里做多模态输入。
数据集
CRUW是一个基于雷达频域图像的自动驾驶数据集:
目前是唯一一个开源的、多场景的、大型雷达频域图像检测数据集
论文:Rethinking of Radar’s Role: A Camera-Radar Dataset and Systematic Annotator via Coordinate Alignment
数据集:https://www.cruwdataset.org/
该数据集为作者团队自行采集的数据,对下载的数据做如下处理:
# download all zip files and unzip
unzip TRAIN_RAD_H.zip
unzip TRAIN_CAM_0.zip
unzip TEST_RAD_H.zip
unzip TRAIN_RAD_H_ANNO.zip
unzip CAM_CALIB.zip
# make folders for data and annotations
mkdir sequences
mkdir annotations
# rename unzipped folders
mv TRAIN_RAD_H sequences/train
mv TRAIN_CAM_0 train
mv TEST_RAD_H sequences/test
mv TRAIN_RAD_H_ANNO annotations/train
# merge folders and remove redundant
rsync -av train/ sequences/train/
rm -r train
可得到如下三个文件夹sequences,annotations,calib,sequences里面包含train和test,train里有40文件夹,每个文件夹里包含2个文件夹,分别是摄像头采集的RGB图像(897张)和雷达采集的RF图像(做两次fft变换,得到的距离-方位角坐标,转化为图像,共897*4张),calib表示摄像头的参数(文中代码读取了,但似乎没用到),annotations里包含40个txt文件,每个文件里有897行数值,每行数值表示了雷达检测的物体所在的位置(距离-角度)及类别(3分类,汽车,自行车,行人)。
data_root
- sequences
| - train
| | - <SEQ_NAME>
| | | - IMAGES_0
| | | | - <FRAME_ID>.jpg
| | | | - ***.jpg
| | | - RADAR_RA_H
| | | - <FRAME_ID>_<CHIRP_ID>.npy
| | | - ***.npy
| | - ***
| |
| - test
| - <SEQ_NAME>
| | - RADAR_RA_H
| | - <FRAME_ID>_<CHIRP_ID>.npy
| | - ***.npy
| - ***
|
- annotations
| - train
| | - <SEQ_NAME>.txt
| | - ***.txt
| - test
| - <SEQ_NAME>.txt
| - ***.txt
- calib
依赖库
首先安装作者提供的cruw库
其他库的要求在requirements里面有
git clone https://github.com/yizhou-wang/cruw-devkit.git
cd cruw-devkit
pip install .
cd ..
如果git指令无法使用,就手动进入该链接下载,然后在命令行进入该项目,输入 pip install . 即可。
首先运行 prepare_data.py 文件进行数据处理,import一些库的时候可能无法找到本地库,在最上面添加以下代码即可,如果使用的是pycharm,直接右键该项目文件,Mark Directory as -> Resource Root 即可。
import sys
sys.path.insert(0,os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
数据处理
以下是prepare_data.py的代码,首先要定义参数,如果使用命令行运行,直接传参即可,如果在pycharm里运行,手动把参数添加进来
def parse_args():
parser = argparse.ArgumentParser(description='Prepare RODNet data.')
parser.add_argument('--config', default='/Users/chenzihao/PycharmProjects/pythonProject/RODNet/configs/config_rodnet_cdc_win16.py',type=str, dest='config', help='configuration file path') #配置文件路径
parser.add_argument('--data_root', default='/Volumes/T7/CRUW/',type=str,
help='directory to the dataset (will overwrite data_root in config file)')#数据集路径
parser.add_argument('--sensor_config', type=str, default='sensor_config_rod2021')#传感器类型
parser.add_argument('--split', type=str, dest='split', default='train,test',
help='choose from train, valid, test, supertest')#数据集划分类型
parser.add_argument('--out_data_dir', type=str, default='/Volumes/T7/cruw_/',
help='data directory to save the prepared data')#数据处理以后的存储路径
parser.add_argument('--overwrite', action="store_true", help="overwrite prepared data if exist")
args = parser.parse_args()
return args
需要跳转到函数里的代码,用数字表示,遇到该注释可先往下看,看完再回来看
if __name__ == "__main__":
args = parse_args() #读取定义参数
data_root = args.data_root # 传入数据路径
if args.split == '':
splits = None
else:
splits = args.split.split(',')# 如果划分类不是空,以逗号分界,划分出train和test
out_data_dir = args.out_data_dir # 读取存储路径
os.makedirs(out_data_dir, exist_ok=True) #如果没有存储路径就创建
overwrite = args.overwrite
dataset = CRUW(data_root=data_root, sensor_config_name=args.sensor_config)# **1**使用cruw类将各种配置文件传入,跳转如下段代码所示
config_dict = load_configs_from_file(args.config)#各种配置文件的路径和名称
config_dict = update_config_dict(config_dict, args) # update configs by args
radar_configs = dataset.sensor_cfg.radar_cfg #雷达信号的信息,距离-角度,帧率,类型等等等等
if splits == None:
prepare_data(dataset, config_dict, out_data_dir, split=None, save_dir=out_data_dir, viz=False,
overwrite=overwrite)
else:
for split in splits:
if split not in SPLITS_LIST:
raise TypeError("split %s cannot be recognized" % split)
for split in splits:
save_dir = os.path.join(out_data_dir, split)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
print('Preparing %s sets ...' % split)
prepare_data(dataset, config_dict, out_data_dir, split, save_dir, viz=False, overwrite=overwrite)
**1-1**
上面提到的
dataset = CRUW(data_root=data_root, sensor_config_name=args.sensor_config)
以下是该类的代码
import os
import json
from cruw.config_classes import SensorConfig, ObjectConfig
from cruw.mapping import confmap2ra, labelmap2ra, get_xzgrid
class CRUW:
""" Dataset class for CRUW. """
def __init__(self,
data_root: str,
sensor_config_name: str = 'sensor_config',
object_config_name: str = 'object_config'):
self.data_root = data_root #数据路径
self.sensor_cfg = self._load_sensor_config(sensor_config_name) #传感器配置文件,进入该函数,如下段代码
self.dataset = self.sensor_cfg.dataset #
self.object_cfg = self._load_object_config(object_config_name)
self.range_grid = confmap2ra(self.sensor_cfg.radar_cfg, name='range')
self.angle_grid = confmap2ra(self.sensor_cfg.radar_cfg, name='angle')
***1-2***
上面class类中的 _load_sensor_config 函数
def _load_sensor_config(self, config_name) -> SensorConfig:
"""
Create a SensorConfig class for CRUW dataset.
The config file is located in 'cruw/dataset_configs' folder.
:param config_name: Name of configuration
:return: SensorConfig
"""
# check if config exists 检查配置文件是否存在
this_dir = os.path.dirname(os.path.abspath(__file__)) #读取当前环境的运行路径,请注意该路径不是项目路径,是你刚才安装的cruw库的路径,路径是 你创建的环境 envs/环境名称/lib/python3.*/site-pachages/cruw
cfg_path = os.path.join(this_dir, 'dataset_configs', '%s.json' % config_name)# 读取该文件夹下的一个json文件,这个json文件记录了传感器的参数,详见下
if os.path.exists(cfg_path):
# load config file from dataset_configs folder
with open(cfg_path, 'r') as f:
data = json.load(f)
elif os.path.exists(config_name):
# load config file from an outside json file
with open(config_name, 'r') as f:
data = json.load(f)
else:
assert os.path.exists(cfg_path), 'Configuration {} not found'.format(config_name)
cfg = SensorConfig.initialize(data) #初始化传感器参数
#加载校准参数,这里会读取数据集里calib文件夹下的yaml文件,但是这个文件的路径记录在了json文件里,因此要么在json文件里改一下路径,要么就把calib文件夹复制到项目文件tools路径下,下面看一下传感器的json文件
cfg.load_cam_calibs(self.data_root, cfg.calib_cfg['cam_calib_paths']) #相机参数矫正,跳转如下**1-3**
#如果没有正确配置路径,就会警告加载校准文件失败
if not cfg.calib_cfg['cam_calib']['load_success']:
print('warning: loading calibration data failed')
return cfg
{
该文件在envs/环境名称/lib/python3.*/site-pachages/cruw/路径下,里面有3个json,根据自己用哪个配置而定
"dataset": "ROD2021", # 数据集类型(名称)
"camera_cfg": {
"image_width": 1440,
"image_height": 864, # 图片的分辨率 1440*864
"frame_rate": 30, # 帧率30
"image_folder": "IMAGES_0", # 图片所在文件夹名称
"ext": "jpg" #格式
},
"radar_cfg": {
"ramap_rsize": 128, #这里的RF图相当于一张图片,r表示range距离
"ramap_asize": 128, #angle角度,把RF图当作一张128*128的图片
"frame_rate": 30, #跟图像的帧率一样
"crop_num": 3,
"n_chirps": 255, # 255个chirp
"chirp_ids": [0, 64, 128, 192], #雷达发射多个chirp的时候,要记录时间,以免跟返回来的信号混淆,可以理解一张图片跟4段雷达匹配
"sample_freq": 4e6, # 采样频率
"sweep_slope": 21.0017e12, # 斜率
"data_type": "ROD2021",
"chirp_folder": "RADAR_RA_H",
"ext": "npy", # 格式
"ramap_rsize_label": 122,
"ramap_asize_label": 121,
"ra_min_label": -60,
"ra_max_label": 60,
"rr_min": 1.0,
"rr_max": 25.0,
"ra_min": -90,
"ra_max": 90,
"xz_dim": [200, 151],
"z_max": 30.0
},
"calib_cfg": {
"cam_calib_paths": {
"2019_04_09": [ # 这里的yaml路径指向自己数据集calib路径,yaml里是相机的参数矩阵
"/Volumes/T7/CRUW/calib/2019_05_09/cam_0.yaml"
],
"2019_04_30": [
"/Volumes/T7/CRUW/calib/2019_05_09/cam_0.yaml"
],
"2019_05_09": [
"/Volumes/T7/CRUW/calib/2019_05_09/cam_0.yaml"
],
"2019_05_23": [
"/Volumes/T7/CRUW/calib/2019_05_09/cam_0.yaml"
],
"2019_05_29": [
"/Volumes/T7/CRUW/calib/2019_05_09/cam_0.yaml"
],
"2019_09_29": [
"/Volumes/T7/CRUW/calib/2019_09_29/cam_0.yaml"
]
},
"t_cl2cr": [0.35, 0.0, 0.0], #相机的平移向量
"t_cl2rh": [0.11, -0.05, 0.06],
"t_cl2rv": [0.21, -0.05, 0.06]
}
}
-**1-3**-
class类中的加载传感器函数中
cfg.load_cam_calibs(self.data_root, cfg.calib_cfg['cam_calib_paths'])
下面看该函数的介绍
def load_cam_calibs(self, data_root, calib_yaml_paths):
self.calib_cfg['cam_calib'] = {} #校准参数-相机 设置一个字典
self.calib_cfg['cam_calib']['load_success'] = True
for date in calib_yaml_paths.keys():
self.calib_cfg['cam_calib'][date] = {} # 在这个相机校准的字典中,先设置一个data
n_paths = len(calib_yaml_paths[date]) #yaml的数量,个人理解是单目相机一个,双目相机连个
calib_yaml_path = os.path.join(data_root, calib_yaml_paths[date][0]) #读取yaml配置文件路径
if os.path.exists(calib_yaml_path):
with open(calib_yaml_path, "r") as stream:
data = yaml.safe_load(stream) # 加载yaml的参数,里面就是一些矩阵,感兴趣的可以自己打开看看
K, D, R, P = parse_cam_matrices(data) #这里加载相机参数,跳转一下看看 **1-4**
self.calib_cfg['cam_calib'][date]['cam_0'] = {} # 赋值相机的各种参数
self.calib_cfg['cam_calib'][date]['cam_0']['camera_matrix'] = K
self.calib_cfg['cam_calib'][date]['cam_0']['distortion_coefficients'] = D
self.calib_cfg['cam_calib'][date]['cam_0']['rectification_matrix'] = R
self.calib_cfg['cam_calib'][date]['cam_0']['projection_matrix'] = P
else:
self.calib_cfg['cam_calib']['load_success'] = False
if n_paths == 2: #如果是双目相机
calib_yaml_path = os.path.join(data_root, calib_yaml_paths[date][1])
if os.path.exists(calib_yaml_path):
with open(calib_yaml_path, "r") as stream:
data = yaml.safe_load(stream)
K, D, R, P = parse_cam_matrices(data)
self.calib_cfg['cam_calib'][date]['cam_1'] = {}
self.calib_cfg['cam_calib'][date]['cam_1']['camera_matrix'] = K
self.calib_cfg['cam_calib'][date]['cam_1']['distortion_coefficients'] = D
self.calib_cfg['cam_calib'][date]['cam_1']['rectification_matrix'] = R
self.calib_cfg['cam_calib'][date]['cam_1']['projection_matrix'] = P
else:
self.calib_cfg['cam_calib']['load_success'] = False
**1-4**
def parse_cam_matrices(data):
camera_matrix = data['camera_matrix'] #相机内参 3*3的矩阵 [fx,0,cx;0,fy,cy;0,0,1]
distortion_coefficients = data['distortion_coefficients'] #畸变系数 1*5的矩阵
rectification_matrix = data['rectification_matrix']# 矫正矩阵 3*3矩阵
projection_matrix = data['projection_matrix'] #投影矩阵 3*4矩阵
#分别reshape成矩阵
K = np.array(camera_matrix['data'])
K = np.reshape(K, (camera_matrix['rows'], camera_matrix['cols']))
D = np.array(distortion_coefficients['data'])
D = np.reshape(D, (distortion_coefficients['rows'], distortion_coefficients['cols']))
D = np.squeeze(D)
R = np.array(rectification_matrix['data'])
R = np.reshape(R, (rectification_matrix['rows'], rectification_matrix['cols']))
P = np.array(projection_matrix['data'])
P = np.reshape(P, (projection_matrix['rows'], projection_matrix['cols']))
return K, D, R, P
到这里CRUW类的模块的内容基本是讲完了,主要就是获取传感器配置文件,数据路径和信息,然后打包成一个dataset,这个dataset包含以下信息:
angle_grid和range_grid表示角度和距离,这里用的还是复值,后面会通过极坐标与笛卡尔坐标的转化,转成正常数字。 object_cfg表示目标参数,一共分3类——行人,自行车和汽车,尺寸分别设置为0.5,1.0和3.0.表示三类物体的大小不同。
再回到prepare_data.py文件
if __name__ == "__main__":
args = parse_args() #读取定义参数
data_root = args.data_root # 传入数据路径
if args.split == '':
splits = None
else:
splits = args.split.split(',')# 如果划分类不是空,以逗号分界,划分出train和test
out_data_dir = args.out_data_dir # 读取存储路径
os.makedirs(out_data_dir, exist_ok=True) #如果没有存储路径就创建
overwrite = args.overwrite
dataset = CRUW(data_root=data_root, sensor_config_name=args.sensor_config)# **1**使用cruw类将各种配置文件传入,跳转如下段代码所示
config_dict = load_configs_from_file(args.config)#各种配置文件的路径和名称
config_dict = update_config_dict(config_dict, args) # update configs by args
radar_configs = dataset.sensor_cfg.radar_cfg #雷达信号的信息,距离-角度,帧率,类型等等等等
if splits == None: #判断是否将数据集分为 train和test,如果是空跳过
prepare_data(dataset, config_dict, out_data_dir, split=None, save_dir=out_data_dir, viz=False,
overwrite=overwrite)
else:
for split in splits:
if split not in SPLITS_LIST: # 如果不是 train和test,则无法识别数据划分的类型
raise TypeError("split %s cannot be recognized" % split)
for split in splits: # 根据自定义的存储路径,将数据划分为train和test
save_dir = os.path.join(out_data_dir, split)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
print('Preparing %s sets ...' % split)
# 准备打包数据,下面跳转 **2**
prepare_data(dataset, config_dict, out_data_dir, split, save_dir, viz=False, overwrite=overwrite)
---**2**---
挺复杂的一个函数,挑重点看
def prepare_data(dataset, config_dict, data_dir, split, save_dir, viz=False, overwrite=False):
# 传入参数,数据集路径,RODNet模型配置,输出路径,划分类型,存储路径
"""
Prepare pickle data for RODNet training and testing
:param dataset: dataset object
:param config_dict: rodnet configurations
:param data_dir: output directory of the processed data
:param split: train, valid, test, demo, etc.
:param save_dir: output directory of the prepared data
:param viz: whether visualize the prepared data
:param overwrite: whether overwrite the existing prepared data
:return:
"""
camera_configs = dataset.sensor_cfg.camera_cfg # 摄像头数据,一个长度为5的字典{宽,高,帧率,文件名,jpg}
radar_configs = dataset.sensor_cfg.radar_cfg# 长度为21的字典,{角度,距离,帧率,chirp数量,chirp间隔,文件名,距离最大最小值,角度最大最小值等等}(有些参数我也不知道是什么)
n_chirp = radar_configs['n_chirps'] # n_chirp=255
n_class = dataset.object_cfg.n_class # 目标类别 3类
data_root = config_dict['dataset_cfg']['data_root'] # 数据集路径
anno_root = config_dict['dataset_cfg']['anno_root'] # 标注路径
if split is None:
set_cfg = {
'subdir': '',
'seqs': sorted(os.listdir(data_root))
}
sets_seqs = sorted(os.listdir(data_root))
else:
set_cfg = config_dict['dataset_cfg'][split]
if 'seqs' not in set_cfg:
sets_seqs = sorted(os.listdir(os.path.join(data_root, set_cfg['subdir'])))
else:
sets_seqs = set_cfg['seqs']
if overwrite:
if os.path.exists(os.path.join(data_dir, split)):
shutil.rmtree(os.path.join(data_dir, split))
os.makedirs(os.path.join(data_dir, split))
for seq in sets_seqs: # sets_seqs是40个文件夹的数据集名称,for循环遍历
seq_path = os.path.join(data_root, set_cfg['subdir'], seq) # 把名称跟路径连接起来,形成绝对路径
seq_anno_path = os.path.join(anno_root, set_cfg['subdir'], seq + config_dict['dataset_cfg']['anno_ext']) #标注数据的路径
save_path = os.path.join(save_dir, seq + '.pkl') # 存储路径,存储格式为 pkl
print("Sequence %s saving to %s" % (seq_path, save_path))
try:
if not overwrite and os.path.exists(save_path):
print("%s already exists, skip" % save_path)
continue
image_dir = os.path.join(seq_path, camera_configs['image_folder']) # 每个数据都包含图像数据和雷达数据,这里加载图像路径
if os.path.exists(image_dir):
image_paths = sorted([os.path.join(image_dir, name) for name in os.listdir(image_dir) if
name.endswith(camera_configs['ext'])])
n_frame = len(image_paths) # 图像的数量,这里为 897
else: # camera images are not available
image_paths = None
n_frame = None
radar_dir = os.path.join(seq_path, dataset.sensor_cfg.radar_cfg['chirp_folder']) #与上面一样,这里加载雷达路径
if radar_configs['data_type'] == 'RI' or radar_configs['data_type'] == 'AP':
radar_paths = sorted([os.path.join(radar_dir, name) for name in os.listdir(radar_dir) if
name.endswith(dataset.sensor_cfg.radar_cfg['ext'])])
n_radar_frame = len(radar_paths)
assert n_frame == n_radar_frame
elif radar_configs['data_type'] == 'RISEP' or radar_configs['data_type'] == 'APSEP':
radar_paths_chirp = []
for chirp_id in range(n_chirp):
chirp_dir = os.path.join(radar_dir, '%04d' % chirp_id)
paths = sorted([os.path.join(chirp_dir, name) for name in os.listdir(chirp_dir) if
name.endswith(config_dict['dataset_cfg']['radar_cfg']['ext'])])
n_radar_frame = len(paths)
assert n_frame == n_radar_frame
radar_paths_chirp.append(paths)
radar_paths = []
for frame_id in range(n_frame):
frame_paths = []
for chirp_id in range(n_chirp):
frame_paths.append(radar_paths_chirp[chirp_id][frame_id])
radar_paths.append(frame_paths)
elif radar_configs['data_type'] == 'ROD2021': # 我们使用的是这个类型
if n_frame is not None:
assert len(os.listdir(radar_dir)) == n_frame * len(radar_configs['chirp_ids'])
else: # camera images are not available
n_frame = int(len(os.listdir(radar_dir)) / len(radar_configs['chirp_ids']))
radar_paths = []
for frame_id in range(n_frame): # 遍历897张图
chirp_paths = []
for chirp_id in radar_configs['chirp_ids']: #chirp_ids为[0,64,128,192]
path = os.path.join(radar_dir, '%06d_%04d.' % (frame_id, chirp_id) +
dataset.sensor_cfg.radar_cfg['ext']) # 把雷达数据跟图像数据名字一样,并且末尾是chirp_id的路径取出来
chirp_paths.append(path)# 把刚才读到的路径加进来
radar_paths.append(chirp_paths) # 里面的for循环4次,外面的for循环897次,给897张图每张题配4个雷达数据
else:
raise ValueError
data_dict = dict(
data_root=data_root,
data_path=seq_path,
seq_name=seq,
n_frame=n_frame,
image_paths=image_paths,
radar_paths=radar_paths,
anno=None,
)
if split == 'demo' or not os.path.exists(seq_anno_path):
# no labels need to be saved
pickle.dump(data_dict, open(save_path, 'wb'))
continue
else:
anno_obj = {} #运行到这里了
if config_dict['dataset_cfg']['anno_ext'] == '.txt': #加载标注文件txt数据进来
#加载完以后,anno_obj里存放了897条数据,每条数据是每张图片的名称,以及每张图的尺寸大小,RF图像大小
#也包含每张RF图中,位置的中心坐标和标签
anno_obj['metadata'] = load_anno_txt(seq_anno_path, n_frame, dataset)
elif config_dict['dataset_cfg']['anno_ext'] == '.json':
with open(os.path.join(seq_anno_path), 'r') as f:
anno = json.load(f)
anno_obj['metadata'] = anno['metadata']
else:
raise
#在这里生成了置信图,跳转如下
anno_obj['confmaps'] = generate_confmaps(anno_obj['metadata'], n_class, viz)
data_dict['anno'] = anno_obj
# save pkl files
pickle.dump(data_dict, open(save_path, 'wb'))
置信图生成
--*/*/--- 生成置信图
def generate_confmaps(metadata_dict, n_class, viz):
confmaps = []
for metadata_frame in metadata_dict: # metadata_dict就是刚才传进来的长度为897的数据
n_obj = metadata_frame['rad_h']['n_objects'] # 识别这张图里有几个目标,同时有车,自行车就是2,只有一类就是1
obj_info = metadata_frame['rad_h']['obj_info'] #目标信息:类别名称,中心坐标,置信度统一为1,也就是100%
if n_obj == 0: # 如果没有任何目标
confmap_gt = np.zeros(
(n_class + 1, radar_configs['ramap_rsize'], radar_configs['ramap_asize']),
dtype=float) #置信度全部为0,置信度的矩阵就是RF图的大小
confmap_gt[-1, :, :] = 1.0 # initialize noise channal
else:
confmap_gt = generate_confmap(n_obj, obj_info, dataset, config_dict) #如果有目标,则生成置信度图,跳转
confmap_gt = normalize_confmap(confmap_gt) # 做标准化
confmap_gt = add_noise_channel(confmap_gt, dataset, config_dict) #添加了一个噪声通道,4*128*128
assert confmap_gt.shape == (
n_class + 1, radar_configs['ramap_rsize'], radar_configs['ramap_asize'])
if viz:
visualize_confmap(confmap_gt)
confmaps.append(confmap_gt)
confmaps = np.array(confmaps)
return confmaps
***--生成置信度的核心模块---***
def generate_confmap(n_obj, obj_info, dataset, config_dict, gaussian_thres=36):
"""
Generate confidence map a radar frame.
:param n_obj: number of objects in this frame
:param obj_info: obj_info includes metadata information
:param dataset: dataset object
:param config_dict: rodnet configurations
:param gaussian_thres: threshold for gaussian distribution in confmaps-------------------------
:return: generated confmap
"""
n_class = dataset.object_cfg.n_class # 数据总类别-3
classes = dataset.object_cfg.classes # 类别名称【pedestrain,cyclist,car】
radar_configs = dataset.sensor_cfg.radar_cfg # 雷达的21个数据
confmap_sigmas = config_dict['confmap_cfg']['confmap_sigmas'] #获得方差,分别是15,20,30,不同目标的方差不同,主要影响概率的大小
confmap_sigmas_interval = config_dict['confmap_cfg']['confmap_sigmas_interval']#方差的误差范围,三个目标不同
confmap_length = config_dict['confmap_cfg']['confmap_length'] #三个类别分别是 1,2,3
range_grid = dataset.range_grid # 横坐标 距离
angle_grid = dataset.angle_grid # 纵坐标 角度(负数)
#先生成一个3*128*128大小的全0图
confmap = np.zeros((n_class, radar_configs['ramap_rsize'], radar_configs['ramap_asize']), dtype=float)
# 遍历这张图实际的类别数量
for objid in range(n_obj):
rng_idx = obj_info['center_ids'][objid][0] # 这里我理解的是获取目标中心点的笛卡尔坐标
agl_idx = obj_info['center_ids'][objid][1]
class_name = obj_info['categories'][objid] # 目标类别
if class_name not in classes:
# print("not recognized class: %s" % class_name)
continue
class_id = get_class_id(class_name, classes) # 类别编号,三个类别的编号是0,1,2,这里获取遍历的这个类别的编号
sigma = 2 * np.arctan(confmap_length[class_name] / (2 * range_grid[rng_idx])) * confmap_sigmas[class_name] # 这里根据之前设置的不同类别的方差,得到即将要做高斯分布的方差
sigma_interval = confmap_sigmas_interval[class_name] # 得到这个类别要分布的方差范围
#之前给每个目标设置了一个方差范围,这里判断获得的方差是不是在这个范围里
# 如果超过了范围,就等于最大值,如果小于这个范围就等于最小值
# 比如自行车设置的方差是20,设置的范围是8-20
if sigma > sigma_interval[1]:
sigma = sigma_interval[1]
if sigma < sigma_interval[0]:
sigma = sigma_interval[0]
for i in range(radar_configs['ramap_rsize']): #遍历横纵坐标
for j in range(radar_configs['ramap_asize']):
#这里我没看懂,大概就是计算一个什么距离的平方,再除以方差的平方
distant = (((rng_idx - i) * 2) ** 2 + (agl_idx - j) ** 2) / sigma ** 2
#如果这个值大于预先设定的值(36),就把这点存下来,直至遍历每一个位置,最终生成一个128*128的矩阵
if distant < gaussian_thres: # threshold for confidence maps
value = np.exp(- distant / 2) / (2 * math.pi)
confmap[class_id, i, j] = value if value > confmap[class_id, i, j] else confmap[class_id, i, j]
return confmap
整个数据处理部分到这里就结束了,最终保存成了一个pkl文件,比较重点的部分就是生成了置信度图,里面还有一些没有搞懂的数据,后面会再补充。文章来源:https://www.toymoban.com/news/detail-675106.html
算法不足之处
这篇写的主要是数据处理部分,后面就是将RF送进网络,计算置信图和预测置信图之间的loss来更新参数。这里的雷视融合主要实现的是自动标注自监督,但是有一个很大的缺点,就是物体的类别很少,一共只有3类。对于检测任务来说,找到物体在图中的那部分是一个任务,识别这个东西是什么又是一个任务,如果只是3分类的话,那么直接蒙也有33%的概率,因此建议在多类别的数据上进行尝试。文章来源地址https://www.toymoban.com/news/detail-675106.html
到了这里,关于雷达视觉融合算法RODNet数据处理代码解读的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!