yolov8实战之torchserve服务化:使用yolov8x来预打标

前言

最近在做一个目标检测的任务,部署在边缘侧,对于模型的速度要求比较严格(yolov8n这种),所以模型的大小不能弄太大,所以原模型的性能受限,更多的重点放在增加数据上。实测yolov8x在数据集上的效果比小模型要好不少,所以想法是用yolov8x来预打标,然后选择一些置信度高的样本加到训练集来训练yolov8n,减少标注的成本。原始数据是在ceph上,比较直观的方式就是一张张读,然后一张张推理。这样效率不高,毕竟GPU适合组batch去推理,所以为了效率就需要自己去组成batch然后推理,然后再把batch的结果再分开对应到单张图上,虽然并不难,还是挺繁琐的,这也是我以前的做法。其实可以不需要这么麻烦,这种batching操作很多的推理服务都会帮我们做掉,我们只需要并发去请求就好了,和做单个没什么区别,要加其他的模型进行组合逻辑也是非常方便。GroundingDINO(一种开集目标检测算法)服务化,根据文本生成检测框_CodingInCV的博客-CSDN博客这个里面我们使用torchserve来实现了算法的服务化,这里我们依旧还是使用torchserve。基础就不做介绍了,可以读上面这篇。与GroundingDINO不同的是,这里我们会启用batch操作,而GroudingDINO里没有支持batch。

导出onnx模型

为了方便起见,我们使用onnx模型,避免去处理yolov8的pytorch环境问题,官网提供了导出的方式:Detect - Ultralytics YOLOv8 Docs
为了支持动态的batch, 我们导出时要以dynamic的方式导出,我这里对导出做了一点修改,只让batch为动态,而输入尺寸固定, 修改engine/exporter.py:
image.png
为了支持我们新增的dynamic_batch参数,我们还需要再default.yaml中增加这个参数,具体可以参考:yolov8训练进阶:新增配置参数_CodingInCV的博客-CSDN博客
image.png
然后自行写脚本转换:

from ultralytics import YOLO

model = YOLO('yolov8x6404/weights/last.pt')  # initialize
model.export(format = "onnx", opset = 11, simplify = True,
             dynamic_batch=True, imgsz=640)  # export to onnx

导出的模型将和输入的模型在同一个路径。

自定义handler

handler的写法

在GroundingDINO(一种开集目标检测算法)服务化,根据文本生成检测框_CodingInCV的博客-CSDN博客我们没有提到怎么写自己的模型handler,所谓模型handler就是告诉torchserve我们的模型如何载入、前处理和后处理。官方教程:Custom Service — PyTorch/Serve master documentation
torchserve自身带了一些handler:
BaseHandler: handler的基类,我们可以继承这个,也可以不继承,如果不继承则至少要实现initializehandle方法。
image.png

我们可以继承他们来实现自己的,也可以不继承,这里以不继承来实现,通用性比较强,不管什么模型都可以搞定,主要就是实现一个类,这个类至少要实现initializehandle方法:
initialize 就是初始化模型,这个方法必须有一个输入参数context(serve/ts/context.py at master · pytorch/serve (github.com)), 从这个参数我们可以拿到比如模型的路径、显卡号等信息。
handle 是接收输入请求和返回处理结果的接口,具有2个参数,第一个参数是输入请求,第二个参数也是context。
对于每个模型我们可以将推理过程拆分为三个过程(方法):preprocess、inference、postprocess,即前处理、推理、后处理,我们的handler只要实现这三个方法,然后依次在handle中调用即可,最后把输出按要求组合起来,handle的返回值必须是list of list,也就是数组的数组,外层list的长度等于输入的batch数(torchserve可以自动组batch),内层的list是单个请求的输出,里面的元素可以是dict,完整代码如下:

import logging
import os,sys
import onnxruntime as ort
import base64
import numpy as np
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
    from common.common import resize_image
except:
    from common import resize_image
import cv2

