KITTI 3D目标检测离线评估工具包说明

KITTI 3D目标检测离线评估工具包说明

本文是KITTI 3D目标检测离线评估工具包的使用说明和相关代码学习文件,从这里可以下载。更新于2018.09.20。

文章目录

  • KITTI 3D目标检测离线评估工具包说明
    • 工具包README文件
    • 代码学习
      • evaluate_object_3d_offline.cpp
        • 主函数
        • eval
        • tBox\tGroundtruth\tDetection
        • eval_class
        • saveAndPlotPlots
        • computeStatistics

工具包README文件

这个工具包是离线运行的,可以在使用者的电脑上评估验证集(从KITTI训练集中选出来的)。评估的指标包括:

  • 重叠率:overlap on image (AP)
  • 旋转重叠率:oriented overlap on image (AOS)
  • 地面重叠率(鸟瞰视角):overlap on ground-plane (AP)
  • 3D重叠率:overlap in 3D (AP)

首先在终端编译evaluate_object_3d_offline.cpp文件,之后运行评估命令:

./evaluate_object_3d_offline groundtruth_dir result_dir

需要注意的是,使用者并不需要评估整个KITTI训练集。Evaluator只评估有结果存在的那些样本。

代码学习

这一部分主要是希望通过学习代码理解所得到的结果和图像,并不深究其中的语法。

总结:

  • 这个函数主要用于评估实验结果,但是评估过程中并未评估所有的结果,而是挑选了置信概率最大的前几个结果(程序中默认取前41个),函数计算了precision和recall并画出二者的关系曲线(关于这两个算法评估概念可以参看这里的说明)。
  • 评估算法是按照类别判断的,对于KITTI库分为3类(人、车、自行车),每个类别中有不同难度(简单、中等、困难),曲线是每个类别对应一个曲线图,图中包括三种难度下算法的评估结果曲线。
  • 算法中还将评估分为2D评估、鸟瞰评估、3D评估三种不同角度,其中2D评估可以有带转角的评估AOS,另外两种则不评估此项。
  • 结果或真值数据的存储格式应当遵从这个顺序:目标类型(人、车、自行车对应的字符串),是否截断(失效值-1),是否遮挡(无遮挡、部分遮挡、全部遮挡)(失效值-1),旋转角度(失效值-10),左上角坐标x1,左上角坐标y1,右下角坐标x2,右下角坐标y2,高,宽,长,box中心坐标t1,box中心坐标t2,box中心坐标t3,box朝向ry,阈值(score)。其中,真值数据不具有最后一项,结果数据是否截断、是否遮挡对应数据无效。
  • t*在函数toPolygon中用到了,但是含义不清楚;ry的含义也不清楚。

evaluate_object_3d_offline.cpp

这一部分记录了evaluate_object_3d_offline.cpp文件的学习笔记,包括其中的主函数、用于评估总过程的eval函数、定义存储信息含义的结构体tBox\tGroundtruth\tDetection、具体实施按类评估的eval_class、和用于存储结果并画出图像的saveAndPlotPlots。

主函数

主导整个评估过程。

int32_t main (int32_t argc,char *argv[]) {

  // 需要2或4个输入
  //如果输入个数为3,显示用法并返回
  if (argc!=3) { 
    cout << "Usage: ./eval_detection_3d_offline gt_dir result_dir" << endl;
    return 1;  return 1;
  }

  //读取输入
  string gt_dir = argv[1];			//第一个输入是真值路径
  string result_dir = argv[2];  	//第二个输入是结果路径

  //定义用于提示的邮件地址
  Mail *mail;
  mail = new Mail();
  mail->msg("Thank you for participating in our evaluation!");

  //运行评估过程
  //如果评估过程成功,有邮箱地址就将结果链接发送到邮箱,没有就保存在本地plot下
  //否则,返回错误信息并删除结果目录
  if (eval(gt_dir, result_dir, mail)) {
    mail->msg("Your evaluation results are available at:");
    mail->msg(result_dir.c_str());
  } else {
    system(("rm -r " + result_dir + "/plot").c_str());
    mail->msg("An error occured while processing your results.");
  } 

  //发送邮件并退出
  delete mail;

  return 0;
}

