AI模型部署 | onnxruntime部署YOLOv8分割模型详细教程
本文首发于公众号【DeepDriving】,欢迎关注。
0. 引言
我之前写的文章《基于YOLOv8分割模型实现垃圾识别》介绍了如何使用YOLOv8分割模型来实现垃圾识别,主要是介绍如何用自定义的数据集来训练YOLOv8分割模型。那么训练好的模型该如何部署呢?YOLOv8分割模型相比检测模型多了一个实例分割的分支,部署的时候还需要做一些后处理操作才能得到分割结果。
本文将详细介绍如何使用onnxruntime框架来部署YOLOv8分割模型,为了方便理解,代码采用Python实现。
1. 准备工作
-
安装onnxruntime
onnxruntime分为GPU版本和CPU版本,均可以通过pip直接安装:pip install onnxruntime-gpu #安装GPU版本pip install onnxruntime #安装CPU版本注意:
GPU版本和CPU版本建议只选其中一个安装,否则默认会使用CPU版本。 -
下载
YOLOv8分割模型权重Ultralytics官方提供了用COCO数据集训练的模型权重,我们可以直接从官方网站https://docs.ultralytics.com/tasks/segment/下载使用,本文使用的模型为yolov8m-seg.pt。
-
转换onnx模型
调用下面的命令可以把
YOLOv8m-seg.pt模型转换为onnx格式的模型:yolo task=segment mode=export model=yolov8m-seg.pt format=onnx转换成功后得到的模型为
yolov8m-seg.onnx。
2. 模型部署
2.1 加载onnx模型
首先导入onnxruntime包,然后调用其API加载模型即可:
import onnxruntime as ortsession = ort.InferenceSession("yolov8m-seg.onnx", providers=["CUDAExecutionProvider"])
因为我使用的是GPU版本的onnxruntime,所以providers参数设置的是"CUDAExecutionProvider";如果是CPU版本,则需设置为"CPUExecutionProvider"。
模型加载成功后,我们可以查看一下模型的输入、输出层的属性:
for input in session.get_inputs():print("input name: ", input.name)print("input shape: ", input.shape)print("input type: ", input.type)for output in session.get_outputs():print("output name: ", output.name)print("output shape: ", output.shape)print("output type: ", output.type)
结果如下:
input name: images
input shape: [1, 3, 640, 640]
input type: tensor(float)
output name: output0
output shape: [1, 116, 8400]
output type: tensor(float)
output name: output1
output shape: [1, 32, 160, 160]
output type: tensor(float)
从上面的打印信息可以知道,模型有一个尺寸为[1, 3, 640, 640]的输入层和两个尺寸分别为[1, 116, 8400]和[1, 32, 160, 160]的输出层。
2.2 数据预处理
数据预处理采用OpenCV和Numpy实现,首先导入这两个包
import cv2
import numpy as np
用OpenCV读取图片后,把数据按照YOLOv8的要求做预处理
image = cv2.imread("soccer.jpg")
image_height, image_width, _ = image.shape
input_tensor = prepare_input(image, model_width, model_height)
print("input_tensor shape: ", input_tensor.shape)
其中预处理函数prepare_input的实现如下:
def prepare_input(bgr_image, width, height):image = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)image = cv2.resize(image, (width, height)).astype(np.float32)image = image / 255.0image = np.transpose(image, (2, 0, 1))input_tensor = np.expand_dims(image, axis=0)return input_tensor
处理流程如下:
1. 把OpenCV读取的BGR格式的图片转换为RGB格式;
2. 把图片resize到模型输入尺寸640x640;
3. 对像素值除以255做归一化操作;
4. 把图像数据的通道顺序由HWC调整为CHW;
5. 扩展数据维度,将数据的维度调整为NCHW。
经过预处理后,输入数据input_tensor的维度变为[1, 3, 640, 640],与模型的输入尺寸一致。
2.3 模型推理
输入数据准备好以后,就可以送入模型进行推理:
outputs = session.run(None, {session.get_inputs()[0].name: input_tensor})
前面我们打印了模型的输入输出属性,可以知道模型有两个输出分支,其中一个output0是目标检测分支,另一个output1则是实例分割分支,这里打印一下它们的尺寸看一下
#squeeze函数是用于删除shape中为1的维度,对output0做transpose操作是为了方便后续操作
output0 = np.squeeze(outputs[0]).transpose()
output1 = np.squeeze(outputs[1])
print("output0 shape:", output0.shape)
print("output1 shape:", output1.shape)
结果如下:
output0 shape: (8400, 116)
output1 shape: (32, 160, 160)
处理后目标检测分支的维度为[8400, 116],表示模型总共可以检测出8400个目标(大部分是无效的目标),每个目标包含116个参数。刚接触YOLOv8分割模型的时候可能会对116这个数字感到困惑,这里有必要解释一下:每个目标的参数包含4个坐标属性(x,y,w,h)、80个类别置信度和32个实例分割参数,所以总共是116个参数。实例分割分支的维度为[32, 160, 160],其中第一个维度32与目标检测分支中的32个实例分割参数对应,后面两个维度则由模型输入的宽和高除以4得到,本文所用的模型输入宽和高都是640,所以这两个维度都是160。
2.4 后处理
首先把目标检测分支输出的数据分为两个部分,把实例分割相关的参数从中剥离。
boxes = output0[:, 0:84]
masks = output0[:, 84:]
print("boxes shape:", boxes.shape)
print("masks shape:", masks.shape)
boxes shape: (8400, 84)
masks shape: (8400, 32)
然后实例分割这部分数据masks要与模型的另外一个分支输出的数据output1做矩阵乘法操作,在这之前要把output1的维度变换为二维。
output1 = output1.reshape(output1.shape[0], -1)
masks = masks @ output1
print("masks shape:", masks.shape)
masks shape: (8400, 25600)
做完矩阵乘法后,就得到了8400个目标对应的实例分割掩码数据masks,可以把它与目标检测的结果boxes拼接到一起。
detections = np.hstack([boxes, masks])
print("detections shape:", detections.shape)
detections shape: (8400, 25684)
到这里读者应该就能理解清楚了,YOLOv8模型总共可以检测出8400个目标,每个目标的参数包含4个坐标属性(x,y,w,h)、80个类别置信度和一个160x160=25600大小的实例分割掩码。
由于YOLOv8模型检测出的8400个目标中有大量的无效目标,所以先要通过置信度过滤去除置信度低于阈值的目标,对于满足置信度满足要求的目标还需要通过非极大值抑制(NMS)操作去除重复的目标。
objects = []
for row in detections:prob = row[4:84].max()if prob < 0.5:continueclass_id = row[4:84].argmax()label = COCO_CLASSES[class_id]xc, yc, w, h = row[:4]// 把x1, y1, x2, y2的坐标恢复到原始图像坐标x1 = (xc - w / 2) / model_width * image_widthy1 = (yc - h / 2) / model_height * image_heightx2 = (xc + w / 2) / model_width * image_widthy2 = (yc + h / 2) / model_height * image_height// 获取实例分割maskmask = get_mask(row[84:25684], (x1, y1, x2, y2), image_width, image_height)// 从mask中提取轮廓polygon = get_polygon(mask, x1, y1)objects.append([x1, y1, x2, y2, label, prob, polygon, mask])// NMS
objects.sort(key=lambda x: x[5], reverse=True)
results = []
while len(objects) > 0:results.append(objects[0])objects = [object for object in objects if iou(object, objects[0]) < 0.5]
这里重点讲一下获取实例分割掩码的过程。
前面说了每个目标对应的实例分割掩码数据大小为160x160,但是这个尺寸是对应整幅图的掩码。对于单个目标来说,还要从这个160x160的掩码中去截取属于自己的掩码,截取的范围由目标的box决定。上面的代码得到的box是相对于原始图像大小,截取掩码的时候需要把box的坐标转换到相对于160x160的大小,截取完后再把这个掩码的尺寸调整回相对于原始图像大小。截取到box大小的数据后,还需要对数据做sigmoid操作把数值变换到0到1的范围内,也就是求这个box范围内的每个像素属于这个目标的置信度。最后通过阈值操作,置信度大于0.5的像素被当做目标,否则被认为是背景。
具体实现的代码如下:
def get_mask(row, box, img_width, img_height):mask = row.reshape(160, 160)x1, y1, x2, y2 = box// box坐标是相对于原始图像大小,需转换到相对于160*160的大小mask_x1 = round(x1 / img_width * 160)mask_y1 = round(y1 / img_height * 160)mask_x2 = round(x2 / img_width * 160)mask_y2 = round(y2 / img_height * 160)mask = mask[mask_y1:mask_y2, mask_x1:mask_x2]mask = sigmoid(mask)// 把mask的尺寸调整到相对于原始图像大小mask = cv2.resize(mask, (round(x2 - x1), round(y2 - y1)))mask = (mask > 0.5).astype("uint8") * 255return mask
这里需要注意的是,160x160是相对于模型输入尺寸为640x640来的,如果模型输入是其他尺寸,那么上面的代码需要做相应的调整。
如果需要检测的是下面这个图片:

