from mindx.sdk import Tensor # mxVision 中的 Tensor 数据结构
from mindx.sdk import base # mxVision 推理接口
import numpy as np
import cv2
# 前处理
def resize_image(image, size, letterbox_image):
"""
对输入图像进行resize
Args:
size:目标尺寸
letterbox_image: bool 是否进行letterbox变换
Returns:指定尺寸的图像
"""
ih, iw, _ = image.shape
h, w = size
if letterbox_image:
scale = min(w/iw, h/ih)
nw = int(iw*scale)
nh = int(ih*scale)
image = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_LINEAR)
image_back = np.ones((h, w, 3), dtype=np.uint8) * 128
image_back[(h-nh)//2: (h-nh)//2 + nh, (w-nw)//2:(w-nw)//2+nw , :] = image
else:
image_back = image
return image_back
def img2input(img):
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW,将形状转换为 channel first
img = np.expand_dims(img, 0).astype(np.float32) # 得到(1, 3, 640, 640),即扩展第一维为 batchsize
img = np.ascontiguousarray(img) / 255.0 # 转换为内存连续存储的数组
img = Tensor(img) # 将numpy转为转为Tensor类
return img
def std_output(pred):
"""
将(1, 16, 8400)处理成(8400, 17); 17 = box:4 conf:1 cls:12
"""
pred = np.squeeze(pred)
pred = np.transpose(pred, (1, 0))
pred_class = pred[..., 4:]
pred_conf = np.max(pred_class, axis=-1)
pred = np.insert(pred, 4, pred_conf, axis=-1)
return pred
def xywh2xyxy(*box):
"""
将xywh转换为左上角点和左下角点
Args:
box:
Returns: x1y1x2y2
"""
ret = [box[0] - box[2] // 2, box[1] - box[3] // 2, \
box[0] + box[2] // 2, box[1] + box[3] // 2]
return ret
def get_inter(box1, box2):
"""
计算相交部分面积
Args:
box1: 第一个框
box2: 第二个狂
Returns: 相交部分的面积
"""
x1, y1, x2, y2 = xywh2xyxy(*box1)
x3, y3, x4, y4 = xywh2xyxy(*box2)
# 验证是否存在交集
if x1 >= x4 or x2 <= x3:
return 0
if y1 >= y4 or y2 <= y3:
return 0
# 将x1,x2,x3,x4排序,因为已经验证了两个框相交,所以x3-x2就是交集的宽
x_list = sorted([x1, x2, x3, x4])
x_inter = x_list[2] - x_list[1]
# 将y1,y2,y3,y4排序,因为已经验证了两个框相交,所以y3-y2就是交集的宽
y_list = sorted([y1, y2, y3, y4])
y_inter = y_list[2] - y_list[1]
# 计算交集的面积
inter = x_inter * y_inter
return inter
def get_iou(box1, box2):
"""
计算交并比: (A n B)/(A + B - A n B)
Args:
box1: 第一个框
box2: 第二个框
Returns: # 返回交并比的值
"""
box1_area = box1[2] * box1[3] # 计算第一个框的面积
box2_area = box2[2] * box2[3] # 计算第二个框的面积
inter_area = get_inter(box1, box2)
union = box1_area + box2_area - inter_area #(A n B)/(A + B - A n B)
iou = inter_area / union
return iou
def nms(pred, conf_thres, iou_thres):
"""
非极大值抑制nms
Args:
pred: 模型输出特征图
conf_thres: 置信度阈值
iou_thres: iou阈值
Returns: 输出后的结果
"""
box = pred[pred[..., 4] > conf_thres] # 置信度筛选
cls_conf = box[..., 5:]
cls = []
for i in range(len(cls_conf)):
cls.append(int(np.argmax(cls_conf[i])))
total_cls = list(set(cls)) # 记录图像内共出现几种物体
output_box = []
# 每个预测类别分开考虑
for i in range(len(total_cls)):
clss = total_cls[i]
cls_box = []
temp = box[:, :6]
for j in range(len(cls)):
# 记录[x,y,w,h,conf(最大类别概率),class]值
if cls[j] == clss:
temp[j][5] = clss
cls_box.append(temp[j][:6])
# cls_box 里面是[x,y,w,h,conf(最大类别概率),class]
cls_box = np.array(cls_box)
sort_cls_box = sorted(cls_box, key=lambda x: -x[4]) # 将cls_box按置信度从大到小排序
# box_conf_sort = np.argsort(-box_conf)
# 得到置信度最大的预测框
max_conf_box = sort_cls_box[0]
output_box.append(max_conf_box)
sort_cls_box = np.delete(sort_cls_box, 0, 0)
# 对除max_conf_box外其他的框进行非极大值抑制
while len(sort_cls_box) > 0:
# 得到当前最大的框
max_conf_box = output_box[-1]
del_index = []
for j in range(len(sort_cls_box)):
current_box = sort_cls_box[j]
iou = get_iou(max_conf_box, current_box)
if iou > iou_thres:
# 筛选出与当前最大框Iou大于阈值的框的索引
del_index.append(j)
# 删除这些索引
sort_cls_box = np.delete(sort_cls_box, del_index, 0)
if len(sort_cls_box) > 0:
# 我认为这里需要将clas_box先按置信度排序, 才能每次取第一个
output_box.append(sort_cls_box[0])
sort_cls_box = np.delete(sort_cls_box, 0, 0)
return output_box
def cod_trf(result, pre, after):
"""
因为预测框是在经过letterbox后的图像上做预测所以需要将预测框的坐标映射回原图像上
Args:
result: [x,y,w,h,conf(最大类别概率),class]
pre: 原尺寸图像
after: 经过letterbox处理后的图像
Returns: 坐标变换后的结果,
"""
res = np.array(result)
x, y, w, h, conf, cls = res.transpose((1, 0))
x1, y1, x2, y2 = xywh2xyxy(x, y, w, h) # 左上角点和右下角的点
h_pre, w_pre, _ = pre.shape
h_after, w_after, _ = after.shape
scale = max(w_pre/w_after, h_pre/h_after) # 缩放比例
h_pre, w_pre = h_pre/scale, w_pre/scale # 计算原图在等比例缩放后的尺寸
x_move, y_move = abs(w_pre-w_after)//2, abs(h_pre-h_after)//2 # 计算平移的量
ret_x1, ret_x2 = (x1 - x_move) * scale, (x2 - x_move) * scale
ret_y1, ret_y2 = (y1 - y_move) * scale, (y2 - y_move) * scale
ret = np.array([ret_x1, ret_y1, ret_x2, ret_y2, conf, cls]).transpose((1, 0))
return ret
def draw(res, image, cls):
"""
将预测框绘制在image上
Args:
res: 预测框数据
image: 原图
cls: 类别列表,类似["apple", "banana", "people"] 可以自己设计或者通过数据集的yaml文件获取
Returns:
"""
for r in res:
# 画框
image = cv2.rectangle(image, (int(r[0]), int(r[1])), (int(r[2]), int(r[3])), (255, 0, 0), 1)
# 表明类别
text = "{}:{}".format(cls[int(r[5])], round(float(r[4]), 2))
print(text)
h, w = int(r[3]) - int(r[1]), int(r[2]) - int(r[0]) # 计算预测框的长宽
font_size = min(h/640, w/640) * 3 # 计算字体大小(随框大小调整)
image = cv2.putText(image, text, (max(10, int(r[0])), max(20, int(r[1]))), cv2.FONT_HERSHEY_COMPLEX, max(font_size, 0.3), (0, 0, 255), 1) # max()为了确保字体不过界
return image
项目所需,需要部署一下YOLOv8,但是发现没有相关案例以及帖子,因此这里分享一下我所部署的案例。
软硬件:CANN8.0、MindSDK、香橙派20T。
模型准备:
1.将pt模型转为onnx。(我记得好像是需要设置opset为12-14之间,可以去对应的CANN文档看下。)
2.将onnx转为om模型,命令去v5的样例看一下吧(难得找了),这里先不调用AIPP,后续推理速度优化再考虑。
代码:
1.新建det_utils.py
from mindx.sdk import Tensor # mxVision 中的 Tensor 数据结构 from mindx.sdk import base # mxVision 推理接口 import numpy as np import cv2 # 前处理 def resize_image(image, size, letterbox_image): """ 对输入图像进行resize Args: size:目标尺寸 letterbox_image: bool 是否进行letterbox变换 Returns:指定尺寸的图像 """ ih, iw, _ = image.shape h, w = size if letterbox_image: scale = min(w/iw, h/ih) nw = int(iw*scale) nh = int(ih*scale) image = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_LINEAR) image_back = np.ones((h, w, 3), dtype=np.uint8) * 128 image_back[(h-nh)//2: (h-nh)//2 + nh, (w-nw)//2:(w-nw)//2+nw , :] = image else: image_back = image return image_back def img2input(img): img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW,将形状转换为 channel first img = np.expand_dims(img, 0).astype(np.float32) # 得到(1, 3, 640, 640),即扩展第一维为 batchsize img = np.ascontiguousarray(img) / 255.0 # 转换为内存连续存储的数组 img = Tensor(img) # 将numpy转为转为Tensor类 return img def std_output(pred): """ 将(1, 16, 8400)处理成(8400, 17); 17 = box:4 conf:1 cls:12 """ pred = np.squeeze(pred) pred = np.transpose(pred, (1, 0)) pred_class = pred[..., 4:] pred_conf = np.max(pred_class, axis=-1) pred = np.insert(pred, 4, pred_conf, axis=-1) return pred def xywh2xyxy(*box): """ 将xywh转换为左上角点和左下角点 Args: box: Returns: x1y1x2y2 """ ret = [box[0] - box[2] // 2, box[1] - box[3] // 2, \ box[0] + box[2] // 2, box[1] + box[3] // 2] return ret def get_inter(box1, box2): """ 计算相交部分面积 Args: box1: 第一个框 box2: 第二个狂 Returns: 相交部分的面积 """ x1, y1, x2, y2 = xywh2xyxy(*box1) x3, y3, x4, y4 = xywh2xyxy(*box2) # 验证是否存在交集 if x1 >= x4 or x2 <= x3: return 0 if y1 >= y4 or y2 <= y3: return 0 # 将x1,x2,x3,x4排序,因为已经验证了两个框相交,所以x3-x2就是交集的宽 x_list = sorted([x1, x2, x3, x4]) x_inter = x_list[2] - x_list[1] # 将y1,y2,y3,y4排序,因为已经验证了两个框相交,所以y3-y2就是交集的宽 y_list = sorted([y1, y2, y3, y4]) y_inter = y_list[2] - y_list[1] # 计算交集的面积 inter = x_inter * y_inter return inter def get_iou(box1, box2): """ 计算交并比: (A n B)/(A + B - A n B) Args: box1: 第一个框 box2: 第二个框 Returns: # 返回交并比的值 """ box1_area = box1[2] * box1[3] # 计算第一个框的面积 box2_area = box2[2] * box2[3] # 计算第二个框的面积 inter_area = get_inter(box1, box2) union = box1_area + box2_area - inter_area #(A n B)/(A + B - A n B) iou = inter_area / union return iou def nms(pred, conf_thres, iou_thres): """ 非极大值抑制nms Args: pred: 模型输出特征图 conf_thres: 置信度阈值 iou_thres: iou阈值 Returns: 输出后的结果 """ box = pred[pred[..., 4] > conf_thres] # 置信度筛选 cls_conf = box[..., 5:] cls = [] for i in range(len(cls_conf)): cls.append(int(np.argmax(cls_conf[i]))) total_cls = list(set(cls)) # 记录图像内共出现几种物体 output_box = [] # 每个预测类别分开考虑 for i in range(len(total_cls)): clss = total_cls[i] cls_box = [] temp = box[:, :6] for j in range(len(cls)): # 记录[x,y,w,h,conf(最大类别概率),class]值 if cls[j] == clss: temp[j][5] = clss cls_box.append(temp[j][:6]) # cls_box 里面是[x,y,w,h,conf(最大类别概率),class] cls_box = np.array(cls_box) sort_cls_box = sorted(cls_box, key=lambda x: -x[4]) # 将cls_box按置信度从大到小排序 # box_conf_sort = np.argsort(-box_conf) # 得到置信度最大的预测框 max_conf_box = sort_cls_box[0] output_box.append(max_conf_box) sort_cls_box = np.delete(sort_cls_box, 0, 0) # 对除max_conf_box外其他的框进行非极大值抑制 while len(sort_cls_box) > 0: # 得到当前最大的框 max_conf_box = output_box[-1] del_index = [] for j in range(len(sort_cls_box)): current_box = sort_cls_box[j] iou = get_iou(max_conf_box, current_box) if iou > iou_thres: # 筛选出与当前最大框Iou大于阈值的框的索引 del_index.append(j) # 删除这些索引 sort_cls_box = np.delete(sort_cls_box, del_index, 0) if len(sort_cls_box) > 0: # 我认为这里需要将clas_box先按置信度排序, 才能每次取第一个 output_box.append(sort_cls_box[0]) sort_cls_box = np.delete(sort_cls_box, 0, 0) return output_box def cod_trf(result, pre, after): """ 因为预测框是在经过letterbox后的图像上做预测所以需要将预测框的坐标映射回原图像上 Args: result: [x,y,w,h,conf(最大类别概率),class] pre: 原尺寸图像 after: 经过letterbox处理后的图像 Returns: 坐标变换后的结果, """ res = np.array(result) x, y, w, h, conf, cls = res.transpose((1, 0)) x1, y1, x2, y2 = xywh2xyxy(x, y, w, h) # 左上角点和右下角的点 h_pre, w_pre, _ = pre.shape h_after, w_after, _ = after.shape scale = max(w_pre/w_after, h_pre/h_after) # 缩放比例 h_pre, w_pre = h_pre/scale, w_pre/scale # 计算原图在等比例缩放后的尺寸 x_move, y_move = abs(w_pre-w_after)//2, abs(h_pre-h_after)//2 # 计算平移的量 ret_x1, ret_x2 = (x1 - x_move) * scale, (x2 - x_move) * scale ret_y1, ret_y2 = (y1 - y_move) * scale, (y2 - y_move) * scale ret = np.array([ret_x1, ret_y1, ret_x2, ret_y2, conf, cls]).transpose((1, 0)) return ret def draw(res, image, cls): """ 将预测框绘制在image上 Args: res: 预测框数据 image: 原图 cls: 类别列表,类似["apple", "banana", "people"] 可以自己设计或者通过数据集的yaml文件获取 Returns: """ for r in res: # 画框 image = cv2.rectangle(image, (int(r[0]), int(r[1])), (int(r[2]), int(r[3])), (255, 0, 0), 1) # 表明类别 text = "{}:{}".format(cls[int(r[5])], round(float(r[4]), 2)) print(text) h, w = int(r[3]) - int(r[1]), int(r[2]) - int(r[0]) # 计算预测框的长宽 font_size = min(h/640, w/640) * 3 # 计算字体大小(随框大小调整) image = cv2.putText(image, text, (max(10, int(r[0])), max(20, int(r[1]))), cv2.FONT_HERSHEY_COMPLEX, max(font_size, 0.3), (0, 0, 255), 1) # max()为了确保字体不过界 return image2.新建YOLOv8-12class-sdk.py
from mindx.sdk import base # mxVision 推理接口 import numpy as np import cv2 from det_utils import * # 模型前后处理相关函数 if __name__ == '__main__': base.mx_init() # 初始化 mxVision 资源 DEVICE_ID = 0 # 设备id # 假设文件名为 labels.txt,与脚本在同一目录 with open('/root/Desktop/YOLO-SDK-config/model-YOLOv8_12class/lable.txt', 'r', encoding='utf-8') as f: lines = f.read().splitlines() # 去掉每行末尾的换行符 # 0-based 行号作为键,行内容作为值 label_dict = {idx: line.strip() for idx, line in enumerate(lines)} print(label_dict) std_h, std_w = 640, 640 # 标准输入尺寸 dic = label_dict # 得到的是模型类别字典 class_list = list(label_dict.values()) model_path = '/root/Desktop/YOLO-SDK-config/model-YOLOv8_12class/YOLOv8_12class.om' input_path = "/root/Desktop/YOLO-SDK-config/test-data/" # 输入图片的根目录路径 img_path = "306.jpg" # 输入图片的文件名 img = cv2.imread(input_path+img_path) if img.size == 0: print("路径有误!") # 前处理 img_after = resize_image(img, (std_w, std_h), True) # (640, 640, 3) # 将图像处理成输入的格式 data = img2input(img_after) # 模型推理, 得到模型输出 model = base.model(modelPath=model_path, deviceId=DEVICE_ID) # 初始化 base.model 类 output = model.infer([data])[0] # 执行推理。输入数据类型:List[base.Tensor], 返回模型推理输出的 List[base.Tensor] # 后处理 output.to_host() # 将Tensor数据转移到内存 output = np.array(output) # 将数据转为 numpy array 类型 print(f"Outputs shape: {[out.shape for out in output]}") pred = std_output(output) # 置信度过滤+nms result = nms(pred, 0.5, 0.4) # [x,y,w,h,conf(最大类别概率),class] # 坐标变换 result = cod_trf(result, img, img_after) image = draw(result, img, class_list) # 保存输出图像 # 保存图片到文件 result_path = "../result/" result_name = "result2.png" cv2.imwrite(result_path + result_name, image) print('save infer result success')3.运行推理程序即可。
注:本模型为12类模型,与官方80类模型不一致,到对应处修改即可。
后续优化:
1.使用DVPP+AIPP加快图像编解码; 2.多batch; 3.多线程(形成异步操作); 4.模型中引入nms算子。