本文主要对
ultralytics\yolov5
的测试代码test.py的解析,由于yolov5还在开发当中,平常多多少少都会修复一些bug或者有一些代码和功能的更新,但基本上不会有很大的改动,故以下注释与解析都是适用的;当然如果有大改动,笔者也会更新注释。
yolov5其他代码解析
if __name__ == '__main__':
opt参数详解
weights:测试的模型权重文件
data:数据集配置文件,数据集路径,类名等
batch-size:前向传播时的批次, 默认32
img-size:输入图片分辨率大小, 默认640
conf-thres:筛选框的时候的置信度阈值, 默认0.001
iou-thres:进行NMS的时候的IOU阈值, 默认0.65
save-json:是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签), 默认False
task:设置测试形式, 默认val, 具体可看下面代码解析注释
device:测试的设备,cpu;0(表示一个gpu设备cuda:0);0,1,2,3(多个gpu设备)
single-cls:数据集是否只有一个类别,默认False
augment:测试时是否使用TTA(test time augmentation), 默认False
merge:在进行NMS时,是否通过合并方式获得预测框, 默认False
verbose:是否打印出每个类别的mAP, 默认False
save-txt:是否以txt文件的形式保存模型预测的框坐标, 默认False
parser = argparse.ArgumentParser(prog='test.py')
parser.add_argument('--weights', nargs='+', type=str, default='runs/exp0/weights/last.pt', help='model.pt path(s)')
parser.add_argument('--data', type=str, default='data/mask.yaml', help='*.data path')
parser.add_argument('--batch-size', type=int, default=32, help='size of each image batch')
parser.add_argument('--img-size', type=int, default=416, help='inference size (pixels)')
parser.add_argument('--conf-thres', type=float, default=0.001, help='object confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.60, help='IOU threshold for NMS')
parser.add_argument('--save-json', action='store_true', help='save a cocoapi-compatible JSON results file')
parser.add_argument('--task', default='val', help="'val', 'test', 'study'")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
parser.add_argument('--augment', action='store_true', help='augmented inference')
parser.add_argument('--merge', action='store_true', help='use Merge NMS')
parser.add_argument('--verbose', action='store_true', help='report mAP by class')
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
opt = parser.parse_args()
opt.save_json = opt.save_json or opt.data.endswith('coco.yaml')
opt.data = check_file(opt.data)
print(opt)
if opt.task in ['val', 'test']:
test(opt.data,
opt.weights,
opt.batch_size,
opt.img_size,
opt.conf_thres,
opt.iou_thres,
opt.save_json,
opt.single_cls,
opt.augment,
opt.verbose)
elif opt.task == 'study':
for weights in ['yolov5s.pt', 'yolov5m.pt', 'yolov5l.pt', 'yolov5x.pt', 'yolov3-spp.pt']:
f = 'study_%s_%s.txt' % (Path(opt.data).stem, Path(weights).stem)
x = list(range(352, 832, 64))
y = []
for i in x:
print('\nRunning %s point %s...' % (f, i))
r, _, t = test(opt.data, weights, opt.batch_size, i, opt.conf_thres, opt.iou_thres, opt.save_json)
y.append(r + t)
np.savetxt(f, y, fmt='%10.4g')
os.system('zip -r study.zip study_*.txt')
plot_study_txt(f, x)
import argparse
import json
from models.experimental import *
from utils.datasets import *
def test(data,
weights=None,
batch_size=16,
imgsz=640,
conf_thres=0.001,
iou_thres=0.6,
save_json=False,
single_cls=False,
augment=False,
verbose=False,
model=None,
dataloader=None,
save_dir='',
merge=False,
save_txt=False):
:param data:
:param weights:
:param batch_size:
:param imgsz:
:param conf_thres:
:param iou_thres:
:param save_json:
:param single_cls:
:param augment:
:param verbose:
:param model: 测试的模型,训练时调用test传入
:param dataloader: 测试集的dataloader,训练时调用test传入
:param save_dir: 保存在测试时第一个batch的图片上画出标签框和预测框的图片路径
:param merge:
:param save_txt:
:return:
training = model is not None
if training:
device = next(model.parameters()).device
else:
device = torch_utils.select_device(opt.device, batch_size=batch_size)
merge, save_txt = opt.merge, opt.save_txt
if save_txt:
out = Path('inference/output')
if os.path.exists(out):
shutil.rmtree(out)
os.makedirs(out)
for f in glob.glob(str(Path(save_dir) / 'test_batch*.jpg')):
os.remove(f)
model = attempt_load(weights, map_location=device)
imgsz = check_img_size(imgsz, s=model.stride.max())
half = device.type != 'cpu' and torch.cuda.device_count() == 1
if half:
model.half()
model.eval()
with open(data) as f:
data = yaml.load(f, Loader=yaml.FullLoader)
nc = 1 if single_cls else int(data['nc'])
iouv = torch.linspace(0.5, 0.95, 10).to(device)
niou = iouv.numel()
if not training:
img = torch.zeros((1, 3, imgsz, imgsz), device=device)
_ = model(img.half() if half else img) if device.type != 'cpu' else None
path = data['test'] if opt.task == 'test' else data['val']
dataloader = create_dataloader(path, imgsz, batch_size, model.stride.max(), opt,
hyp=None, augment=False, cache=False, pad=0.5, rect=True)[0]
seen = 0
names = model.names if hasattr(model, 'names') else model.module.names
获取coco数据集的类别索引
这里要说明一下,coco数据集有80个类别(索引范围应该为0~79),
但是他的索引却属于0~90(笔者是通过查看coco数据测试集的json文件发现的,具体原因不知)
coco80_to_coco91_class()就是为了与上述索引对应起来,返回一个范围在0~90的索引数组
coco91class = coco80_to_coco91_class()
s = ('%20s' + '%12s' * 6) % ('Class', 'Images', 'Targets', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
p, r, f1, mp, mr, map50, map, t0, t1 = 0., 0., 0., 0., 0., 0., 0., 0., 0.
loss = torch.zeros(3, device=device)
jdict, stats, ap, ap_class = [], [], [], []
for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
img = img.to(device)
img = img.half() if half else img.float()
img /= 255.0
targets = targets.to(device)
nb, _, height, width = img.shape
whwh = torch.Tensor([width, height, width, height]).to(device)
with torch.no_grad():
time_synchronized()函数里面进行了torch.cuda.synchronize(),再返回的time.time()
torch.cuda.synchronize()等待gpu上完成所有的工作
总的来说就是这样测试时间会更准确
t = torch_utils.time_synchronized()
inf_out, train_out = model(img, augment=augment)
t0 += torch_utils.time_synchronized() - t
if training:
loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3]
t = torch_utils.time_synchronized()
non_max_suppression进行非极大值抑制;
conf_thres为置信度阈值,iou_thres为iou阈值
merge为是否合并框
output = non_max_suppression(inf_out, conf_thres=conf_thres, iou_thres=iou_thres, merge=merge)
t1 += torch_utils.time_synchronized() - t
for si, pred in enumerate(output):
labels = targets[targets[:, 0] == si, 1:]
nl = len(labels)
tcls = labels[:, 0].tolist() if nl else []
seen += 1
if pred is None:
if nl:
stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
continue
if save_txt:
gn = torch.tensor(shapes[si][0])[[1, 0, 1, 0]]
txt_path = str(out / Path(paths[si]).stem)
pred[:, :4] = scale_coords(img[si].shape[1:], pred[:, :4], shapes[si][0],
shapes[si][1])
for *xyxy, conf, cls in pred:
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()
with open(txt_path + '.txt', 'a') as f:
f.write(('%g ' * 5 + '\n') % (cls, *xywh))
clip_coords(pred, (height, width))
if save_json:
image_id = int(Path(paths[si]).stem.split('_')[-1])
box = pred[:, :4].clone()
scale_coords(img[si].shape[1:], box, shapes[si][0], shapes[si][1])
box = xyxy2xywh(box)
值得注意的是,之前所说的xyxy格式为左上角右下角的坐标
xywh是中心点坐标和长和宽
而coco的json格式中的框坐标格式为xywh,此处的xy为左上角坐标
也就是coco的json格式的坐标格式为:左上角坐标+长宽
所以下面一行代码就是将:中心点坐标->左上角
box[:, :2] -= box[:, 2:] / 2
image_id:图片id, 即属于哪张图
category_id: 类别, coco91class()从索引0~79映射到索引0~90
bbox:框的坐标
score:置信度
for p, b in zip(pred.tolist(), box.tolist()):
jdict.append({'image_id': image_id,
'category_id': coco91class[int(p[5])],
'bbox': [round(x, 3) for x in b],
'score': round(p[4], 5)})
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool, device=device)
if nl:
detected = []
tcls_tensor = labels[:, 0]
tbox = xywh2xyxy(labels[:, 1:5]) * whwh
for cls in torch.unique(tcls_tensor):
ti = (cls == tcls_tensor).nonzero().view(-1)
pi = (cls == pred[:, 5]).nonzero().view(-1)
if pi.shape[0]:
pred shape[N, 4]
tbox shape[M, 4]
box_iou shape[N, M]
ious shape[N, 1]
i shape[N, 1], i里的值属于0~M
ious, i = box_iou(pred[pi, :4], tbox[ti]).max(1)
for j in (ious > iouv[0]).nonzero():
d = ti[i[j]]
if d not in detected:
detected.append(d)
correct[pi[j]] = ious[j] > iouv
if len(detected) == nl:
break
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
if batch_i < 1:
f = Path(save_dir) / ('test_batch%g_gt.jpg' % batch_i)
plot_images(img, targets, paths, str(f), names)
f = Path(save_dir) / ('test_batch%g_pred.jpg' % batch_i)
plot_images(img, output_to_target(output, width, height), paths, str(f), names)
stats = [np.concatenate(x, 0) for x in zip(*stats)]
if len(stats):
p, r, ap, f1, ap_class = ap_per_class(*stats)
p, r, ap50, ap = p[:, 0], r[:, 0], ap[:, 0], ap.mean(1)
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
nt = np.bincount(stats[3].astype(np.int64), minlength=nc)
else:
nt = torch.zeros(1)
pf = '%20s' + '%12.3g' * 6
print(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
if verbose and nc > 1 and len(stats):
for i, c in enumerate(ap_class):
print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
t = tuple(x / seen * 1E3 for x in (t0, t1, t0 + t1)) + (imgsz, imgsz, batch_size)
if not training:
print('Speed: %.1f/%.1f/%.1f ms inference/NMS/total per %gx%g image at batch-size %g' % t)
if save_json and map50 and len(jdict):
imgIds = [int(Path(x).stem.split('_')[-1]) for x in dataloader.dataset.img_files]
f = 'detections_val2017_%s_results.json' % \
(weights.split(os.sep)[-1].replace('.pt', '') if isinstance(weights, str) else '')
print('\nCOCO mAP with pycocotools... saving %s...' % f)
with open(f, 'w') as file:
json.dump(jdict, file)
try:
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
cocoGt = COCO(glob.glob('../coco/annotations/instances_val*.json')[0])
cocoDt = cocoGt.loadRes(f)
cocoEval = COCOeval(cocoGt, cocoDt, 'bbox')
cocoEval.params.imgIds = imgIds
cocoEval.evaluate()
cocoEval.accumulate()
cocoEval.summarize()
map, map50 = cocoEval.stats[:2]
except:
print('WARNING: pycocotools must be installed with numpy==1.17 to run correctly. '
'See https://github.com/cocodataset/cocoapi/issues/356')
model.float()
maps = np.zeros(nc) + map
for i, c in enumerate(ap_class):
maps[c] = ap[i]
return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
以上我根据ultralytics\yolov5的train.py代码对其整体流程做一个梳理,讲解每个部分的代码的作用,但是对于一些细节函数还没做详细解析,就比如说计做NMS处理的non_max_suppression()函数等,这些函数在utils.py文件里,之后更新解析utils.py。
【为什么要学习这门课】 Linux创始人Linus Torvalds有一句名言:Talk is cheap. Show me the code. 冗谈不够,放码过来!代码阅读是从基础到提高的必由之路。 YOLOv5是最近推出的轻量且高性能的实时目标检测方法。YOLOv5使用PyTorch实现,含有很多业界前沿和常用的技巧,可以作为很好的代码阅读案例,让我们深入探究其实现原理,其中不少知识点的代码可以作为相关项目的借鉴。 【课程内容与收获】 本课程将详细解析YOLOv5的实现原理和源码,对关键代码使用PyCharm的debug模式逐行分析解读。 本课程将提供注释后的YOLOv5的源码程序文件。 【相关课程】 本人推出了有关YOLOv5目标检测的系列课程。请持续关注该系列的其它视频课程,包括:《YOLOv5(PyTorch)目标检测实战:训练自己的数据集》Ubuntu系统 https://edu.csdn.net/course/detail/30793 Windows系统 https://edu.csdn.net/course/detail/30923 《YOLOv5(PyTorch)目标检测:原理与源码解析》https://edu.csdn.net/course/detail/31428 《YOLOv5(PyTorch)目标检测实战:Flask Web部署》https://edu.csdn.net/course/detail/31087 《YOLOv5(PyTorch)目标检测实战:TensorRT加速部署》https://edu.csdn.net/course/detail/32303
YOLOv5代码详解 (第二部分)2. test.py2.1 设置超参数2.2 设置任务(验证,测试,学习)2.3 测试函数2.3.1 初始化模型
2. test.py
该部分主要用于运行train.py时,计算每个epoch的mAP。
PS,与train.py相似的部分就不再阐述。
2.1 设置超参数
权重,数据,batch size,图像尺寸,使用哪张显卡,数据增强,计算mAP。
if __name__ == '__main__':
parser = argparse.ArgumentParse
yolov5 test.py val.py detec.py 区别在哪里呢?
用户在训练数据的时候必须使用 train.py 来进行 数据训练和验证,但我很难理解detect.py 和test.py 之间的区别。应该在一个数据集中的(看不见的)测试数据上运行这两者中的哪一个?
首先: test.py 最近被重命名为 val.py!
这 3 个文件是为不同目的而设计的,并使用具有不同设置的不同数据加载器。
train.py 数据加载器旨在兼顾速度和准确性,
val.py 旨在获得验证数据集上的最佳 mAP
----------------------------------3.自定义模块----------------------------------from models.common import DetectMultiBackend # yolov5的网络结构(yolov5)from utils.callbacks import Callbacks # 和日志相关的回调函数from utils.datasets import create_dataloader # 加载数据集的函数。
值给提出来,所以绘图就比较潦草,直接用的matplotlib画的,如果要用于论文中的绘图,一般使用。(作用都是一样的,都是用已训练好权重对测试集进行验证,然后打印出一系列指标)(PR、F1的表格长得差不多,就是数据内容不同,表头相同,行数相同)(注意:本代码最重要的作用是将验证时得到的一系列。,即可运行得到整合图(可见博客最上面的效果图)的时候把F1值给提出来,所以我在下图代码中。,不是的话运行之后就不会生成。中做与YOLOv7同样的更改。中做与YOLOv7同样的更改。得到v7、v5、v8验证后的。
模块是一个包含所有你定义的函数和变量的文件,其后缀名是.py。模块可以被别的程序引入,以使用该模块中的函数等功能。这也是使用python 标准库的方法。
一、import语句
使用 Python 源文件,只需在另一个源文件里执行 import 语句,语法如下
import module1[, module2[,... moduleN]
当解释器遇到 import 语句,如果模块在当前的搜索路径就会...