通过上面的代码可以得到最左边那个人的分割掩码为

但是我们需要的并不是这样一张图片,而是需要用于表示这个目标的轮廓,这可以通过OpenCV的findContours函数来实现。findContours函数返回的是一个用于表示该目标的点集,然后我们可以在原始图像中用fillPoly函数画出该目标的分割结果。

全部目标的检测与分割结果如下:

3. 一点其他的想法
从前面的部署过程可以知道,做后处理的时候需要对实例分割的数据做矩阵乘法、sigmoid激活、维度变换等操作,实际上这些操作也可以在导出模型的时候集成到onnx模型中去,这样就可以简化后处理操作。
首先需要修改ultralytics代码仓库中ultralytics/nn/modules/head.py文件的代码,把Segment类Forward函数最后的代码修改为:
if self.export:output1 = p.reshape(p.shape[0], p.shape[1], -1)boxes = x.permute(0, 2, 1)masks = torch.sigmoid(mc.permute(0, 2, 1) @ output1)out = torch.cat([boxes, masks], dim=2)return out
else:return (torch.cat([x[0], mc], 1), (x[1], mc, p))
然后修改ultralytics/engine/exporter.py文件中torch.onnx.export的参数,把模型的输出数量改为1个。

代码修改完成后,执行命令pip install -e '.[dev]'使之生效,然后再重新用yolo命令导出模型。用netron工具可以看到模型只有一个shape为[1,8400,25684]的输出。

