Yolo v5学习笔记

Yolo v5学习笔记

安装环境

下载miniconda

Index of /anaconda/miniconda/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

image-20240727100941370

打开Anaconda Prompt

conda create -n yolov5 python=3.8

配置PYPI 镜像

打开https://mirrors.tuna.tsinghua.edu.cn/help/pypi/

复制下面这句

image-20240730202046206

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

安装pytorch

这里选择的是 v1.8.2版本,我直接下载的cpu的嘻嘻~

pip install torch==1.8.2 torchvision==0.9.2 torchaudio==0.8.2 --extra-index-url https://download.pytorch.org/whl/lts/1.8/cpu

安装yolov5

解压文件,将文件中的requirements.txt打开,将

image-20240730203108761

然后打开yolo文件夹所在位置,运行

pip install -r requirements.txt

运行测试

运行两次

python detect.py

image-20240730203812728

成功

image-20240730203915363

关于yolov5安装的补充

image-20240730204613140

image-20240730204953702

实战!Yolov5模型检测

一、关键参数

1.weights 参数

image-20240730212209740

2.source参数

7c7478ac274a5d6a4f55aa1e9774d27

例如,对某个目录下的某张图片进行检测,

python detect.py --weights weights/yolov5x.pt --source data/images/bus.jpg

哈哈哈哈笑死我啦,运行上一行代码让我把Pillow降级或者numpy升级,我把Pillow降级为了8.4.0,现在又让我升级啦,如何呢??嘻嘻

Ultralytics(YOLOv5 的维护者)要求安装一个特定版本或更高版本的 Pillow(至少为 10.3.0),在运行代码时,会将pillow自动升级,所以我们选择升级numpy~

下边是对屏幕进行检测

python detect.py --weights weights/yolov5x.pt --source screen

3.conf-thres 置信度阈值

置信度阈值,越低框越多,越高框越少(当检测到的目标的轮廓小于置信度阈值的时候,这个框不显示)

python detect.py --weights weights/yolov5s.pt --conf 0.25

image-20240730220039219

4.iou-thres IOU阈值

IOU阈值,越低框越少,越高框越多(与上一个相反)

image-20240730220107296

5.其他参数(其他参数在detect.py文件里可以看到)

  • imgsz 输入图片的默认大小
  • max-det 在一个图片中最大的检测数量
  • device 设备,如果过不指定的话,系统会自己匹配一个合适的设备
  • view-img 会把检测结果弹出一个框来
  • classes 指定检测哪几类

二、基于torch.hub的检测方法

提供更为简单的检测途径,特别是放到一个可视化界面中

这里我们用jupyter进行编写

如果没有的话可以先安装:

pip install jupyterlab
import torch
# Model 加载模型 模型所在位置 模型名字 来源为本地
model = torch.hub.load("./","yolov5s",source="local")
# Images 指定检测的内容
img="./data/images/zidane.jpg'
# Inference
results = model(img)
# Results
results.show()

ok了,又说我numpy版本不行了,笑一下好了Successfully installed numpy-1.24.4~

还是不对,Successfully installed numpy-1.20.3~

都不行,怎么回事老弟,我学不学了?

Successfully installed numpy-1.23.0 解决问题

三、数据集构建

image-20240731085147176

数据收集

import cv2
import matplotlib.pyplot as plt
# 打开视频文件
video = cv2.VideoCapture("./BVN.mp4")
# 读取一帧 这个ret是boolean类型的,如果能获取图片,则为true
ret,frame = video.read()
plt.imshow(frame)

这里读取图片不是按RGB通道读取的,是用的BGR通道,所以我们下边转换一下通道

plt.imshow(cv2.cvtColor(frame,cv2.COLOR_BGR2RGB))

这里是读取视频,将截取的图片存到文件夹里

video = cv2.VideoCapture("./BVN.mp4")
num = 0 # 计数器
save_step = 30 # 间隔帧
while True:
ret,frame = video.read()
if not ret:
break
num += 1
if num % save_step == 0:
cv2.imwrite("./images/"+str(num)+".jpg",frame)

标注工具

下载标注工具

pip install labelimg

在终端运行这个,然后打开新页面

labelimg

image-20240731094552042

第一步,将模式设置为yolo

image-20240731094619692

第二步,点击view,点击Auto Save mode,设置为自动保存

第三步,点击左侧菜单中的Open Dir,第一次选择的文件夹是标注的图片位置(images),第二次我们创建一个和images同级的labels文件夹,用于存放标注图片

image-20240731095107975

第四步,点击右侧打开图片,右击图片选择第一个选项开始标注(或者按W)

+

按D是下一张,A是上一张

标注完成后,我们labels文件里就有东西了

image-20240731100214137

里边的classes.txt文件存放的是我们是别的标签

四、yolo模型训练

数据调整

image-20240731101804878

像这样分好

image-20240801100229124

关键参数

image-20240801100358147

第一步,把datasets文件移进来

image-20240801101445244

第二步,把下边这个yaml文件复制并且改名

image-20240801101639471

第三步,配置这个刚复制过来的yaml文件

image-20240801101757733

第四步,打开train.py文件,改一个参数

image-20240801102759209

第五步,运行!点击右上角的运行标志

然后我又报错了,AttributeError: module ‘numpy.typing’ has no attribute ‘NDArray’

还是numpy,如何呢老弟?

