Yolov8的基本使用
一、引言
1.1 声明
声明:
本文章仅供学习交流使用,不提供完整代码,严禁用于商业用途和非法用途,否则由此产生的一切后果均与本人无关,请各位自觉遵守相关法律法规。
本文章未经许可禁止转载,禁止任何二次修改(加工)后的传播;若有侵权,联系删除。
交流、合作请留言,24小时内回复!
1.2 简介
yolov8的安装与基本使用,标记工具的介绍,自定义数据集的训练及后续。
1.3 代办
由于时间有限,本文目前只描述了思路和自己实际用到的部分代码,未完成所有内容的撰写,后续有时间再补充。(本文按照自己所涉及到的工具和遇见的问题进行适当更新,文章内容的完整度会不断完善)
二、Yolov8安装部署
2.1 环境依赖
英文不好的,可看 gitee之readme.md文档 ,此处的环境要求:Python>=3.8 and PyTorch>=1.8。
2.1.1 Python>=3.8
这个好弄,本机有3.10的主环境,也有虚拟环境的管理工具,直接再生成一个3.10的虚拟环境使用即可。
mkvirtualenv --python="D:\Program Files\Python310\python.exe" yolo8310
2.1.2 PyTorch>=1.8
建议先将GPU的驱动更新至最新版本
进入任务管理器,查看自己GPU的型号
官网下载 对应型号的驱动 https://www.nvidia.cn/Download/index.aspx?lang=cn
下载完成后,开始安装。安装位置、安装选项等等安装默认即可(过程忘记截图了)。
打开终端,输入nvidia-smi
命令,查看CUDA Driver的版本,本机为 CUDA Version:12.3
那么选择CUDA Runtime时,要选择小于12.3的最大版本。
去Pytorch 官网 https://pytorch.org/ 选择适合自己的Pytorch进行下载安装。
下载速度还可以,就不切换源了,下载较慢的可以修改其他下载源试试效果。
数分钟安装完毕,查看pytorch包是否可用
import torch
print("版本:",torch.__version__,"\tGPU是否可用:",torch.cuda.is_available(),"\tGPU个数:",torch.cuda.device_count())
2.2 ultralytics(yolov8)安装
前面的环境配置好之后,可以开始安装yolov8了。
上图所示:官网中介绍了多种安装方式,基础较弱的可以选择 pip install ultralytics 这种方式,本文选择 Git clone方式。
2.2.1 pip install -e . 可编辑模式安装
以下命令copy自官网,若不想配置git,可以绕过第一步的clone命令,手动打开github地址,下载项目的zip压缩包,然后解压。
# Clone the ultralytics repository
git clone https://github.com/ultralytics/ultralytics
# Navigate to the cloned directory
cd ultralytics
# Install the package in editable mode for development
pip install -e .
安装完之后pip list查看一下,通过pip install -e 模式安装的包,后面会有一个路径显示。
2.2.2 测试效果
- 可以直接在命令行界面(CLI)运行yolo命令。
下图中的命令含义是:使用yolo的yolov8n.pt模型对bus.jpg这张图进行预测。yolov8n.pt模型不存在的话,会自动下载,但是可能下载速度比较慢
预测结果为: 4 persons, 1 bus, 1 stop sign。
标记图片保存在:项目目录下的 runs\detect\predict 文件夹内,名称同source。
- 也可以在python中使用
from ultralytics import YOLO
# 从头开始创建新的YOLO模型
model = YOLO('yolov8n.yaml')
# 加载预先训练的YOLO模型(建议用于训练)
model = YOLO('yolov8n.pt')
# 使用coco128.yaml数据集对模型进行3轮的训练
results = model.train(data='coco128.yaml', epochs=3)
# 评估模型在验证集上的性能
results = model.val()
# 使用模型对图像执行对象检测
results = model('https://ultralytics.com/images/bus.jpg')
# 将模型导出为ONNX格式
success = model.export(format='onnx')
加载预先训练的YOLO模型
使用coco128.yaml数据集对模型进行3轮的训练,会有一些过程输出。
Results saved to runs\detect\train2
识别图片,结果输出。results.names可以看到训练集只有80个分类,所以使用此数据集也仅仅只能识别这80个分类中的物品
自己训练的模型,最终只识别到了1个人,效果暂时不论,现在已经能够把流程跑通了,后续就可以尝试训练自己需要的模型了。
三、数据标注工具
当我们想要自己训练模型时,就需要自己准备数据集,前期的数据标注工作就很重要,这时就得先选择一个合适的标注工具。
3.1 labelImg
一种常用的矩形标注工具,常用于目标识别和目标检测。labelimg是使用Python语言编写,界面用Qt设计的,因此需要先安装pyqt5。
pip install pyqt5
pip install labelimg
labelimg # 命令行启动即可
安装完之后,在命令行输入labelimg启动。
使用方式可以参考下面的截图注释:
- Open Dir打开图片所在文件夹
- Change Save Dir 选择标注文件保存的路径
- YOLO 选择自己需要的标记格式
- Auto Save Mode 打开自动保存模式
设置完上面四个选项之后,接下来就是使用快捷键"一路狂标":w 画标注框,d 下一张,a 上一张
终端里也能看到保存日志输出。
3.1 labelImg 使用时闪退的情况
问题:在python3.10版本中,使用labelimg 1.8.6 出现了闪退,(之前标注未报错)过了一段时间再次使用时,闪退 无法标注。
解决方案:根据提示信息去对应文件内 修改参数类型,基本都是 加个 int转换即可。
也曾 参考过 labelImg1.8.6在python3.10下使用 ,但是替换此文章中所说的两个文件信息之后,运行时会提示有新的依赖文件,还需要自己添加进去,稍微繁琐。本人使用了如下截图中方式,测试可以(2024-09)。
-
放大、缩小图片时闪退
\lib\site-packages\labelImg\labelImg.py
-
Create RectBox(标注)时闪退
lib\site-packages\libs\canvas.py
四、训练
4.1 数据集
本次使用的数据集为 易盾增强版滑块验证码的 缺口标记,下载了500张,前期标记了200张,后又增加至350张左右。
数据集可以划分为 训练集、验证集、测试集(非必须)。比例不严格 7:2:1 、8:2 等等皆可。数据集划分的代码使用GPT写的,如下:
import os
import shutil
import random
from pathlib import Path
import yaml # 用于创建 data.yaml 文件
# 配置参数(这几个参数 按照自己的实际情况进行调整)
image_dir = r"D:\code\my_yolov8\待标记的原始数据\易盾增强版滑块\images" # 原始图片所在文件夹
label_dir = r"D:\code\my_yolov8\待标记的原始数据\易盾增强版滑块\labels" # 标签文件所在文件夹
output_dir = "D:/code/my_yolov8/datasets/yidunslide" # 输出的数据集根目录
train_ratio = 0.8 # 训练集比例
val_ratio = 0.2 # 验证集比例
test_ratio = 0.1 # 测试集比例
classes_file = os.path.join(label_dir, 'classes.txt') # 类别文件路径
# 从 classes.txt 文件中读取类别
def load_classes_from_file(classes_path):
if os.path.exists(classes_path):
with open(classes_path, 'r') as f:
classes_list = [line.strip() for line in f.readlines()]
return classes_list
else:
return None
# 创建标准的数据集目录结构
def create_dir_structure(output_path):
os.makedirs(os.path.join(output_path, 'images', 'train'), exist_ok=True)
os.makedirs(os.path.join(output_path, 'images', 'val'), exist_ok=True)
os.makedirs(os.path.join(output_path, 'images', 'test'), exist_ok=True)
os.makedirs(os.path.join(output_path, 'labels', 'train'), exist_ok=True)
os.makedirs(os.path.join(output_path, 'labels', 'val'), exist_ok=True)
os.makedirs(os.path.join(output_path, 'labels', 'test'), exist_ok=True)
# 将文件复制到目标目录
def move_files(file_list, target_image_dir, target_label_dir):
for image_path in file_list:
label_path = os.path.join(label_dir, Path(image_path).stem + ".txt")
if os.path.exists(label_path): # 确保标签存在
shutil.copy(image_path, target_image_dir)
shutil.copy(label_path, target_label_dir)
# 获取有对应标签的图片文件
image_files = [os.path.join(image_dir, f) for f in os.listdir(image_dir)
if f.endswith(('.jpg', '.png')) and os.path.exists(os.path.join(label_dir, Path(f).stem + ".txt"))]
# 随机打乱文件列表
random.shuffle(image_files)
# 计算每个数据集的大小
total_images = len(image_files)
train_count = int(total_images * train_ratio)
val_count = int(total_images * val_ratio)
test_count = total_images - train_count - val_count # 剩余的放入测试集
# 分割数据集
train_files = image_files[:train_count]
val_files = image_files[train_count:train_count + val_count]
test_files = image_files[train_count + val_count:]
# 创建输出目录结构
create_dir_structure(output_dir)
# 移动文件到相应的目录
move_files(train_files, os.path.join(output_dir, 'images', 'train'), os.path.join(output_dir, 'labels', 'train'))
move_files(val_files, os.path.join(output_dir, 'images', 'val'), os.path.join(output_dir, 'labels', 'val'))
move_files(test_files, os.path.join(output_dir, 'images', 'test'), os.path.join(output_dir, 'labels', 'test'))
print(f"数据集拆分完成:训练集 {train_count} 张,验证集 {val_count} 张,测试集 {test_count} 张。")
# 创建 data.yaml 文件
def create_data_yaml(output_path, classes_list):
data = {
'path': output_path, # 数据集的根目录路径
'train': 'images/train', # 训练集的相对路径
'val': 'images/val', # 验证集的相对路径
'test': 'images/test', # 测试集的相对路径(可选)
'nc': len(classes_list), # 类别数量
'names': classes_list # 类别名称列表
}
yaml_path = os.path.join(output_path, 'data.yaml')
with open(yaml_path, 'w') as yaml_file:
yaml.dump(data, yaml_file, default_flow_style=False)
print(f"data.yaml 文件已创建,路径为:{yaml_path}")
# 尝试从 classes.txt 加载类别
classes = load_classes_from_file(classes_file)
# 如果找不到 classes.txt 文件,使用默认的类别
if classes is None:
print("未找到 classes.txt 文件,使用默认类别。")
classes = ['class1', 'class2', 'class3'] # 替换为实际类别
# 创建 data.yaml 文件
create_data_yaml(output_dir, classes)
4.2 配置文件
数据集的指定 data.yaml (名称自定义即可),内容包含以下信息:
# 数据集路径
path: D:/code/my_yolov8/datasets/yidunslide
train: images/train
val: images/val
test: images/test
# 类别数量
nc: 1
# 类别名称
names: ["缺口"]
4.3 模型选择
下载自己所需要的模型
本例中使用的是 YOLOv8n。
4.4 训练
yolo detect train data="D:\code\my_yolov8\datasets\yidunslide\data.yaml" model=yolov8n.pt epochs=100 batch=16 imgsz=640
4.4.1 输入参数解析
yolo detect train
:这是 YOLOv8 命令行工具中的一个指令,表示对检测任务进行训练。detect 任务通常用于目标检测,模型会输出目标物体的边界框以及类别。data="D:\code\my_yolov8\datasets\yidunslide\data.yaml"
:指定了数据集的配置文件路径。data.yaml 文件包含了训练、验证、测试集的路径以及类别信息。该文件用于定义模型所使用的训练和验证数据。model=yolov8n.pt
:使用 YOLOv8n(Nano 版本)的预训练权重文件 yolov8n.pt 作为模型的初始权重。YOLOv8n 是 YOLOv8 模型的一个轻量版本,适用于计算资源有限的环境。这个预训练模型将作为模型的起点,进行后续的微调。epochs=100
:表示模型训练的迭代次数为 100 个 epoch。每个 epoch 表示模型遍历一次整个训练集的数据。通常,更多的 epoch 可以让模型学习得更充分,但过多的 epoch 也可能导致过拟合。batch=16
:设置批量大小为 16。每次迭代中,模型将处理 16 张图像。批量大小会影响训练的内存占用和模型收敛速度。更大的批量通常需要更多的显存资源,但会让模型更新得更加稳定。imgsz=640
:设置输入图像的尺寸为 640x640 像素。YOLO 模型在训练时会将所有图像调整为这个大小。更大的图像尺寸可能会提升检测精度,但也需要更多的计算资源。
4.4.2 输出日志解析
训练过程中的日志输出如下图:
下面这张图显示的日志输出是有问题的:box_loss、cls_loss、dfl_loss 都为nan。
在 YOLOv8 的训练过程中,以下参数会不断变化,并帮助理解模型的表现(以下来源于GPT):
- Epoch:
含义:当前的训练轮次。每个 epoch 表示模型完成了一次对整个训练数据集的训练。
趋势:随着 epoch 增加,模型的各项损失和评估指标应逐渐改善。 - GPU_mem:
含义:当前使用的 GPU 显存大小(单位:GB)。
趋势:这个值表示 GPU 的内存占用,通常是固定的,除非有显存溢出或者内存管理问题。 - box_loss:
含义:边界框损失,衡量预测框和真实目标框之间的差距。越小表示模型对物体位置的预测越准确。
趋势:随着训练的进行,box_loss 应该逐渐减少,表示模型在定位物体时表现越来越好。通常在训练前期下降较快,后期趋于平稳。 - cls_loss:
含义:类别损失,衡量模型预测的类别和真实类别之间的差异。越小表示模型在分类方面的表现越好。
趋势:随着训练进行,cls_loss 也应该逐渐减少。前期可能下降较快,后期趋于平稳。 - dfl_loss:
含义:分布式焦点损失(Distribution Focal Loss),主要用于增强回归的精准度,特别是用于更好地预测边界框的精细化位置。
趋势:随着训练的深入,dfl_loss 应该逐渐减少,表示模型边界框的预测越来越准确。 - Instances:
含义:当前图像中物体实例的数量,通常是每个 batch 中检测到的物体数量。
趋势:这个数值通常与训练数据集相关,不会有显著的趋势变化。 - Size (640):
含义:训练图像的尺寸,这里是 640×640 像素。通常设置为固定值。
趋势:这个值是静态的,不会在训练中变化。
验证指标:
- Class:
含义:验证时所有类别的汇总信息。 - Images:
含义:验证集中用于验证的图片数量。 - Instances:
含义:所有验证图片中标注的物体总数。
评估指标:
- Box(P)(精确度,Precision):
含义:在模型预测的框中,有多少比例的框是正确的,即预测为正的样本中有多少是真正的正样本。
趋势:随着训练进行,精度应该逐渐提高,通常在训练初期较低,后期逐渐稳定在高值。 - R(召回率,Recall):
含义:实际为正的样本中有多少被正确预测出来。即实际存在的目标中,模型找到了多少。
趋势:召回率在训练过程中应该提高,表示模型能够找到更多的物体。 - mAP50(平均精度,IoU 阈值为 50%):
含义:平均精度,表示在 IoU 阈值为 50% 时,所有类别的预测准确率。mAP50 越高,表示模型在检测不同物体时越准确。
趋势:随着训练进行,mAP50 应逐渐上升,通常在训练前期变化较快,后期趋于稳定。 - mAP50-95(平均精度,IoU 阈值从 50% 到 95% 的平均值):
含义:在不同 IoU 阈值下(从 50% 到 95%)的平均精度。这个值越高,表示模型对位置和类别的预测越准确。
趋势:类似于 mAP50,但更严格,数值通常比 mAP50 低。随着训练深入,这个值也应逐渐上升。
这些指标可以帮助判断训练的效果。如果损失值不下降或精度和召回率无法提高,可能需要调整超参数或模型结构。
4.4.3 训练结果解析
最终100轮全部跑完了,没有提前终止,说明模型可能没有达到最优(可以增加训练轮次,再次训练),或者还没有触发终止条件。
YOLOv8 提供了一个 patience 参数,它用于提前停止训练,如果在指定轮次内验证集的性能没有显著提升,即提前停止训练。默认情况下,patience 通常设置为 50。这意味着如果在 50 个 epoch 中模型性能没有提升,它才会提前停止。
如果模型在训练中性能一直在提升,比如 mAP、Precision 和 Recall 的值在验证集中没有出现停滞或下降,那么它会继续训练,直至达到设置的轮次 (本例为100 轮)。
如果希望提前停止训练以节省时间,或者模型在性能上达到一个较高的点时停止训练,你可以通过以下方式设置:
yolo train data="your_data.yaml" model="yolov8n.pt" epochs=100 batch=16 imgsz=640 patience=10
这样设置后,如果验证集的性能在 10 个轮次内没有提升,模型将会提前停止训练。
通过日志可可看到 YOLOv8 训练的结果已保存到 runs\detect\train 目录中。在这个目录中,可以找到以下主要文件和信息:
-
权重文件:
best.pt: 训练过程中性能最好的模型。
last.pt: 最终训练完成后的模型。 -
日志和图表:
results.png: 包含训练过程中各类损失、精度、召回率等指标的变化趋势图,帮助直观了解训练进展。
labels.jpg: 可视化标签分布情况的图表。
events.out.tfevents 文件: 可用于 TensorBoard 进行详细的训练过程分析。 -
验证结果:
在验证集上的评估结果,包括 Precision、Recall、mAP50 和 mAP50-95,可以在终端输出中看到,同时在日志文件中也保存了这些信息。
4.4.4 推理新图片
最优模型已保存在: runs\detect\train\weights\best.pt,使用此模型测试推理情况,挑选一张数据集之外的图片进行测试。
yolo predict model="D:\code\my_yolov8\runs\detect\train\weights\best.pt" source="D:\code\my_yolov8\待标记的原始数据\易盾增强版滑块\images\bg_xz_416.jpg"
4.4.5 .pt 模型 转 .onxx
yolo export model=yolov8n.pt format=onnx # export official model
yolo export model=path/to/best.pt format=onnx # export custom trained model
导出需要 onnx库,若不存在,则会自动安装最新版本(建议先手动安装指定版本)。
目前 最新版为 1.16.2,使用这个版本在导出过程中会报错,可以卸载重新安装 1.16.1
4.4.6 模型输入、输出解释
4.5 遇见的问题及解决方案
4.5.1 box_loss、cls_loss、dfl_loss 为 nan。
从输出日志中可以看到,在首次训练时,box_loss、cls_loss、dfl_loss 都是 nan,表明训练中发生了问题。将完整的日志丢给GPT之后,它回答了nan可能是由于以下几个原因:
-
数据集问题:
标签问题:可能是标签数据有问题,例如标签中的边界框坐标超出了图像尺寸范围,或者标签文件格式不正确。检查一下 labels 文件夹中的标签文件,确保它们的格式正确。
图片和标签不匹配:确保每一张图片都有相应的标签文件,并且图片尺寸与标签中的坐标是一致的。 -
学习率或优化器设置问题:
YOLOv8 自动选择了 AdamW 作为优化器,你可以尝试手动设置优化器为 SGD 或 Adam,并明确指定学习率。例如:
yolo train data=data.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640 optimizer=SGD lr0=0.001
-
训练数据问题:
过少的训练数据:可能数据过少,尤其是对于复杂的任务,可能会导致模型不收敛。如果数据量过少,可以尝试进行数据增强。
数据不平衡:如果某些类别的样本太少或者标签分布不均匀,可能导致训练时 cls_loss 出现问题。 -
混合精度问题:
YOLOv8 默认使用 AMP (自动混合精度),你可以尝试禁用 AMP,以排除混合精度训练导致的问题:
yolo train data=data.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640 amp=False
- 冻结层问题:
在日志中可以看到冻结了一部分层:Freezing layer ‘model.22.dfl.conv.weight’。你可以尝试不冻结任何层,以确保模型可以在你的数据上充分训练:
yolo train data=data.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640 freeze=0
按照上述步骤,尝试到第4步(混合精度问题)时:amp=False
效果得到了改善。
4.5.2 中文字体不支持 (Glyph missing from current font)
d:\code\ultralytics-main\ultralytics\utils\metrics.py:313: UserWarning: Glyph 32570 (\N{CJK UNIFIED IDEOGRAPH-7F3A}) missing from current font.
fig.savefig(plot_fname, dpi=250)
虽然不影响训练效果,但是看着这个提示挺烦人的,就想给他去除掉。
- 修改 Matplotlib 配置文件
import matplotlib
print(matplotlib.matplotlib_fname()) # 找到 Matplotlib 的配置文件路径
# 打开配置文件,找到 font.family,将其设置为支持中文的字体,例如 SimHei
- C:\Windows\Fonts\simhei.ttf 复制 到 与 matplotlibrc文件同目录的下的 ./fonts/ttf 文件夹下
- 清除 matplotlib 字体缓存,以确保设置的字体被使用。
import matplotlib
print(matplotlib.get_cachedir()) # C:\Users\用户\.matplotlib 删除下面的fontlist.json
- 修改\ultralytics-main\ultralytics\utils 下的 metrics.py 和 plotting.py
这个py文件的路径 可以 根据 输出日志 确认
# 两个文件 都添加 如下三行
plt.rcParams['font.family'] = 'SimHei' # 全局设置
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
以上几部分处理之后,无论是训练还是推理,都没有再提示:
UserWarning: Glyph 32570 (\N{CJK UNIFIED IDEOGRAPH-7F3A}) missing from font(s) Arial.
五、onnx模型的使用
在使用onnx模型进行推理时,可以使用YOLO提供的API,也可以使用ONNX Runtime 来运行 ONNX 模型。
5.1 YOLO
YOLOv8 官方代码库已经提供了许多与目标检测相关的功能,涵盖模型加载、推理、预处理、后处理等任务。
1.图像的预处理与推理:通过 YOLOv8 模型加载和推理。
2.推理后的结果处理:包括框的绘制、非极大值抑制(NMS)等。
3.保存和可视化结果。
import cv2
from ultralytics import YOLO
import matplotlib.pyplot as plt
# 1 加载YOLOv8模型
model = YOLO(r'D:\code\my_yolov8\runs\detect\train\weights\best.onnx')
# 加载图像
image_path = r'D:\code\my_yolov8\待标记的原始数据\易盾增强版滑块\images\bg_xz_416.jpg'
image = cv2.imread(image_path)
# 2 进行推理
results = model(image)
boxes = results[0].boxes # 获取边界框
class_names = results[0].names # 获取类别名称
for box in boxes:
xyxy = box.xyxy.numpy() # 边界框的左上角和右下角 (x1, y1, x2, y2)
conf = box.conf.numpy() # 置信度
cls = int(box.cls.numpy()) # 类别索引
label = class_names[cls] # 根据类别索引获取名称
print(f"类别: {label}, 置信度: {conf}, 边界框: {xyxy}")
# 3 绘制结果:使用 results[0].plot() 直接在图像上绘制预测框,并保存
annotated_image = results[0].plot() # 获取带有标注的图片
# 保存结果图像
output_path = r"D:\code\my_yolov8\test_output.jpg"
cv2.imwrite(output_path, annotated_image)
print(f"结果保存到: {output_path}")
# 使用Matplotlib可视化结果
plt.imshow(cv2.cvtColor(annotated_image, cv2.COLOR_BGR2RGB))
plt.show()
5.2 ONNX Runtime
要使用 ONNX Runtime 来运行 ONNX 模型进行验证码图片的缺口检测和位置识别,主要流程包括以下几步:加载模型、输入数据的前处理、模型推理和后处理。
1. 前处理
前处理的目的是将输入的验证码图像转化为模型可以接受的格式,通常包括 尺寸调整、归一化 和 通道排列。
- 尺寸调整:根据模型输入要求,调整验证码图像的尺寸。通常模型要求固定大小的输入,如 640x640,或模型可能要求不同的宽高比。
- 归一化:将像素值从 [0, 255] 范围缩放到 [0, 1],有时也会进行均值和标准差的标准化。
- 通道排列:确保图像的通道顺序是 CHW (通道,高度,宽度),因为深度学习模型通常期望输入为这种格式。
2.模型推理
模型推理部分使用 ONNX Runtime 来执行推理,并将预处理后的图像传入模型进行计算。
3.后处理
后处理的目的是将模型的输出结果(通常是归一化的坐标或其他信息)还原到原图像的尺寸,具体步骤包括:非极大值抑制 (NMS)、解码边界框、还原到原始图像尺寸 等。
- 非极大值抑制 (NMS):去除重复预测的边界框,留下置信度最高的框。
- 还原到原始图像尺寸:因为图像在前处理时进行了缩放和填充,需要将模型输出的坐标进行逆变换,还原到原始图像尺寸。
4.可视化结果
将检测出的验证码缺口在原始图像上绘制出来,方便验证。
完整代码如下,测试过,可以跑通。
import os
import onnxruntime as rt
import numpy as np
import cv2
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont
# 中文路径的图像读取问题
def imread_unicode(image_path):
"""
解决含中文路径的图像读取问题
:param image_path: image_path: 图像的路径(包含中文路径)。
:return: image: 使用 OpenCV 读取的图像。
"""
# 使用 open() 读取二进制数据,避免中文路径问题
with open(image_path, 'rb') as f:
image_data = f.read()
# 将读取的二进制数据转为 numpy 数组
image_np = np.frombuffer(image_data, np.uint8)
# 使用 OpenCV 解码为图像
image = cv2.imdecode(image_np, cv2.IMREAD_COLOR)
return image
# 预处理:letterbox 图片缩放
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True):
"""
将图像按比例缩放,并填充到指定尺寸,保持宽高比。
参数:
img: 输入的图像 (numpy array)
new_shape: 期望的目标尺寸 (宽, 高)
color: 填充的颜色
auto: 是否自动调整填充使其成为32的倍数
scaleFill: 是否强行拉伸图片到指定尺寸
scaleup: 是否允许放大图片,如果为False,则只允许缩小
返回:
img: 调整后的图像
ratio: 缩放比例 (宽度, 高度)
(dw, dh): 在宽度和高度上的填充
"""
# 获取原图的尺寸
shape = img.shape[:2] # (高, 宽) (160, 320)
# 计算缩放比例
r = min(new_shape[0] / shape[1], new_shape[1] / shape[0]) # 比较宽度和高度的缩放比例
if not scaleup: # 如果不允许放大图片
r = min(r, 1.0) # 只缩小图片
# 计算缩放后的新尺寸
ratio = r, r # 宽度和高度的缩放比例相同
new_unpad = (int(shape[1] * r), int(shape[0] * r)) # 缩放后的宽度和高度
dw, dh = new_shape[0] - new_unpad[0], new_shape[1] - new_unpad[1] # 计算需要填充的宽度和高度
# 自动调整填充,使其成为32的倍数
if auto: # 不采用此方式,这种方式修改之后的尺寸 不一定等于640*640,调用模型时可能会报 尺寸不符的错误
# onnxruntime.capi.onnxruntime_pybind11_state.InvalidArgument: [ONNXRuntimeError] : 2 : INVALID_ARGUMENT : Got invalid dimensions for input: images for the following indices
# index: 2 Got: 320 Expected: 640
# Please fix either the inputs/outputs or the model.
dw = np.mod(dw,32) # 将填充宽度调整为32的倍数
dh = np.mod(dh,32) # 将填充高度调整为32的倍数
elif scaleFill: # 强制缩放到指定尺寸
dw, dh = 0, 0
new_unpad = new_shape
ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # 宽和高的缩放比例
# 将填充分成两边,一半填充在左/上,另一半填充在右/下
dw /= 2 # 宽度两边填充一半
dh /= 2 # 高度两边填充一半
# 调整图像大小并进行填充
img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) # 按比例缩放图像
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) # 上下填充像素数
left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) # 左右填充像素数
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # 填充图像
# cv2.imshow("img", img)
# cv2.waitKey()
return img, ratio, (dw, dh) # 返回处理后的图像,缩放比例,以及填充的宽度和高度
# 缩放之后的图片 调整通道顺序 HWC -> CHW 归一化
def img_normalization(img):
img = np.transpose(img, (2, 0, 1)) # 坐标转换 调整通道顺序 HWC -> CHW
img = img / 255 # 归一化
return np.expand_dims(img, axis=0).astype(np.float32) # 增加 batch 维度 ,并 转换为浮点数
# 后处理: 将(1,4+nc,8400)处理成(8400,4+nc) 4+nc = box:4 conf:nc(nc个类别的置信度)
def pred_std(pred):
"""
将(1,4+nc,8400)处理成(8400,4+nc) 4+nc = box:4 conf:nc(nc个类别的置信度)
"""
pred = np.squeeze(pred) # 去除批次batch维度(它总是1的话)
pred = np.transpose(pred, (1, 0)) # 对数组进行转置操作 (5,8400) --> (8400,5)
pred_class = pred[..., 4:] # 每个类别对应的置信度
max_conf = np.max(pred_class, axis=-1) # 计算每个样本的最大置信度
max_class_indices = np.argmax(pred_class, axis=-1) # 计算每个样本的最大置信度对应的类别索引
result = np.column_stack((max_conf, max_class_indices)) # 将最大置信度和对应类别索引合并为一个二维数组
pred = np.concatenate((pred[..., :4], result), axis=1) # [x,y,w,h,conf,cls_index]
return pred
# 后处理:非极大值抑制(NMS)函数
def non_max_suppression(predictions, conf_threshold=0.5, iou_threshold=0.4):
"""
非极大值抑制 (NMS):在多个候选框之间筛选出最佳框,去除重叠的冗余框。
Args:
predictions: 模型输出的预测框,包括 (x, y, w, h, conf, class)。
conf_threshold: 置信度阈值,筛选出置信度较高的框。
iou_threshold: IOU 阈值,筛选出没有严重重叠的框。
Returns:
nms_boxes: 经过 NMS 处理后的有效框。
"""
boxes = predictions[..., :4] # 预测框位置
confs = predictions[..., 4] # 置信度
classes = predictions[..., 5] # 类别
# 筛选出置信度大于阈值的框
mask = confs >= conf_threshold
boxes = boxes[mask]
confs = confs[mask]
classes = classes[mask]
# 如果没有框的置信度高于阈值,则直接返回空列表
if len(boxes) == 0:
return [], [], []
# 使用 OpenCV 提供的 NMS 函数
indices = cv2.dnn.NMSBoxes(boxes.tolist(), confs.tolist(), conf_threshold, iou_threshold)
# 提取有效的框
if len(indices) > 0:
nms_boxes = boxes[indices.flatten()]
nms_confs = confs[indices.flatten()]
nms_classes = classes[indices.flatten()]
else:
nms_boxes = []
nms_confs = []
nms_classes = []
return nms_boxes, nms_confs, nms_classes
# 后处理:将 xywh 转换为 xyxy 坐标
def xywh2xyxy(box):
"""
将预测框从 (中心 x, 中心 y, 宽, 高) 转换为 (x1, y1, x2, y2) 左上角和右下角坐标。
Args:
box: 包含 xywh 的边界框。
Returns:
转换后的 xyxy 坐标。
"""
x_c, y_c, w, h = box
x1 = x_c - w / 2
y1 = y_c - h / 2
x2 = x_c + w / 2
y2 = y_c + h / 2
return [x1, y1, x2, y2]
# 后处理:边界坐标还原
def scale_coords(coords, img0_shape, img1_shape=(640,640), ratio_pad=None):
"""
将边界框坐标从 letterbox 处理后的图像映射回原图尺寸。
:param coords: 预测得到的边界框坐标
:param img0_shape: 原图尺寸
:param img1_shape: letterbox 处理后的图像尺寸
:param ratio_pad: 缩放比例和填充信息
:return: 恢复后的边界框坐标
"""
coords = np.array(coords)
if ratio_pad is None: # 默认值情况下,计算缩放比例和填充量
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # 缩放比例
pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # 计算填充量
else:
gain = ratio_pad[0][0] # 缩放比例 (r,r) 理论上 长和宽 缩放比例 一致(以最小的为准),取其中一个
pad = ratio_pad[1] # 填充量 (宽,高)
# 去除填充并按缩放比例恢复
coords[:, [0, 2]] -= pad[0] # x 坐标去除填充
coords[:, [1, 3]] -= pad[1] # y 坐标去除填充
coords[:, :4] /= gain # 缩放坐标
# 限制坐标范围在原图内
coords[:, [0, 2]] = coords[:, [0, 2]].clip(0, img0_shape[1]) # x 坐标范围
coords[:, [1, 3]] = coords[:, [1, 3]].clip(0, img0_shape[0]) # y 坐标范围
return coords
# 可视化
def draw_boxes(image, box_data,font_path='C:/Windows/Fonts/simhei.ttf'):
"""
在图像上绘制检测框和类别标签(支持中文)。
Args:
image: 输入图像(numpy array)。
box_data: {"coord":[x1 y1 x2 y2],"label": 标签,"conf":置信度}
font_path: 支持中文的字体文件路径。
Returns:
image: 绘制了边界框和中文标签的图像(numpy array)。
"""
# 将numpy数组转换为PIL图像
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(image_pil)
for data in box_data:
x1, y1, x2, y2 = data.get("coord")
label = data.get("label")
conf = data.get("conf")
# 绘制矩形框(在PIL图像上)
draw.rectangle([(x1, y1), (x2, y2)], outline=(255, 0, 0), width=1)
label = f"{label} {conf:.2f}"
# 使用Pillow绘制中文文本
try:
font = ImageFont.truetype(font_path, 10) # 字体大小和路径
bbox = draw.textbbox((0,0), label, font=font)
text_width = bbox[2] - bbox[0] # 边界框的右坐标减去左坐标得到宽度
text_height = bbox[3] - bbox[1] # 边界框的下坐标减去上坐标得到高度
text_x = x1
text_y = y1 - text_height
# 确保文本不会超出图像边界
if text_x < 0:
text_x = 0
if text_y < 0:
text_y = max(0, y1)
draw.text((text_x, text_y), label, fill=(255, 0, 0), font=font)
except IOError:
print(f"无法加载字体: {font_path}")
# 将PIL图像转换回numpy数组
image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
return image
class YOLOV8ONNX:
"""
1 图像预处理:尺寸调整、归一化、通道转换
2 模型推理
3 输出结果维度转化 (1,5,8400) --> (1,8400,5)
4 筛选有效框并进行NMS
5 坐标转换 xyhw --> xyxy
6 坐标还原到原图尺寸
7 绘制边界框
8 保存结果
9 可视化
"""
# 初始变量定义
def __init__(self, onnx_path='', img_path='',class_list = None, img_size=640, conf_thres=0.25, iou_thres=0.45, output_path=None):
if not onnx_path:
raise NotImplementedError("ONNX模型路径未定义")
elif not class_list:
raise NotImplementedError("类别列表未定义")
elif not img_path:
raise NotImplementedError("图像路径未定义")
self.onnx_model_path = onnx_model_path
self.image_path = img_path
self.class_list = class_list
self.img_size = (img_size,img_size) if isinstance(img_size,int) else img_size
self.conf_thres = conf_thres
self.iou_thres = iou_thres
if output_path:
self.output_path = output_path
else:
self.output_path = os.path.join(os.getcwd(),"pred_runtime.jpg")
# 加载 ONNX 模型
self.session = rt.InferenceSession(onnx_model_path)
self.input_name = self.session.get_inputs()[0].name
self.output_name = self.session.get_outputs()[0].name
def run(self):
image = imread_unicode(self.image_path)
# 1 图像预处理:尺寸调整、归一化、通道转换
resize_img, ratio, (dw, dh) = letterbox(image) # 缩放
normal_img = img_normalization(resize_img) # 归一化、通道转换
# 2 进行推理
predictions = self.session.run([self.output_name], {self.input_name: normal_img})[0]
# 3 输出结果维度转化 (1,5,8400) --> (1,8400,5)
predictions = pred_std(predictions)
# 4 筛选有效框并进行NMS
nms_boxes, nms_confs, nms_classes = non_max_suppression(predictions, self.conf_thres, self.iou_thres)
# 5 坐标转换 xyhw --> xyxy
nms_boxes = [xywh2xyxy(box) for box in nms_boxes]
# 6 坐标还原到原图尺寸
nms_boxes = scale_coords(nms_boxes, image.shape, resize_img.shape, ratio_pad=(ratio, (dw, dh)))
# 7 绘制边界框
result = []
for box, conf, cls in zip(nms_boxes, nms_confs, nms_classes):
item = {
"coord": [int(coord) for coord in box],
"label": self.class_list[int(cls)],
"conf": round(conf, 2)
}
print(f"标签:{item['label']} 置信度:{item['conf']} 坐标:{item['coord']}")
result.append(item)
result_image = draw_boxes(image, result)
# 8 保存结果
cv2.imwrite(self.output_path, result_image)
print("推理完成!结果已保存到:", self.output_path)
# 9 可视化
plt.imshow(cv2.cvtColor(result_image, cv2.COLOR_BGR2RGB))
plt.show()
return result
if __name__ == '__main__':
onnx_model_path = r'D:\code\my_yolov8\runs\detect\train\weights\best.onnx' # 替换为实际模型路径
image_path = r'D:\code\my_yolov8\待标记的原始数据\易盾增强版滑块\images\bg_xz_416.jpg' # 替换为实际图像路径
class_list = ["缺口"] # 类别名称,替换为实际类别
yolo = YOLOV8ONNX(onnx_path=onnx_model_path, img_path=image_path, class_list=class_list)
print(yolo.run())