logger = logging.getLogger(__name__)
console_logger = logging.StreamHandler(sys.stdout)
console_logger.setLevel(logging.DEBUG)
console_logger.setFormatter(logging.Formatter("%(asctime)s %(name)s [%(levelname)s] %(message)s"))
logger.addHandler(console_logger)

class YOLOV8Handler(object):
    def __init__(self):
        self.context = None
        self.initialized = False
        self.model = None
        self.input_name = None
        self.input_shape = None
        self.conf_thres = 0.45
        self.iou_thres = 0.45
        self.class2label = {
            0: "body",
            1: "head",
        }
        self.device = None

    def initialize(self, context):
        #  load the model
        logger.info("initialize grounding dino handler")
        self.context = context
        self.manifest = context.manifest
        properties = context.system_properties
        model_dir = properties.get("model_dir")

        # Read model serialize/pt file
        serialized_file = self.manifest['model']['serializedFile']
        model_pt_path = os.path.join(model_dir, serialized_file)
        if not os.path.isfile(model_pt_path):
            raise RuntimeError("Missing the model file")
        
        # get device

        available_providers =  ort.get_available_providers()
        provide_options = {}
        if "CUDAExecutionProvider" in available_providers:
            self.device = str(properties.get("gpu_id"))
            provide_options["device_id"] = self.device
            privider = "CUDAExecutionProvider"
            logger.info("using gpu {}".format(self.device))
        else:
            privider = "CPUExecutionProvider"
        
        self.model = ort.InferenceSession(model_pt_path, providers=[privider], provider_options=[provide_options])
        self.initialized = True
        # get input shape
        self.input_name = self.model.get_inputs()[0].name
        self.input_shape = self.model.get_inputs()[0].shape
        logger.info("model loaded successfully")

    def preprocess(self, data):
        logger.info("preprocess data")
        preprocessed_data = []
        preprocessed_params = []
        network_input_height = self.input_shape[2]
        network_input_width = self.input_shape[3]
        for row in data:
            input = row.get("data") or row.get("body")
            if isinstance(input, dict) and "image" in input:
                image = input["image"]
            else:
                logger.error("No image found in the request")
                assert False, "No  image found in the request"
            if isinstance(image, str):
                # if the image is a string of bytesarray.
                image = base64.b64decode(image)
            # If the image is sent as bytesarray
            if isinstance(image, (bytearray, bytes)):
                image = cv2.imdecode(np.frombuffer(image, dtype=np.uint8), cv2.IMREAD_ANYCOLOR)
            else:
                logger.error("No caption or image found in the request")
                assert False, "No caption or image found in the request"
            
            image_h, image_w, _ = image.shape
            image, newh, neww, top, left  = resize_image(image, keep_ratio=True, dst_width=network_input_width, dst_height=network_input_height)
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            preprocessed_data.append(image)
            preprocessed_params.append((newh, neww, top, left, image_h, image_w))
        logger.info("preprocess data done")
        preprocessed_data = np.array(preprocessed_data).astype(np.float32)
        preprocessed_data /= 255.0
        preprocessed_data = np.transpose(preprocessed_data, (0, 3, 1, 2))
        return preprocessed_data, preprocessed_params
    
    def inference(self, data, *args, **kwargs):
        logger.info("inference data")
        outputs = self.model.run(None, {self.input_name: data})
        return outputs[0]
    
    def postprocess_one(self, output, request_param):
        newh, neww, top, left, image_h, image_w = request_param
        x_factor = image_w / neww
        y_factor = image_h / newh
        outputs = output
        outputs = np.transpose(np.squeeze(outputs))
        boxes = {}
        for row in outputs:
            classes_scores = row[4:]
            max_score = np.max(classes_scores)
            if max_score < self.conf_thres:
                continue
            class_id = np.argmax(classes_scores)
            x, y, w, h = row[0], row[1], row[2], row[3]

            # Calculate the scaled coordinates of the bounding box
            x1 = x - w / 2
            y1 = y - h / 2
            x1 = x1-left
            y1 = y1-top
            x2 = x1 + w
            y2 = y1 + h

            # Scale the coordinates according to the original image
            x1 = x1 * x_factor
            y1 = y1 * y_factor
            x2 = x2 * x_factor
            y2 = y2 * y_factor
            if class_id not in boxes:
                boxes[class_id] = [[],[]]
            boxes[class_id][0].append([x1, y1, x2, y2])
            boxes[class_id][1].append(float(max_score))
        
        # NMS
        nms_boxes = []
        for class_id in boxes:
            candidate_boxes, scores = boxes[class_id]
            indices = cv2.dnn.NMSBoxes(candidate_boxes, scores, self.conf_thres, self.iou_thres)
            for index in indices:
                nms_boxes.append((candidate_boxes[index], scores[index], self.class2label[class_id]))
        return nms_boxes

    def postprocess(self, data):
        outputs, request_params = data
        boxes = []
        for i in range(len(outputs)):
            output = outputs[i]
            request_param = request_params[i]
            nms_boxes = self.postprocess_one(output, request_param)
            boxes.append(nms_boxes)

        return boxes
    
    def handle(self, data, context):
        self.context = context
        image, request_params = self.preprocess(data)
        outputs = self.inference(image)
        boxes_batch = self.postprocess((outputs, request_params))
        results = []
        for boxes in boxes_batch:
            ret = []
            for box, score, label in boxes:
                ret.append({"box": box, "score": score, "label": label})
            results.append(ret)
        return results

