前言:
在前面的几篇文章中,我们从正向传播,反向传播和最后评价这三个角度把模型训练时的整个过程经历剖析完成了,如果对于这些内容感兴趣可以看看作者本专栏的内容。上一篇文章的链接如下:
从验证训练角度解读YOLOV5的源码:程序是如何得到最后输出的maps,正确率等信息的?_vindicater的博客-优快云博客
以上内容的主体是train.py,同时也使用了一些其余的内容:比如说yolo.py用来提供model的forward过程,loss.py用来提供损失函数“compute_loss”,common.py用来提供最基础的部件的代码等等。
最后得到的结果是last.pt和另一个最好的模型best.pt
那么对于这篇博客来讲,作者希望介绍清楚的是整个detect.py的内容,也就是对于之前得到的best.pt,我应该如何使用这个权重文件?YOLO V5模型到底能够接受什么样的输入?输出又是什么呢?输出速度如何?作为单阶检测器究竟能否达到30帧的“实时检测”效果?本博客希望重新采取从问题出发的方式进行研究,带大家把实际运用这一步打通。如果对于本文内容觉得有帮助请点个赞吧ovo。
问题一、输入的形式是什么?
被处理内容的输入
YOLO v5 6.0ver提供三种形式的输入:图片或者图片集,视屏或者视屏集,和外接摄像头
具体的提供输出内容的方式就是通过修改命令行语句中source的参数值,比如下面这个就是告诉程序读取的位置是在data文件夹下的Images文件夹中
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob/screen/0(webcam)')
注意在这个文件夹之中统一包括了所有想要让机器处理的视频或者图片,机器会自动进行分类然后利用模型进行处理。这是非常方便的。
而如果把default置零,那么就是本机的摄像头,如下图所写:
parser.add_argument('--source', type=str, default=0, help='file/dir/URL/glob/screen/0(webcam)')
模型权重内容的输入
同样是在命令行中,这一次要修改的就是这个参数了。如果不在configuration中直接进行修改的话那么就是在这里修改default成为在这个目录下的相对路径
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path or triton URL')
判断读入的内容的过程
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
# 判断是不是一个文件地址suffix是后缀
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
webcam = source.isnumeric() or source.endswith('.streams') or (is_url and not is_file)
# isnumeric()source后面是否跟着是数字,如果source(0)那么就说明是调取本机的第一个摄像头
# 整个是判断是不是摄像头或者网络流形式
screenshot = source.lower().startswith('screen')
if is_url and is_file:
source = check_file(source) # download如果是网络流,下载图片或者视屏
问题二、我的模型文件后缀名是否必须是之前程序运行的结果:xxx.pt呢?是否可以使用从网络上下载的别的后缀名的权重文件呢?
common中提供了一个函数:detectMultiBackend,其目的就是确定模型是使用什么方式进行保存的,以使得后续的加载能够方便进行。
代码中加载函数的对应代码如下:
device = select_device(device)
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
# 在common中具体应该用什么语言:pytorch?onnx?之类的顺便定义了一些新参数,下面这一行的所有参数都在其中
stride, names, pt = model.stride, model.names, model.pt
DetectMultiBackend函数:
对于多种后缀名的模型文件进行加载处理:
可以看出其对于多种后缀名的模型读取方式都支持兼容
以.pt后缀名的权重文件为例子阐释DetectMultiBackend函数执行了什么准备工作:
代码展示:
if pt: # PyTorch
model = attempt_load(weights if isinstance(weights, list) else w, device=device, inplace=True, fuse=fuse)
# 如果输入的是一个列表那么就把所有的路径全部加载出来,其余情况下就直接用w进行加载,如果是有很多不同的路径那么就用experimental中的ensemble构造集成模型
# 问题?
stride = max(int(model.stride.max()), 32) # model stride
# 如果没有属性,就用default的stride=32;若有自己的属性
names = model.module.names if hasattr(model, 'module') else model.names # get class names
# 如果没有classname,那么就使用default的classname
model.half() if fp16 else model.float()
# 如果是半精度那就选择使用半精度,反之就不启用
self.model = model # explicitly assign for to(), cpu(), cuda(), half()
内容解析:
1、获得下采样的总步长(stride)
2、采用的是cuda还是CPU(cuda)
3、用attempt_load加载单个/集成模型(model)
4、种类相关(数量,名称)(names)
(用于detect.py中)
问题三、输入的内容如何读入模型之中?(以图片为例)
基础信息:
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride)
采用函数:LoadImages()
函数位置:dataloaders.py
结果:dataset:数据集,包括所有的图片和视频
数据集加载
Step1:从路径读取文件到files
1.取得绝对路径
if isinstance(path, str) and Path(path).suffix == '.txt': # *.txt file with img/vid/dir on each line
path = Path(path).read_text().rsplit()
2.从路径中加载图片
for p in sorted(path) if isinstance(path, (list, tuple)) else [path]:
# 这里的(list, tuple)意思是or的意思,意思是如果path是个数组,那么对这个数组进行排序
# 怀疑这里的修改是现在可以一次读取多张图片了
p = str(Path(p).resolve())
# 把p转化成绝对路径
if '*' in p:
files.extend(sorted(glob.glob(p, recursive=True))) # glob
# 如果p中有*,那么也就意味着取得是某一个目录下所有某一类的文件,那么把他们分类添加即可
elif os.path.isdir(p):
files.extend(sorted(glob.glob(os.path.join(p, '*.*')))) # dir
# 如果p是绝对路径且是目录,那么我们就把目录中的全部文件全部扔进files之中,和上面的意思其实相近似乎只是不同的表达方式
elif os.path.isfile(p):
files.append(p) # files
# 如果本身就已经是files了那么就把他扔进文件的文件夹中
else:
raise FileNotFoundError(f'{p} does not exist')
情况1:如果在p的内容中有*,取当前目录的所有文件
情况2:如果确定是一条路径,把路径文件夹中的文件取出
情况3:如果是一个文件的文件名,把这个文件路径放入files中
Step2:把files中的内容分类
1.将files中的Image和video分别根据后缀名区分开来放入不同的文件夹并统计
images = [x for x in files if x.split('.')[-1].lower() in IMG_FORMATS]
# 把files里面的图片单独拿出来放进images文件夹中
videos = [x for x in files if x.split('.')[-1].lower() in VID_FORMATS]
# 把files中后缀名是video的内容拿出来单独存放
2.如果有视频存在,创建一个视屏播放器
if any(videos):
# 如果有videos那是true,否则是false
self._new_video(videos[0]) # new video
# 以第一个video为内容,创建一个视屏处理器
从此:返回的dataset中多了两个几个参数:video文件夹和image文件夹
Step3:通过iter()和next()读入:
for path, im, im0s, vid_cap, s in dataset:
# 这一步调用了dataloader中的iter函数,这也就说明了对于loadimages类的内容进行遍历的时候就自动会使用魔法函数
# 看dataloader可以得到以下的一些参数:图片的路径,处理后的新图片,老图片,cap参数,和第几张图片的索引
通过遍历的方式读取传入的文件(图片、视屏)路径
else:
# Read image
self.count += 1
im0 = cv2.imread(path) # BGR
assert im0 is not None, f'Image Not Found {path}'
s = f'image {self.count}/{self.nf} {path}: '
如图:在next()中通过cv2.imread()函数读入图片进入循环
问题四、模型怎么进行结果的输出和展示?
Step1:利用模型前向传播得到这个文件(图片/视频)的预测框的信息的集合(pred)
这一块内容使用的和之前完全一样,进行处理之后使用DetectionModel类的构造函数进行前向传播得到对应的矩阵之后使用NMS方式得到置信度比较大比较合理的预测框。
Step2.1:对于得到的pred进行处理和一些函数的初始化
p = Path(p) # to Path
save_path = str(save_dir / p.name) # im.jpg "runs\\detect\\exp3","bus.jpg"
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # im.txt
s += '%gx%g ' % im.shape[2:] # print size of the picture
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh
# 获得原来的图片的宽和高的大小
imc = im0.copy() if save_crop else im0 # for save_crop
# 是否把图片框裁剪下来单独保存
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
具体得到的参数:
1.save_path:得到保存地址
2.annotator:构造一个标注函数用来将运算得到的结果标注在图片上
annotator的结构(plot.py):
1.构造函数:init()
self.pil = pil or non_ascii
if self.pil: # use PIL
self.im = im if isinstance(im, Image.Image) else Image.fromarray(im)
self.draw = ImageDraw.Draw(self.im)
self.font = check_pil_font(font='Arial.Unicode.ttf' if non_ascii else font,
size=font_size or max(round(sum(self.im.size) / 2 * 0.035), 12))
else: # use cv2
self.im = im
self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2) # line width
确定应该使用什么方式来读入图片:
1.应该使用哪个图片格式:PIL/cv2(默认是cv2)
2.画框的边线的粗细
2.画框用的函数:box_label()
if label:
w, h = self.font.getsize(label) # text width, height (WARNING: deprecated) in 9.2.0
# _, _, w, h = self.font.getbbox(label) # text width, height (New)
outside = box[1] - h >= 0 # label fits outside box
self.draw.rectangle(
(box[0], box[1] - h if outside else box[1], box[0] + w + 1,
box[1] + 1 if outside else box[1] + h + 1),
fill=color,
)
# self.draw.text((box[0], box[1]), label, fill=txt_color, font=self.font, anchor='ls') # for PIL>8.0
self.draw.text((box[0], box[1] - h if outside else box[1]), label, fill=txt_color, font=self.font)
主要函数:cv2.rectangle()
具体操作:给定对角的坐标做出一个长方形的框
3.初始化改变了些什么?
通过scale_boxes函数调整预测结果中框的位置和大小信息
det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round()
Step2.1:对于得到的pred进行处理和一些函数的初始化
1.通过打印输出找到了几种物体,分别数量是多少
for c in det[:, 5].unique():
# 。unique之后只剩下不同的元素了
n = (det[:, 5] == c).sum() # detections per class
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " # add to string
2.通过annotator.box_label函数将得到的label和处理过得到的真实的标注框的位置信息做可视化,具体就是将预测框标注在图片上
annotator.box_label(xyxy, label, color=colors(c, True))
3.最后将annotator.result()函数得到最后的结果保存在save_path中,最后print出来得到的结果和保存的路径。
if dataset.mode == 'image':
cv2.imwrite(save_path, im0)
至此,已经以图片为例得到了输出的结果,保存在了打印出来的路径之中,模型任务完成。
总结:
本文是从头到尾解析了具体什么样的内容可以放在模型中处理,这些内容究竟是如何被处理的等内容。并且就问题出发完整的展示出了从输入到最后输出的全过程。考虑到如果再把视频处理和摄像头处理的内容放在本文中,会导致文章冗长而大部分内容又有所重复。若对此感兴趣后续我会再出一篇文章解析。至此,结合之前发表的文章,将模型是如何训练和如何运用的内容讲解了一遍。如果读者认为哪里有谬误或者讲的不清楚的欢迎在评论区批评指正,我也会尽全力修改。如果觉得本文对于理解有所帮助不妨点赞关注一下作者ovo