代码主要是参考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的我们都记录下来了
-
ground-truth文件夹
- 记录真实框信息,格式 类名 x1,y1,x2,y2
- 记录真实框信息,格式 类名 x1,y1,x2,y2
-
调用get_map的方法(
重点
):- 详细解读放在了这篇blog: juejin.cn/post/708155…
-
调用get_coco_map方法:
- 首先安装pycocotools,直接pip就能安装
- 遇到错误
Microsoft Visual C++ 14.0 is required解决方法
详情见:juejin.cn/post/707851…
- 遇到错误
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的数据变换
我们看到在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
保存完的数据格式如下
- 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
- 数据中存在三个部分:
- 信息部分:
- 类别部分:
- 数据部分:
- 信息部分:
- 首先安装pycocotools,直接pip就能安装