注意:为了实现batch操作,我们实现的接口都应该是对batch来的,而不是只对一张图。

调试handler

我们可以模仿context的内容来初始化handler, 然后调用handle方法来调试结果是否正常。

if __name__=="__main__":
    import addict
    context = addict.Dict()
    context.system_properties = {
        "gpu_id": 0,
        "model_dir": "./weights"

    }
    context.manifest = {
        "model": {
            "serializedFile": "yolov8x.onnx"
        }
        }
    handler = YOLOV8Handler()
    handler.initialize(context)
    image_path = "./body.png"
    with open(image_path, "rb") as f:
        image = f.read()

    data = [
        {
            "data": {
                "image": image
            }
        },
        {
            "data": {
                "image": image
            }
        }
    ]

    outputs = handler.handle(data, context)
    print(outputs)

镜像制作

在GroundingDINO(一种开集目标检测算法)服务化,根据文本生成检测框_CodingInCV的博客-CSDN博客中镜像的基础上安装onnxruntime-gpu, 或者在启动时安装

转换模型

这个操作和上一篇文章一样,只是权重文件和需要handler修改一下,不赘述:

docker run --rm -it -v $(pwd):/data -w /data torchserve:groundingdino bash -c "torch-model-archiver --model-name yolov8x --version 1.0 --serialized-file weights/yolov8x.onnx --handler yolov8/yolov8_handler.py --extra-files common/*.py"

启动服务

与上一篇服务化不同,我们启动时不载入所有模型,而是通过post接口去开启,方便设置模型的batch size, 其中端口号根据需要设置。

docker run -d --name groundingdino -v $(pwd)/model_store:/model_store -p 8080:8080 -p 8081:8081 -p 8082:8082 torchserve:groundingdino bash -c "pip install onnxruntime-gpu && torchserve --start --foreground --model-store /model_store

使用Management API载入模型

Management API — PyTorch/Serve master documentation
可以用curl也可以用postman, 如

curl -X POST "localhost:8081/models?url=yolov8x.mar&batch_size=8&max_batch_delay=50"

如果需要再修改batchsize, 要先调用卸载模型的接口写在,然后再调用上面的接口。
通过上面的操作,torchserve会帮我们组batch, 最大为8.

调用

import json
import base64
import requests
import threadpool

url = "http://localhost:8080/predictions/yolov8x"
headers = {"Content-Type": "application/json"}

def request_worker(arg):
    image_path = "./b03492798d5b44eeb70856b9253386df.jpeg"
    data = {
        "image": base64.b64encode(open(image_path, "rb").read()).decode("utf-8")
    }

    response = requests.post(url, headers=headers, json=data)
    print(response.text)

