1. 介绍
这是有关 YOLOv8 系列文章的第二篇。在上一篇文章中我们介绍了YOLOv8以及如何使用它,然后展示了如何使用 Python 和基于 PyTorch 的官方 YOLOv8 库创建一个 Web 服务来检测图像上的对象。
在本文中,将展示如何在不需要PyTorch和官方API的情况下使用 YOLOv8 模型,将模型部署在不同的端上,使得模型使用的资源减少十倍,并且不仅可以在 Python 上创建服务,还可以在 Node.js、Go 上创建同样的服务。
本文内容将在上一篇文章中开发的Web服务基础上做扩展,前端不做修改,仅使用不同的语言重写后端。
2. YOLOv8 部署
YOLOv8 使用 PyTorch 框架并输出为“.pt”文件。我们使用 Ultralytics API 来训练这些模型或基于它们进行预测。要运行它们,需要有一个包含 Python 和 PyTorch 的环境。
PyTorch 是一个用于设计、训练和评估神经网络模型的框架。然而,我们在应用环境中并不需要PyTorch。我们使用 YOLOv8,在应用中所做的就是把输入图像给模型,通过模型的输出计算目标的边界框、种类、置信度等。这个过程并不一定非得依靠Python,我们可以把YOLOv8训练的模型导出成其他任何类型,从而使用其他编程语言完成这个过程。
目前,我们可以把模型导出为以下格式:TorchScript, ONNX, OpenVINO, TensorRT, CoreML, TF_SavedModel, TF_GraphDef, TF_Lite, TF_Edge_TPU, TF.js, PaddlePaddle。
例如,CoreML 是可在iOS上程序使用的神经网络格式。
本文主要使用ONNX,它由 Microsoft 提出的,可在不同平台和编程语言上运行神经网络模型。它不是一个框架,而只是一个用 C 语言编写的库。对于 Linux 来说,它的大小只有 16 MB,但它提供了主要编程语言的API,包括 Python、PHP、JavaScript、 Node.js、C++、Go 和 Rust。
3. 将 YOLOv8 导出到 ONNX
首先,我们加载 YOLOv8 模型并导出为 ONNX 格式。
from ultralytics import YOLO
model = YOLO("yolov8m.pt")
model.export(format="onnx")
运行上述的代码后,会产生一个和pt模型名称一样,扩展名是.onnx 的文件。比如,上述例子产生yolov8m.onnx 文件。
4. 使用 ONNX 做对象检测
现在,使用 ONNX 来做对象检测。为简单起见,我们将从 Python 开始,因为我们已经有一个使用 PyTorch 和 Ultralytics API 的 Python Web 应用程序。因此,将其转移到 ONNX 会更容易。
通过在 Jupyter 中运行以下命令来安装适用于 Python 的 ONNX 库:
!pip install onnxruntime
导入ONNX
import onnxruntime as ort
我们把库重命名为ort 。
用下面的方式就能加载onnx的模型:
model = ort.InferenceSession("yolov8m.onnx", providers=['CPUExecutionProvider'])
在上一篇的Python版中,只需运行:
outputs = model.predict("image_file")
就能获得结果。该方法会执行以下操作:
- 从文件中读取图像
- 将其转换为YOLOv8神经网络输入层的格式
- 通过模型传递它
- 接收原始模型输出
- 解析原始模型输出
- 返回有关检测到的对象及其边界框的结构化信息
ONNX 有类似的方法run,但它只实现了步骤 3 和 4。其他一切都需要开发,因为 ONNX 不知道这是 YOLOv8 模型。就 ONNX 而言,模型是一个黑匣子,它接收多维浮点数数组作为输入,并将其转换为其他多维数字数组。它不知道输入和输出的含义。那么,我们我们要怎么做呢?
模型的输入层和输出层的是固定的,它们是在模型创建时定义的,并保存于模型中。
ONNX 有一个有用的方法
get_inputs()
来获取有关此模型期望接收的输入的信息,以及
get_outputs()
来获取有关的信息模型在返回的输出。
让我们首先获取输入:
inputs = model.get_inputs();len(inputs)
输出为:
1
这里我们得到了输入数组并显示了该数组的长度。结果很明显:网络期望获得单个输入。让我们访问到这个输入:
input= inputs[0]
输入对象具有三个字段:name、type 和 shape。让我们获取 YOLOv8 模型的这些值:
print("Name:",input.name)print("Type:",input.type)print("Shape:",input.shape)
输出如下:
Name: images
Type: tensor(float)
Shape:[1,3,640,640]
从中我们可以看出:
- 预期输入的名称是images。
- 输入类型为tensor(float)。 我们需要将图像转换为浮点数的多维数组。
- 形状显示了该Tensor的维度。能看到该数组是四维的,表示输入是1个图像 ,包含 3 个 640x640 浮点数矩阵。每个矩阵表示红、绿、蓝的分量。每个颜色分量的值可以是 0 到 255。
5. 准备输入
我们需要把输入图像小调整为 640x640,提取有关每个像素的红色、绿色和蓝色分量的信息,并构建 3 个适当颜色分量的矩阵。
假设图像是上一篇我们用到的cat_dog.jpg
使用Pillow完成上述处理。
from PIL import Image
img = Image.open("cat_dog.jpg")
img_width, img_height = img.size
img = img.resize((640,640))
上述代码先把输入图片调整到640x640,接着需要提取每个像素的每个颜色分量并从中构造 3 个矩阵。
首先取消输入图片的Alpha通道:
img = img.convert("RGB");
构建分量数组:
import numpy as np
input= np.array(img)
我们导入了 NumPy 并将图像加载到 input 这个NumPy 数组中。现在让我们看看这个数组的形状:
input.shape
输出为:
(640,640,3)
根据输出发现尺寸顺序错误,我们需要将 3 放在开头。 transpose函数可以切换NumPy数组的维度:
input=input.transpose(2,0,1)input.shape
输出为:
(3,640,640)
我们需要在开始处再添加一个维度来使其成为 (1,3,640,640):
input=input.reshape(1,3,640,640)
现在我们有了正确的输入内容,如果查看该数组的内容,例如第一个像素的红色分量:
input[0,0,0,0]
输出为:
71
这里是整数,正确的输出应该是Float,我们需要对此数据做归一化处理,将其缩放到0到1的范围:
input=input/255.0input[0,0,0,0]
输出为:
0.2784313725490196
这里显示的就是输入数据的样子。
6. 运行模型
现在,在运行推理过程之前,让我们看看 YOLOv8 模型应返回哪些输出。如上所述,这可以使用 ONNX 的
get_outputs()
方法来完成。
outputs = model.get_outputs()
output = outputs[0]print("Name:",output.name)print("Type:",output.type)print("Shape:",output.shape)
输出为:
Name: output0
Type: tensor(float)
Shape:[1,84,8400]
从输出中可以看出,ONNX的YOLOv8 有一个输出,它是 outputs 对象的第一项,类型是tensor(float)的格式,形状为 [1,84,8400],这意味着这是一个嵌套到单个数组的 84x8400 矩阵。实际上, YOLOv8 返回 8400 个边界框,每个边界框有 84 个参数。这里每个边界框都是列,而不是行。这是神经网络算法的要求。我认为最好将其转置为 8400x84,因此,有 8400 行与检测到的对象匹配,并且每行都是具有 84 个参数的边界框。
稍后我们将讨论为什么单个边界框有这么多参数。现在,ONNX可以用run函数来运行模型并获取输出:
model.run(output_names,inputs)
- output_names:接收的输出的数组。
- inputs :输入字典,以 {name:tensor} 格式传递到网络,其中 name 是输入名称,tensor 是我们之前准备好的图像数据数组。
具体而言,代码如下:
outputs = model.run(["output0"],{
"images":input})len(outputs)
输出为:
1
输出表示outputs数组的长度为1,如果提示错误输入,必须采用 float 格式,可以用以下代码转换输入:
input=input.astype(np.float32)
然后再次运行run函数。
7. 处理输出
从输出中提取内容:
output = outputs[0]
output.shape
输出为:
(1,84,8400)
返回了正确的输出格式。由于第一个维度只有1个内容,我们可以直接获取它:
output = output[0]
output.shape
输出为:
(84,8400)
显示是一个84 行、8400 列的矩阵。如前文讨论,我们需要把它转置一下,以方便后续计算:
output = output.transpose()
输出为:
(8400,84)
现在更清楚了:8400 行,84列个数据。 8400 是 YOLOv8 可以检测的最大边界框数量,并且无论实际检测到多少个对象,它都会为任何图像返回 8400 行,这是因为YOLOv8的网络设计决定。因此,每次都会返回 8400 行,但其中大部分行只包含垃圾。如何检测这些行中哪些有有意义的数据,哪些是垃圾数据?可以看出每一行都有84个数据,其中前 4 个是边界框的坐标,剩余其他的80个数据是该模型可以检测到的所有对象类的置信度。如果使用的是我们自训练的模型,假设能检测到3个对象类,那么输出有 7 个数据(4+3)。
现在来看看第一行的内容:
row = output[0]print(row)
显示为:
[5.11828.966213.24719.4592.5034e-062.0862e-075.6624e-071.1921e-072.0862e-071.1921e-071.7881e-071.4901e-071.1921e-072.6822e-071.7881e-071.1921e-071.7881e-074.1723e-075.6624e-072.0862e-071.7881e-072.3842e-073.8743e-073.2783e-071.4901e-078.9407e-083.8743e-072.9802e-072.6822e-072.6822e-072.3842e-072.0862e-075.9605e-082.0862e-071.4901e-071.1921e-074.7684e-072.6822e-071.7881e-071.1921e-078.9407e-081.4901e-071.7881e-072.6822e-078.9407e-082.6822e-073.8743e-071.4901e-072.0862e-074.1723e-071.9372e-066.5565e-072.6822e-075.3644e-071.2815e-063.5763e-072.0862e-072.3842e-074.1723e-072.6822e-078.3447e-078.9407e-084.1723e-071.4901e-073.5763e-072.0862e-071.1921e-075.9605e-085.9605e-081.1921e-071.4901e-071.4901e-071.7881e-075.9605e-088.9407e-082.3842e-071.4901e-072.0862e-072.9802e-071.7881e-071.1921e-072.3842e-071.1921e-071.1921e-07]
可以看到这一行代表一个坐标为 [5.1182, 8.9662, 13.247, 19.459] 的边界框。边框表示信息如下:
x_center =5.1182
y_center =8.9662
width =13.247
height =19.459
提取这个边框:
xc,yc,w,h = row[:4]
剩余其他数值表示检测到的对象属于 80 个类的置信度。比如:数组索引 4 的数据表示类别 0 的置信度 (2.5034e-06),数组索引 5 的数据表示类别 1 的置信度 (2.0862e-07) ),以此类推。
现在,我们把数据解析为我们在上一篇文章中的格式:[x1, y1, x2 y2,类标签,置信度]。
- 计算边界框的四个角的坐标:
x1 = xc-w/2
y1 = yc-h/2
x2 = xc+w/2
y2 = yc+h/2
注意:由于输入图像尺寸是640x640,模型返回的坐标也是以640x640来输出的。为了获得原始图像的边界框的坐标,我们需要根据原始图像的尺寸按比例缩放它们。我们将原始宽度和高度保存到了img_width和img_height变量中,为了缩放边界框的角点,我们需要如下计算:
x1 =(xc - w/2)/640* img_width
y1 =(yc - h/2)/640* img_height
x2 =(xc + w/2)/640* img_width
y2 =(yc + h/2)/640* img_height
- 找到最大的对象置信度 我们需要在剩余的80个数据中找到数值最大的那个,在NumPy中可以通过以下方法做到:
prob = row[4:].max()
class_id = row[4:].argmax()print(prob, class_id)
输出为:
2.503395e-060
第一个数据是识别对象的最大置信度。第二个数据是该对象的索引。
接着把对象类索引替换为类标签,由于此模型用的是COCO数据,它的80个数据类如下:
yolo_classes =["person","bicycle","car","motorcycle","airplane","bus","train","truck","boat","traffic light","fire hydrant","stop sign","parking meter","bench","bird","cat","dog","horse","sheep","cow","elephant","bear","zebra","giraffe","backpack","umbrella","handbag","tie","suitcase","frisbee","skis","snowboard","sports ball","kite","baseball bat","baseball glove","skateboard","surfboard","tennis racket","bottle","wine glass","cup","fork","knife","spoon","bowl","banana","apple","sandwich","orange","broccoli","carrot","hot dog","pizza","donut","cake","chair","couch","potted plant","bed","dining table","toilet","tv","laptop","mouse","remote","keyboard","cell phone","microwave","oven","toaster","sink","refrigerator","book","clock","vase","scissors","teddy bear","hair drier","toothbrush"]
接着类标签是:
label = yolo_classes[class_id]
以上就是解析 YOLOv8 输出的每一行的方式。
然而,这个置信度太低了,因为 2.503395e-06 = 2.503395 / 1000000 = 0.000002503。所以,这个边界框,也许只是应该过滤掉的垃圾。在实际中我们会滤掉所有置信度小于 0.5 的边界框。
把上述内容写成函数就是:
defparse_row(row):
xc,yc,w,h = row[:4]
x1 =(xc-w/2)/640*img_width
y1 =(yc-h/2)/640*img_height
x2 =(xc+w/2)/640*img_width
y2 =(yc+h/2)/640*img_height
prob = row[4:].max()
class_id = row[4:].argmax()
label = yolo_classes[class_id]return[x1,y1,x2,y2,label,prob]
接着解析模型输出的所有行:
boxes =[row for row in[parse_row(row)for row in output]if row[5]>0.5]len(boxes)
输出为:
20
这里我们解析了所有的行,并过滤掉置信度低于0.5的边框,共得到20个边框。这20个框比8400的结果更接近预期结果,但仍然太多,因为我们的图像只有一只猫和一只狗。这是为什么?让我们显示这些数据:
[261.28302669525146,95.53291285037994,461.15666942596437,313.4492515325546,'dog',0.9220365][261.16701192855834,95.61400711536407,460.9202187538147,314.0579136610031,'dog',0.92195505][261.0219168663025,95.50403118133545,460.9265221595764,313.81584787368774, 'dog,0.9269446][260.7873046875,95.70514416694641,461.4101188659668,313.7423722743988,'dog',0.9269207][139.5556526184082,169.4101345539093,255.12585411071777,314.7275745868683,'cat',0.8986903][139.5316062927246,169.63674533367157,255.05698356628417,314.6878091096878,'cat',0.90628827][139.68495998382568,169.5753903388977,255.12413234710692,315.06962299346924,'cat',0.88975877][261.1445414543152,95.70124578475952,461.0543995857239,313.6095304489136,'dog',0.926944][260.9405124664307,95.77976751327515,460.99450263977053,313.57664155960083,'dog',0.9247296][260.49400663375854,95.79500484466553,461.3895306587219,313.5762457847595,'dog',0.9034922][139.59658827781678,169.2822597026825,255.2673086643219,314.9018738269806,'cat',0.88215613][139.46405625343323,169.3733571767807,255.28112654685975,314.9132820367813,'cat',0.8780577][139.633131980896,169.65343713760376,255.49261894226075,314.88970375061035,'cat',0.8653987][261.18754177093507,95.68838310241699,461.0297842025757,313.1688747406006,'dog',0.9215225][260.8274451255798,95.74608707427979,461.32597131729125,313.3906273841858,'dog',0.9093932][260.5131794929504,95.89693665504456,461.3481791496277,313.24405217170715<
版权归原作者 guohuang 所有, 如有侵权,请联系我们删除。