YOLOv3的源代码精度理解(十一) get_map文件

283 阅读6分钟

代码主要是参考bubbliiing的github YOLOv3的代码:github.com/bubbliiiing…

对于源代码的解读

map部分

get_map.py

  • 这个部分是对模型的结果进行一个评估
if __name__ == "__main__":
    '''
    Recall和Precision不像AP是一个面积的概念,在门限值不同时,网络的Recall和Precision值是不同的。
    map计算结果中的Recall和Precision代表的是当预测时,门限置信度为0.5时,所对应的Recall和Precision值。

    此处获得的./map_out/detection-results/里面的txt的框的数量会比直接predict多一些,这是因为这里的门限低,
    目的是为了计算不同门限条件下的Recall和Precision值,从而实现map的计算。
    '''
    #------------------------------------------------------------------------------------------------------------------#
    #   map_mode用于指定该文件运行时计算的内容
    #   map_mode为0代表整个map计算流程,包括获得预测结果、获得真实框、计算VOC_map。
    #   map_mode为1代表仅仅获得预测结果。
    #   map_mode为2代表仅仅获得真实框。
    #   map_mode为3代表仅仅计算VOC_map。
    #   map_mode为4代表利用COCO工具箱计算当前数据集的0.50:0.95map。需要获得预测结果、获得真实框后并安装pycocotools才行
    #-------------------------------------------------------------------------------------------------------------------#
    
    # 我们选择不同map_mode的值对应不同的处理方案
    map_mode        = 0
    #-------------------------------------------------------#
    #   此处的classes_path用于指定需要测量VOC_map的类别
    #   一般情况下与训练和预测所用的classes_path一致即可
    #-------------------------------------------------------#
    
    # 这个是类别名的文件
    classes_path    = 'model_data/voc_classes.txt'
    #-------------------------------------------------------#
    #   MINOVERLAP用于指定想要获得的mAP0.x
    #   比如计算mAP0.75,可以设定MINOVERLAP = 0.75。
    #-------------------------------------------------------#
    
    # 阈值
    MINOVERLAP      = 0.5
    #-------------------------------------------------------#
    #   map_vis用于指定是否开启VOC_map计算的可视化
    #-------------------------------------------------------#
    map_vis         = False
    #-------------------------------------------------------#
    #   指向VOC数据集所在的文件夹
    #   默认指向根目录下的VOC数据集
    #-------------------------------------------------------#
    VOCdevkit_path  = 'VOCdevkit'
    #-------------------------------------------------------#
    #   结果输出的文件夹,默认为map_out
    #-------------------------------------------------------#
    map_out_path    = 'map_out'

    # 加载测试集
    image_ids = open(os.path.join(VOCdevkit_path, "VOC2007/ImageSets/Main/test.txt")).read().strip().split()

    # 过程数据,存在不用创建直接覆盖,不存在就创建文件夹
    if not os.path.exists(map_out_path):
        os.makedirs(map_out_path)
    if not os.path.exists(os.path.join(map_out_path, 'ground-truth')):
        os.makedirs(os.path.join(map_out_path, 'ground-truth'))
    if not os.path.exists(os.path.join(map_out_path, 'detection-results')):
        os.makedirs(os.path.join(map_out_path, 'detection-results'))
    if not os.path.exists(os.path.join(map_out_path, 'images-optional')):
        os.makedirs(os.path.join(map_out_path, 'images-optional'))
	
    # 获取类名的列表
    class_names, _ = get_classes(classes_path)

    # mode = 0 或者 mode = 1
    if map_mode == 0 or map_mode == 1:
        print("Load model.")
        
        # 加载模型,我们将置信度大于0.001的都保存下来,nms的阈值设置成0.5
        yolo = YOLO(confidence = 0.001, nms_iou = 0.5)
        print("Load model done.")

        print("Get predict result.")
        # 对上面的test测试文件进行循环
        for image_id in tqdm(image_ids):
            # 获得路径
            image_path  = os.path.join(VOCdevkit_path, "VOC2007/JPEGImages/"+image_id+".jpg")
            # 打开文件
            image       = Image.open(image_path)
            # 开启map_voc的可视化后我们将图片保存到images-optional这个文件夹下
            if map_vis:
                image.save(os.path.join(map_out_path, "images-optional/" + image_id + ".jpg"))
            # 调用模型的get_map_txt方法
            yolo.get_map_txt(image_id, image, class_names, map_out_path)
        print("Get predict result done.")
        
    # mode = 0 或者 mode = 2
    if map_mode == 0 or map_mode == 2:
        print("Get ground truth result.")
        # 我们循环上面得到的测试集图片的id的list
        for image_id in tqdm(image_ids):
            # 我们在ground-truth文件夹下,对每一张image都创建一个txt文件,用于记录真实框的信息
            with open(os.path.join(map_out_path, "ground-truth/"+image_id+".txt"), "w") as new_f:
                # 从对应的xml信息中抽出来真实信息
                root = ET.parse(os.path.join(VOCdevkit_path, "VOC2007/Annotations/"+image_id+".xml")).getroot()
                for obj in root.findall('object'):
                    difficult_flag = False
                    if obj.find('difficult')!=None:
                        difficult = obj.find('difficult').text
                        if int(difficult)==1:
                            difficult_flag = True
                    # 名称
                    obj_name = obj.find('name').text
                    if obj_name not in class_names:
                        continue
                    # 坐标信息
                    bndbox  = obj.find('bndbox')
                    left    = bndbox.find('xmin').text
                    top     = bndbox.find('ymin').text
                    right   = bndbox.find('xmax').text
                    bottom  = bndbox.find('ymax').text

                    # 将信息记录到上面准备好的txt文件中
                    if difficult_flag:
                        new_f.write("%s %s %s %s %s difficult\n" % (obj_name, left, top, right, bottom))
                    else:
                        new_f.write("%s %s %s %s %s\n" % (obj_name, left, top, right, bottom))
        print("Get ground truth result done.")

    # mode = 0 或者 mode = 2
    if map_mode == 0 or map_mode == 3:
        print("Get map.")
        # 调用get_map(核心)代码进行map的计算
        get_map(MINOVERLAP, True, path = map_out_path)
        print("Get map done.")
	
    # mode = 4
    if map_mode == 4:
        print("Get map.")
        # 直接使用coco的工具包对数据进行统计,我们需要整理成工具包需要的格式即可
        get_coco_map(class_names = class_names, path = map_out_path)
        print("Get map done.")
  • 调用到 get_map_txt方法
