您现在的位置是:首页 > 技术教程 正文

如何使用 Python、Node.js 和 Go 创建基于 YOLOv8 的对象检测 Web 服务

admin 阅读: 2024-03-30
后台-插件-广告管理-内容页头部广告(手机)

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")
  • 1
  • 2
  • 3

运行上述的代码后,会产生一个和pt模型名称一样,扩展名是.onnx 的文件。比如,上述例子产生yolov8m.onnx 文件。

4. 使用 ONNX 做对象检测

现在,使用 ONNX 来做对象检测。为简单起见,我们将从 Python 开始,因为我们已经有一个使用 PyTorch 和 Ultralytics API 的 Python Web 应用程序。因此,将其转移到 ONNX 会更容易。
通过在 Jupyter 中运行以下命令来安装适用于 Python 的 ONNX 库:

!pip install onnxruntime
  • 1

导入ONNX

import onnxruntime as ort
  • 1

我们把库重命名为ort 。

用下面的方式就能加载onnx的模型:

model = ort.InferenceSession("yolov8m.onnx", providers=['CPUExecutionProvider'])
  • 1

在上一篇的Python版中,只需运行:outputs = model.predict("image_file") 就能获得结果。该方法会执行以下操作:

  1. 从文件中读取图像
  2. 将其转换为YOLOv8神经网络输入层的格式
  3. 通过模型传递它
  4. 接收原始模型输出
  5. 解析原始模型输出
  6. 返回有关检测到的对象及其边界框的结构化信息

ONNX 有类似的方法run,但它只实现了步骤 3 和 4。其他一切都需要开发,因为 ONNX 不知道这是 YOLOv8 模型。就 ONNX 而言,模型是一个黑匣子,它接收多维浮点数数组作为输入,并将其转换为其他多维数字数组。它不知道输入和输出的含义。那么,我们我们要怎么做呢?
请添加图片描述

模型的输入层和输出层的是固定的,它们是在模型创建时定义的,并保存于模型中。
ONNX 有一个有用的方法get_inputs() 来获取有关此模型期望接收的输入的信息,以及 get_outputs() 来获取有关的信息模型在返回的输出。

让我们首先获取输入:

inputs = model.get_inputs(); len(inputs)
  • 1
  • 2

输出为:

1
  • 1

这里我们得到了输入数组并显示了该数组的长度。结果很明显:网络期望获得单个输入。让我们访问到这个输入:

input = inputs[0]
  • 1

输入对象具有三个字段:name、type 和 shape。让我们获取 YOLOv8 模型的这些值:

print("Name:",input.name) print("Type:",input.type) print("Shape:",input.shape)
  • 1
  • 2
  • 3

输出如下:

Name: images Type: tensor(float) Shape: [1, 3, 640, 640]
  • 1
  • 2
  • 3

从中我们可以看出:

  • 预期输入的名称是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))
  • 1
  • 2
  • 3
  • 4

上述代码先把输入图片调整到640x640,接着需要提取每个像素的每个颜色分量并从中构造 3 个矩阵。
首先取消输入图片的Alpha通道:

img = img.convert("RGB");
  • 1

构建分量数组:

import numpy as np input = np.array(img)
  • 1
  • 2

我们导入了 NumPy 并将图像加载到 input 这个NumPy 数组中。现在让我们看看这个数组的形状:

input.shape
  • 1

输出为:

(640, 640, 3)
  • 1

根据输出发现尺寸顺序错误,我们需要将 3 放在开头。 transpose函数可以切换NumPy数组的维度:

input = input.transpose(2,0,1) input.shape
  • 1
  • 2

输出为:

(3,640,640)
  • 1

我们需要在开始处再添加一个维度来使其成为 (1,3,640,640):

input = input.reshape(1,3,640,640)
  • 1

现在我们有了正确的输入内容,如果查看该数组的内容,例如第一个像素的红色分量:

input[0,0,0,0]
  • 1

输出为:

71
  • 1

这里是整数,正确的输出应该是Float,我们需要对此数据做归一化处理,将其缩放到0到1的范围:

input = input/255.0 input[0,0,0,0]
  • 1
  • 2

输出为:

0.2784313725490196
  • 1

这里显示的就是输入数据的样子。

6. 运行模型

现在,在运行推理过程之前,让我们看看 YOLOv8 模型应返回哪些输出。如上所述,这可以使用 ONNX 的 get_outputs() 方法来完成。

outputs = model.get_outputs() output = outputs[0] print("Name:",output.name) print("Type:",output.type) print("Shape:",output.shape)
  • 1
  • 2
  • 3
  • 4
  • 5

输出为:

Name: output0 Type: tensor(float) Shape: [1, 84, 8400]
  • 1
  • 2
  • 3

从输出中可以看出,ONNX的YOLOv8 有一个输出,它是 outputs 对象的第一项,类型是tensor(float)的格式,形状为 [1,84,8400],这意味着这是一个嵌套到单个数组的 84x8400 矩阵。实际上, YOLOv8 返回 8400 个边界框,每个边界框有 84 个参数。这里每个边界框都是列,而不是行。这是神经网络算法的要求。我认为最好将其转置为 8400x84,因此,有 8400 行与检测到的对象匹配,并且每行都是具有 84 个参数的边界框。
稍后我们将讨论为什么单个边界框有这么多参数。现在,ONNX可以用run函数来运行模型并获取输出:

model.run(output_names,inputs)
  • 1
  • output_names:接收的输出的数组。
  • inputs :输入字典,以 {name:tensor} 格式传递到网络,其中 name 是输入名称,tensor 是我们之前准备好的图像数据数组。

