Yolov8 Detect 在 RK3588上的推理实现

目录

1. Yolov5 和 Yolov8 差异简介

2. Yolov8 pt 转 onnx 模型拆解思路

3. onnx 转 rknn 后推理代码

4. 推理结果


1. Yolov5 和 Yolov8 差异简介

        Yolov8 相较于  Yolov5最大的区别就是不再有之前的 objectness 分支,只有解耦的分类和bbox回归分支,如下图所示。

        YOLOv5 输出特征图尺度为 80x80、40x40 和 20x20 的三个特征图,将三个不同尺度的类别预测分支、bbox 预测分支和 obj 预测分支(对象置信度预测分支)进行拼接,并进行维度变换。为了后续方便处理,会将原先的通道维度置换到最后,类别预测分支、bbox 预测分支和 obj 预测分支的 shape 分别为 (b, 3x80x80+3x40x40+3x20x20, 80)=(b,25200,80),(b,25200,4),(b,25200,1)。

        YOLOv8 输出特征图尺度为 80x80、40x40 和 20x20 的三个特征图。 Head 部分输出分类和回归共 6 个尺度的特征图。将 3 个不同尺度的类别预测分支、bbox 预测分支进行拼接,并进行维度变换。为了后续方便处理,会将原先的通道维度置换到最后,类别预测分支 和 bbox 预测分支 shape 分别为 (b, 80x80+40x40+20x20, 80)=(b,8400,80),(b,8400,4)。

        推理过程如下:

YOLOv5 推理流程

YOLOv8 推理流程

2. Yolov8 pt 转 onnx 模型拆解思路

        由于原生 YOLOv8 模型转 onnx 模型后,bounding box 的解码包含在 onnx 模型当中,使用 rk3588 的推理效率不高,所以将 decode 部分拆解出来,放在 cpu 上运行。同时类别输出和框输出的数值范围不一致,量化后可能导致精度的下降。

        YOLOv8 检测模型输出的 shape 是1x84x8400,为三个特征图的分类和回归分支,故可以将原始输出拆分为6个输出:3对分类和3对回归。

        拆解前后 onnx 输出对比图如下:

修改前 ONNX 输出

修改后 RKNN 输出

        需要注意的是,在修改模型时需要使用 利用 Softmax 和 Conv 对 Distribution Focal Loss 中的积分表示 bbox 形式进行解码,变成常规的 4 维度 bbox。

3. onnx 转 rknn 后推理代码

以 reg1 为例:1x1x4x6400 包含了目标边界框相对于网格中心的偏移量。

以 cls1 为例:1x80x80x80 代表了80个类的置信度,需要通过softmax将其转化成概率分布。

        因此需要遍历每个头,得到每个头、每个网格上、每个类的置信度,然后进行阈值过滤,只保留阈值超过规定的网格,再进行坐标变换,将reg1格式的 bbox 输出转化为实际原图上的坐标,最后对保留的 box 框进行 box nms 后,得到最终输出结果。

        rk3588上推理代码如下:

import cv2
import time
import numpy as np
from rknnlite.api import RKNNLite
from math import exp

CLASSES = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
           'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
           'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
           'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
           'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
           'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
           'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
           'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
           'hair drier', 'toothbrush']

class_num = len(CLASSES)
meshgrid = []
headNum = 3                                 # 检测头个数
mapSize = [[80, 80], [40, 40], [20, 20]]    # 网格大小
strides = [8, 16, 32]                       # 每层特征图的步长,表示该特征图上的每个像素在原始图像中对应的像素距离。它用于将特征图上的坐标偏移转换为原始图像上的偏移。
object_threshold = 0.25                     # 置信度阈值
nms_threshold = 0.45                        # nms阈值


def sigmoid(x):
    return 1 / (1 + exp(-x))


def GenerateMeshgrid():
    """
        生成每个特征图上的网格中心坐标
        meshgrid = [
            x0, y0, x1, y1, ..., x6399, y6399,  # 第一层特征图 (80x80)
            x0, y0, x1, y1, ..., x1599, y1599,  # 第二层特征图 (40x40)
            x0, y0, x1, y1, ..., x399, y399     # 第三层特征图 (20x20)
        ]
    """
    for index in range(headNum):
        for i in range(mapSize[index][0]):
            for j in range(mapSize[index][1]):
                meshgrid.append(j + 0.5)        # 加 0.5 是为了将坐标移到每个单元格的中心位置
                meshgrid.append(i + 0.5)



def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True, stride=32):
    # Resize and pad image while meeting stride-multiple constraints
    shape = im.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better val mAP)
        r = min(r, 1.0)

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding

    if auto:  # 当 auto=True 时,函数会调整填充的大小,使得最终的宽高仍符合 stride 的整数倍。这样做的目的是确保图像尺寸与网络的步长匹配,但结果可能会导致图像的最终尺寸不完全是 (640, 640)。
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh padding
    elif scaleFill:  # 当 scaleFill=True 时,图像会被直接拉伸(不保持宽高比)到目标尺寸 (640, 640)
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]  # width, height ratios

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        im = cv2.resize(im, 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))
    im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return im, ratio, (dw, dh)


