雷达视觉融合算法RODNet数据处理代码解读

论文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

可得到如下三个文件夹sequencesannotationscalib,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) # 类别编号,三个类别的编号是012,这里获取遍历的这个类别的编号
        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文件,比较重点的部分就是生成了置信度图,里面还有一些没有搞懂的数据,后面会再补充。

算法不足之处
这篇写的主要是数据处理部分,后面就是将RF送进网络,计算置信图和预测置信图之间的loss来更新参数。这里的雷视融合主要实现的是自动标注自监督,但是有一个很大的缺点,就是物体的类别很少,一共只有3类。对于检测任务来说,找到物体在图中的那部分是一个任务,识别这个东西是什么又是一个任务,如果只是3分类的话,那么直接蒙也有33%的概率,因此建议在多类别的数据上进行尝试。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Carry陈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值