具体而言,代码如下:

outputs = model.run(["output0"], { "images":input}) len(outputs)
  • 1
  • 2
  • 3

输出为:

1
  • 1

输出表示outputs数组的长度为1,如果提示错误输入,必须采用 float 格式,可以用以下代码转换输入:

input = input.astype(np.float32)
  • 1

然后再次运行run函数。

7. 处理输出

从输出中提取内容:

output = outputs[0] output.shape
  • 1
  • 2

输出为:

(1, 84, 8400)
  • 1

返回了正确的输出格式。由于第一个维度只有1个内容,我们可以直接获取它:

output = output[0] output.shape
  • 1
  • 2

输出为:

(84, 8400)
  • 1

显示是一个84 行、8400 列的矩阵。如前文讨论,我们需要把它转置一下,以方便后续计算:

output = output.transpose()
  • 1

输出为:

(8400, 84)
  • 1

现在更清楚了:8400 行,84列个数据。 8400 是 YOLOv8 可以检测的最大边界框数量,并且无论实际检测到多少个对象,它都会为任何图像返回 8400 行,这是因为YOLOv8的网络设计决定。因此,每次都会返回 8400 行,但其中大部分行只包含垃圾。如何检测这些行中哪些有有意义的数据,哪些是垃圾数据?可以看出每一行都有84个数据,其中前 4 个是边界框的坐标,剩余其他的80个数据是该模型可以检测到的所有对象类的置信度。如果使用的是我们自训练的模型,假设能检测到3个对象类,那么输出有 7 个数据(4+3)。

现在来看看第一行的内容:

row = output[0] print(row)
  • 1
  • 2

显示为:

[ 5.1182 8.9662 13.247 19.459 2.5034e-06 2.0862e-07 5.6624e-07 1.1921e-07 2.0862e-07 1.1921e-07 1.7881e-07 1.4901e-07 1.1921e-07 2.6822e-07 1.7881e-07 1.1921e-07 1.7881e-07 4.1723e-07 5.6624e-07 2.0862e-07 1.7881e-07 2.3842e-07 3.8743e-07 3.2783e-07 1.4901e-07 8.9407e-08 3.8743e-07 2.9802e-07 2.6822e-07 2.6822e-07 2.3842e-07 2.0862e-07 5.9605e-08 2.0862e-07 1.4901e-07 1.1921e-07 4.7684e-07 2.6822e-07 1.7881e-07 1.1921e-07 8.9407e-08 1.4901e-07 1.7881e-07 2.6822e-07 8.9407e-08 2.6822e-07 3.8743e-07 1.4901e-07 2.0862e-07 4.1723e-07 1.9372e-06 6.5565e-07 2.6822e-07 5.3644e-07 1.2815e-06 3.5763e-07 2.0862e-07 2.3842e-07 4.1723e-07 2.6822e-07 8.3447e-07 8.9407e-08 4.1723e-07 1.4901e-07 3.5763e-07 2.0862e-07 1.1921e-07 5.9605e-08 5.9605e-08 1.1921e-07 1.4901e-07 1.4901e-07 1.7881e-07 5.9605e-08 8.9407e-08 2.3842e-07 1.4901e-07 2.0862e-07 2.9802e-07 1.7881e-07 1.1921e-07 2.3842e-07 1.1921e-07 1.1921e-07]
  • 1
  • 2
  • 3
  • 4

可以看到这一行代表一个坐标为 [5.1182, 8.9662, 13.247, 19.459] 的边界框。边框表示信息如下:

x_center = 5.1182 y_center = 8.9662 width = 13.247 height = 19.459
  • 1
  • 2
  • 3
  • 4

提取这个边框:

xc,yc,w,h = row[:4]
  • 1

剩余其他数值表示检测到的对象属于 80 个类的置信度。比如:数组索引 4 的数据表示类别 0 的置信度 (2.5034e-06),数组索引 5 的数据表示类别 1 的置信度 (2.0862e-07) ),以此类推。
现在,我们把数据解析为我们在上一篇文章中的格式:[x1, y1, x2 y2,类标签,置信度]。

  1. 计算边界框的四个角的坐标:
x1 = xc-w/2 y1 = yc-h/2 x2 = xc+w/2 y2 = yc+h/2
  • 1
  • 2
  • 3
  • 4

注意:由于输入图像尺寸是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
  • 1
  • 2
  • 3
  • 4
  1. 找到最大的对象置信度
    我们需要在剩余的80个数据中找到数值最大的那个,在NumPy中可以通过以下方法做到:
prob = row[4:].max() class_id = row[4:].argmax() print(prob, class_id)
  • 1
  • 2
  • 3

输出为:

2.503395e-06 0
  • 1

第一个数据是识别对象的最大置信度。第二个数据是该对象的索引。

接着把对象类索引替换为类标签,由于此模型用的是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" ]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

接着类标签是:

label = yolo_classes[class_id]
  • 1

以上就是解析 YOLOv8 输出的每一行的方式。

然而,这个置信度太低了,因为 2.503395e-06 = 2.503395 / 1000000 = 0.000002503。所以,这个边界框,也许只是应该过滤掉的垃圾。在实际中我们会滤掉所有置信度小于 0.5 的边界框。

把上述内容写成函数就是:

def parse_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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

接着解析模型输出的所有行:

boxes = [row for row in [parse_row(row) for row in output] if row[5] > 0.5] len(boxes)
  • 1
  • 2

输出为:

20
  • 1

这里我们解析了所有的行,并过滤掉置信度低于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<
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索
排行榜