eval

从主函数中可以看到,起评估作用的是eval函数,下面贴出eval函数和学习说明:

bool eval(string gt_dir, string result_dir, Mail* mail){

  //设置全局变量CLASS_NAMES,其中包括car, pedestrain, cyclist
  initGlobals();

  // 真值和结果路径:
  // string gt_dir         = "data/object/label_2";  真值路径
  // string result_dir     = "results/" + result_sha; 结果路径

  //保存eval结果图的路径
  string plot_dir       = result_dir + "/plot";

  // 按照上面定义的plot路径创建输出目录
  system(("mkdir " + plot_dir).c_str()); 

  //定义了两个二维数组groundtruth和detections,用于存储真值和检测结果
  //定义了一个名为groundtruth的二维数组,其中每个位置上的数据类型是tGroundtruth,其中存有box的类型、两组对角坐标(左上、右下)、图像转角等信息。具体见tBox和tGroundtruth说明。
  vector< vector > groundtruth; 
  //参考上面真值定义
  vector< vector >   detections;

  //存储是否计算旋转重叠率AOS(在加载检测结果的时候可能被设成false),并记录本次提交都包含哪些labels
  //默认计算AOS(仅对于2D评估)
  bool compute_aos=true; 
  //定义eval_image,存储bool变量,默认值为false,长度为3
  vector eval_image(NUM_CLASS, false);
  vector eval_ground(NUM_CLASS, false);
  vector eval_3d(NUM_CLASS, false);

  // 读取所有图像的真值和检测结果
  mail->msg("Loading detections...");
  //存储所有有结果的图像编号
  std::vector indices = getEvalIndices(result_dir + "/data/");
  printf("number of files for evaluation: %d\n", (int)indices.size()); 

  //对于所有图像,读取真值并检查是否都数据读取成功
  for (int32_t i=0; i gt   = loadGroundtruth(gt_dir + "/" + file_name,gt_success); 
    //读取检测结果,共16个值(最后一个为score),如果转角的值(第4个)为-10,则不计算AOS
    vector   det  = loadDetections(result_dir + "/data/" + file_name,   
            compute_aos, eval_image, eval_ground, eval_3d, det_success); 
    groundtruth.push_back(gt);			//将gt存入groundtruth,也就是直到此时才给之前定义的goundtruth赋值
    detections.push_back(det); 			//将det存入detections

    //检查是否有读取失败,如果有,输出提示并返回
    if (!gt_success) { 
      mail->msg("ERROR: Couldn't read: %s of ground truth. Please write me an email!", file_name);
      return false;
    }
    if (!det_success) {
      mail->msg("ERROR: Couldn't read: %s", file_name);
      return false;
    }
  } 
  mail->msg("  done.");

  // 定义指向结果文件的指针
  FILE *fp_det=0, *fp_ori=0; 		//FILE是定义在C++标准库中的一个结构体,以指针的方式存储与内存中,其内容描述了一个文件

  //对于所有类别评估2D窗口
  for (int c = 0; c < NUM_CLASS; c++) {
    CLASSES cls = (CLASSES)c; 		//找到序号对应的类别(此时cls的值为CAR、PEDESTRAIN或CYCLIST)
    if (eval_image[c]) { 			//如果存在这一类别的图像(在loadDetections里面判断了)才计算
      fp_det = fopen((result_dir + "/stats_" + CLASS_NAMES[c] + "_detection.txt").c_str(), "w"); 			//让fp_det指针指向用于存储结果的文件
      if(compute_aos) 				//如果需要计算AOS,就让fp_ori指向用于存储AOS的文件(这里默认不计算)
        fp_ori = fopen((result_dir + "/stats_" + CLASS_NAMES[c] + "_orientation.txt").c_str(),"w");
      vector precision[3], aos[3];			 //定义两个长度为3的容器(对应简单、中等、困难三个级别),分别用于存储准确率和AOS
      //如果有任意一个难度计算失败,则返回提示并退出(具体计算过程见eval_class说明)
      if(   !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, imageBoxOverlap, precision[0], aos[0], EASY, IMAGE)
         || !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, imageBoxOverlap, precision[1], aos[1], MODERATE, IMAGE)
         || !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, imageBoxOverlap, precision[2], aos[2], HARD, IMAGE)) {
        mail->msg("%s evaluation failed.", CLASS_NAMES[c].c_str());
        return false;
      }
      fclose(fp_det); 			//关闭detection的存储文件
      saveAndPlotPlots(plot_dir, CLASS_NAMES[c] + "_detection", CLASS_NAMES[c], precision, 0); 			//画出曲线图(具体见saveAndPlotPlots说明)
      if(compute_aos){ 			//如果需要计算AOS
        saveAndPlotPlots(plot_dir, CLASS_NAMES[c] + "_orientation", CLASS_NAMES[c], aos, 1); //画出AOS曲线
        fclose(fp_ori);
      }
    }
  }
  printf("Finished 2D bounding box eval.\n"); 			//结束2D评估
  
  //对于鸟瞰图和3D box不要计算AOS
  compute_aos = false;

  //对于所有类别评估鸟瞰角度的bounding box
  for (int c = 0; c < NUM_CLASS; c++) {
    CLASSES cls = (CLASSES)c;
    if (eval_ground[c]) { 			//如果存在该类型的图片才计算
      fp_det = fopen((result_dir + "/stats_" + CLASS_NAMES[c] + "_detection_ground.txt").c_str(), "w");  			//将指针指向用于存储鸟瞰结果的文件
      vector precision[3], aos[3];  			//同2D,分别用于存储简单、中等和困难的情况
      printf("Going to eval ground for class: %s\n", CLASS_NAMES[c].c_str());
      //如果任意一个难度评估出错,提示并返回
      if(   !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, groundBoxOverlap, precision[0], aos[0], EASY, GROUND)
         || !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, groundBoxOverlap, precision[1], aos[1], MODERATE, GROUND)
         || !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, groundBoxOverlap, precision[2], aos[2], HARD, GROUND)) {
        mail->msg("%s evaluation failed.", CLASS_NAMES[c].c_str());
        return false;
      }
      fclose(fp_det);
      saveAndPlotPlots(plot_dir, CLASS_NAMES[c] + "_detection_ground", CLASS_NAMES[c], precision, 0); 			 //画出评估图像(具体参见saveAndPlotPlots说明)
    }
  }
  printf("Finished Birdeye eval.\n");			//结束鸟瞰评估

  //对于所有类别评估3D bounding boxes
  for (int c = 0; c < NUM_CLASS; c++) { 
    CLASSES cls = (CLASSES)c;
    if (eval_3d[c]) {			 //如果评估3D结果
      fp_det = fopen((result_dir + "/stats_" + CLASS_NAMES[c] + "_detection_3d.txt").c_str(), "w"); 			//指针指向保存3D评估结果的文件
      vector precision[3], aos[3];
      //如果任意一个难度评估出错,则提示并返回
      printf("Going to eval 3D box for class: %s\n", CLASS_NAMES[c].c_str());
      if(   !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, box3DOverlap, precision[0], aos[0], EASY, BOX3D)
         || !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, box3DOverlap, precision[1], aos[1], MODERATE, BOX3D)
         || !eval_class(fp_det, fp_ori, cls, groundtruth, detections, compute_aos, box3DOverlap, precision[2], aos[2], HARD, BOX3D)) {
        mail->msg("%s evaluation failed.", CLASS_NAMES[c].c_str());
        return false;
      }
      fclose(fp_det);
      saveAndPlotPlots(plot_dir, CLASS_NAMES[c] + "_detection_3d", CLASS_NAMES[c], precision, 0); 
      CLASS_NAMES[c], precision, 0);
    }
  }
  printf("Finished 3D bounding box eval.\n");

  // 成功完成评估,返回true
  return true;
}

