介绍
对象检测是一项计算机视觉任务,涉及识别和定位图像或视频中的对象。它是许多应用的重要组成部分,例如自动驾驶汽车、机器人和视频监控。
多年来,已经开发了许多方法和算法来查找图像中的对象及其位置。卷积神经网络对于此类任务有着非常好的性能和质量。
用于此任务的最流行的神经网络之一是 YOLO,由 Joseph Redmon、Santosh Divvala、Ross Girshick 和 Ali Farhadi 在 2015 年在他们著名的研究论文“You Only Look Once: Unified, Real-Time Object Detection”中创建。
从那时起,YOLO 已经出现了相当多的版本。最近的版本可以做的不仅仅是对象检测,还可做对象分割、对象追踪等、姿势识别等任务,本系列文章展示了如何使用YOLOv8。
本系列文章分为3个大模块:
- 如何使用对象识别:我们将使用预先训练的好的模型来检测常见的对象类,比如猫和狗。
- 如何训练自己的模型:我将展示如何训练自己的模型来检测您选择的特定对象类型,以及如何准备训练数据。
- 应用模型:最后将创建一个 Web 应用程序,使用自定义训练模型直接在 Web 浏览器中检测图像上的对象。
YOLOv8适用场景
YOLOv8可以解决分类、对象检测和图像分割问题。所有这些方法都以不同的方式检测图像或视频中的对象,如下图所示:
分类 | 检测 | 分割 |
---|---|---|
- 图像分类任务: 识别图像上的对象类别并返回其类型名称和置信度。例如,左侧图像实例中中,它识别是“cat”。并且该预测的置信度为 92% (0.92)。
- 对象检测任务:除了获取图片上物体类型名称和置信度之外,还返回物体在图像上的边框,包括位置坐标xy、边框宽度和边框高度。如第二张图所示。此外,对象检测可以检测图像上的多个对象及其边界框。
- 图像分割任务:除了识别对象类型和边界框之外,图像分割还可以检测对象的形状,如上述的右图所示。
在本文中,讨论如何使用 YOLOv8 的对象检测, 我将创建一个 Web 应用程序,用它来检测图像上的交通灯和路标。在本系列的后续的其他文章中,我将介绍其他功能,包括图像分割。
YOLOv8 入门
从本质上讲,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 对象。现在可以使用该对象完成以下任务:
- train: 用于在图像数据集上训练模型。
- predict: 用于对指定图像进行预测,例如检测模型可以在此图像上找到的所有对象的边界框。
- export: 用于将此模型从默认 PyTorch 格式导出为指定格式。
所有官方的 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 射线发现脑肿瘤。
因此,面对这些问题的时候,必须训练自己的模型来完成检测任务。
如何准备数据来训练 YOLOv8 模型
要训练模型,需要准备带注释的图像并将其拆分为训练和验证数据集。训练集将用于训练模型,验证集将用于验证训练结果。一般而言,可以将 80% 的图像放入训练集,将 20% 放入验证集。
创建数据集的步骤:
- 编码模型检测的对象类别。例如,如果只想检测猫和狗,那么可以声明“0”是猫,“1”是是狗。
- 为数据集创建一个文件夹并在其中创建两个子文件夹:“images"和"labels”。
- 将图片放入"images"文件夹中。图像越多,越具有一般性,训练效果就越好。
- 对于每个图像,在"labels"文件夹下创建注释文本。注释文本是.txt的文件,并与对应的图像具有相同的名称。在注释文件中,添加有关每个对象的数据,格式如下:
{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/imagesnc: 2
names: ['cat','dog']
train
和 val
分别表示训练集和验证集的路径,路径可以是相对于当前文件夹的相对路径,也可以是绝对路径。
nc
表示识别的种类数量,这里只有猫和狗,所以是2。
names
是按正确顺序排列的类名数组,对应索引就是之前注释图像时使用的种类编号。
此 YAML 文件会递给模型的train
方法,用来启动训练过程。
有很多工具能够帮我们完成上述的标注图像工作,比如 Roboflow Annotate 、 labelimg等。
训练模型的TIPS:
- 要训练一个好的模型,您应该拥有数百或数千张带注释的图像。
- 训练的图像数据,应尽量平衡,每个类应该有相同数量的对象,例如狗和猫的数量相等。
数据准备好后,我们就可以开始训练模型了。
如何训练YOLOv8模型
我们将使用其一套交通信号数据集进行训练。它包含交通灯和路标。
数据我已经上传至百度网盘,可以通过以下链接获取:
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 浏览器中在线图像上的对象。
如何创建对象检测 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 jsonapp = 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 outputserve(app, host='0.0.0.0', port=8080)
首先,我们导入所需的库,
- Flask 创建 Web 应用程序,从前端接收
requests
并发送responses
到前端。另外,导入jsonify
把结果转换为 JSON。 - Waitress 运行网络
serve
和Flask的app
。 - PIL 将上传的文件转化为
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 应用程序。