报错是因为之前的requirements.txt文件中写了numpy的版本,运行之后会自动匹配文件里的版本,这样才会报错。改一下就好了

image-20240801150206844

运行了一下,电脑黑屏了(嗯…..)

常见问题

image-20240801153422514

训练结果

我没训出来,半个小时进度8/99太慢了,学完再慢慢训吧,别把我训没了

我是笨蛋,因为路径打错了,所以人家自动下载了别的数据集,所以下载慢

训好的东西存在run/train/exp里边

训练集:

image-20240801194822945

验证:

image-20240801194852703

运行tensorboard --logdir runs可以看到很多表

检测视频

python detect.py --weights
runs/train/exp/weights/best.pt --source
datasets/mp4/bvn.mp4 --view-img

五、Pyside6可视化界面

环境安装

image-20240801162444851

在终端运行where python查看python路径·

image-20240801200723532

按路径找到pydesigner.exe后,创建快捷方式到桌面

需要把Qt生成的UI文件转换为python文件,需要一个插件,我们在vscode里搜索下载

image-20240801201304383

接下来打开Qt开始设计

第一步,将元素拖拽组合

image-20240801202804433

第二步,设置属性

image-20240801202908690

为了调整图片自适应大小,把下边这个也勾选上

image-20240801203010720

我们再对各个元素进行一个命名

image-20240801203258531

到这里我们的界面就设计完成啦,ctrl+s保存。

把这个ui文件放在yolov5文件夹里,然后右键

image-20240801204653231

点击之后我们就获得了一个main_window_ui.py文件

接着我们创建一个base_ui.py文件,把下边的代码复制进去

import sys
from PySide6.QtWidgets import QMainWindow,QApplication

from main_window_ui import Ui_MainWindow