这样在后处理的时候就可以直接去解析box和mask了,并且mask的数据不需要进行sigmoid激活。
4. 参考资料
- How to implement instance segmentation using YOLOv8 neural network
- https://github.com/AndreyGermanov/yolov8_segmentation_python
相关文章:
AI模型部署 | onnxruntime部署YOLOv8分割模型详细教程
本文首发于公众号【DeepDriving】,欢迎关注。 0. 引言 我之前写的文章《基于YOLOv8分割模型实现垃圾识别》介绍了如何使用YOLOv8分割模型来实现垃圾识别,主要是介绍如何用自定义的数据集来训练YOLOv8分割模型。那么训练好的模型该如何部署呢?…...
模拟电路学习笔记(一)之芯片篇(持续更新)
模拟电路学习笔记(一)之芯片篇(持续更新) 1.CD4047BE芯片 CD4047是一种包含高电压的多谐振荡器,该器件的操作可以在两种模式下完成,分别是单稳态和非稳态。CD4047需要一个外部电阻器和电容器来决定单稳态…...
如何利用CentOS7+docker+jenkins+gitee部署springboot+vue前后端项目(保姆教程)
博主介绍:Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 🍅文末获取源码下载地址🍅 👇🏻 精彩专栏推荐订阅👇🏻…...
qt 5.15.2 主窗体事件及绘制功能
qt 5.15.2 主窗体事件及绘制功能 显示主窗体效果图如下所示: main.cpp #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;w.setFixedWidth(600);w.setFixedHeight(6…...
(2)(2.4) TerraRanger Tower/Tower EVO(360度)
文章目录 前言 1 安装传感器并连接 2 通过地面站进行配置 3 参数说明 前言 TeraRanger Tower 可用于在 Loiter 和 AltHold 模式下进行目标规避。传感器的最大可用距离约为 4.5m。 TeraRanger Tower EVO 可用于在 Loiter 和 AltHold 模式下进行目标规避。传感器的最大可用…...
Redis_主从复制、哨兵模式、集群模式详解
Redis的主从复制 为什么Redis要引入主从复制?what? 在这里博主为小伙伴们简单的做下解释,可以了解一下 实际生产环境下,单机的redis服务器是无法满足实际的生产需求的。 第一,单机的redis服务器很容易发生单点故障&am…...
关于神舟-战神TA5NS系统重装问题
加装固态卡在log处无法开机问题 下面是我的步骤 1.按f7选择pe安装系统,然后发现卡在战神log处不转动 2.下载驱动 TA5NS驱动地址 下载RAID驱动(如果没有私信我,我网盘里有),拷到u盘中,然后进入pe系统里面…...
前端大文件上传webuploader(react + umi)
使用WebUploader还可以批量上传文件、支持缩略图等等众多参数选项可设置,以及多个事件方法可调用,你可以随心所欲的定制你要的上传组件。 分片上传 1.什么是分片上传 分片上传,就是将所要上传的文件,按照一定的大小,将…...
人大金仓(kingbase)数据库常用sql命令
一. 字段 1. 添加 alter table book add column book_id varchar not null, book_title varchar(10) default ;2. 删除 alter table book drop book_id, book_title;// 外键时 alter table book drop book_id, book_title cascade;3. 修改类型 alter table book alter colu…...
HashMap相关专题
前置知识:异或运算 异或运算介绍 异或有什么神奇之处(应用)? (1)快速比较两个值 (2)我们可以使用异或来使某些特定的位翻转,因为不管是0或者是1与1做异或将得到原值的相…...
threejs WebGLRenderer 像素比对画布大小的影响
官方文档 - WebGLRenderer .setPixelRatio ( value : number ) : undefined 设置设备像素比。通常用于避免HiDPI设备上绘图模糊 .setSize ( width : Integer, height : Integer, updateStyle : Boolean ) : undefined 将输出canvas的大小调整为(width, height)并考虑设备像素比…...
RocketMQTemplate.send() 与 RocketMQTemplate.syncSend() 方法详解
Apache RocketMQ 是一款强大的分布式消息中间件,与 Spring Boot 集成后,通过 RocketMQTemplate 提供了多种方法来发送消息。其中,send() 和 syncSend() 是两个常用的发送消息方法,本文将深入探讨它们的区别以及详细解释这两个方法…...
波奇学C++:类型转换和IO流
隐式类型转换 int i0; double pi; 强制类型转换 int* pnullptr; int a(int)p; 单参数构造函数支持隐式类型转换 class A { public:A(string a):_a(a){} private:string _a; }; A a("xxxx"); //"xxx" const char* 隐式转换为string 多参数也可以通过{…...
集成开发环境 PyCharm 的安装【侯小啾python基础领航计划 系列(二)】
集成开发环境PyCharm的安装【侯小啾python基础领航计划 系列(二)】 大家好,我是博主侯小啾, 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔…...
Java核心知识点整理大全27-笔记(已完结)
目录 30. 云计算 30.1.1. SaaS 30.1.2. PaaS 30.1.3. IaaS 30.1.4. Docker 30.1.4.1. 概念 30.1.4.2. Namespaces 30.1.4.3. 进程(CLONE_NEWPID 实现的进程隔离) 30.1.4.4. Libnetwork 与网络隔离 30.1.4.5. 资源隔离与 CGroups 30.1.4.6. 镜像与 UnionFS 30.1.4.7.…...
1. 使用poll或epoll创建echo服务器
1. 说明: 此篇博客主要记录一种客户端实现方式,和两种使用poll或者epoll分别创建echo服务器的方式,具体可看代码注释: 2. 相关代码: 2.1 echoClient.cpp #include <iostream> #include <cstdio> #incl…...
【对象数组根据属性排序】
// sort使用的排序方法 // 传入对象数组用于排序的对象的属性,升序/降序 function compare(property, sortType "asc") {debugger// 如果不是 asc,desc,不做下一步比较if (!(sortType "desc" || sortType "asc")) {return;}return function (…...
BACnet I/O模块:楼宇自动化的未来选择
在楼宇自动化领域,BACnet通信协议在确保设备之间无缝高效的数据交换方面发挥着至关重要的作用。该领域使用广泛的协议是BACnet。它使传感器、执行器和控制器等设备能够相互通信,从而促进工业过程的自动化。 BACNET介绍 BACnet是专门为楼宇自动化和控制系…...
android项目实战之使用框架 集成多图片、视频的上传
效果图 实现方式,本功能使用PictureSelector 第三方库 。作者项目地址:https://github.com/LuckSiege/PictureSelector 1. builder.gradle 增加 implementation io.github.lucksiege:pictureselector:v3.11.1implementation com.tbruyelle.rxpermissio…...
MyBatis查询优化:枚举在条件构建中的妙用
🚀 作者主页: 有来技术 🔥 开源项目: youlai-mall 🍃 vue3-element-admin 🍃 youlai-boot 🌺 仓库主页: Gitee 💫 Github 💫 GitCode 💖 欢迎点赞…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...
抽象类和接口(全)
一、抽象类 1.概念:如果⼀个类中没有包含⾜够的信息来描绘⼀个具体的对象,这样的类就是抽象类。 像是没有实际⼯作的⽅法,我们可以把它设计成⼀个抽象⽅法,包含抽象⽅法的类我们称为抽象类。 2.语法 在Java中,⼀个类如果被 abs…...
MySQL体系架构解析(三):MySQL目录与启动配置全解析
MySQL中的目录和文件 bin目录 在 MySQL 的安装目录下有一个特别重要的 bin 目录,这个目录下存放着许多可执行文件。与其他系统的可执行文件类似,这些可执行文件都是与服务器和客户端程序相关的。 启动MySQL服务器程序 在 UNIX 系统中,用…...
「Java基本语法」变量的使用
变量定义 变量是程序中存储数据的容器,用于保存可变的数据值。在Java中,变量必须先声明后使用,声明时需指定变量的数据类型和变量名。 语法 数据类型 变量名 [ 初始值]; 示例:声明与初始化 public class VariableDemo {publi…...
python打卡day47
昨天代码中注意力热图的部分顺移至今天 知识点回顾: 热力图 作业:对比不同卷积层热图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import D…...
.Net Framework 4/C# 面向对象编程进阶
一、继承 (一)使用继承 子类可以继承父类原有的属性和方法,也可以增加原来父类不具备的属性和方法,或者直接重写父类中的某些方法。 C# 中使用“:”来表示两个类的继承。子类不能访问父类的私有成员,但是可以访问其公有成员,即只要使用 public 声明类成员,就既可以让一…...
《架构即未来》笔记
思维导图 第一部分:可扩展性组织的人员配置 第二部分:构建可扩展的过程 第三部分:可扩展的架构方案 第四部分:其他的问题和挑战 资料 问软件工程研究所: https://www.sei.cmu.edu/ AKF公司博客: http://www.akfpart…...
