当前位置:   article > 正文

最细致讲解yolov8模型推理完整代码--(前处理,后处理)_yolov8推理

yolov8推理

研究yolov8时,一直苦寻不到Yolov8完整的模型推理代码演示,大部分都是基于Yolo已经封装好的函数调用,这个网上教程很多,本文就不赘述这方面的内容了,接下来将细致全面的讲解yolov8模型推理代码,也就是yolov8的predict的前处理(letterbox缩放),后处理(坐标转换,置信度过滤,NMS,绘图)的代码实现(附完整代码)。

前处理

letterbox缩放

yolov8预设的图片输入是640x640大小的,所以我们需要将一般大小的图像resize成标准大小,但是单纯的只是用resize来操作的话有可能会造成图像的失真:

原图:   直接resize后:

所以yolov5提出letterbox缩放(v8也沿用了),其原理就是等比例缩放,其他的部分用背景色填充:

                        

 前处理代码如下:

  1. def resize_image(image, size, letterbox_image):
  2. """
  3. 对输入图像进行resize
  4. Args:
  5. size:目标尺寸
  6. letterbox_image: bool 是否进行letterbox变换
  7. Returns:指定尺寸的图像
  8. """
  9. from PIL import Image
  10. ih, iw, _ = image.shape
  11. h, w = size
  12. if letterbox_image:
  13. scale = min(w/iw, h/ih) # 缩放比例
  14. nw = int(iw*scale)
  15. nh = int(ih*scale)
  16. image = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_LINEAR)
  17. # 生成画布
  18. image_back = np.ones((h, w, 3), dtype=np.uint8) * 128
  19. # 将image放在画布中心区域-letterbox
  20. image_back[(h-nh)//2: (h-nh)//2 + nh, (w-nw)//2:(w-nw)//2+nw, :] = image
  21. else:
  22. image_back = image
  23. return image_back

 经过前处理后得到的图像尺寸为(640x640x3),为了对应yolov8模型的输入尺寸(N,C,H,W),我们对其进行预处理操作:

数据预处理

  1. def img2input(img):
  2. img = np.transpose(img, (2, 0, 1))
  3. img = img/255
  4. return np.expand_dims(img, axis=0).astype(np.float32) # (1,3,640,640)

因为只是做预测,所以N取1,C为通道数3。

现在就可以放进模型里计算了,本文采用的yolov8模型是onnx格式的。

  1. sess = rt.InferenceSession('runs/detect/train49/weights/best.onnx')
  2. input_name = sess.get_inputs()[0].name
  3. label_name = sess.get_outputs()[0].name
  4. pred = sess.run([label_name], {input_name: data})[0] # (bs, 84=80cls+4reg, 8400=3种尺度的特征图叠加), 这里的预测框的回归参数是xywh,而不是中心点到框边界的距离

模型得到的输出格式为(84x8400),84=边界框预测值4+数据集类别80, yolov8不另外对置信度预测, 而是采用类别里面最大的概率作为置信度score,8400是v8模型各尺度输出特征图叠加之后的结果(具体如何叠加可以看源码,一般推理不需要管)。本文对模型的输出进行如下操作,方便后处理:

  1. def std_output(pred):
  2. """
  3. 将(1,84,8400)处理成(8400, 85) 85= box:4 conf:1 cls:80
  4. """
  5. pred = np.squeeze(pred) # 因为只是推理,所以没有Batch
  6. pred = np.transpose(pred, (1, 0))
  7. pred_class = pred[..., 4:]
  8. pred_conf = np.max(pred_class, axis=-1)
  9. pred = np.insert(pred, 4, pred_conf, axis=-1)
  10. return pred #(840085

得到输出(8400,85)。8400个特征图的cell,每个cell里面有4+1+80的输出值,对应4个预测框+1个置信度(最大类别概率)+80类别概率。

后处理

置信度过滤+NMS非极大值抑制

接下来就对刚刚的(8400,85)进行后处理,先进行置信度过滤,再进行NMS非极大值抑制,本文将这两步筛选操作放在了一个函数中:

  1. def nms(pred, conf_thres, iou_thres):
  2. """
  3. 非极大值抑制nms
  4. Args:
  5. pred: 模型输出特征图
  6. conf_thres: 置信度阈值
  7. iou_thres: iou阈值
  8. Returns: 输出后的结果
  9. """
  10. box = pred[pred[..., 4] > conf_thres] # 置信度筛选
  11. cls_conf = box[..., 5:]
  12. cls = []
  13. for i in range(len(cls_conf)):
  14. cls.append(int(np.argmax(cls_conf[i])))
  15. total_cls = list(set(cls)) # 记录图像内共出现几种物体
  16. output_box = []
  17. # 每个预测类别分开考虑
  18. for i in range(len(total_cls)):
  19. clss = total_cls[i]
  20. cls_box = []
  21. temp = box[:, :6]
  22. for j in range(len(cls)):
  23. # 记录[x,y,w,h,conf(最大类别概率),class]值
  24. if cls[j] == clss:
  25. temp[j][5] = clss
  26. cls_box.append(temp[j][:6])
  27. # cls_box 里面是[x,y,w,h,conf(最大类别概率),class]
  28. cls_box = np.array(cls_box)
  29. sort_cls_box = sorted(cls_box, key=lambda x: -x[4]) # 将cls_box按置信度从大到小排序
  30. # box_conf_sort = np.argsort(-box_conf)
  31. # 得到置信度最大的预测框
  32. max_conf_box = sort_cls_box[0]
  33. output_box.append(max_conf_box)
  34. sort_cls_box = np.delete(sort_cls_box, 0, 0)
  35. # 对除max_conf_box外其他的框进行非极大值抑制
  36. while len(sort_cls_box) > 0:
  37. # 得到当前最大的框
  38. max_conf_box = output_box[-1]
  39. del_index = []
  40. for j in range(len(sort_cls_box)):
  41. current_box = sort_cls_box[j]
  42. iou = get_iou(max_conf_box, current_box)
  43. if iou > iou_thres:
  44. # 筛选出与当前最大框Iou大于阈值的框的索引
  45. del_index.append(j)
  46. # 删除这些索引
  47. sort_cls_box = np.delete(sort_cls_box, del_index, 0)
  48. if len(sort_cls_box) > 0:
  49. output_box.append(sort_cls_box[0])
  50. sort_cls_box = np.delete(sort_cls_box, 0, 0)
  51. return output_box
  52. def xywh2xyxy(*box):
  53. """
  54. 将xywh转换为左上角点和左下角点
  55. Args:
  56. box:
  57. Returns: x1y1x2y2
  58. """
  59. ret = [box[0] - box[2] // 2, box[1] - box[3] // 2, \
  60. box[0] + box[2] // 2, box[1] + box[3] // 2]
  61. return ret
  62. def get_inter(box1, box2):
  63. """
  64. 计算相交部分面积
  65. Args:
  66. box1: 第一个框
  67. box2: 第二个框
  68. Returns: 相交部分的面积
  69. """
  70. x1, y1, x2, y2 = xywh2xyxy(*box1)
  71. x3, y3, x4, y4 = xywh2xyxy(*box2)
  72. # 验证是否存在交集
  73. if x1 >= x4 or x2 <= x3:
  74. return 0
  75. if y1 >= y4 or y2 <= y3:
  76. return 0
  77. # 将x1,x2,x3,x4排序,因为已经验证了两个框相交,所以x3-x2就是交集的宽
  78. x_list = sorted([x1, x2, x3, x4])
  79. x_inter = x_list[2] - x_list[1]
  80. # 将y1,y2,y3,y4排序,因为已经验证了两个框相交,所以y3-y2就是交集的宽
  81. y_list = sorted([y1, y2, y3, y4])
  82. y_inter = y_list[2] - y_list[1]
  83. # 计算交集的面积
  84. inter = x_inter * y_inter
  85. return inter
  86. def get_iou(box1, box2):
  87. """
  88. 计算交并比: (A n B)/(A + B - A n B)
  89. Args:
  90. box1: 第一个框
  91. box2: 第二个框
  92. Returns: # 返回交并比的值
  93. """
  94. box1_area = box1[2] * box1[3] # 计算第一个框的面积
  95. box2_area = box2[2] * box2[3] # 计算第二个框的面积
  96. inter_area = get_inter(box1, box2)
  97. union = box1_area + box2_area - inter_area #(A n B)/(A + B - A n B)
  98. iou = inter_area / union
  99. return iou

坐标转换

筛选完之后得到的输出output_box格式为N * [x,y,w,h,conf(最大类别概率),class] , N是筛选后预测框的个数, 通过[x,y,w,h,conf(最大类别概率),class]这些数据我们就可以将预测框输出绘制在原图像上, 但是要注意,我们此时模型的输入是经过letterbox处理的,所以需要先将预测框的坐标转换回原坐标系的坐标,

  1. def cod_trf(result, pre, after):
  2. """
  3. 因为预测框是在经过letterbox后的图像上做预测所以需要将预测框的坐标映射回原图像上
  4. Args:
  5. result: [x,y,w,h,conf(最大类别概率),class]
  6. pre: 原尺寸图像
  7. after: 经过letterbox处理后的图像
  8. Returns: 坐标变换后的结果,并将xywh转换为左上角右下角坐标x1y1x2y2
  9. """
  10. res = np.array(result)
  11. x, y, w, h, conf, cls = res.transpose((1, 0))
  12. x1, y1, x2, y2 = xywh2xyxy(x, y, w, h) # 左上角点和右下角的点
  13. h_pre, w_pre, _ = pre.shape
  14. h_after, w_after, _ = after.shape
  15. scale = max(w_pre/w_after, h_pre/h_after) # 缩放比例
  16. h_pre, w_pre = h_pre/scale, w_pre/scale # 计算原图在等比例缩放后的尺寸
  17. x_move, y_move = abs(w_pre-w_after)//2, abs(h_pre-h_after)//2 # 计算平移的量
  18. ret_x1, ret_x2 = (x1 - x_move) * scale, (x2 - x_move) * scale
  19. ret_y1, ret_y2 = (y1 - y_move) * scale, (y2 - y_move) * scale
  20. ret = np.array([ret_x1, ret_y1, ret_x2, ret_y2, conf, cls]).transpose((1, 0))
  21. return ret # x1y1x2y2

绘制预测框

输出的ret的格式为N * [x1,y1,x2,y2,conf(最大类别概率),class],接下来就可以进行最后一步操作了,对预测框进行绘制,但是为了美观需要注意将字体大小随着预测框的大小进行动态调整,以及字体显示不能超过边界。

  1. def draw(res, image, cls):
  2. """
  3. 将预测框绘制在image上
  4. Args:
  5. res: 预测框数据
  6. image: 原图
  7. cls: 类别列表,类似["apple", "banana", "people"] 可以自己设计或者通过数据集的yaml文件获取
  8. Returns:
  9. """
  10. for r in res:
  11. # 画框
  12. image = cv2.rectangle(image, (int(r[0]), int(r[1])), (int(r[2]), int(r[3])), (255, 0, 0), 1)
  13. # 表明类别
  14. text = "{}:{}".format(cls[int(r[5])], \
  15. round(float(r[4]), 2))
  16. h, w = int(r[3]) - int(r[1]), int(r[2]) - int(r[0]) # 计算预测框的长宽
  17. font_size = min(h/640, w/640) * 3 # 计算字体大小(随框大小调整)
  18. 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()为了确保字体不过界
  19. cv2.imshow("result", image)
  20. cv2.waitKey()
  21. cv2.destroyWindow("result")

输出结果

到此,最后输出结果展示:

 

 完整代码:(转载请注明本文,谢谢!)

  1. import copy
  2. import onnxruntime as rt
  3. import numpy as np
  4. import cv2
  5. import matplotlib.pyplot as plt
  6. import yaml
  7. # 前处理
  8. def resize_image(image, size, letterbox_image):
  9. """
  10. 对输入图像进行resize
  11. Args:
  12. size:目标尺寸
  13. letterbox_image: bool 是否进行letterbox变换
  14. Returns:指定尺寸的图像
  15. """
  16. ih, iw, _ = image.shape
  17. print(ih, iw)
  18. h, w = size
  19. # letterbox_image = False
  20. if letterbox_image:
  21. scale = min(w/iw, h/ih)
  22. nw = int(iw*scale)
  23. nh = int(ih*scale)
  24. image = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_LINEAR)
  25. # cv2.imshow("img", img)
  26. # cv2.waitKey()
  27. # print(image.shape)
  28. # 生成画布
  29. image_back = np.ones((h, w, 3), dtype=np.uint8) * 128
  30. # 将image放在画布中心区域-letterbox
  31. image_back[(h-nh)//2: (h-nh)//2 + nh, (w-nw)//2:(w-nw)//2+nw , :] = image
  32. else:
  33. image_back = image
  34. # cv2.imshow("img", image_back)
  35. # cv2.waitKey()
  36. return image_back
  37. def img2input(img):
  38. img = np.transpose(img, (2, 0, 1))
  39. img = img/255
  40. return np.expand_dims(img, axis=0).astype(np.float32)
  41. def std_output(pred):
  42. """
  43. 将(1,84,8400)处理成(8400, 85) 85= box:4 conf:1 cls:80
  44. """
  45. pred = np.squeeze(pred)
  46. pred = np.transpose(pred, (1, 0))
  47. pred_class = pred[..., 4:]
  48. pred_conf = np.max(pred_class, axis=-1)
  49. pred = np.insert(pred, 4, pred_conf, axis=-1)
  50. return pred
  51. def xywh2xyxy(*box):
  52. """
  53. 将xywh转换为左上角点和左下角点
  54. Args:
  55. box:
  56. Returns: x1y1x2y2
  57. """
  58. ret = [box[0] - box[2] // 2, box[1] - box[3] // 2, \
  59. box[0] + box[2] // 2, box[1] + box[3] // 2]
  60. return ret
  61. def get_inter(box1, box2):
  62. """
  63. 计算相交部分面积
  64. Args:
  65. box1: 第一个框
  66. box2: 第二个狂
  67. Returns: 相交部分的面积
  68. """
  69. x1, y1, x2, y2 = xywh2xyxy(*box1)
  70. x3, y3, x4, y4 = xywh2xyxy(*box2)
  71. # 验证是否存在交集
  72. if x1 >= x4 or x2 <= x3:
  73. return 0
  74. if y1 >= y4 or y2 <= y3:
  75. return 0
  76. # 将x1,x2,x3,x4排序,因为已经验证了两个框相交,所以x3-x2就是交集的宽
  77. x_list = sorted([x1, x2, x3, x4])
  78. x_inter = x_list[2] - x_list[1]
  79. # 将y1,y2,y3,y4排序,因为已经验证了两个框相交,所以y3-y2就是交集的宽
  80. y_list = sorted([y1, y2, y3, y4])
  81. y_inter = y_list[2] - y_list[1]
  82. # 计算交集的面积
  83. inter = x_inter * y_inter
  84. return inter
  85. def get_iou(box1, box2):
  86. """
  87. 计算交并比: (A n B)/(A + B - A n B)
  88. Args:
  89. box1: 第一个框
  90. box2: 第二个框
  91. Returns: # 返回交并比的值
  92. """
  93. box1_area = box1[2] * box1[3] # 计算第一个框的面积
  94. box2_area = box2[2] * box2[3] # 计算第二个框的面积
  95. inter_area = get_inter(box1, box2)
  96. union = box1_area + box2_area - inter_area #(A n B)/(A + B - A n B)
  97. iou = inter_area / union
  98. return iou
  99. def nms(pred, conf_thres, iou_thres):
  100. """
  101. 非极大值抑制nms
  102. Args:
  103. pred: 模型输出特征图
  104. conf_thres: 置信度阈值
  105. iou_thres: iou阈值
  106. Returns: 输出后的结果
  107. """
  108. box = pred[pred[..., 4] > conf_thres] # 置信度筛选
  109. cls_conf = box[..., 5:]
  110. cls = []
  111. for i in range(len(cls_conf)):
  112. cls.append(int(np.argmax(cls_conf[i])))
  113. total_cls = list(set(cls)) # 记录图像内共出现几种物体
  114. output_box = []
  115. # 每个预测类别分开考虑
  116. for i in range(len(total_cls)):
  117. clss = total_cls[i]
  118. cls_box = []
  119. temp = box[:, :6]
  120. for j in range(len(cls)):
  121. # 记录[x,y,w,h,conf(最大类别概率),class]值
  122. if cls[j] == clss:
  123. temp[j][5] = clss
  124. cls_box.append(temp[j][:6])
  125. # cls_box 里面是[x,y,w,h,conf(最大类别概率),class]
  126. cls_box = np.array(cls_box)
  127. sort_cls_box = sorted(cls_box, key=lambda x: -x[4]) # 将cls_box按置信度从大到小排序
  128. # box_conf_sort = np.argsort(-box_conf)
  129. # 得到置信度最大的预测框
  130. max_conf_box = sort_cls_box[0]
  131. output_box.append(max_conf_box)
  132. sort_cls_box = np.delete(sort_cls_box, 0, 0)
  133. # 对除max_conf_box外其他的框进行非极大值抑制
  134. while len(sort_cls_box) > 0:
  135. # 得到当前最大的框
  136. max_conf_box = output_box[-1]
  137. del_index = []
  138. for j in range(len(sort_cls_box)):
  139. current_box = sort_cls_box[j]
  140. iou = get_iou(max_conf_box, current_box)
  141. if iou > iou_thres:
  142. # 筛选出与当前最大框Iou大于阈值的框的索引
  143. del_index.append(j)
  144. # 删除这些索引
  145. sort_cls_box = np.delete(sort_cls_box, del_index, 0)
  146. if len(sort_cls_box) > 0:
  147. # 我认为这里需要将clas_box先按置信度排序, 才能每次取第一个
  148. output_box.append(sort_cls_box[0])
  149. sort_cls_box = np.delete(sort_cls_box, 0, 0)
  150. return output_box
  151. def cod_trf(result, pre, after):
  152. """
  153. 因为预测框是在经过letterbox后的图像上做预测所以需要将预测框的坐标映射回原图像上
  154. Args:
  155. result: [x,y,w,h,conf(最大类别概率),class]
  156. pre: 原尺寸图像
  157. after: 经过letterbox处理后的图像
  158. Returns: 坐标变换后的结果,
  159. """
  160. res = np.array(result)
  161. x, y, w, h, conf, cls = res.transpose((1, 0))
  162. x1, y1, x2, y2 = xywh2xyxy(x, y, w, h) # 左上角点和右下角的点
  163. h_pre, w_pre, _ = pre.shape
  164. h_after, w_after, _ = after.shape
  165. scale = max(w_pre/w_after, h_pre/h_after) # 缩放比例
  166. h_pre, w_pre = h_pre/scale, w_pre/scale # 计算原图在等比例缩放后的尺寸
  167. x_move, y_move = abs(w_pre-w_after)//2, abs(h_pre-h_after)//2 # 计算平移的量
  168. ret_x1, ret_x2 = (x1 - x_move) * scale, (x2 - x_move) * scale
  169. ret_y1, ret_y2 = (y1 - y_move) * scale, (y2 - y_move) * scale
  170. ret = np.array([ret_x1, ret_y1, ret_x2, ret_y2, conf, cls]).transpose((1, 0))
  171. return ret
  172. def draw(res, image, cls):
  173. """
  174. 将预测框绘制在image上
  175. Args:
  176. res: 预测框数据
  177. image: 原图
  178. cls: 类别列表,类似["apple", "banana", "people"] 可以自己设计或者通过数据集的yaml文件获取
  179. Returns:
  180. """
  181. for r in res:
  182. # 画框
  183. image = cv2.rectangle(image, (int(r[0]), int(r[1])), (int(r[2]), int(r[3])), (255, 0, 0), 1)
  184. # 表明类别
  185. text = "{}:{}".format(cls[int(r[5])], \
  186. round(float(r[4]), 2))
  187. h, w = int(r[3]) - int(r[1]), int(r[2]) - int(r[0]) # 计算预测框的长宽
  188. font_size = min(h/640, w/640) * 3 # 计算字体大小(随框大小调整)
  189. 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()为了确保字体不过界
  190. cv2.imshow("result", image)
  191. cv2.waitKey()
  192. return image
  193. # 加载配置文件
  194. config_file = "my_datasets/my_datasets.yaml"
  195. with open(config_file, "r") as config:
  196. config = yaml.safe_load(config)
  197. if __name__ == '__main__':
  198. std_h, std_w = 640, 640 # 标准输入尺寸
  199. dic = config["names"] # 得到的是模型类别字典
  200. class_list = list(dic.values())
  201. input_path = "my_datasets/images/" # 输入图片的根目录路径
  202. img_path = "000000000074.jpg" # 输入图片的文件名
  203. img = cv2.imread(input_path+img_path)
  204. if img.size == 0:
  205. print("路径有误!")
  206. # 前处理
  207. img_after = resize_image(img, (std_w, std_h), True) # (6406403
  208. # 将图像处理成输入的格式
  209. data = img2input(img_after)
  210. # 输入模型
  211. sess = rt.InferenceSession('runs/detect/train49/weights/best.onnx') # yolov8模型onnx格式
  212. input_name = sess.get_inputs()[0].name
  213. label_name = sess.get_outputs()[0].name
  214. pred = sess.run([label_name], {input_name: data})[0] # 输出(8400x84, 84=80cls+4reg, 8400=3种尺度的特征图叠加), 这里的预测框的回归参数是xywh, 而不是中心点到框边界的距离
  215. pred = std_output(pred)
  216. # 置信度过滤+nms
  217. result = nms(pred, 0.5, 0.4) # [x,y,w,h,conf(最大类别概率),class]
  218. # 坐标变换
  219. result = cod_trf(result, img, img_after)
  220. image = draw(result, img, class_list)
  221. # 保存输出图像
  222. out_path = "./runs/my_predicts/"
  223. cv2.imwrite(out_path + img_path, image)
  224. cv2.destroyWindow("result")

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/130948
推荐阅读
相关标签
  

闽ICP备14008679号