class MainWindow(QMainWindow,Ui_MainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.setupUi(self)

if __name__ == "__main__":
app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

在这里运行自己的模型的时候,出来的图片没有被检测到,不知道为什么捏

image-20240801221025508

是因为我的训练太少啦!改一下训练轮次

image-20240803074351792

有convert2QImage是因为模型预测得到的 list数据要转化为 QImage类型后,才能继续转化为QPixmap类型,进而才能显示出来

import sys
from PySide6.QtWidgets import QMainWindow,QApplication,QFileDialog
from PySide6.QtGui import QPixmap,QImage
from main_window_ui import Ui_MainWindow
import torch

def convert2QImage(img):
height,width,channel = img.shape
return QImage(img,width,height,width*channel,QImage.Format_RGB8888)

class MainWindow(QMainWindow,Ui_MainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.setupUi(self)
self.model = torch.hub.load("./","custom",path = "runs/train/exp8/weights/best.pt",source="local")
self.bind_slots()

def image_pred(self,file_path):
results = self.model(file_path)
image = results.render()[0]
return convert2QImage(image)

def open_image(self):
print("点击了图片")
file_path = QFileDialog.getOpenFileName(self,dir = "./datasets/images/train",filter = "*.jpg;*.png;*.jpeg")
if file_path[0]:
file_path = file_path[0]
qimage = self.image_pred(file_path)
self.input.setPixmap(QPixmap(file_path))
self.output.setPixmap(QPixmap,fromImage(qimage))

def open_video(self):
pass

def bind_slots(self):
self.det_image.clicked.connect(self.open_image)
self.det_video.clicked.connect(self.open_video)

if __name__ == "__main__":
app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

现在图像的检测就搞定了(视频里在这里卸载了pywin32)

下边我们对视频进行检测

当我们写完进行检测时发现,页面很卡,这是因为Qt的事件阻塞(connect+while true),我们可以启用计时器来解决这个问题(QTime),也就相当于把while true这里的逻辑替换成了QTimer

import cv2
import sys
from PySide6.QtWidgets import QMainWindow,QApplication,QFileDialog
from PySide6.QtGui import QPixmap,QImage
from main_window_ui import Ui_MainWindow
import torch
from PySide6.QtCore import QTimer

def convert2QImage(img):
height,width,channel = img.shape
return QImage(img,width,height,width*channel,QImage.Format_RGB888)

class MainWindow(QMainWindow,Ui_MainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.setupUi(self)
self.model = torch.hub.load("./","custom",path = "runs/train/exp9/weights/best.pt",source="local")
self.timer = QTimer()
self.timer.setInterval(100)
self.vide = None
self.bind_slots()

def image_pred(self,file_path):
results = self.model(file_path)
image = results.render()[0]
return convert2QImage(image)

def open_image(self):
print("点击了图片")
file_path = QFileDialog.getOpenFileName(self,dir = "./datasets/images/train",filter = "*.jpg;*.png;*.jpeg")
if file_path[0]:
file_path = file_path[0]
qimage = self.image_pred(file_path)
self.input.setPixmap(QPixmap(file_path))
self.output.setPixmap(QPixmap.fromImage(qimage))

def open_video(self):
print("点击了检测视频")
file_path = QFileDialog.getOpenFileName(self,dir = "./datasets",filter = "*.mp4")
if file_path[0]:
file_path = file_path[0]
self.video = cv2.VideoCapture(file_path)
self.timer.start()



def video_pred(self,img):
ret,frame = self.video.read()
if not ret:
self.timer.stop()
else:
frame = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)
self.output.setPixmap(QPixmap.fromImage(convert2QImage(image)))
results = self.model(frame)
image = results.render()[0]
self.input.setPixmap(QPixmap.fromImage(convert2QImage(frame)))




def bind_slots(self):
self.det_image.clicked.connect(self.open_image)
self.det_video.clicked.connect(self.open_video)
self.timer.timeout.connect(self.video_pred)

if __name__ == "__main__":
app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

但是我们在检测视频的时候打开图片,图片会一闪然后消失,这是因为计时器还在干活

,我们优化一下逻辑。

image-20240803085557432

效果展示

image-20240803100836319

六、Yolov5 Gradio搭建Web GUI

相比于pyside6,少了很多代码,可以在web上显示

image-20240803085901290

在终端输入pip install gradio,新建文件夹gradio_demo.py

import torch
import gradio as gr

model = torch.hub.load("./","custom",path = "runs/train/exp9/weights/best.pt",source="local")

title = "基于Gradio的Yolov5演示项目"

desc = "这是一个基于Gradio的Yolov5演示项目,太酷啦"

base_conf,base_iou = 0.25,0.45

def det_image(img,conf_thres,iou_thres):
model.conf = conf_thres
model.iou = iou_thres
return model(img).render()[0]

gr.Interface(inputs=["image",gr.Slider(minimum=0,maximum=1,value=base_conf),gr.Slider(minimum=0,maximum=1,value=base_conf)],
outputs=["image"],
fn=det_image,
title = title,
description=desc,
live = title, # 可以不点提交就看结果
examples=[
["./datasets/images/train/0b4a52e3-e15e-4117-b2e8-7cdb5dca3ce9___FREC_Scab 3137 - 副本.JPG", 0.3, base_iou],
["./datasets/images/train/0a5e9323-dbad-432d-ac58-d291718345d9___FREC_Scab 3417_90deg.JPG", base_conf, base_iou]
]).launch(share=True) # share = True的意思是可以在公网访问

这是结果展示

image-20240803101730808

Yolov5拓展进阶

image-20240803110431874

一、模型结构与构建原理

image-20240803110458391

我们打开models文件夹,里边有很多模型的yaml文件,我们点开yolov5s.yaml来看一下

image-20240803110946651

  • nc 是类别数量
  • 中间的表示图片的大小
  • anchors 对应了三组模框

image-20240803111114876

backbone解释:

  • from:输入来自那一层,-1代表上一次,1代表第1层,3代表第3层
  • number:模块的数量,最终数量需要乘 depth_multiple,然后四舍五入取整,如果小于1,取1(看上图,除了C3模块不为1,其他都是等于1的)。
  • module:子模块 (用的啥模块)
  • args:模块参数,channel,kernel_size,stride,padding,bias等

二、使用Tensorboard可视化

在终端输入命令下载:

pip install tensorboard==2.12.0

输入命令打开可视化界面

tensorboard --logdir runs

我打不开,报错:

image-20240803162647024

image-20240803164336196

image-20240803164431951

找了解决办法,但是还是不行。

所以下边的图是用的何老师的

进入浏览器后,点击“graphs”,然后双击detection Model,进入了模型得内部细节

image-20240803165257453

三、数据流动过程(二刷明白基础流程逻辑了,但还是感觉不太透彻)

向上采样(*2)(Upsampling):
向上采样是指增加图像的分辨率,通常通过插值方法在图像中插入新的像素来实现。常见的插值方法包括最近邻插值、双线性插值和双三次插值等。通过向上采样,可以增加图像的细节和清晰度,但同时也增加了图像的计算量和存储空间。

向下采样(/2)(Downsampling):
向下采样是指降低图像的分辨率,通常通过从图像中删除一些像素来实现。这些被删除的像素通常是通过取样的方式进行选择。向下采样可以减少图像的计算量和存储空间,但也可能导致信息丢失和图像质量下降。

下面从input的图片开始,整个走一遍

image-20240803170805546

input输入的图片是 1×3×640×640

image-20240803180118099

经过Conv[0] 之后,变成了 1 × 32 × 320 × 320

image-20240803180245206

1×3×640×640 变成了 1 × 32 × 320 × 320,图片缩小了1/2,参考yolov5s。yaml中的代码,如下图(#0 表示第0层,#1表示第一层)

image-20240803180318505

到了#2,C3的输出是 1 × 64 × 160 × 160

到了#3 Conv的输出是 1×128×80×80

到了#5,又经过一次卷积,1 × 256 × 40 × 40 ,也就是 640 / 16= 40,缩小了16倍。到了#7, 又经过一次卷积,输出是 1 × 512 × 20 × 20

到了#9,其实是第10层了(SPPF),输出还是 1 × 512 × 20 ×20,如下图

image-20240803180458620

下面是head部分

image-20240803183607122

head的第一层是Conv[10],输出是 1 × 256 × 20 × 20

由于在#11做了上采样,输出是 1 × 256 × 40 × 40

image-20240803183730829

那么在#12,进行拼接,一个来自#6层(1 × 256 × 40 × 40),

一个来自#11(上采样后 1 × 256 × 40 × 40)

最后一层,来自#17,#20和#23的特征图

[[17, 20, 23], 1, Detect, [nc, anchors]], #Detect(P3, P4, P5)

head里的[-1,3,c3,[256,False]],#17(P3/8-small)是八倍下采样,是用来检测小目标的

[-1,3,C3,[512,False]],#20(P4/16-medium)是十六倍下采样(40*40),是用来检测中等大小的目标的

[-1,3,C3,[1024,False]],#23(P5/32-large)是32倍下采样,是用来检测比较大的物体的

接着我们看,代码是怎么实现这个文件的

我们打开train.py文件

image-20240803184609868

cfg就是config文件,就是我们的yolov5s.yaml文件,ch=3就是通道数是3,nc是我们要识别的东西(我们传过去之后会自动更新)

可以在这里创建新的模型common.py

image-20240803200701494

可以在这里引入模型,进入模型解析,并传入需要的参数

image-20240803201349767

有关深度和宽度的控制,如下:

image-20240803183841274

上图中,args里的第一个为定义通道数,实际的通道数要 乘以width_multiple

四、修改网络结构–以C2f为例

image-20240803203106342

v8希望框架以命令行的形式去执行,做了很多额外的东西,所以跟v5长得不太一样

如果要看源码的话,咱们就去ultralytics文件里找

打开ultralytics-nn-moudles.py.(moudles.py在新版本里是没有的,我们这里用的是8.0.90)

这个文件其实就相当于yolov5中的common.py文件

然后我们看这个c2f,把这个模块复制到yolov5的common文件里去

image-20240803205838985

我们放在了c3模块的前边

image-20240803210403258

因为模块c2f的Bottleneck里有参数k,是yolov5中不包含的,所以我们把Bottleneck相应的部分也复制过去。

为了避免这个会对yolov5其他的部分造成影响,我们这里将Bottleneck重命名为C2fBottleneck

通过比较可以看出,C2f和C3所需要的参数基本是一样的,所以我们打开yolov5的yolo.py文件,找C3所在的位置,把C2f也放进去就行了

先导入我们新加的C2f

image-20240803211542406

主要是在parse_model这个模块里边

image-20240803211818914

然后我们修改一下yaml文件,我们把yolov5s.yaml文件复制一份,命名为yolov4sC2f.yaml,将文件里的C3替换为C2f

image-20240803212341582

下面我们开始训练

打开train.py文件,找到parse_opt模块

如果我们要训练自己的模型,就要更改这个cfg参数

image-20240803212933909

但是weights参数对我们的影响还是有的,有参数的话,就根据这个权重继续训练,如果没有的话,就相当于完全的从头开始训练。

我们看一下这部分的相关代码:

image-20240803213629860

如果pretrained识别到了这个以pt结尾的文件,就根据这个权重继续训练。

下面我们运行命令直接训练一下

python train.py

如果报错tensorflow.python.framework.errors_impl.FailedPreconditionError: runs is not a directory是因为文件路径里有中文,改一下就好了

训练过程中我们会发现,结果是很多零,这是因为我们Cf2生成的一些随机的坐标是很难对应上的,但是不用担心,让它多训练会儿就好了,视频中训练到25轮次的时候就出现数字了,但是我呢,您猜怎么着,41轮了还没有(爽啦)

image-20240805074917390

我到第59轮才有的数字

image-20240805080126475

运行完成之后,我这里有一个报错:AttributeError: module 'PIL.Image' has no attribute 'ANTIALIAS'

image-20240805082302153

是因为在较新的版本的 Pillow(PIL 的一个分支)中,ANTIALIAS 常量已被移除。在 Pillow 10.0.0 及以后的版本中,需要使用 Image.LANCZOS 来替代 Image.ANTIALIAS

image-20240805082235758

咱们的训练结果已经出来了,这个错误主要影响 TensorBoard 日志的生成和记录。咱们暂时不需要 TensorBoard 的可视化结果,所以忽略这个错误,训练模型的性能不会受到影响。

但是非常不幸,咱们训练的这个模型效果不太好,并没有检测到目标,可以增加训练轮次?但是他们说按理来说这是可以训练出来的,不知道我的为啥检测不到,就算我再训练一次也不见得会好(瘫)

image-20240805082747420

不对啊,我这个怎么跟从头训练的一样?怎么回事

我们把train.py里的weights参数改一下,pt文件就用刚刚训练出来的(看的何老师笔记,试试肿么样)

这里更换pt文件是因为新的pt文件里训练出来的权重更适合咱们的模型

parser.add_argument("--weights", type=str, default=ROOT / "runs/train/exp17/weights/best.pt", help="initial weights path")

结果还没出来,但是感觉能行,训练的时间好长,我的小电脑好慢(偷偷学教资)

结果出来了

image-20240805092238986

对比上一次的,嗯…..怎么比的有大有小的

image-20240805092344705

但是这次咱们识别到了(庆祝!)

image-20240805092519567

Yolo进阶之——引入注意力机制—以SE为例

借鉴代码

https://github.com/ZhugeKongan/Attention-mechanism-implementation

image-20240805075931144

一、加入新增网络结构

打开common.py,将下面这个文件的内容粘贴进去

image-20240807215212687

二、新建yaml文件

复制yolov5文件,并重新命名

image-20240807215341138

在backbone下边再添加一行,作为第十层,将head中大于10的层数+1

image-20240807215448420

三、配置参数

然后进入yolo.py,parse_model模块

添加

image-20240807220829462

为什么要设定c2,而不是直接使用c1?因为parse_model中,C2 是用来记录输出维度的,最后会将c2的值存入ch中,所以务必要对c2的值进行设定

四、开始训练

然后我们运行train.py文件进行训练

报错TypeError: init() takes 3 positional arguments but 4 were given

是因为这里我添加了SE,去掉就好了

image-20240808083820385

Yolo进阶之替换主干网络——以MobileNet为例

一、加入新的网络结构

我们下载的包里有,所以咱们在主目录下边新建demo.ipynb文件,下边是代码

torchvision 0.13开始,加载预训练模型函数的参数从pretrained = True 改为 weights=预训练模型参数版本 。

如果你不知道哪个权重文件的版本是最新的,没关系,直接选择默认DEFAULT即可。官方会随着 torchvision 的升级而让 DEFAULT权重文件版本保持在最新。

progress=True 是显示一个进度条,如果下载过了,就不显示进度条了

import torchvision.models as models
from torchinfo import summary
model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT,progress=True)

然后我们通过model就可以看到这个模型了

为了更好地查看这个库,我们`pip install torchinfo,并导入这个包.

summary(model, input_size=(1, 3, 640, 640))

如果在你觉得无比正确的某一步报错了,不要怀疑自己,切换一下环境,再换回来,就好了。这个编辑器喜欢发癫,适应了就好了。(思考)

运行后的输出:

image-20240808213717302

可以点击text editor查看更详细的部分

因为MobileNet本来是用于分类的,但是咱们现在只需要特征提取那一部分,所以咱们就只看 features部分就好了

所以我们修改一下代码,运行下边的部分,看一下 features部分

summary(model.features, input_size=(1, 3, 640, 640))

运行结果

===============================================================================================
Layer (type:depth-idx) Output Shape Param #
===============================================================================================
Sequential [1, 576, 20, 20] --
├─Conv2dNormActivation: 1-1 [1, 16, 320, 320] --
│ └─Conv2d: 2-1 [1, 16, 320, 320] 432
│ └─BatchNorm2d: 2-2 [1, 16, 320, 320] 32
│ └─Hardswish: 2-3 [1, 16, 320, 320] --
├─InvertedResidual: 1-2 [1, 16, 160, 160] --
│ └─Sequential: 2-4 [1, 16, 160, 160] --
│ │ └─Conv2dNormActivation: 3-1 [1, 16, 160, 160] 176
│ │ └─SqueezeExcitation: 3-2 [1, 16, 160, 160] 280
│ │ └─Conv2dNormActivation: 3-3 [1, 16, 160, 160] 288
├─InvertedResidual: 1-3 [1, 24, 80, 80] --
│ └─Sequential: 2-5 [1, 24, 80, 80] --
│ │ └─Conv2dNormActivation: 3-4 [1, 72, 160, 160] 1,296
│ │ └─Conv2dNormActivation: 3-5 [1, 72, 80, 80] 792
│ │ └─Conv2dNormActivation: 3-6 [1, 24, 80, 80] 1,776
├─InvertedResidual: 1-4 [1, 24, 80, 80] --
│ └─Sequential: 2-6 [1, 24, 80, 80] --
│ │ └─Conv2dNormActivation: 3-7 [1, 88, 80, 80] 2,288
│ │ └─Conv2dNormActivation: 3-8 [1, 88, 80, 80] 968
│ │ └─Conv2dNormActivation: 3-9 [1, 24, 80, 80] 2,160
├─InvertedResidual: 1-5 [1, 40, 40, 40] --
│ └─Sequential: 2-7 [1, 40, 40, 40] --
│ │ └─Conv2dNormActivation: 3-10 [1, 96, 80, 80] 2,496
│ │ └─Conv2dNormActivation: 3-11 [1, 96, 40, 40] 2,592
│ │ └─SqueezeExcitation: 3-12 [1, 96, 40, 40] 4,728
│ │ └─Conv2dNormActivation: 3-13 [1, 40, 40, 40] 3,920
├─InvertedResidual: 1-6 [1, 40, 40, 40] --
│ └─Sequential: 2-8 [1, 40, 40, 40] --
│ │ └─Conv2dNormActivation: 3-14 [1, 240, 40, 40] 10,080
│ │ └─Conv2dNormActivation: 3-15 [1, 240, 40, 40] 6,480
│ │ └─SqueezeExcitation: 3-16 [1, 240, 40, 40] 31,024
│ │ └─Conv2dNormActivation: 3-17 [1, 40, 40, 40] 9,680
├─InvertedResidual: 1-7 [1, 40, 40, 40] --
│ └─Sequential: 2-9 [1, 40, 40, 40] --
│ │ └─Conv2dNormActivation: 3-18 [1, 240, 40, 40] 10,080
│ │ └─Conv2dNormActivation: 3-19 [1, 240, 40, 40] 6,480
│ │ └─SqueezeExcitation: 3-20 [1, 240, 40, 40] 31,024
│ │ └─Conv2dNormActivation: 3-21 [1, 40, 40, 40] 9,680
├─InvertedResidual: 1-8 [1, 48, 40, 40] --
│ └─Sequential: 2-10 [1, 48, 40, 40] --
│ │ └─Conv2dNormActivation: 3-22 [1, 120, 40, 40] 5,040
│ │ └─Conv2dNormActivation: 3-23 [1, 120, 40, 40] 3,240
│ │ └─SqueezeExcitation: 3-24 [1, 120, 40, 40] 7,832
│ │ └─Conv2dNormActivation: 3-25 [1, 48, 40, 40] 5,856
├─InvertedResidual: 1-9 [1, 48, 40, 40] --
│ └─Sequential: 2-11 [1, 48, 40, 40] --
│ │ └─Conv2dNormActivation: 3-26 [1, 144, 40, 40] 7,200
│ │ └─Conv2dNormActivation: 3-27 [1, 144, 40, 40] 3,888
│ │ └─SqueezeExcitation: 3-28 [1, 144, 40, 40] 11,704
│ │ └─Conv2dNormActivation: 3-29 [1, 48, 40, 40] 7,008
├─InvertedResidual: 1-10 [1, 96, 20, 20] --
│ └─Sequential: 2-12 [1, 96, 20, 20] --
│ │ └─Conv2dNormActivation: 3-30 [1, 288, 40, 40] 14,400
│ │ └─Conv2dNormActivation: 3-31 [1, 288, 20, 20] 7,776
│ │ └─SqueezeExcitation: 3-32 [1, 288, 20, 20] 41,832
│ │ └─Conv2dNormActivation: 3-33 [1, 96, 20, 20] 27,840
├─InvertedResidual: 1-11 [1, 96, 20, 20] --
│ └─Sequential: 2-13 [1, 96, 20, 20] --
│ │ └─Conv2dNormActivation: 3-34 [1, 576, 20, 20] 56,448
│ │ └─Conv2dNormActivation: 3-35 [1, 576, 20, 20] 15,552
│ │ └─SqueezeExcitation: 3-36 [1, 576, 20, 20] 166,608
│ │ └─Conv2dNormActivation: 3-37 [1, 96, 20, 20] 55,488
├─InvertedResidual: 1-12 [1, 96, 20, 20] --
│ └─Sequential: 2-14 [1, 96, 20, 20] --
│ │ └─Conv2dNormActivation: 3-38 [1, 576, 20, 20] 56,448
│ │ └─Conv2dNormActivation: 3-39 [1, 576, 20, 20] 15,552
│ │ └─SqueezeExcitation: 3-40 [1, 576, 20, 20] 166,608
│ │ └─Conv2dNormActivation: 3-41 [1, 96, 20, 20] 55,488
├─Conv2dNormActivation: 1-13 [1, 576, 20, 20] --
│ └─Conv2d: 2-15 [1, 576, 20, 20] 55,296
│ └─BatchNorm2d: 2-16 [1, 576, 20, 20] 1,152
│ └─Hardswish: 2-17 [1, 576, 20, 20] --
===============================================================================================
Total params: 927,008
Trainable params: 927,008
Non-trainable params: 0
Total mult-adds (M): 444.86
===============================================================================================
Input size (MB): 4.92
Forward/backward pass size (MB): 184.55
Params size (MB): 3.71
Estimated Total Size (MB): 193.17
===============================================================================================

一个重要的值得关注的问题

如果要替换backbone,由于YOLO采用的是金字塔结构,具有一定的对称性,体现在之后可以将输入拼起来(这里的拼起来指的是,前几层的输出,可以在后几层,跟别的层一起拼起来:就比如说,一般某一层的输入是上一层的输出,但是head里边,某些层的输入=是上一层+前边某一层的输出,是一个列表,不是单个的前一层的输入)这个不理解的话可以看咱们yolov5给的图(Tensorboard可视化)

我们以yolov5s.yaml文件解释一下这个问题:

image-20240808220740434

backbone的八倍采样、十六倍采样和三十二倍采样都很重要,因为head里边是要用的。

然后来看mobileNet的结构,正好有我们需要的这个几个下采样

所以我们可以把1-4层作为第一个模块

image-20240808222146653

把这个作为16倍下采样

image-20240808222250751

从第10层开始,对应32倍下采样

image-20240809072034318

另外,查看 model.features 的类型可知,如下图

image-20240809072655650

其特性在于可以通过切片来取模型结构

例如,参考下图,取前四个模块,如下图

image-20240809073037751

就可以使用 如下命令,如下图

image-20240809073949170

了解了原理,我们下边开始写代码

二、修改common.py文件

代码如下

import torchvision.models as models

class MobileNetV3(nn.Module):
def __init__(self,slice ):
super(MobileNetV3, self).__init__()
self.model = None
if slice == 1:
self.model = models.mobilenet_v3_small(pretained=True).features[:4]
elif slice == 2:
self.model = models.mobilenet_v3_small(pretained=True).features[4:9]
else:
self.model = models.mobilenet_v3_small(pretained=True).features[9:]

def forward(self,x):
return self.model(x)

然后我们开始写yaml文件,还是跟原来一样,复制一份yolov5s.yaml并进行重命名

image-20240809084253668

输出通道数看最后一层的输出通道数,第一层的输出通道是24

image-20240809084238615

这里是yaml文件修改完成之后的代码

# Ultralytics YOLOv5 🚀, AGPL-3.0 license

# Parameters
nc: 80 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
anchors:
- [10, 13, 16, 30, 33, 23] # P3/8
- [30, 61, 62, 45, 59, 119] # P4/16
- [116, 90, 156, 198, 373, 326] # P5/32

# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[
# [-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 320*320
# [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 160*160
# [-1, 3, C3, [128]],
# [-1, 1, Conv, [256, 3, 2]], # 3-P3/8 80*80
# [-1, 6, C3, [256]],
# [-1, 1, Conv, [512, 3, 2]], # 5-P4/16 40*40
# [-1, 9, C3, [512]],
# [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 20*20
# [-1, 3, C3, [1024]],
# [-1, 1, SPPF, [1024, 5]], # 9 20*20
[-1, 1, MobileNetV3,[24,1]], # 0-P3/8 80*80 24是通道数,1是传的slice参数
[-1, 1, MobileNetV3,[48,2]], # 1-P4/16 40*40
[-1, 1, MobileNetV3,[576,3]], # 2-P5/32 20*20
]

# YOLOv5 v6.0 head
head: [
[-1, 1, Conv, [512, 1, 1]], # 3
[-1, 1, nn.Upsample, [None, 2, "nearest"]], # 4 40*40
[[-1, 1], 1, Concat, [1]], # cat backbone P4 # 5 40*40
[-1, 3, C3, [512, False]], # 13

[-1, 1, Conv, [256, 1, 1]], # 7
[-1, 1, nn.Upsample, [None, 2, "nearest"]], # 8 80*80
[[-1, 0], 1, Concat, [1]], # cat backbone P3 # 9 80*80
[-1, 3, C3, [256, False]], # 17 (P3/8-small) 10

[-1, 1, Conv, [256, 3, 2]], # 11
[[-1, 7], 1, Concat, [1]], # 12 cat head P4 # 40*40
[-1, 3, C3, [512, False]], # 13 20 (P4/16-medium) # 40*40

[-1, 1, Conv, [512, 3, 2]], # 14
[[-1, 3], 1, Concat, [1]], # 15cat head # 20*20
[-1, 3, C3, [1024, False]], # 16 (P5/32-large) # 20*20

[[10, 13, 16], 1, Detect, [nc, anchors]], # 17 Detect(P3, P4, P5)
]

三、修改yolo文件

在yolo中注册

image-20240809090743974

四、修改train文件

首先我们先把这个参数改为空

image-20240809091850781

运行查看一下它的参数量

image-20240809092027840

然后我们将参数改为我们刚刚创建好的mobilenet的yaml文件,下图是mobilenet的参数量

image-20240809093216107

好!本节结束

Yolov5 项目部署

一、TensorRt环境安装与配置

TensorRt可以用来实现推理加速

ecf6da424005b5932194e03c6daeba9

安装好第二个之后,将里边的文件剪切到cuda的c盘的文件里边

路径是这样的

image-20240809110026895

移动好之后,我们打开cmd测试是否安装成功

image-20240809105921811

下载好第三个之后解压,我们进入conda环境,查看python版本,我这里是3.8.19.

image-20240811134945869

然后我们打开刚刚解压的文件

image-20240811135049675

我们在终端进入这个文件夹,并且安装相应python版本,这里安装的是cp38版本的

pip install tensorrt-8.5.3.1-cp38-none-win_amd64.whl

image-20240811135545243

接下来我们把lib中的文件复制到CUDA中,我的路径是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.6\bin,如果复制过来的话,编译的时候会报错找不到。

image-20240811135714394

我没有下载第四个。

我们打开vscode 打开yolov5里的export.py文件

可以看到,我们需要下载很多东西

image-20240811141625709

其中有一个onnx,它是一个中间状态的东西,我们可以把模型转为onnx,然后再用onnx转出去。

我们需要安装一下onnx,直接在环境中pip就可以 pip install onnx

这里说了导出的用法

image-20240811142148898

咱们现在导出的是TensorRT,所以咱们是用engine

先把Usage里的这段复制下来python export.py --weights yolov5s.pt --include

在后边加上engine,再加上--device 0,这样就可以了,下边是命令

python export.py --weights yolov5s.pt --include engine --device 0

但是–device 0是说用的GPU,但是我没有GPU!

最终我选择了不导出 TensorRT,而是导出ONNX。

导出之后,视频用detect对视频进行了检测python detect.py --weights yolov5s.engine

二、正确使用TensorRT推理加速

我们先来对比一下,咱们导出的TensorRT模型,对比于pythorch模型,他们的处理速度之间会有怎样的差异

我们先来跑一下,看他们平均每一帧需要多长时间来做一个对比

首先是pythorch检测python detect.py --weights yolov5s.pt

1ffb364fc0d8b97922795ce7796081c

接着是TensorRT:python detect.py --weights yolov5s.engine

85aae7d679649cdeed8ee6291e53581

但是我们发现TensorRT的时间竟然比之前还要长了,这是为什么捏?

其实他们两个输入数据的维度不太一样,pythorch是384x640,TensorRT是640x640,所以TensorRT才会慢

但是为什么他们的数据不一样呢?

TensorRT要求输入维度和输出维度是一样的,我们可以通过这样写命令来python detect.py --weights yolov5s.engine --imgsz 384 640更改输入维度,但是报错了,那么这个输入输出唯独是怎么来的呢?

其实是在我们导出的时候,在这里定义的。

如果说我们需要它检测384x640维度的,要在这里进行修改,因为TensorRT会根据你的输入维度对模型进行一个加速,这个维度是定死的

image-20240811152929542

所以我们重新导出一下

python export.py --weights yolov5s.pt --include engine --device 0 img 384 640

视频重新运行了

python detect.py --weights yolov5s-fp32.engine --imgsz 384 640

9ae786c766bcaf80071be2c606c4af5

我们现在可以看到,推理时间只有10ms

我们在导出的时候,还可以加一个half参数,用半精度推理实现,加速深度学习训练

image-20240811154550097

导出命令是这样的

python export.py --weights yolov5s.pt --include engine --device 0 img 384 640 --half

然后我们看一下半精度模型的训练速度怎么样

python detect.py --weights yolov5s-fp16.engine --imgsz 384 640 --half

b88da796a10f1cc581921ce5dc630c6

这个速度还可以提升!!

如果我们不考虑精度的损失的话,我们还可以进一步缩小这个图像的维度,但是我们不要缩小的太小,只要能满足32倍、16倍、8倍下采样的话,这个是可以缩到192x320的(这个没听太懂,笔记也没做太好)

视频又导出了一版,是192x320精度的

python detect.py --weights yolov5s-halfsize.engine --imgsz 192 320 

1f87e34e60d95e7ffdf754e6657373c

只花了5.7ms

在导出模型的时候。一定要记得把输入维度跟最终预测的这些图像的维度对齐,只有这样的话才能保证咱们的加速是有效的。

三、Torchhub模型预测使用进阶

bcf7c844f2d2d74b794068764c94a0c

我们来看一下load这个方法,第一个参数是路径,第二个参数是model在这里就是custom

image-20240811163719775

然后我们看一下里边的内容

首先是source,他只接受GitHub和local两个参数,并调用相应的方法-

image-20240811164913753

然后下边是load_local方法,里边第一个参数是路径,这里有一个非常值得注意的点,就是我们本地加载的模型要在yolov5-master目录下,不然会报错

image-20240811165412579

我们看一下hubconf.py

image-20240811165747096

我们第二个参数custom方法对应的就是这个,它会根据我们后边的path路径,最终create,然后把模型返回

如果我们不用自己训练的模型,而是用官方训练好的模型,我们可以去修改这个custom参数

image-20240811170234990

咱们下边再看一下预测

之前的预测咱们是直接给图片的路径

现在咱们优化一下

imgs = [
'data/images/zidan.jpg' # filename
Path('data/imags/zidan.jpg'), # Path
'https://ultraytics.com/images/zidan.jpg', # URI
cv2.imread('data/images/bus.jpg')[:,:,::-1], # OpenCv
Image.open('data/images/bus.jpg'), # PIL
np.zeros((320,640,3)) # numpy
]

那我们有个问题,该怎么去找这个模型的预测方法呢

其实创建了我们autoship类型的模型之后,在这个文件下可以看到能用的方法

image-20240811172559001

我们可以看这里的源码,来判断用哪个,怎么拿到我们需要的东西

如果我们要拿到每一个类别的检测结果。可以用这个

image-20240811173039066

bd0f7cef86f012208879846c7b08c95

59f6140ea884e6d77ef0008bd8b2ca4

如果我们希望把每一个识别到的实体(也就是那个框)裁剪出来,可以用crop方法

但是我们可以看到,调取crop方法出来的是这个

42b0594c6eaa8611b42ffff00f5ef7d

我们可以根据这些信息去选择将哪一部分去做一个可视化

from IPython.display import display
from PIL import Image
Image.fromarrary(results.crop(save=False)[3]["im"][:,:,::-1])

这样就可以可视化了

如果想要得到检测结果中文本的那一部分

我们可以

str(results)

但是我们将模型换为TensorRT的模型的时候,框起来的的值不是person、tie这样的

2d40d37fad9e2ce091cee2b6406eff3

我们要设置一下

model.names = {0:"person",27:"tie"}

这样就好了

四、基于Flask的Yolov5项目部署

cd9cded25ab01ac19d73eedd2915e30

官方写的在这里

image-20240811184657256

example_request.py里边是连接方式

restapi.py里边的东西可以直接拿来用,改一下里边的模型参数就行

接下来我们把这两个文件复制到主目录下

修改一下

restapi:

image-20240811192536706

example_request:

image-20240811192555538

基于图像的预测方式代码:

restapi.py里加入:

image-20240811192424604

example_request:

image-20240811192632619

到这里我们实现了往Flask服务器发图片,下边实现返回图片

example_request:

# Ultralytics YOLOv5 🚀, AGPL-3.0 license
"""Perform test request."""

import pprint
import cv2
import numpy as np
import requests
import matplotlib.pyplot as plt

DETECTION_URL = "http://localhost:5000/v1/object-detection/yolov5s"
IMAGE = "data/images/zidane.jpg"

# # Read image
# with open(IMAGE, "rb") as f:
# image_data = f.read()

img = cv2.imread(IMAGE)
# 转换RGB通道
img = cv2.cvtColor(img,cv2.COLOR_BAYER_BG2BGR)
# 编码
img = cv2.imencode(".jpg",img)[1].tobytes()

response = requests.post(DETECTION_URL, data=img)

img = cv2.imdecode(np.frombuffer(response.content,dtype=np.uint8),cv2.IMREAD_COLOR)

# pprint.pprint(response)
plt.imshow(img)
plt.show()

restapi.py

if request.data:
img = cv2.imdecode(np.frombuffer(request.data,dtype=np.uint8),cv2.IMREAD_COLOR)
if model in models:
results = models[model](img) # reduce size=320 for faster inference
results = results.render()[0]
return cv2.imdecode(".jpg",results)[1].tobytes()

到这里就完成了,结课!