tBox\tGroundtruth\tDetection

tBox、tGroundtruth和tDetection结构体定义(用于存储真值和检测结果,eval代码中用到):

// holding bounding boxes for ground truth and detections
struct tBox {
  string  type;     // 存储目标类型
  double   x1;     
  double   y1;      //与x1共同定位左上角坐标
  double   x2;      
  double   y2;      //与x2共同定位右下角坐标
  double   alpha;   //图像转角
  tBox (string type, double x1,double y1,double x2,double y2,double alpha) : //定义与结构体同名的构造函数,在调用时赋值
    type(type),x1(x1),y1(y1),x2(x2),y2(y2),alpha(alpha) {}    
};

//存储真值
struct tGroundtruth {
  tBox    box;        //存储目标类型、box、朝向
  double  truncation; // truncation 0..1 这个目前还不理解干什么用的
  int32_t occlusion;  // 是否遮挡,0代表无遮挡,1代表部分遮挡,2代表完全遮挡
  double ry;  //目前未知含义
  double  t1, t2, t3; //目前未知含义
  double h, w, l; //高、宽、长
  tGroundtruth () : //这是这个结构体内所包含的同名构造函数,在调用时赋值
    box(tBox("invalild",-1,-1,-1,-1,-10)),truncation(-1),occlusion(-1) {}  
  tGroundtruth (tBox box,double truncation,int32_t occlusion) :
    box(box),truncation(truncation),occlusion(occlusion) {}   
  tGroundtruth (string type,double x1,double y1,double x2,double y2,double alpha,double truncation,int32_t occlusion) :
    box(tBox(type,x1,y1,x2,y2,alpha)),truncation(truncation),occlusion(occlusion) {}   
};