if __name__ == "__main__":
    pool = threadpool.ThreadPool(24)
    requests_task = threadpool.makeRequests(request_worker, range(100))
    [pool.putRequest(req) for req in requests_task]
    pool.wait()

这里,我们用多线程模仿了高并发的去调用模型,这样torchserve就可以自动的根据负载情况来组成batch了,提高模型的吞吐量。类似的,我们就可以方便的使用多线程去读取数据然后调用模型来得到预打标的结果,而不用去处理模型的依赖、组batch等逻辑,也可以很方便的提供给其他需要的同事来使用。

结语

本文简述了将yolov8服务化的过程,服务化后,我们可以方便的用模型来进行数据的预打标、分享模型给他人使用。
f77d79a3b79d6d9849231e64c8e1cdfa~tplv-dy-resize-origshort-autoq-75_330.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/92188.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C/C++】父类指针指向子类对象 | 隐藏

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

(四)CUDA应用程序编程接口详解

C语言扩展 CUDA的编程接口是C语言的扩展集&#xff0c;其中主要的是Runtime库&#xff0c;该库分为三个组件&#xff1a;主机组件、设备组件以及公共组件 主机组件&#xff1a;在主机上运行并提供函数来控制和访问一个或多个计算设备 设备组件&#xff1a;设备运行并且提供特…

考研C语言进阶题库——更新41-50题

目录 41.编写程序要求输出整数a和b若a和b的平方和大于100&#xff0c;则输出a和b的平方和&#xff0c;否则输出a和b的和 42.现代数学的著名证明之一是Georg Cantor证明了有理数是可枚举的。他是用下面这一张表来证明这一命题的&#xff1a;第一项是1/1&#xff0c;第二项是是…

unity-AI自动导航

unity-AI自动导航 给人物导航 一.地形创建 1.首先我们在Hierarchy面板中创建一个地形对象terrian&#xff0c;自行设定地形外貌&#xff0c;此时我们设置一个如下的地形外观。 二.创建导航系统 1.在主人公的Inspector、面板中添加Nav Mesh Agent &#xff08;导航网格代理&…

SQLSTATE[IMSSP]: The active result for the query contains no fields.

我的是SQL server 报错场景&#xff0c;代码&#xff1a; $psendmx_sql"SET IDENTITY_INSERT PSENDMX ON;INSERT INTO psendmx (DJBH,MIBH,MXBH,SPDM,GG1DM,GG2DM,SL,SL_2,CKJ,ZK,DJ,DJ_1,JE,HH) VALUES {$mx_values};SET IDENTITY_INSERT PSENDMX OFF;"; $a$db_er…

mysql数据库root密码遗忘后,修改root密码

目录 方式一&#xff1a; 方式二&#xff1a; 2.1 也可以像我这样&#xff0c;普通用户登录进去后 2.2 执行如下命令&#xff0c;将已知的user1的加密密文更新到root中 2.3 查询数据库 2.4 用root用户登录 2.5 登录正常&#xff0c;但这会root登录进去后&#xff0c;无法…

【Terraform学习】使用 Terraform 托管 S3 静态网站(Terraform-AWS最佳实战学习)

使用 Terraform 托管 S3 静态网站 实验步骤 前提条件 安装 Terraform&#xff1a; 地址 下载仓库代码模版 本实验代码位于 task_s3 文件夹中。 变量文件 variables.tf 在上面的代码中&#xff0c;您将声明&#xff0c;aws_access_key&#xff0c;aws_secret_key和区域变量…

Web自动化测试之图文验证码的解决方案

对于web应用程序来讲&#xff0c;处于安全性考虑&#xff0c;在登录的时候&#xff0c;都会设置验证码&#xff0c; 验证码的类型种类繁多&#xff0c;有图片中辨别数字字母的&#xff0c;有点击图片中指定的文字的&#xff0c;也有算术计算结果的&#xff0c;再复杂一点就是滑…

小研究 - Java虚拟机垃圾收集器的性能分析与调节

