yolov5 调用cocotools 评价自己的模型和数据集(AP低的问题已解决)

文章目录

  • 1.使用方法
  • 2.yolov5代码修改
  • 3.运行示例
  • 4.结论
  • 修改&原理(低AP原因)
  • 1.使用方法

    “前排提示,精度低的问题已经解决,方法可用”

    yolov5 的数据集是.txt格式,coco数据集是.json格式,因此需要转换
    安装cocotools

  • 代码1:
  • import os
    import cv2
    '''
    data:2022.4.6
    check:Y
    function:可将yolo格式的数据集转换为coco格式的(1), 生成annos.txt
    
    需要准备:
    labels:yolo格式的标签,是txt格式,名字为图片名
    images:原始标签对应的图片,需要有序号
    '''
    
    
    # 原始标签路径
    originLabelsDir = 'C:/My/Py_CODE/train/yolo_hostdata/valid/labels'
    # 转换后的文件保存路径
    saveDir = 'C:/My/Py_CODE/train/coco/annos1.txt'
    # 原始标签对应的图片路径
    originImagesDir = 'C:/My/Py_CODE/train/yolo_hostdata/valid/images'
    
    txtFileList = os.listdir(originLabelsDir)
    with open(saveDir, 'w') as fw:
        for txtFile in txtFileList:
            with open(os.path.join(originLabelsDir, txtFile), 'r') as fr:
                labelList = fr.readlines()
                for label in labelList:
                    label = label.strip().split()
                    x = float(label[1])
                    y = float(label[2])
                    w = float(label[3])
                    h = float(label[4])
    
                    # convert x,y,w,h to x1,y1,x2,y2
                    imagePath = os.path.join(originImagesDir,
                                             txtFile.replace('txt', 'jpg'))
                    image = cv2.imread(imagePath)
                    H, W, _ = image.shape
                    x1 = (x - w / 2) * W
                    y1 = (y - h / 2) * H
                    x2 = (x + w / 2) * W
                    y2 = (y + h / 2) * H
                    # 为了与coco标签方式对,标签序号从1开始计算
                    fw.write(txtFile.replace('txt', 'jpg') + ' {} {} {} {} {}\n'.format(int(label[0]) + 1, x1, y1, x2, y2))
    
            print('{} done'.format(txtFile))
    
  • 代码2
  • import json
    import os
    import cv2
    
    #-------------------可用-----------------------------------
    '''
    data:2022.4.6
    check:Y
    function:可将yolo格式的数据集转换为coco格式的(2)
    
    需要准备:
    classes.txt:一行就是一个类,不需要数字,只要类名称
    annos.txt:由上一个.py文件生成
    images:与annos.txt对应的图片,需要有序号
    
    生成.json文件,在annotations文件下
    '''
    
    # ------------用os提取images文件夹中的图片名称,并且将BBox都读进去------------
    # 根路径,里面包含images(图片文件夹),annos.txt(bbox标注),classes.txt(类别标签),
    # 以及annotations文件夹(如果没有则会自动创建,用于保存最后的json)
    root_path = 'C:/My/Py_CODE/train/coco'
    # 用于创建训练集或验证集
    phase = 'val'  # 需要修正,保存后的json文件名
    
    # dataset用于保存所有数据的图片信息和标注信息
    dataset = {'categories': [], 'annotations': [], 'images': []}
    
    # 打开类别标签
    with open(os.path.join(root_path, 'classes.txt')) as f:
        classes = f.read().strip().split()
    
    # 建立类别标签和数字id的对应关系
    for i, cls in enumerate(classes, 1):
        dataset['categories'].append({'id': i, 'name': cls, 'supercategory': 'mark'})
    
    # 读取images文件夹的图片名称
    indexes = os.listdir(os.path.join(root_path, './valid/images'))
    
    # 统计处理图片的数量
    global count
    count = 0
    
    # 读取Bbox信息
    with open(os.path.join(root_path, './valid/annos.txt')) as tr:
        annos = tr.readlines()
    
        # ---------------接着将,以上数据转换为COCO所需要的格式---------------
        for k, index in enumerate(indexes):
            count += 1
            # 用opencv读取图片,得到图像的宽和高
            im = cv2.imread(os.path.join(root_path, './valid/images/') + index)
            height, width, _ = im.shape
    
            # 添加图像的信息到dataset中
            dataset['images'].append({'file_name': index.replace("\\", "/"),
                                      'id': int(index[5:10]),  # 提取文件名 里的数字标号 必须是int类型,不能是str
                                      'width': width,
                                      'height': height})
    
            for ii, anno in enumerate(annos):
                parts = anno.strip().split()
    
                # 如果图像的名称和标记的名称对上,则添加标记
                if parts[0] == index:
                    # 类别
                    cls_id = parts[1]
                    # x_min
                    x1 = float(parts[2])
                    # y_min
                    y1 = float(parts[3])
                    # x_max
                    x2 = float(parts[4])
                    # y_max
                    y2 = float(parts[5])
                    width = max(0, x2 - x1)
                    height = max(0, y2 - y1)
                    dataset['annotations'].append({
                        'area': width * height,
                        'bbox': [x1, y1, width, height],
                        'category_id': int(cls_id),
                        'id': ii,
                        'image_id': int(index[5:10]),	# 提取文件名里的数字标号  必须是int类型,不能是str
                        'iscrowd': 0,
                        # mask, 矩形是从左上角点按顺时针的四个顶点
                        'segmentation': [[x1, y1, x2, y1, x2, y2, x1, y2]]
                    })
    
            print('{} images handled'.format(count))
    
    # 保存结果的文件夹
    folder = os.path.join(root_path, './valid/annotations')
    if not os.path.exists(folder):
        os.makedirs(folder)
    json_name = os.path.join(root_path, './valid/annotations/{}.json'.format(phase))
    with open(json_name, 'w') as f:
        json.dump(dataset, f, ensure_ascii=False, indent=1)
    
  • 代码3
  • '''
    data:2022.4.6
    check:Y
    function:载入coco格式的数据集
    '''
    import json
    
    json_path = r'instances_val2017.json'
    json_labels = json.load(open(json_path, "r"))
    
    
    # load coco data
    print(json_labels)
    
  • 代码4
  • import os
    from pycocotools.coco import COCO
    from PIL import Image, ImageDraw
    import matplotlib.pyplot as plt
    '''
    data:2022.4.6
    check:Y
    function:绘制标注的数据集和对应图片的位置是否正确
    绘制前三张,如果yoyo转换标注的json数据集没有问题,则也能在图上画出框
    
    '''
    json_path = 'C:/My/Py_CODE/train/coco/valid/annotations/train.json'
    # json_path = r'predictions.json'
    img_path = 'C:/My/Py_CODE/train/coco/valid/images'  #json对应的图片
    
    # load coco data
    coco = COCO(annotation_file=json_path)
    
    # get all image index info
    ids = list(sorted(coco.imgs.keys()))
    print("number of images: {}".format(len(ids)))
    
    # get all coco class labels
    coco_classes = dict([(v["id"], v["name"]) for k, v in coco.cats.items()])
    
    # 遍历前三张图像
    for img_id in ids[:3]:
        # 获取对应图像id的所有annotations idx信息
        ann_ids = coco.getAnnIds(imgIds=img_id)
    
        # 根据annotations idx信息获取所有标注信息
        targets = coco.loadAnns(ann_ids)
    
        # get image file name
        path = coco.loadImgs(img_id)[0]['file_name']
    
        # read image
        img = Image.open(os.path.join(img_path, path)).convert('RGB')
        draw = ImageDraw.Draw(img)
        # draw box to image
        for target in targets:
            x, y, w, h = target["bbox"]
            x1, y1, x2, y2 = x, y, int(x + w), int(y + h)
            draw.rectangle((x1, y1, x2, y2))
            draw.text((x1, y1), coco_classes[target["category_id"]])
    
        # show image
        plt.imshow(img)
        plt.show()
    
    
  • 代码5
  • from pycocotools.coco import COCO
    from pycocotools.cocoeval import COCOeval
    '''
    data:2022.4.6
    check:Y
    function:验证标注的数据集和预测的数据集
    如果格式不对,则会报错
    如果无误,会返回检测结果
    '''
    
    # accumulate predictions from all images
    # 载入coco2017验证集标注文件
    # coco_true = COCO(annotation_file='coco/train.json')
    coco_true = COCO(annotation_file=r'instances_val2017.json')  # 标准数据集(真值)
    # coco_pre = coco_true.loadRes('predictions.json')
    coco_pre = coco_true.loadRes('predict_results.json')  # 预测数据集(预测值)
    # 载入网络在coco2017验证集上预测的结果
    coco_evaluator = COCOeval(cocoGt=coco_true, cocoDt=coco_pre, iouType="bbox")    #计算bbox值
    coco_evaluator.evaluate()
    coco_evaluator.accumulate()
    coco_evaluator.summarize()
    

    以上5个代码是yolo数据集到coco数据集标签,和使用cocotools的全过程代码

    2.yolov5代码修改

    直接对val.py修改

    '--save-json' 添加 default=True
    parser.add_argument('--save-json', default=True, action='store_true', help='save a COCO-JSON results file')
    
    注释下句
    # opt.save_json |= opt.data.endswith('coco.yaml')
    
    修改save_one_json()函数
           # Save/log
           if save_txt:
                save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
           if save_json:
                save_one_json(predn, jdict, path, class_map)  # append to COCO-JSON dictionary
           if wandb_logger and wandb_logger.wandb_run:
                wandb_logger.val_one_image(pred, predn, path, names, img[si])  
    
    def save_one_json(predn, jdict, path, class_map):
        # Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
        image_id = int(path.stem) if path.stem.isnumeric() else path.stem
        box = xyxy2xywh(predn[:, :4])  # xywh
        box[:, :2] -= box[:, 2:] / 2  # xy center to top-left corner
        for p, b in zip(predn.tolist(), box.tolist()):
            jdict.append({'image_id': image_id,
                          'category_id': class_map[int(p[5])],
                          'bbox': [round(x, 3) for x in b],
                          'score': round(p[4], 5)})
    

    save_one_json()函数主要注意image_id = int(path.stem) if path.stem.isnumeric() else path.stem这一参数,要和数据集的对应上(需要检测),内容是图片名,必须是数字,因此可能需要截取图片名的字符段。
    经测试 image_id 必须为 int类型,不能是字符串

    image_id = int(path.stem[5:])  #这个视情况而定
    
    path.stem是指验证集图片名,如host0000001.jpg  那么path.stem为host0000001,则取数字部分:path.stem[5:]  #为0000001,这里需要和标的验证集里.txt转.json时相同。
    

    如果没有对应上,那么会报以下错误:
    pycocotools unable to run: Results do not correspond to current coco set

        # Save JSON
        if save_json and len(jdict):
            w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else ''  # weights
            anno_json = str(Path(data.get('path', '../mydata')) / 'labels/coco_val/annotations/train.json')  # annotations json
            pred_json = str(save_dir / f"{w}predictions.json")  # predictions json
            print(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
            with open(pred_json, 'w') as f:
                json.dump(jdict, f)
    
    

    anno_json 是指验证集json的路径
    pred_json 是指模型预测生成的路径,这个不需要改

    需要改
    json.dump(jdict, f)
    为
    json.dump(jdict, f, ensure_ascii=False, indent=1)
    

    json.dump是json文件写入函数,加上以上命令是为了生成的json文件是多行的,区别如下:

    不加

    加入:

    这样就方便自查格式了

    3.运行示例

    修改后,全流程下来:
    运行val.py

    自动cocotools评估:

    4.结论

    与yolo自身评价的mAP@50相比,用cocotools获得的评价值很低,不知道为什么,有知道的可以分享一下


    上面那个精度低的问题解决了,经过测试分析原来是 json格式里的关键字:‘category_id’ 出的问题,它的意思是 “一张图片里的类别标号”

    原因简单来说就是:
    假设图片里两个目标类别,如果用我的yolo(txt)转coco(json)代码,那么它生成的json文件(真值)里category_id分别为 1,2,原因是在我的第一个代码里:

     # 为了与coco标签方式对,标签序号从1开始计算
                    fw.write(txtFile.replace('txt', 'jpg') + ' {} {} {} {} {}\n'.format(int(label[0]) + 1, x1, y1, x2, y2))
    

    他是从1开始标记类别号的。

    而val.py里,按照我的方法修改后,它预测的时候是从0开始标记类别号的,这样就错位了,它生成的json文件(预测值)里category_id分别为 0,1

    所以完全对不上,精度就很低了

    修改&原理(低AP原因)

    方法一:
    category_id以 0 开始标记

    从制作json数据集的第一个代码开始改:

     # 为了与coco标签方式对,标签序号从0开始计算
                    fw.write(txtFile.replace('txt', 'jpg') + ' {} {} {} {} {}\n'.format(int(label[0]), x1, y1, x2, y2))
    

    代码 int(label[0]) 去掉 +1

    val.py其余地方不用改,按照 2.yolov5代码修改 的方法修改就行

    方法二:
    category_id以1 开始标记类别号

    不用修改制作数据集的代码,仍以1开始标记类别号

    在原来的修改基础上,再修改val.py

    原代码:
    model.eval()
    is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt')  # COCO dataset
    nc = 1 if single_cls else int(data['nc'])  # number of classes
    
    修改为:
    model.eval()
    is_coco = True
    nc = 1 if single_cls else int(data['nc'])  # number of classes
    

    原理:
    原来的代码:

    is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt')  # COCO dataset
    

    bool(is_coco ) = False
    因为他不是标准的coco数据集

    那么就会造成下面语句:

    class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
    

    class_map = list(range(1000)) 显然是从0~999取值

    而save_one_json()函数里
    ‘category_id’: class_map[int(p[5])]
    所以也是从0开始的

    如果修改
    is_coco = True

    那么:

    class_map = coco80_to_coco91_class()
    

    coco80_to_coco91_class()的作用是从80类映射到91类的coco索引,他是从1开始取值的,而且有的值还没有。为了符合标注coco数据集的映射,如下图所示

    所以还是建议将 is_coco = False,如果用自己的数据集,不需要将 “80类映射到91类” 此方法二不建议使用

    最后所得评价结果是:

    这个结果就很舒服,,,,,,,
    哈哈哈哈哈哈哈哈

    来源:Yang_4881002

    物联沃分享整理
    物联沃-IOTWORD物联网 » yolov5 调用cocotools 评价自己的模型和数据集(AP低的问题已解决)

    发表评论