//存储检测结果
struct tDetection {
  tBox    box;    //存储目标类型、box、朝向
  double  thresh; //检测概率(detection score)
  double  ry;  //目前未知含义
  double  t1, t2, t3;  //目前未知含义
  double  h, w, l;  //高、宽、长
  tDetection (): //定义与结构体同名的构造函数,在调用时赋值
    box(tBox("invalid",-1,-1,-1,-1,-10)),thresh(-1000) {}    
  tDetection (tBox box,double thresh) :
    box(box),thresh(thresh) {}
  tDetection (string type,double x1,double y1,double x2,double y2,double alpha,double thresh) :
    box(tBox(type,x1,y1,x2,y2,alpha)),thresh(thresh) {}  
};

总结:
基本存储内容和顺序为:类型、左上角x、左上角y、右下角x、右下角y、朝向(角度);如果是真值那么后面会加上:截断信息(truncation)、遮挡情况;如果是检测结果后面会加上:检测概率(score)。


eval_class

eval_class代码部分:

bool eval_class (FILE *fp_det, FILE *fp_ori, CLASSES current_class,
    const vector< vector > &groundtruth,
    const vector< vector > &detections, bool compute_aos,
    double (*boxoverlap)(tDetection, tGroundtruth, int32_t),
    vector &precision, vector &aos,
    DIFFICULTY difficulty, METRIC metric) {
assert(groundtruth.size() == detections.size());

  // 初始化
  int32_t n_gt=0;                                     // 真值图像总数(recall的分母)
  vector v, thresholds;                       //用于存储检测得到的概率detection scores,对其评估的结果用于recall的离散化
  
  vector< vector > ignored_gt, ignored_det;  //用于存储对于当前类别/难度忽略的图像标号
  vector< vector > dontcare;            //用于存储在真值中包含的不关心区域的编号

  //对于所有待测试图像进行:
  for (int32_t i=0; i i_gt, i_det;
    vector dc;

    //只评估当前类别下的目标(忽略遮挡、截断的目标)
    cleanData(current_class, groundtruth[i], detections[i], i_gt, dc, i_det, n_gt, difficulty);
    ignored_gt.push_back(i_gt);
    ignored_det.push_back(i_det);
    dontcare.push_back(dc);

    //计算数据以得到recall的值 
    tPrData pr_tmp = tPrData();			//用于存储相似度、true positives、false positives和false negatives
    pr_tmp = computeStatistics(current_class, groundtruth[i], detections[i], dc, i_gt, i_det, false, boxoverlap, metric);				//具体分析见ComputeStatistics说明,输出为tPrData类型

    //将所有图片的detection scores存入向量
    for(int32_t j=0; j pr;
  pr.assign(thresholds.size(),tPrData());
  for (int32_t i=0; i recall;
  precision.assign(N_SAMPLE_PTS, 0);
  if(compute_aos)
    aos.assign(N_SAMPLE_PTS, 0);
  double r=0;
  for (int32_t i=0; i

saveAndPlotPlots

saveAndPlotPlots说明。

void saveAndPlotPlots(string dir_name,string file_name,string obj_type,vector vals[],bool is_aos){

  char command[1024];
  //保存结果图像到指定路径
  FILE *fp = fopen((dir_name + "/" + file_name + ".txt").c_str(),"w");			//保存在plot文件夹下对应类别的文件中
  printf("save %s\n", (dir_name + "/" + file_name + ".txt").c_str());
  
  //对于截取数量的样本,按正确率从高到低输出结果(格式:占40个样本的前百分之多少、简单类别、中等类别、困难类别分别对应的精度)
  for (int32_t i=0; i<(int)N_SAMPLE_PTS; i++)
    fprintf(fp,"%f %f %f %f\n",(double)i/(N_SAMPLE_PTS-1.0),vals[0][i],vals[1][i],vals[2][i]); 
  fclose(fp);

  //求解三种难度下的精度之和,计算AP并显示
  float sum[3] = {0, 0, 0};
  for (int v = 0; v < 3; ++v)
      for (int i = 0; i < vals[v].size(); i = i + 4)
          sum[v] += vals[v][i];
  printf("%s AP: %f %f %f\n", file_name.c_str(), sum[0] / 11 * 100, sum[1] / 11 * 100, sum[2] / 11 * 100);


  //创建png + eps
  for (int32_t j=0; j<2; j++) {

    //打开文件
    FILE *fp = fopen((dir_name + "/" + file_name + ".gp").c_str(),"w");

    //保存gnuplot指令
    if (j==0) {
      fprintf(fp,"set term png size 450,315 font \"Helvetica\" 11\n");
      fprintf(fp,"set output \"%s.png\"\n",file_name.c_str());
    } else {
      fprintf(fp,"set term postscript eps enhanced color font \"Helvetica\" 20\n");
      fprintf(fp,"set output \"%s.eps\"\n",file_name.c_str());
    }

    //设置labels和范围
    fprintf(fp,"set size ratio 0.7\n");
    fprintf(fp,"set xrange [0:1]\n");
    fprintf(fp,"set yrange [0:1]\n");
    fprintf(fp,"set xlabel \"Recall\"\n");
    if (!is_aos) fprintf(fp,"set ylabel \"Precision\"\n");
    else         fprintf(fp,"set ylabel \"Orientation Similarity\"\n");
    obj_type[0] = toupper(obj_type[0]);
    fprintf(fp,"set title \"%s\"\n",obj_type.c_str());

    //线宽
    int32_t   lw = 5;
    if (j==0) lw = 3;

    //画error曲线
    fprintf(fp,"plot ");
    fprintf(fp,"\"%s.txt\" using 1:2 title 'Easy' with lines ls 1 lw %d,",file_name.c_str(),lw); 
    fprintf(fp,"\"%s.txt\" using 1:3 title 'Moderate' with lines ls 2 lw %d,",file_name.c_str(),lw); 
    fprintf(fp,"\"%s.txt\" using 1:4 title 'Hard' with lines ls 3 lw %d",file_name.c_str(),lw);

    //关闭文件
    fclose(fp);

    //运行gnuplot以生成png + eps
    sprintf(command,"cd %s; gnuplot %s",dir_name.c_str(),(file_name + ".gp").c_str());
    system(command);
  }

  //生成pdf并截取
  sprintf(command,"cd %s; ps2pdf %s.eps %s_large.pdf",dir_name.c_str(),file_name.c_str(),file_name.c_str());
  system(command);
  sprintf(command,"cd %s; pdfcrop %s_large.pdf %s.pdf",dir_name.c_str(),file_name.c_str(),file_name.c_str());
  system(command);
  sprintf(command,"cd %s; rm %s_large.pdf",dir_name.c_str(),file_name.c_str()); 
  system(command);
}

computeStatistics

用于计算必要的数据,为recall的计算做准备:

tPrData computeStatistics(CLASSES current_class, const vector >,
    const vector &det, const vector &dc,
    const vector &ignored_gt, const vector  &ignored_det,
    bool compute_fp, double (*boxoverlap)(tDetection, tGroundtruth, int32_t),
    METRIC metric, bool compute_aos=false, double thresh=0, bool debug=false){

  tPrData stat = tPrData();
  const double NO_DETECTION = -10000000;
  vector delta;            //用于存储TP需要的角度的不同(AOS计算需要)
  vector assigned_detection; //用于存储一个检测结果是被标注有效还是忽略
  assigned_detection.assign(det.size(), false);
  vector ignored_threshold;
  ignored_threshold.assign(det.size(), false); //如果计算FP,用于存储低于阈值的检测结果

  //在计算precision时,忽略低score的检测结果(需要FP)
  if(compute_fp)
    for(int32_t i=0; i 0.5) (logical len(det)) 
    =======================================================================*/
    int32_t det_idx          = -1;
    double valid_detection = NO_DETECTION;
    double max_overlap     = 0;

    //寻找可能的检测结果
    bool assigned_ignored_det = false; 
    for(int32_t j=0; jMIN_OVERLAP[metric][current_class] && det[j].thresh>valid_detection){
        det_idx         = j;
        valid_detection = det[j].thresh;
      }

      //为计算precision曲线值,需要考虑拥有最大重叠率的候选  
      //如果该候选是一个被忽略的检测(min_height),启用重叠率检测
      else if(compute_fp && overlap>MIN_OVERLAP[metric][current_class] && (overlap>max_overlap || assigned_ignored_det) && ignored_det[j]==0){ 
        max_overlap     = overlap;
        det_idx         = j;
        valid_detection = 1;
        assigned_ignored_det = false;;
      }
      else if(compute_fp && overlap>MIN_OVERLAP[metric][current_class] && valid_detection==NO_DETECTION && ignored_det[j]==1){
        det_idx              = j; 
        valid_detection      = 1;
        assigned_ignored_det = true;
      }
    }
    /*=======================================================================
    compute TP, FP and FN  compute TP, FP and FN
    =======================================================================

    //如果没有给当前有效的真值分配任何东西
    if(valid_detection==NO_DETECTION && ignored_gt[i]==0) {  
      stat.fn++;
    }

    //只评估有效真值等同于 detection assignments (considering difficulty level)
    else if(valid_detection!=NO_DETECTION && (ignored_gt[i]==1 || ignored_det[det_idx]==1))
      assigned_detection[det_idx] = true;

    //找到一个有效的true positive
    else if(valid_detection!=NO_DETECTION){

      //向阈值向量写入最高的
      stat.tp++;
      stat.v.push_back(det[det_idx].thresh);

      //真值和检测结果之间的计算角度差异(如果提供了有效的角度检测)
      if(compute_aos) 
        delta.push_back(gt[i].box.alpha - det[det_idx].box.alpha);  

      //清空
      assigned_detection[det_idx] = true;
    }
  }

  //如果需要计算FP are requested,则考虑stuff area 
  if(compute_fp){

    //计数fp
    for(int32_t i=0; iMIN_OVERLAP[metric][current_class]){
          assigned_detection[j] = true;
          nstuff++;
        }
      }
    }

    // FP = 所有未分配真值的点的个数(no. of all not to ground truth assigned detections) - 分配到stuff area的点的个数(detections assigned to stuff areas)
    stat.fp -= nstuff;

    //如果所有角度值有效则计算AOS
    if(compute_aos){
      vector tmp;

      // FP have a similarity of 0, for all TP compute AOS
      tmp.assign(stat.fp, 0);
      for(int32_t i=0; i0 || stat.fp>0)
        stat.similarity = accumulate(tmp.begin(), tmp.end(), 0.0);

      // there was neither a FP nor a TP, so the similarity is ignored in the evaluation
      else
        stat.similarity = -1;
    }
  }
  return stat;
}

你可能感兴趣的:(论文代码学习,KITTI,3D目标识别,评价算法)