def get_map_txt(self, image_id, image, class_names, map_out_path):
    # 首先先创建detection-results这个文件夹下的指定图片id的txt文件
    f = open(os.path.join(map_out_path, "detection-results/"+image_id+".txt"),"w") 
    
    # 获取高宽
    image_shape = np.array(np.shape(image)[0:2])
    #---------------------------------------------------------#
    #   在这里将图像转换成RGB图像,防止灰度图在预测时报错。
    #   代码仅仅支持RGB图像的预测,所有其它类型的图像都会转化成RGB
    #---------------------------------------------------------#
     
    # 将图片都变成RGB彩色图片
    image       = cvtColor(image)
    #---------------------------------------------------------#
    #   给图像增加灰条,实现不失真的resize
    #   也可以直接resize进行识别
    #---------------------------------------------------------#
    
    # 将图片进行指定大小的resize(416,416),letterbox_image是否进行不失真填充
    image_data  = resize_image(image, (self.input_shape[1],self.input_shape[0]), self.letterbox_image)
    #---------------------------------------------------------#
    #   添加上batch_size维度
    #---------------------------------------------------------#
    
    # 对图片增维,通道调整
    image_data  = np.expand_dims(np.transpose(preprocess_input(np.array(image_data, dtype='float32')), (2, 0, 1)), 0)

    with torch.no_grad():
        # ndarray转化成tensor
        images = torch.from_numpy(image_data)
        # 加载到gpu
        if self.cuda:
            images = images.cuda()
        #---------------------------------------------------------#
        #   将图像输入网络当中进行预测!
        #---------------------------------------------------------#
        
        # 这个部分详细的分析过,输入网络进行预测,对预测的结果进行解码,调用nms去除重叠比较大的框,留下最终的预测框
        outputs = self.net(images)
        outputs = self.bbox_util.decode_box(outputs)
        #---------------------------------------------------------#
        #   将预测框进行堆叠,然后进行非极大抑制
        #---------------------------------------------------------#
        results = self.bbox_util.non_max_suppression(torch.cat(outputs, 1), self.num_classes, self.input_shape, 
                    image_shape, self.letterbox_image, conf_thres = self.confidence, nms_thres = self.nms_iou)
        
        # 没预测到东西
        if results[0] is None: 
            return 

        # 预测框标签列
        top_label   = np.array(results[0][:, 6], dtype = 'int32')
        # 预测框的置信度
        top_conf    = results[0][:, 4] * results[0][:, 5]
        # 预测框的坐标
        top_boxes   = results[0][:, :4]

    # 循环所有框的信息
    for i, c in list(enumerate(top_label)):
        predicted_class = self.class_names[int(c)]
        box             = top_boxes[i]
        score           = str(top_conf[i])

        top, left, bottom, right = box
        if predicted_class not in class_names:
            continue
		# 将上面的得到的框的信息都放入txt文件中
        f.write("%s %s %s %s %s %s\n" % (predicted_class, score[:6], str(int(left)), str(int(top)), str(int(right)),str(int(bottom))))

    f.close()
    return 
  • detection-results文件夹效果展示

    • 记录对于测试文件的预测框信息,格式 类名 置信度 x1,y1,x2,y2
    • 我们看到了,只要是置信度大于0.001的我们都记录下来了 image.png
  • ground-truth文件夹

    • 记录真实框信息,格式 类名 x1,y1,x2,y2 image.png
  • 调用get_map的方法(重点):

  • 调用get_coco_map方法:

    • 首先安装pycocotools,直接pip就能安装
    def get_coco_map(class_names, path):
    from pycocotools.coco import COCO
    from pycocotools.cocoeval import COCOeval
    
    GT_PATH     = os.path.join(path, 'ground-truth')
    DR_PATH     = os.path.join(path, 'detection-results')
    COCO_PATH   = os.path.join(path, 'coco_eval')
    
    if not os.path.exists(COCO_PATH):
        os.makedirs(COCO_PATH)
    
    # 其实和get_map中构建真实框信息和预测框的信息是一样的,最终整合成json数据
    GT_JSON_PATH = os.path.join(COCO_PATH, 'instances_gt.json')
    DR_JSON_PATH = os.path.join(COCO_PATH, 'instances_dr.json')
    
    # 处理真实ground-truth的
    with open(GT_JSON_PATH, "w") as f:
        results_gt  = preprocess_gt(GT_PATH, class_names)
        json.dump(results_gt, f, indent=4)
    
    with open(DR_JSON_PATH, "w") as f:
        results_dr  = preprocess_dr(DR_PATH, class_names)
        json.dump(results_dr, f, indent=4)
    
    # 剩下部分都是套招的部分,数据传入,直接得到结果
    cocoGt      = COCO(GT_JSON_PATH)
    cocoDt      = cocoGt.loadRes(DR_JSON_PATH)
    cocoEval    = COCOeval(cocoGt, cocoDt, 'bbox') 
    cocoEval.evaluate()
    cocoEval.accumulate()
    cocoEval.summarize()
    

    我们看使用coco的工具进行map的计算,注意coco默认是,map0.5:0.95的数据变换 image.png 我们看到在IOU = 0.5的阈值的时候,计算出来的,map的值是0.828,我们使用get_map的自行计算出来的值是0.832,基本是一致的。 其中调用函数如下,实际上就是将我们的txt数据转化成coco类能操作的json数据的格式:

    • preprocess_dr的调用
    # 处理预测框
    def preprocess_dr(dr_path, class_names):
        image_ids = os.listdir(dr_path)
        results = []
        for image_id in image_ids:
            lines_list      = file_lines_to_list(os.path.join(dr_path, image_id))
            image_id        = os.path.splitext(image_id)[0]
            for line in lines_list:
                line_split  = line.split()
                # 获取整理好的一行五个数据
                confidence, left, top, right, bottom = line_split[-5:]
                class_name  = ""
                for name in line_split[:-5]:
                    class_name += name + " "
                class_name  = class_name[:-1]
                left, top, right, bottom = float(left), float(top), float(right), float(bottom)
                result                  = {}
                result["image_id"]      = str(image_id)
                result["category_id"]   = class_names.index(class_name) + 1
                # 只有在这个地方注意一下,使用的是x1,y1,w,h
                result["bbox"]          = [left, top, right - left, bottom - top]
                result["score"]         = float(confidence)
                results.append(result)
        return results
    

    保存完的数据格式如下
    image.png

    • preprocess_gt的调用
    # 处理真实框、统计三个方面的信息(基本信息部分、类别部分、数据部分)
    def preprocess_gt(gt_path, class_names):
        image_ids   = os.listdir(gt_path)
        results = {}
    
        images = []
        bboxes = []
        # 基本图片信息
        for i, image_id in enumerate(image_ids):
            lines_list      = file_lines_to_list(os.path.join(gt_path, image_id))
            boxes_per_image = []
            image           = {}
            image_id        = os.path.splitext(image_id)[0]
            image['file_name'] = image_id + '.jpg'
            image['width']     = 1
            image['height']    = 1
            image['id']        = str(image_id)
    
            for line in lines_list:
                difficult = 0 
                if "difficult" in line:
                    line_split  = line.split()
                    left, top, right, bottom, _difficult = line_split[-5:]
                    class_name  = ""
                    for name in line_split[:-5]:
                        class_name += name + " "
                    class_name  = class_name[:-1]
                    difficult = 1
                else:
                    line_split  = line.split()
                    left, top, right, bottom = line_split[-4:]
                    class_name  = ""
                    for name in line_split[:-4]:
                        class_name += name + " "
                    class_name = class_name[:-1]
    
                left, top, right, bottom = float(left), float(top), float(right), float(bottom)
                cls_id  = class_names.index(class_name) + 1
                bbox    = [left, top, right - left, bottom - top, difficult, str(image_id), cls_id, (right - left) * (bottom - top) - 10.0]
                boxes_per_image.append(bbox)
            images.append(image)
            bboxes.extend(boxes_per_image)
        results['images']        = images
    
        # 类别整理
        categories = []
        for i, cls in enumerate(class_names):
            category = {}
            category['supercategory']   = cls
            category['name']            = cls
            category['id']              = i + 1
            categories.append(category)
        results['categories']   = categories
    
        # 数据(box坐标信息、图片id信息等)
        annotations = []
        for i, box in enumerate(bboxes):
            annotation = {}
            annotation['area']        = box[-1]
            annotation['category_id'] = box[-2]
            annotation['image_id']    = box[-3]
            annotation['iscrowd']     = box[-4]
            annotation['bbox']        = box[:4]
            annotation['id']          = i
            annotations.append(annotation)
        results['annotations'] = annotations
        return results
    
    • 数据中存在三个部分:
      • 信息部分: image.png
      • 类别部分: image.png
      • 数据部分: image.png