def box_iou(x_min1, y_min1, x_max1, y_max1, x_min2, y_min2, x_max2, y_max2, eps=1e-7):
    """
        两个 box 框的交集除以并集,iou 越大代表两个框重叠度越高
        args:
            eps: 预防除0的情况
    """
    
    # 交集
    x_min = max(x_min1, x_min2)
    y_min = max(y_min1, y_min2)
    x_max = min(x_max1, x_max2)
    y_max = min(y_max1, y_max2)
    
    # 交集面积
    iter_width = max(x_max - x_min, 0)
    iter_height = max(y_max - y_min, 0)
    iter_area = iter_width * iter_height
    
    # 并集面积
    union_area = (x_max1 - x_min1) * (y_max1 - y_min1) + (x_max2 - x_min2) * (y_max2 - y_min2)
    
    # 交并比
    return iter_area / (union_area - iter_area + eps)


def nms_boxes(boxes):
    """
        执行nms来过滤检测框
        box格式: [class, score, xmin, ymin, xmax, ymax]
    """
    
    nms_boxes = []
    
    # 根据检测框置信度进行降序排序
    sorted_boxes = sorted(boxes, key=lambda x: x[1], reverse=True)

    for i in range(len(sorted_boxes)):
        x_min1 = sorted_boxes[i][2]
        y_min1 = sorted_boxes[i][3]
        x_max1 = sorted_boxes[i][4]
        y_max1 = sorted_boxes[i][5]
        class_id = sorted_boxes[i][0]
        
        # 检测框 id != -1 ,表示当前检测框未被删除(抑制)
        if sorted_boxes[i][0] != -1:
            nms_boxes.append(sorted_boxes[i])
            
            # 遍历剩余检测框
            for j in range(i+1, len(sorted_boxes), 1):
                # 剩余检测框中仅判断当前和当前classId相同
                if sorted_boxes[i][0] == sorted_boxes[j][0]:
                    x_min2 = sorted_boxes[j][2]
                    y_min2 = sorted_boxes[j][3]
                    x_max2 = sorted_boxes[j][4]
                    y_max2 = sorted_boxes[j][5]
                    
                    # 计算IOU
                    iou = box_iou(x_min1, y_min1, x_max1, y_max1, x_min2, y_min2, x_max2, y_max2)
                    # 如果 iou 大于阈值,则当前检测框box2和box1属于同一目标框,将classId赋值为-1,后续不必继续进行比较
                    if iou > nms_threshold:
                        sorted_boxes[j][0] = -1
    
    return nms_boxes


def postprocess(out, ori_img_h, ori_img_w, ratio, dw, dh):
    output = []
    detect_result = []
    
    for i in range(len(out)):
        output.append(out[i].reshape((-1)))
        
    gridIndex = -2
    
    for index in range(headNum):
        cls = output[index * 2 + 1]     # 第1、3、5个head的cls,即类别置信度
        reg = output[index * 2 + 0]     # 第0、2、4个head的reg,即回归框,它包含了目标边界框相对于网格中心的偏移量。一般情况下,回归预测会有 4 个值(左、上、右、下的偏移量)

        for h in range(mapSize[index][0]):
            for w in range(mapSize[index][1]):
                gridIndex += 2
                for cl in range(class_num):
                    cls_val = sigmoid(cls[cl * mapSize[index][0] * mapSize[index][1] + h * mapSize[index][1] + w])
                    if cls_val > object_threshold:
                        # x1, y1: 使用网格中心的 x, y 坐标减去左、上边界的回归偏移量,得到左上角坐标。
                        x1 = (meshgrid[gridIndex + 0] - reg[0 * mapSize[index][0] * mapSize[index][1] + h * mapSize[index][1] + w]) * strides[index]
                        y1 = (meshgrid[gridIndex + 1] - reg[1 * mapSize[index][0] * mapSize[index][1] + h * mapSize[index][1] + w]) * strides[index]
                        # x2, y2: 使用网格中心的 x, y 坐标加上右、下边界的回归偏移量,得到右下角坐标。
                        x2 = (meshgrid[gridIndex + 0] + reg[2 * mapSize[index][0] * mapSize[index][1] + h * mapSize[index][1] + w]) * strides[index]
                        y2 = (meshgrid[gridIndex + 1] + reg[3 * mapSize[index][0] * mapSize[index][1] + h * mapSize[index][1] + w]) * strides[index]
                        
                        # 复原到原图
                        x_min = (x1 - dw) / ratio[0]
                        y_min = (y1 - dh) / ratio[1]
                        x_max = (x2 - dw) / ratio[0]
                        y_max = (y2 - dh) / ratio[1]
                        # 将边界框限制在图片内
                        x_min = max(0, x_min)
                        y_min = max(0, y_min)
                        x_max = min(ori_img_w, x_max)
                        y_max = min(ori_img_h, y_max)
                        
                        # 组合成 box 数组:[类别,置信度,x1,y1,x2,y2]
                        box = [cl, cls_val, x_min, y_min, x_max, y_max]
                        detect_result.append(box)
    
    # nms
    pred_boxes = nms_boxes(detect_result)
    return pred_boxes