垃圾收集器是&#xff2a;&#xff41;&#xff56;&#xff41;虚拟机&#xff08;&#xff2a;&#xff36;&#xff2d;&#xff09;的核心组成部分之一&#xff0c;对&#xff2a;&#xff41;&#xff56;&#xff41;虚拟机的性能有非常重要的影响。本文将介绍&#xff2…

C语言练习5(巩固提升)

C语言练习5 选择题 选择题 1&#xff0c;下面代码的结果是&#xff1a;( ) #include <stdio.h> #include <string.h> int main() {char arr[] { b, i, t };printf("%d\n", strlen(arr));return 0; }A.3 B.4 C.随机值 D.5 &#x1f4af;答案解析&#…

python3/pip3 SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

环境&#xff1a; mac os 背景&#xff1a; 电脑之前安装的是python3.9 &#xff0c; 现在升级到python3.10。 从python官网下载macos版本的python3.10 pkg。 双击安装。 程序使用aiohttp访问ebay 。 出错&#xff1a; aiohttp.client_exceptions.ClientConnectorCertifi…

SpringBoot入门篇2 - 配置文件格式、多环境开发、配置文件分类

目录 1.配置文件格式&#xff08;3种&#xff09; 例&#xff1a;修改服务器端口。&#xff08;3种&#xff09; src/main/resources/application.properties server.port80 src/main/resources/application.yml&#xff08;主要用这种&#xff09; server:port: 80 src/m…

STTran: Spatial-Temporal Transformer for Dynamic Scene Graph Generation

文章目录 0 Abstract1 Introduction2 Related Work3 Method3.1 Transformer3.2 Relationship Representation3.3 Spatio-Temporal Transformer3.3.1 Spatial Encoder3.3.2 Frame Encoding3.3.3 Temporal Decoder 3.4 Loss Function3.5 Graph Generation Strategies 4 Experimen…

C++的静态栈以及有点鸡肋的array数组

目录 1.静态栈 1.举例展示 2.注意事项 2.array 1.静态栈 1.举例展示 1.我们想到栈&#xff0c;就会想到是一个数组来维护它的&#xff0c;并且一般由于不知道存储的多少内容&#xff0c;所以一般都是用动态数组不断的在堆上开辟新的空间。 但是C支持了一个新的语法就是静…

2. 两数相加(中等系列)

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 …

芯讯通SIMCOM A7680C (4G Cat.1)AT指令测试 TCP通信过程

A7680C TCP通信 1、文档准备 去SIMCOM官网找到A7680C的AT指令集 AT指令官网 进入官网有这么多AT指令文件&#xff0c;只需要找到你需要用到的&#xff0c;这里我们用到了HTTP和TCP的&#xff0c;所以下载这两个即可。 2、串口助手 任意准备一个串口助手即可 这里我使用的是XC…

浏览器的事件循环

其实在我们电脑的操作系统中&#xff0c;每一个运行的程序都会由自己的进程&#xff08;可能是一个&#xff0c;也可能有多个&#xff09;&#xff0c;浏览器就是一个程序&#xff0c;它的运行在操作系统中&#xff0c;拥有一组自己的进程&#xff08;主进程&#xff0c;渲染进…

【springboot】Spring Cache缓存:

文章目录 一、导入Maven依赖&#xff1a;二、实现思路&#xff1a;三、代码开发&#xff1a; 一、导入Maven依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId><…

Nacos集群

需要与Nginx配合。 这是使用三个Nacos来搭建集群。 创建mysql数据库nacos。 配置Nacos 进入nacos的conf目录&#xff0c;修改配置文件cluster.conf.example&#xff0c;重命名为cluster.conf。 在cluster.conf文件的最后加上&#xff1a; #it is ip #example 127.0.0.1:8…

postman接口参数化设置

为什么需要参数化&#xff1f; 我们在做接口测试的过程中&#xff0c;会遇到需要测试同一个接口使用不同的数据的情况&#xff0c;如果每次去一个个填写数据就太麻烦了&#xff0c;这时我们就需要用到接口参数化&#xff0c;我们把数据单独的存放在一个文件中管理&#xff0c;…