在RK3588/RK3588s中提升yolov8推理速度
前言
在上一篇文章中介绍了RK3588/RK3588s运行yolov8的方法。在文章的最后挖了一个坑,明明推理的速度是48ms,标题非要写27ms。这篇文章就来填坑啦!提升计算速度的方法比较简单,就两种,第一种思路是采用多线程异步的检测方法,另外一种思路是改变网络结构。在这篇文章中,主要介绍第二种方法,使用这种方法无需多线程异步直接进行推理,推理的速度就可以达到27ms。
更改yolov8中SiLU为ReLU
这一步需要下载yolov8源码。在./ultralytics-8.0.151\ultralytics\nn\modules\conv.py文件中,将所有的SiLU注释掉,换成ReLU就好了。(至于具体的原因,好像是因为ReLU激活函数对RK3588/RK3588s比较友好吧,推理速度比较快。而一个网络中有太多的激活层,所以对网络推理就有不小的提升)
完成了这一步之后,再进行网络训练就好啦。然后再参照RK3588/RK3588s运行yolov8推理速度达到27ms_rk3588s yolo-CSDN博客 这篇文章中的方法,进行rknn文件的导出即可。
再按照我上一篇文章中的操作流程,就可以把识别速度提升到27ms(28ms)啦~~
异步执行
异步执行可以采用线程池的方法,具体的可以参照下面这篇文章:
多线程异步提高RK3588的NPU占用率,进而提高yolov5s帧率_rk3588 多线程-CSDN博客
修改后处理流程
emmm...这篇文章是yolov5的。小伙伴们用起来可能不太方便,这里可以修改一下后处理的流程。yolov5和yolov8思路都是类似的,前处理都一样,然后使用RK3588/RK3588s进行推理,最后进行后处理。最大的区别就是后处理,因为在推理结束之后,根据onnx网络的输出不同,那要进行的后处理自然也就不同了。
这个思路不仅可以将yolov5转成yolov8,同样也可以应用在yolo11上,使用RK3588/RK3588s进行推理。
这里简单贴一下我的后处理代码,这里解释一下哈,我用的后处理方法就是从./rknn_model_zoo/examples/yolov8/python中Copy过来的,替换一下就好了。
coco_utils.py
from copy import copy
import os
import cv2
import numpy as np
import json
class Letter_Box_Info():
def __init__(self, shape, new_shape, w_ratio, h_ratio, dw, dh, pad_color) -> None:
self.origin_shape = shape
self.new_shape = new_shape
self.w_ratio = w_ratio
self.h_ratio = h_ratio
self.dw = dw
self.dh = dh
self.pad_color = pad_color
def coco_eval_with_json(anno_json, pred_json):
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
anno = COCO(anno_json)
pred = anno.loadRes(pred_json)
eval = COCOeval(anno, pred, 'bbox')
# eval.params.useCats = 0
# eval.params.maxDets = list((100, 300, 1000))
# a = np.array(list(range(50, 96, 1)))/100
# eval.params.iouThrs = a
eval.evaluate()
eval.accumulate()
eval.summarize()
map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
print('map --> ', map)
print('map50--> ', map50)
print('map75--> ', eval.stats[2])
print('map85--> ', eval.stats[-2])
print('map95--> ', eval.stats[-1])
class COCO_test_helper():
def __init__(self, enable_letter_box = False) -> None:
self.record_list = []
self.enable_ltter_box = enable_letter_box
if self.enable_ltter_box is True:
self.letter_box_info_list = []
else:
self.letter_box_info_list = None
def letter_box(self, im, new_shape, pad_color=(0,0,0), info_need=False):
# 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
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
# Compute padding
ratio = 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
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=pad_color) # add border
if self.enable_ltter_box is True:
self.letter_box_info_list.append(Letter_Box_Info(shape, new_shape, ratio, ratio, dw, dh, pad_color))
if info_need is True:
return im, ratio, (dw, dh)
else:
return im
def direct_resize(self, im, new_shape, info_need=False):
shape = im.shape[:2]
h_ratio = new_shape[0]/ shape[0]
w_ratio = new_shape[1]/ shape[1]
if self.enable_ltter_box is True:
self.letter_box_info_list.append(Letter_Box_Info(shape, new_shape, w_ratio, h_ratio, 0, 0, (0,0,0)))
im = cv2.resize(im, (new_shape[1], new_shape[0]))
return im
def get_real_box(self, box, in_format='xyxy'):
bbox = copy(box)
if self.enable_ltter_box == True:
# unletter_box result
if in_format=='xyxy':
bbox[:,0] -= self.letter_box_info_list[-1].dw
bbox[:,0] /= self.letter_box_info_list[-1].w_ratio
bbox[:,0] = np.clip(bbox[:,0], 0, self.letter_box_info_list[-1].origin_shape[1])
bbox[:,1] -= self.letter_box_info_list[-1].dh
bbox[:,1] /= self.letter_box_info_list[-1].h_ratio
bbox[:,1] = np.clip(bbox[:,1], 0, self.letter_box_info_list[-1].origin_shape[0])
bbox[:,2] -= self.letter_box_info_list[-1].dw
bbox[:,2] /= self.letter_box_info_list[-1].w_ratio
bbox[:,2] = np.clip(bbox[:,2], 0, self.letter_box_info_list[-1].origin_shape[1])
bbox[:,3] -= self.letter_box_info_list[-1].dh
bbox[:,3] /= self.letter_box_info_list[-1].h_ratio
bbox[:,3] = np.clip(bbox[:,3], 0, self.letter_box_info_list[-1].origin_shape[0])
return bbox
def get_real_seg(self, seg):
#! fix side effect
dh = int(self.letter_box_info_list[-1].dh)
dw = int(self.letter_box_info_list[-1].dw)
origin_shape = self.letter_box_info_list[-1].origin_shape
new_shape = self.letter_box_info_list[-1].new_shape
if (dh == 0) and (dw == 0) and origin_shape == new_shape:
return seg
elif dh == 0 and dw != 0:
seg = seg[:, :, dw:-dw] # a[0:-0] = []
elif dw == 0 and dh != 0 :
seg = seg[:, dh:-dh, :]
seg = np.where(seg, 1, 0).astype(np.uint8).transpose(1,2,0)
seg = cv2.resize(seg, (origin_shape[1], origin_shape[0]), interpolation=cv2.INTER_LINEAR)
if len(seg.shape) < 3:
return seg[None,:,:]
else:
return seg.transpose(2,0,1)
def add_single_record(self, image_id, category_id, bbox, score, in_format='xyxy', pred_masks = None):
if self.enable_ltter_box == True:
# unletter_box result
if in_format=='xyxy':
bbox[0] -= self.letter_box_info_list[-1].dw
bbox[0] /= self.letter_box_info_list[-1].w_ratio
bbox[1] -= self.letter_box_info_list[-1].dh
bbox[1] /= self.letter_box_info_list[-1].h_ratio
bbox[2] -= self.letter_box_info_list[-1].dw
bbox[2] /= self.letter_box_info_list[-1].w_ratio
bbox[3] -= self.letter_box_info_list[-1].dh
bbox[3] /= self.letter_box_info_list[-1].h_ratio
# bbox = [value/self.letter_box_info_list[-1].ratio for value in bbox]
if in_format=='xyxy':
# change xyxy to xywh
bbox[2] = bbox[2] - bbox[0]
bbox[3] = bbox[3] - bbox[1]
else:
assert False, "now only support xyxy format, please add code to support others format"
def single_encode(x):
from pycocotools.mask import encode
rle = encode(np.asarray(x[:, :, None], order="F", dtype="uint8"))[0]
rle["counts"] = rle["counts"].decode("utf-8")
return rle
if pred_masks is None:
self.record_list.append({"image_id": image_id,
"category_id": category_id,
"bbox":[round(x, 3) for x in bbox],
'score': round(score, 5),
})
else:
rles = single_encode(pred_masks)
self.record_list.append({"image_id": image_id,
"category_id": category_id,
"bbox":[round(x, 3) for x in bbox],
'score': round(score, 5),
'segmentation': rles,
})
def export_to_json(self, path):
with open(path, 'w') as f:
json.dump(self.record_list, f)
func.py
#以下代码改自https://github.com/rockchip-linux/rknn-toolkit2/tree/master/examples/onnx/yolov5
import cv2
import numpy as np
OBJ_THRESH, NMS_THRESH, IMG_SIZE = 0.25, 0.45, 640
CLASSES = ("person", "bicycle", "car", "motorbike ", "aeroplane ", "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", "sofa",
"pottedplant", "bed", "diningtable", "toilet ", "tvmonitor", "laptop ", "mouse ", "remote ", "keyboard ", "cell phone", "microwave ",
"oven ", "toaster", "sink", "refrigerator ", "book", "clock", "vase", "scissors ", "teddy bear ", "hair drier", "toothbrush ")
MODEL_SIZE = (640, 640)
def filter_boxes(boxes, box_confidences, box_class_probs):
"""Filter boxes with object threshold.
"""
box_confidences = box_confidences.reshape(-1)
candidate, class_num = box_class_probs.shape
class_max_score = np.max(box_class_probs, axis=-1)
classes = np.argmax(box_class_probs, axis=-1)
_class_pos = np.where(class_max_score* box_confidences >= OBJ_THRESH)
scores = (class_max_score* box_confidences)[_class_pos]
boxes = boxes[_class_pos]
classes = classes[_class_pos]
return boxes, classes, scores
def nms_boxes(boxes, scores):
"""Suppress non-maximal boxes.
# Returns
keep: ndarray, index of effective boxes.
"""
x = boxes[:, 0]
y = boxes[:, 1]
w = boxes[:, 2] - boxes[:, 0]
h = boxes[:, 3] - boxes[:, 1]
areas = w * h
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x[i], x[order[1:]])
yy1 = np.maximum(y[i], y[order[1:]])
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)
h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
inter = w1 * h1
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= NMS_THRESH)[0]
order = order[inds + 1]
keep = np.array(keep)
return keep
def softmax(x, axis=None):
x = x - x.max(axis=axis, keepdims=True)
y = np.exp(x)
return y / y.sum(axis=axis, keepdims=True)
def dfl(position):
# Distribution Focal Loss (DFL)
import torch
x = torch.tensor(position)
n,c,h,w = x.shape
p_num = 4
mc = c//p_num
y = x.reshape(n,p_num,mc,h,w)
y = y.softmax(2)
acc_metrix = torch.tensor(range(mc)).float().reshape(1,1,mc,1,1)
y = (y*acc_metrix).sum(2)
return y.numpy()
# def dfl(position):
# # Distribution Focal Loss (DFL)
# n, c, h, w = position.shape
# p_num = 4
# mc = c // p_num
# y = position.reshape(n, p_num, mc, h, w)
# exp_y = np.exp(y)
# y = exp_y / np.sum(exp_y, axis=2, keepdims=True)
# acc_metrix = np.arange(mc).reshape(1, 1, mc, 1, 1).astype(float)
# y = (y * acc_metrix).sum(2)
# return y
# def dfl(position):
# # Distribution Focal Loss (DFL)
# # x = np.array(position)
# n,c,h,w = position.shape
# p_num = 4
# mc = c//p_num
# y = position.reshape(n,p_num,mc,h,w)
# # Vectorized softmax
# e_y = np.exp(y - np.max(y, axis=2, keepdims=True)) # subtract max for numerical stability
# y = e_y / np.sum(e_y, axis=2, keepdims=True)
# acc_metrix = np.arange(mc).reshape(1,1,mc,1,1)
# y = (y*acc_metrix).sum(2)
# return y
def box_process(position):
grid_h, grid_w = position.shape[2:4]
col, row = np.meshgrid(np.arange(0, grid_w), np.arange(0, grid_h))
col = col.reshape(1, 1, grid_h, grid_w)
row = row.reshape(1, 1, grid_h, grid_w)
grid = np.concatenate((col, row), axis=1)
stride = np.array([MODEL_SIZE[1]//grid_h, MODEL_SIZE[0]//grid_w]).reshape(1,2,1,1)
position = dfl(position)
box_xy = grid +0.5 -position[:,0:2,:,:]
box_xy2 = grid +0.5 +position[:,2:4,:,:]
xyxy = np.concatenate((box_xy*stride, box_xy2*stride), axis=1)
return xyxy
def yolov8_post_process(input_data):
boxes, scores, classes_conf = [], [], []
defualt_branch=3
pair_per_branch = len(input_data)//defualt_branch
# Python 忽略 score_sum 输出
for i in range(defualt_branch):
boxes.append(box_process(input_data[pair_per_branch*i]))
classes_conf.append(input_data[pair_per_branch*i+1])
scores.append(np.ones_like(input_data[pair_per_branch*i+1][:,:1,:,:], dtype=np.float32))
def sp_flatten(_in):
ch = _in.shape[1]
_in = _in.transpose(0,2,3,1)
return _in.reshape(-1, ch)
boxes = [sp_flatten(_v) for _v in boxes]
classes_conf = [sp_flatten(_v) for _v in classes_conf]
scores = [sp_flatten(_v) for _v in scores]
boxes = np.concatenate(boxes)
classes_conf = np.concatenate(classes_conf)
scores = np.concatenate(scores)
# filter according to threshold
boxes, classes, scores = filter_boxes(boxes, scores, classes_conf)
# nms
nboxes, nclasses, nscores = [], [], []
for c in set(classes):
inds = np.where(classes == c)
b = boxes[inds]
c = classes[inds]
s = scores[inds]
keep = nms_boxes(b, s)
if len(keep) != 0:
nboxes.append(b[keep])
nclasses.append(c[keep])
nscores.append(s[keep])
if not nclasses and not nscores:
return None, None, None
boxes = np.concatenate(nboxes)
classes = np.concatenate(nclasses)
scores = np.concatenate(nscores)
return boxes, classes, scores
def draw(image, boxes, scores, classes):
for box, score, cl in zip(boxes, scores, classes):
top, left, right, bottom = [int(_b) for _b in box]
print("%s @ (%d %d %d %d) %.3f" % (CLASSES[cl], top, left, right, bottom, score))
cv2.rectangle(image, (top, left), (right, bottom), (255, 0, 0), 2)
cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),
(top, left - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
def myFunc(rknn_lite, helper, img_src):
pad_color = (0,0,0)
img = helper.letter_box(im= img_src.copy(), new_shape=(MODEL_SIZE[1], MODEL_SIZE[0]), pad_color=(0,0,0))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
input = np.expand_dims(img, axis=0)
outputs = rknn_lite.inference(inputs=[input])
boxes, classes, scores = yolov8_post_process(outputs)
img_p = img_src.copy()
if boxes is not None:
draw(img_p, helper.get_real_box(boxes), scores, classes)
return img_p
PS
在我的板卡中,在识别720P60HZ的视频时,设置线程数大于6的之后,帧率始终稳定在54左右,不太清楚具体原因。但是使用C++的线程池之后,帧率确实会不断上升,但是识别的视频一时快一时慢的。有知道对应原因的小伙伴可以私信一下我,万分感谢!!!
贴一下C++线程池实现yolov5加速推理方法:leafqycc/rknn-cpp-Multithreading: A simple demo of yolov5s running on rk3588/3588s using c++ (about 142 frames). / 一个使用c++在rk3588/3588s上运行的yolov5s简单demo(142帧/s)。