def rknn_infer(RKNNModulePath, data):

    rknn_lite = RKNNLite(verbose=False)
    
    ret = rknn_lite.load_rknn(path=RKNNModulePath)
    if ret != 0:
        print("load rknn model fialed!")
        exit(ret)

    ret = rknn_lite.init_runtime()
    if ret != 0:
        print("init runtime env fialed!")
        exit(ret)
        
        
    t1 = time.time()
    pred_rknn = rknn_lite.inference(inputs=[data], data_format="nchw")
    t2 = time.time()
    print(f"infer_time:{t2-t1}") 
    
    return pred_rknn


if __name__ == '__main__':
    
    GenerateMeshgrid()

    image_path = "./images/04.jpg"
    model_path = "./weights/yolov8s.rknn"
    imgsz = (640, 640)
    
    img = cv2.imread(image_path)
    print(img.shape)
    img_h, img_w = img.shape[:2]
    im, ratio, pad = letterbox(img, imgsz)
    im = im.transpose((2, 0, 1))[::-1]  # HWC to CHW, BGR to RGB
    im = np.ascontiguousarray(im) 
    im = im.astype(np.float32)
    im /= 255                           # 0 - 255 to 0.0 - 1.0
    if len(im.shape) == 3:
        im = im[None]                   # expand for batch dim
    
    
    pred = rknn_infer(model_path, im)
    t1 = time.time()
    pred_boxes = postprocess(pred, img_h, img_w, ratio, pad[0], pad[1])
    t2 = time.time()

    print(f"postprocess_time:{t2-t1}")
    
    for i in range(len(pred_boxes)):
        xmin = int(pred_boxes[i][2])
        ymin = int(pred_boxes[i][3])
        xmax = int(pred_boxes[i][4])
        ymax = int(pred_boxes[i][5])
        classId = pred_boxes[i][0]
        score = pred_boxes[i][1]

        cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2)
        ptext = (xmin, ymin)
        title = CLASSES[classId] + ":%.2f" % (score)
        cv2.putText(img, title, ptext, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2, cv2.LINE_AA)

    cv2.imwrite('./images/04_result.jpg', img)
    t3 = time.time()
    print(f"total_time:{t3-t1}")

4. 推理结果

没有量化的情况下,推理速度在0.1秒左右,后处理速度0.4秒左右,总时长0.55秒左右

### 实现YOLOv5 Face检测或识别的关键步骤 #### 准备环境和依赖项 为了在RK3588平台上成功运行YOLOv5进行人脸检测或识别,首先需要准备合适的开发环境。这包括安装必要的软件包和支持库。 对于OpenCV的特定版本(如4.5.5),可以通过交叉编译来适配ARM架构下的硬件平台[^1]。确保所使用的工具链支持aarch64架构,并且能够处理后续可能遇到的各种优化需求。 #### 获取预训练模型及配置文件 下载适用于人脸检测任务的YOLOv5权重文件是非常重要的一步。这些权重通常由社区提供或者自行训练获得。如果计划使用官方提供的`yolov5s-face.pt`或其他类似的预训练模型,则可以直接从GitHub仓库获取。 然而,在某些情况下,自定义训练的数据集可能会导致生成的不同结构的`.pt`文件。此时就需要调整代码逻辑以适应新的输出格式,比如将原始的PyTorch模型转换成ONNX格式以便更好地兼容不同的推理引擎。根据描述,当涉及到多尺度预测时(即不同大小的目标),输出张量形状会有所变化,例如变为`1×3×7×20×20`的形式[^2]。 #### 转换模型至适合部署的状态 一旦拥有了正确的`.pt`文件之后,下一步就是将其转化为可以在嵌入式设备上高效执行的形式——通常是通过导出为ONNX格式完成这一过程。此操作不仅简化了跨平台移植的工作量,还允许利用NPU加速等功能提升性能表现[^3]。 #### 编写测试脚本验证功能正常性 最后编写一段简单的Python程序用于加载上述准备工作后的资源并调用人脸检测函数。这里给出一个基本框架作为参考: ```python import cv2 import torch from pathlib import Path def load_model(weights_path='best_face_detection.onnx'): net = cv2.dnn.readNetFromONNX(str(Path.cwd() / weights_path)) return net def detect_faces(image, model): blob = cv2.dnn.blobFromImage( image, scalefactor=1/255., size=(640, 640), swapRB=True, crop=False) model.setInput(blob) outputs = model.forward() # Process the output here... pass if __name__ == '__main__': img = cv2.imread('test.jpg') detector = load_model() results = detect_faces(img, detector) ``` 以上代码片段展示了如何读取图像数据并通过DNN模块传递给已经加载好的ONNX模型来进行前向传播计算。实际应用中还需要进一步解析返回的结果数组,提取感兴趣区域的信息并绘制边界框等可视化效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不想起名字呢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值