对象检测是一项计算机视觉任务,涉及识别和定位图像或视频中的对象。它是许多应用的重要组成部分,例如自动驾驶汽车、机器人和视频监控。
多年来,已经开发了许多方法和算法来查找图像中的对象及其位置。卷积神经网络对于此类任务有着非常好的性能和质量。
用于此任务的最流行的神经网络之一是 YOLO,由 Joseph Redmon、Santosh Divvala、Ross Girshick 和 Ali Farhadi 在 2015 年在他们著名的研究论文“You Only Look Once: Unified, Real-Time Object Detection”中创建。
从那时起,YOLO 已经出现了相当多的版本。最近的版本可以做的不仅仅是对象检测,还可做对象分割、对象追踪等、姿势识别等任务,本系列文章展示了如何使用YOLOv8。
本系列文章分为3个大模块:
YOLOv8可以解决分类、对象检测和图像分割问题。所有这些方法都以不同的方式检测图像或视频中的对象,如下图所示:
分类 | 检测 | 分割 |
---|---|---|
在本文中,讨论如何使用 YOLOv8 的对象检测, 我将创建一个 Web 应用程序,用它来检测图像上的交通灯和路标。在本系列的后续的其他文章中,我将介绍其他功能,包括图像分割。
从本质上讲,YOLOv8 是一组卷积神经网络模型,使用 PyTorch 框架。
此外,YOLOv8 提供了 Python API,我们也会利用Python在Jupyter环境中展示内容。
我们在Jupyter中建立对应的notebook,然后过运行以下命令在其中安装 YOLOv8 包:
!pip install ultralytics
接着,导入YOLO包:
from ultralytics import YOLO
现在一切准备就绪,可以创建模型了:
model = YOLO("yolov8m.pt")
YOLOv8 是一组神经网络模型,这些模型使用 PyTorch 创建和训练的,并导出到扩展名为 .pt
的文件。存在三种类型的模型,每种类型有 5 个不同尺寸的模型:
分类 | 检测 | 分割 | 种类 |
---|---|---|---|
yolov8n-cls.pt | yolov8n.pt | yolov8n-seg.pt | 纳米 |
yolov8s-cls.pt | yolov8s.pt | yolov8s-seg.pt | 小 |
yolov8m-cls.pt | yolov8m.pt | yolov8m-seg.pt | 中 |
yolov8l-cls.pt | yolov8l.pt | yolov8l-seg.pt | 大 |
yolov8x-cls.pt | yolov8x.pt | yolov8x-seg.pt | 巨大 |
使用的模型越大,可以实现的预测质量就越好,但运行速度就越慢。在本文中,我使用“yolov8m.pt”,它是用于对象检测的中型模型。
第一次运行此代码时,它将从 Ultralytics 服务器下载 yolov8m.pt
文件到当前文件夹,然后创建 YOLO 对象。现在可以使用该对象完成以下任务:
所有官方的 YOLOv8 模型均已在 COCO 数据集 上进行预训练,该数据集是 80 种类型的图像的大集合。
假设我们有张图像名为"cat_dog.jpg":
运行predict
以检测其上的所有对象:
results = model.predict("cat_dog.jpg")
predict
方法接受许多不同的输入类型,包括单个图像的路径、图像路径数组等。
通过模型运行输入后,它会返回每个输入图像的结果数组。由于我们只提供了单个图像,因此它返回一个包含单个项目的数组,可以通过以下方式提取该数组:
result = results[0]
result包含检测到的对象以及使用它们的属性。其中最重要的是 boxes
数组,包含有关图像上检测到的边界框的信息。您可以通过运行 len
函数来确定检测到的对象数量:
len(result.boxes)
当我运行此代码时,输出是“2”,这意味着检测到两个对象。
然后,循环或手动分析每个框。先看第一个:
box = result.boxes[0]
box 对象包含边界框的属性,包括:
xyxy
:边框的的坐标作数组 [x1,y1,x2,y2]cls
:对象类型的IDconf
:该对象的置信度。如果它非常低,比如 < 0.5,那么你可以忽略该框。打印检测到的框的信息:
print("Object type:", box.cls)
print("Coordinates:", box.xyxy)
print("Probability:", box.conf)
得到如下输出:
Object type: tensor([16.])
Coordinates: tensor([[261.1901, 94.3429, 460.5649, 312.9910]])
Probability: tensor([0.9528])
如上所述,YOLOv8 包含 PyTorch 模型。 PyTorch 模型的输出是 Tensor 对象数组,因此需要从每个数组中提取第一项:
print("Object type:",box.cls[0])
print("Coordinates:",box.xyxy[0])
print("Probability:",box.conf[0])
输出为:
Object type: tensor(16.)
Coordinates: tensor([261.1901, 94.3429, 460.5649, 312.9910])
Probability: tensor(0.9528)
要从Tensor 中获取实际值,需要对内部带有数组的张量使用 .tolist()
方法,对带有标量值的张量使用 .item()
方法。让我们将数据提取到适当的变量中:
cords = box.xyxy[0].tolist()
class_id = box.cls[0].item()
conf = box.conf[0].item()
print("Object type:", class_id)
print("Coordinates:", cords)
print("Probability:", conf)
输出为:
Object type: 16.0
Coordinates: [261.1900634765625, 94.3428955078125, 460.5649108886719, 312.9909973144531]
Probability: 0.9528293609619141
现在看到了实际数据。坐标和概率都可以四舍五入到点后两位数。
此处的对象类型为16
。这是什么意思呢?对于 YOLOv8 预训练模型,有 80 种对象类型,ID 从 0 到 79。此处的对象类型表示的是这里的ID,另外,YOLOv8 结果中也包含 names
属性来输出这80种对象名称:
print(result.names)
输出为:
{0: 'person',
1: 'bicycle',
2: 'car',
3: 'motorcycle',
4: 'airplane',
5: 'bus',
6: 'train',
7: 'truck',
8: 'boat',
9: 'traffic light',
10: 'fire hydrant',
11: 'stop sign',
12: 'parking meter',
13: 'bench',
14: 'bird',
15: 'cat',
16: 'dog',
17: 'horse',
18: 'sheep',
19: 'cow',
20: 'elephant',
21: 'bear',
22: 'zebra',
23: 'giraffe',
24: 'backpack',
25: 'umbrella',
26: 'handbag',
27: 'tie',
28: 'suitcase',
29: 'frisbee',
30: 'skis',
31: 'snowboard',
32: 'sports ball',
33: 'kite',
34: 'baseball bat',
35: 'baseball glove',
36: 'skateboard',
37: 'surfboard',
38: 'tennis racket',
39: 'bottle',
40: 'wine glass',
41: 'cup',
42: 'fork',
43: 'knife',
44: 'spoon',
45: 'bowl',
46: 'banana',
47: 'apple',
48: 'sandwich',
49: 'orange',
50: 'broccoli',
51: 'carrot',
52: 'hot dog',
53: 'pizza',
54: 'donut',
55: 'cake',
56: 'chair',
57: 'couch',
58: 'potted plant',
59: 'bed',
60: 'dining table',
61: 'toilet',
62: 'tv',
63: 'laptop',
64: 'mouse',
65: 'remote',
66: 'keyboard',
67: 'cell phone',
68: 'microwave',
69: 'oven',
70: 'toaster',
71: 'sink',
72: 'refrigerator',
73: 'book',
74: 'clock',
75: 'vase',
76: 'scissors',
77: 'teddy bear',
78: 'hair drier',
79: 'toothbrush'}
这就是该模型可以检测到的所有内容。可以发现16
是"dog",所以,这个边界框就是检测到的狗的边界框。让我们修改下输出,更方便显示结果:
cords = box.xyxy[0].tolist()
cords = [round(x) for x in cords]
class_id = result.names[box.cls[0].item()]
conf = round(box.conf[0].item(), 2)
print("Object type:", class_id)
print("Coordinates:", cords)
print("Probability:", conf)
得到以下输出:
Object type: dog
Coordinates: [261, 94, 461, 313]
Probability: 0.95
循环获取所有检测到的框的信息:
for box in result.boxes:
class_id = result.names[box.cls[0].item()]
cords = box.xyxy[0].tolist()
cords = [round(x) for x in cords]
conf = round(box.conf[0].item(), 2)
print("Object type:", class_id)
print("Coordinates:", cords)
print("Probability:", conf)
print("---")
代码输出以下内容:
Object type: dog
Coordinates: [261, 94, 461, 313]
Probability: 0.95
---
Object type: cat
Coordinates: [140, 170, 256, 316]
Probability: 0.92
---
这样,就可以处理其他图像并查看 COCO 训练模型可以检测到的所有内容。
到目前为止,我们可以在COCO训练集上完成分类任务了,但在实践中,COCO训练集不一定总能满足我们的需求。
例如,可能需要检测超市货架上的特定产品或者通过 X 射线发现脑肿瘤。
因此,面对这些问题的时候,必须训练自己的模型来完成检测任务。
要训练模型,需要准备带注释的图像并将其拆分为训练和验证数据集。训练集将用于训练模型,验证集将用于验证训练结果。一般而言,可以将 80% 的图像放入训练集,将 20% 放入验证集。
创建数据集的步骤:
{object_class_id} {x_center} {y_center} {width} {height}
示例如下:
实际上,这是训练模型中最耗时的手动工作:测量所有对象的边界框并将其添加到注释文件中。此外,坐标应该被标准化标准化以适应0到1的范围。使用以下公式完成标准化:
x_center = (box_x_left+box_x_width/2)/image_width
y_center = (box_y_top+box_height/2)/image_height
width = box_width/image_width
height = box_height/image_height
例如,如果想添加"cat_dog.jpg"到数据集,需要将其复制到"images"文件夹,然后测量图像内所有边界框的数据:
图像数据如下:
image_width = 612
image_height = 415
边界框数据如下:
Dog | Cat |
---|---|
box_x_left=261 box_x_top=94 box_width=200 box_height=219 | box_x_left=140 box_x_top=170 box_width=116 box_height=146 |
然后,在"labels"文件夹下创建"cat_dog.txt"文件,并使用上面的标准化公式计算坐标:
狗 (class id=1):
x_center = (261+200/2)/612 = 0.589869281 y_center = (94+219/2)/415 = 0.490361446 width = 200/612 = 0.326797386 height = 219/415 = 0.527710843
猫 (class id=0)
x_center = (140+116/2)/612 = 0.323529412 y_center = (170+146/2)/415 = 0.585542169 width = 116/612 = 0.189542484 height = 146/415 = 0.351807229
并将以下行添加到cat_dog.txt文件中:
1 0.589869281 0.490361446 0.326797386 0.527710843
0 0.323529412 0.585542169 0.189542484 0.351807229
第一行包含狗的边界框(种类id=1),第二行包含猫的边界框(种类id=0)。
添加并注释所有图像后,数据集就准备好了。需要创建两个数据集并将它们放在不同的文件夹中,一个是训练数据集,另一个是验证数据集。最终的文件结构可能如下所示:
“train”目录下的是训练数据集。“val”目录下是验证数据集。
最后,创建一个YAML 文件来描述数据集的组成。比如:
train: ../train/images
val: ../val/images
nc: 2
names: ['cat','dog']
train
和 val
分别表示训练集和验证集的路径,路径可以是相对于当前文件夹的相对路径,也可以是绝对路径。
nc
表示识别的种类数量,这里只有猫和狗,所以是2。
names
是按正确顺序排列的类名数组,对应索引就是之前注释图像时使用的种类编号。
此 YAML 文件会递给模型的train
方法,用来启动训练过程。
有很多工具能够帮我们完成上述的标注图像工作,比如 Roboflow Annotate 、 labelimg等。
训练模型的TIPS:
数据准备好后,我们就可以开始训练模型了。
我们将使用其一套交通信号数据集进行训练。它包含交通灯和路标。
数据我已经上传至百度网盘,可以通过以下链接获取:
https://pan.baidu.com/s/1_B6LE4nDERER9CJmFduTew
提取码:ewtm
该数据集可用于训练 YOLOv8 检测道路上的不同物体,如下图所示:
将数据包解压到包含 Python 代码的文件夹下,执行train
方法开始训练:
model.train(data="data.yaml", epochs=30)
其中data.yaml就是对数据集中的描述yaml文件。 epochs
选项指定训练周期数(默认为 100)。
每个训练周期由两个阶段组成:训练阶段和验证阶段。
在训练阶段,train
方法执行以下操作:
batch
参数指定批次中的图像数量)。optimizer
,根据误差量向正确的方向调整模型权重,以减少下一个周期的误差。默认情况下,使用SGD优化器,但您可以尝试其他优化器,例如Adam看看差异。在验证阶段,train
执行以下操作:
每个时期的每个阶段的进度和结果都会输出到控制台,可以看到模型如何从一个epoch到另一个epoch的学习和改进。如下:
第 1 行和第 2 行显示训练阶段的结果,第 3 行和第 4 行显示每个epoch验证阶段的结果。
训练阶段包括计算损失函数中的误差量,因此,这里最有价值的指标是 box_loss
和cls_loss
。
box_loss
显示检测到的边界框中的错误量。cls_loss
显示检测到的对象类中的错误量。为什么损失会分成几个指标?因为模型可以正确检测到对象周围的边界框,但错误地检测到该框中的对象类。例如,它将狗检测为马,但物体的尺寸被正确检测。
如果模型确实从数据中学习了一些东西,那么可以看到这些值随着epoch的推移而减少。在上述输出中,box_loss
减小为:0.7751,0.7473,0.742,cls_loss
也减小为:0.702,0.6422,0.6211。
在验证阶段,它使用验证数据集中的图像计算训练后模型的质量。最有价值的质量指标是 mAP50-95,它是平均精度。如果模型学习并改进,精度会增长。在上述输出中,它缓慢增长:0.788、0.788、0.791。
如果在训练完成后没有得到可接受的精度,我们可以调整数再次训练。例如 batch
、lr0
、lrf
或optimizer
。这里并没有明确的规则要做什么,但是有很多关于调参的建议。总而言之,需要反复训练并比较结果。
除此之外,train
在磁盘上工作期间还会写入大量统计信息。训练开始时,它会在当前文件夹中创建 runs/detect/train
子文件夹,并在每个时期之后向其中记录不同的日志文件。
同时,它将每个 epoch 后训练的模型导出到 /runs/detect/train/weights/last.pt
文件,并将精度最高的模型导出到 /runs/detect/train/weights/best.pt
文件。训练完成后,你可以获得best.pt
文件,并正式使用。
接着,我们将创建一个 Web 服务来检测 Web 浏览器中在线图像上的对象。
之前我们在 Jupyter Notebook 中完成模型实验,接下来我们来构建一个对象检测Web服务。
我们要创建的 Web 服务将上传一张图片到服务器,服务器端将图像传递给我们训练的模型,并将检测到的边界框数组返回到前端。收到此消息后,前端将在画布元素上绘制图像,并在其顶部检测到的边界框。
首先,为项目创建一个文件夹,并在其中创建用于前端网页的 index.html
文件。这是该文件的内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>YOLOv8 Object Detection</title>
<style>
canvas {
display:block;
border: 1px solid black;
margin-top:10px;
}
</style>
</head>
<body>
<input id="uploadInput" type="file"/>
<canvas></canvas>
<script>
const input = document.getElementById("uploadInput");
input.addEventListener("change", async(event) => {
const file = event.target.files[0];
const data = new FormData();
data.append("image_file", file, "image_file");
const response = await fetch("/detect",{method:"post",body:data});
const boxes = await response.json();
draw_image_and_boxes(file,boxes);
})
function draw_image_and_boxes(file,boxes) {
const img = new Image()
img.src = URL.createObjectURL(file);
img.onload = () => {
const canvas = document.querySelector("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img,0,0);
ctx.strokeStyle = "#00FF00";
ctx.lineWidth = 3;
ctx.font = "18px serif";
boxes.forEach(([x1,y1,x2,y2,label]) => {
ctx.strokeRect(x1,y1,x2-x1,y2-y1);
ctx.fillStyle = "#00ff00";
const width = ctx.measureText(label).width;
ctx.fillRect(x1,y1,width+10,25);
ctx.fillStyle = "#000000";
ctx.fillText(label,x1,y1+18);
});
}}
</script>
</body>
</html>
HTML 部分非常少,仅提交图片文件到服务器,然后,在 Javascript 部分,我们定义一个“onChange”输入字段的事件处理程序。当用户选择图像文件时,处理程序使用 fetch
向 /detect
后端程序发出 POST 请求并发送该图像文件。后端检测到上传的图像上的对象,并以 JSON 形式返回包含 boxes
数组的响应。然后该响应被解码并被传递到“draw_image_and_boxes”。该函数从文件加载图像,加载后立即将其绘制在画布上。然后,它在带有图像的画布上层绘制包括的类标签的每个边界框。
现在再创建一个带有 /detect
端点的后端。
使用Flask创建后端。 此外,使用 Pillow 库将上传的二进制文件读取为图像。
pip3 install flask
pip3 install waitress
pip3 install pillow
创建backends文件夹并在其中创建名为object_detector.py
的文件:
from ultralytics import YOLO
from flask import request, Flask, jsonify
from waitress import serve
from PIL import Image
import json
app = Flask(__name__)
@app.route("/")
def root():
with open("index.html") as file:
return file.read()
@app.route("/detect", methods=["POST"])
def detect():
buf = request.files["image_file"]
boxes = detect_objects_on_image(Image.open(buf.stream))
return jsonify(boxes)
def detect_objects_on_image(buf):
model = YOLO("best.pt")
results = model.predict(buf)
result = results[0]
output = []
for box in result.boxes:
x1, y1, x2, y2 = [
round(x) for x in box.xyxy[0].tolist()
]
class_id = box.cls[0].item()
prob = round(box.conf[0].item(), 2)
output.append([x1, y1, x2, y2, result.names[class_id], prob])
return output
serve(app, host='0.0.0.0', port=8080)
首先,我们导入所需的库,
requests
并发送 responses
到前端。另外,导入jsonify
把结果转换为 JSON。serve
和Flask的app
。Image
对象。然后,我们定义2个路由:
/
返回“index.html”的内容。/detect
响应来自前端的图像上传请求。它将 二进制 文件转换为 Pillow Image 对象,然后将此图像传递给 detect_objects_on_image
函数。该函数根据我们之前训练的 best.pt
模型创建一个模型对象。然后调用 predict
方法返回检测到的边界框。然后,对于每个框,它会以某种方式提取坐标、类名称和置信度,并将这些信息添加到输出数组中。最后,将数组转为 JSON 格式并返回到前端。通过以下命令运行该服务:
python3 object_detector.py
接着就可以在浏览器中访问http:///``localhost:8080
。它显示 index.html
页面。当我们选择图片上传后,会看到所有检测到的对象周围的边界框(或者没有检测到目标,则仅显示图像)。
在本文中,学习到了创建YOLOv8支持的 Web 应用程序的过程。我们介绍了创建模型、使用预训练模型、准备数据来训练自定义模型等步骤,最后创建了一个具有前端和后端的 Web 应用程序。