yolov5 调用cocotools 评价自己的模型和数据集(AP低的问题已解决)
文章目录
1.使用方法
“前排提示,精度低的问题已经解决,方法可用”
yolov5 的数据集是.txt格式,coco数据集是.json格式,因此需要转换
安装cocotools
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))
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)
'''
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)
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()
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