YOLOv4 libtorch推理【附代码】

简介: 笔记

大致推理过程为:


1.定义model;


2.对输入图像shape进行调整并转为tensor   tensor_image;


3.将图像送入网络,获得输出张量output = model(tensor_image);


4.获得YOLO的三个head,对output中的边界框进行decode;


5.对4中得到的输出用confidence和NMS进行筛选;


6.对筛选出的输出在原图上绘制检测框和其他信息;


1.model定义:


可以通过定义YOLOV4类,在构造函数中传入model路径进行加载:


代码中的is_cuda_available()是用来判断cuda是否可用


model_path中的权重是将pytorch中的pth转pt文件

YOLOV4::YOLOV4(std::string& model_path)
{
  model = torch::jit::load(model_path);
  if (is_cuda_available())
  {
    model.to(torch::kCUDA);
  }
  else
  {
    model.to(torch::kCPU);
  }
}

2.image2tensor


先对图像进行裁剪(变成适合网络大小),下面代码是对图像进行不失真的reshape,并在reshape后的图像上增加“灰条”为的是让图像变为网络输入大小

cv::Mat letterbox_image(cv::Mat image, float size[])
{
  // 图片真实大小
  float iw = image.cols, ih = image.rows;
  // 网络输入图片的大小
  float w = size[0], h = size[1];
  float scale = std::min(w / iw, h / ih);
  // 调整后的大小
  int nw = int(iw * scale), nh = int(ih * scale);
  // 
  cv::resize(image, image, { nw, nh });
  // 创建图片
  cv::Mat new_image(w, h, CV_8UC3, cv::Scalar(128, 128, 128));
  // 设置画布绘制区域并复制
  cv::Rect roi_rect = cv::Rect((w - nw) / 2, (h - nh) / 2, nw, nh);
  image.copyTo(new_image(roi_rect));
  return new_image;
};

将图像从BGR转为RGB,并且将图像类型转为Tensor,再对维度进行改变,使其变成(batch_size,channels,W,H)

// 调整图片格式
cv::cvtColor(crop_img, crop_img, cv::COLOR_BGR2RGB);
crop_img.convertTo(crop_img, CV_32FC3, 1.f / 255.f);
// 转换为tensor
auto tensor_image = at::from_blob(crop_img.data, { 1, crop_img.rows, crop_img.cols, 3 }).to(torch::kCUDA);
  tensor_image = tensor_image.permute({ 0,3,1,2 }).contiguous();

3.获得输出张量output


model.forward()可用获得output,这里需要主要的是由于yolo的输出是个元组(out1,out2,out3),所以不能像分类和分割一样,直接用model.forward().toTensor(),而是要用toTuple(),这是libtorch中的一个需要注意的问题!


然后再将三个头部转为toTensor()


此刻三个output的shape为【batch_size,255,feature_map[0],feature_map[1]】

// 输入初始化
std::vector<torch::jit::IValue> input;
input.emplace_back(tensor_image);
auto outputs = model.forward(input).toTuple();
// 提取三个head
std::vector<at::Tensor> output(3);
output[0] = outputs->elements()[0].toTensor().to(at::kCPU);
output[1] = outputs->elements()[1].toTensor().to(at::kCPU);
output[2] = outputs->elements()[2].toTensor().to(at::kCPU);

4.对output中的边界框进行decode


这一部分很重要!先附上代码

    std::vector<at::Tensor> feature_out(3);
  for (size_t i = 0; i < 3; i++)
  {
    if (i == 0) feature_out[0] = yolo_decodes1.decode_box(output[0]);
    if (i == 1) feature_out[1] = yolo_decodes2.decode_box(output[1]);
    if (i == 2) feature_out[2] = yolo_decodes3.decode_box(output[2]);
  }
  // 在第二维度上做拼接, shape: (bs, 3*(13*13+26*26+52*52), 5+num_classes)
  at::Tensor out = at::cat({ feature_out[0], feature_out[1], feature_out[2] }, 1);

这里的yolo_decodes1..是通过DecodeBox定义的3个对象,里面的构造函数中已经传入了每个特征层的锚框和input_size。

DecodeBox的C++解码类的定义如下,anchors是锚框,3行2列:

class DecodeBox
{
public:
  DecodeBox(float t_anchors[][2], float t_image_size[]);
  at::Tensor decode_box(at::Tensor input);
private:
  /**************************************************************************
   * 存放每个特征层的先验框
   **************************************************************************/
  float anchors[3][2];
  float image_size[2];
  int num_anchors = 3;
  int num_classes = 80;
  int bbox_attrs = num_classes + 5;
};

构造函数中,t_anchors[][2]是锚框,分别对应不同尺寸的预测特征图,t_image_size[],是输入网络的图像大小


t_anchors和t_image_size[]如下:我这里的输入大小是416*416


float all_anchors[3][3][2] = { {{142, 110}, {192, 243}, {459, 401}},
          {{36,   75}, {76,   55}, {72,  146}},
          {{12,   16}, {19,   36}, {40,   28}} };
float model_image_size[2] = { 416, 416 };

构造函数的实现如下, 主要是实现获取每层锚框参数,以及获得input_shape,并将这些参数赋值给DecodeBox类中私有成员变量anchors和image_size。


DecodeBox::DecodeBox(float t_layer_anchors[][2], float t_model_image_size[])
{
  // 获取当前特征图的anchor参数
  for (size_t i = 0; i < 3; i++)
  for (size_t j = 0; j < 2; j++)
    anchors[i][j] = t_layer_anchors[i][j];
  // 获取模型中图像的尺寸
  for (size_t i = 0; i < 2; i++)
  image_size[i] = t_model_image_size[i];
}

Decode中定义decode_box函数,输入类型为张量(即将model(image)的输出作为输入)。大致过程为:


1.获取input的参数  ,input.shape = 【batch_size,255,feature_map[0],feature_map[1]】


2.计算缩放比例(步长)


3.计算每个特征层缩放后对应的尺寸


4.将[batch_size,255,feature_map[0],feature_map[1]]  转[batch_size,3,feature_map[0],feature_map[1],5+num_classes]


5.prediction的前4个参数对应box参数,需要进行调整,第5个参数为置信度,剩下的对应分类概率


在libtorch中张量类似prediction[5:]切片用


prediction.index({ "...",torch::indexing::Slice{5, torch::indexing::None} })


6.对特征层划分网格,生成先验框,将tensor转数组,shape bs,3,h,w


7.对先验框进行调整后,得到最终的output  (batch_size, (x, y, w, h, conf, pred_cls))=【bs,6】


at::Tensor DecodeBox::decode_box(at::Tensor input)

{
  // 获取尺寸等参数
  int batch_size = input.size(0);
  int input_height = input.size(2);
  int input_width = input.size(3);
  // 步长
  int stride_w = image_size[0] / input_width;
  int stride_h = image_size[1] / input_height;
  // 此时获得的scaled_anchors大小是相对于每个特征层的
  float scaled_anchors[3][2];
  for (int i = 0; i < 3; i++)
  {
  scaled_anchors[i][0] = anchors[i][0] / stride_w;
  scaled_anchors[i][1] = anchors[i][1] / stride_h;
  }
  // (bs, 3*(5+num_classes), h, w)  -->  (bs, 3, h, w, (5+num_classes))
  at::Tensor prediction = input.view({ batch_size, num_anchors, bbox_attrs, input_height, input_width }).permute({ 0, 1, 3, 4, 2 }).contiguous();
  // 先验框中心位置的调整参数
  at::Tensor x = at::sigmoid(prediction.index({ "...", 0 }));
  at::Tensor y = at::sigmoid(prediction.index({ "...", 1 }));
  // 先验框宽高参数调整
  at::Tensor w = prediction.index({ "...", 2 });
  at::Tensor h = prediction.index({ "...", 3 });
  // 置信度获取
  at::Tensor conf = at::sigmoid(prediction.index({ "...", 4 }));
  // 物体类别置信度
  at::Tensor pred_cls = at::sigmoid(prediction.index({ "...",torch::indexing::Slice{5, torch::indexing::None} }));
  // 生成网格  先验框中心,网格左上角 bs, 3, h, w
  at::Tensor grid_x = at::linspace(0, input_width - 1, input_width).repeat({ input_width, 1 }).repeat({ batch_size*num_anchors, 1, 1 }).view({ x.sizes() }).toType(torch::kFloat);
  at::Tensor grid_y = at::linspace(0, input_height - 1, input_height).repeat({ input_height, 1 }).t().repeat({ batch_size*num_anchors, 1, 1 }).view({ y.sizes() }).toType(torch::kFloat);
  // 按照网格格式生成先验框的宽高  数组转换为tensor   最终shape  bs, 3, h, w
  at::Tensor anchor_w = at::from_blob(scaled_anchors, { 3, 2 }, at::kFloat).index_select(1, at::tensor(0).toType(at::kLong))\
  .repeat({ batch_size, input_height*input_width }).view(w.sizes());
  at::Tensor anchor_h = at::from_blob(scaled_anchors, { 3, 2 }, at::kFloat).index_select(1, at::tensor(1).toType(at::kLong))\
  .repeat({ batch_size, input_height*input_width }).view(h.sizes());
  /*
  利用预测结果对先验框进行调整
    首先调整先验框的中心,从先验框中心向右下角偏移再调整先验框的宽高。
  */
  at::Tensor pred_boxes = at::zeros({ prediction.index({"...", torch::indexing::Slice({torch::indexing::None, 4})}).sizes() }).toType(at::kFloat);
  // 填充调整到特征图上的尺寸值
  pred_boxes.index_put_({ "...", 0 }, (x.data() + grid_x));
  pred_boxes.index_put_({ "...", 1 }, (y.data() + grid_y));
  pred_boxes.index_put_({ "...", 2 }, (at::exp(w.data()) * anchor_w));
  pred_boxes.index_put_({ "...", 3 }, (at::exp(h.data()) * anchor_h));
  // 生成转换tensor  (batch_size, 6) -->  (batch_size, (x, y, w, h, conf, pred_cls))
  at::Tensor _scale = at::tensor({ stride_w, stride_h, stride_w, stride_h }).toType(at::kFloat);
  //输出结果重新拼接
  at::Tensor output = at::cat({ pred_boxes.view({batch_size, -1, 4}) * _scale, \
        conf.view({batch_size, -1, 1}), \
        pred_cls.view({batch_size, -1, num_classes}) }, - 1);
  return output.data();
};


5.用confidence和NMS进行筛选


先要计算出输出的框坐标,为后面计算NMS iou做准备。在之前需要通过阈值对置信度进行筛选。代码中的nms用的官方提供的。

std::vector<at::Tensor> yolo_nms(at::Tensor prediction, int num_classes, float conf_thres, float nms_thres)
{
  //(bs, 3*(13*13+26*26+52*52), 5+num_classes)
  at::Tensor box_corner = at::zeros(prediction.sizes());
  // 求左上角和右下角
  box_corner.index_put_({ "...", 0 }, prediction.index({ "...", 0 }) - prediction.index({ "...", 2 }) / 2);
  box_corner.index_put_({ "...", 1 }, prediction.index({ "...", 1 }) - prediction.index({ "...", 3 }) / 2);
  box_corner.index_put_({ "...", 2 }, prediction.index({ "...", 0 }) + prediction.index({ "...", 2 }) / 2);
  box_corner.index_put_({ "...", 3 }, prediction.index({ "...", 1 }) + prediction.index({ "...", 3 }) / 2);
  // 赋值 x1 y1 x2 y2
  prediction.index_put_({ "...", torch::indexing::Slice(torch::indexing::None,4) }, box_corner.index({ "...", torch::indexing::Slice(torch::indexing::None,4) }));
  std::vector<at::Tensor> nms_output;
  at::Tensor output = prediction[0];
  std::tuple<at::Tensor, at::Tensor> temp = at::max(output.index({ "...", torch::indexing::Slice(5, 5 + num_classes) }), 1, true);
  at::Tensor class_conf = std::get<0>(temp);
  at::Tensor class_pred = std::get<1>(temp);
  // 利用置信度筛选
  at::Tensor conf_mask = (output.index({ "...", 4 }) * class_conf.index({ "...", 0 }) >= conf_thres).squeeze();
  // 留下有目标的部分
  output = output.index({ conf_mask });
  // 没有目标,直接返回空结果
  if (output.size(0) == 0)
  {
    return nms_output;
  }
  class_conf = class_conf.index({ conf_mask });
  class_pred = class_pred.index({ conf_mask });
  // 获得的内容为(x1, y1, x2, y2, obj_conf, class_conf, class_pred)
  at::Tensor detections = at::cat({ output.index({"...", torch::indexing::Slice(torch::indexing::None, 5)}), class_conf.toType(at::kFloat), class_pred.toType(at::kFloat) }, -1);
  std::tuple<at::Tensor, at::Tensor, at::Tensor> unique_labels_tuple = at::unique_consecutive(detections.index({ "...", -1 }));
  at::Tensor unique_labels = std::get<0>(unique_labels_tuple);
  // 遍历所有的种类
  for (int i = 0; i < unique_labels.size(0); i++)
  {
    // 获取某个类初步筛选后的预测结果
    at::Tensor detections_class = detections.index({ detections.index({"...", -1}) == unique_labels[i] });
    at::Tensor keep = nms_cpu(detections_class.index({ "...", torch::indexing::Slice(torch::indexing::None,4) }), detections_class.index({ "...", 4 })*detections_class.index({ "...", 5 }), nms_thres);
    at::Tensor max_detection = detections_class.index({ keep });
    if (i == 0)
    {
      nms_output.push_back(max_detection);
    }
    else
    {
      nms_output[0] = at::cat({ nms_output[0], max_detection });
    }
  }
  return nms_output;

6.绘制检测框和其他信息


void YOLOV4::Show_Detection_Restults(cv::Mat image,std::vector < std::vector<float>>boxes, std::vector<std::string>class_names,std::string mode)
{
  for (size_t i = 0; i < boxes.size(); i++)
  {
    // 打印种类及位置信息
    std::cout << i + 1 << "、" << class_names[int(boxes[i][5])] << ": (xmin:" \
      << boxes[i][1] << ", ymin:" << boxes[i][0] << ", xmax:" << boxes[i][3] << ", ymax:" << boxes[i][2] << ") --" \
      << "confidence: " << boxes[i][4] << std::endl;
    // 计算位置
    cv::Rect rect(int(boxes[i][1]), int(boxes[i][0]), int(boxes[i][3] - boxes[i][1]), int(boxes[i][2] - boxes[i][0]));
    cv::rectangle(image, rect, cv::Scalar(0, 0, 255), 1, cv::LINE_8, 0);
    // 获取文本框的大小
    cv::Size text_size = cv::getTextSize(class_names[int(boxes[i][5])], fontFace, fontScale, thickness, &baseline);
    // 绘制的起点
    cv::Point origin;
    origin.x = int(boxes[i][1]);
    origin.y = int(boxes[i][0]) + text_size.height;
    // cv::putText(InputOutputArray img, const String &text, Point org, int fontFace, double fontScale, Scalar color)
    cv::putText(image, class_names[int(boxes[i][5])], origin, fontFace, fontScale, cv::Scalar(0, 0, 255), thickness);
    // 置信度显示
    std::string text = std::to_string(boxes[i][4]);
    text = text.substr(0, 5);
    cv::Size text_size2 = cv::getTextSize(text, fontFace, fontScale, thickness, &baseline);
    origin.x = origin.x + text_size.width + 3;
    origin.y = int(boxes[i][0]) + text_size2.height;
    cv::putText(image, text, origin, fontFace, fontScale, cv::Scalar(0, 0, 255), thickness);
  }
  // 如果没检测到任何目标
  if (boxes.size() == 0)
  {
    const std::string text = "NO object";
    float fontScale = 2.0;
    // 获取文本框的大小
    cv::Size text_size = cv::getTextSize(text, fontFace, fontScale, thickness, &baseline);
    std::cout << "no target detected!" << std::endl;
    cv::Point origin; // 绘制的起点
    origin.x = 0;
    origin.y = 0 + text_size.height;
    // cv::putText(InputOutputArray img, const String &text, Point org, int fontFace, double fontScale, Scalar color)
    cv::putText(image, text, origin, fontFace, fontScale, cv::Scalar(255, 0, 0), thickness);
  }
};

1.jpeg

pth权重转为pt,将权重放在与main.cpp同一路径下(放在别的地方也可用,但需要将路径填写正确,建议绝对路径且不要有中文),同时填写类的txt文件路径,比如我这里用的coco_classes.txt。mode是预测模式,如果是image,表示预测图像,并在image_path填写路径,如果是video,为视频预测,填写image_path视频路径。


   std::string model_path = "./yolov4.pt";

   std::string image_path = "street.jpg";

   std::string classes_path = "./coco_classes.txt";

   std::string mode = "image";


如果是采用的自己的数据集,utils.h中修改NUM_CLASSES,conf_thres和nms_thres也可用进行修改以及INPUT_SHAPE。


不过视频推理的时候,发现效果并不是多好,应该还需要硬件加速处理,libtorch也没有感觉倒明显的优势,后面会再研究研究trt


目录
相关文章
|
6月前
|
机器学习/深度学习 PyTorch 算法框架/工具
【PyTorch实战演练】使用Cifar10数据集训练LeNet5网络并实现图像分类(附代码)
【PyTorch实战演练】使用Cifar10数据集训练LeNet5网络并实现图像分类(附代码)
415 0
|
1月前
|
数据处理 算法框架/工具 计算机视觉
手把手教你使用YOLOV5训练自己的目标检测模型
本教程由肆十二(dejahu)撰写,详细介绍了如何使用YOLOV5训练口罩检测模型,涵盖环境配置、数据标注、模型训练、评估与使用等环节,适合大作业及毕业设计参考。提供B站视频、CSDN博客及代码资源链接,便于学习实践。
84 1
手把手教你使用YOLOV5训练自己的目标检测模型
|
1月前
|
计算机视觉
目标检测笔记(二):测试YOLOv5各模块的推理速度
这篇文章是关于如何测试YOLOv5中不同模块(如SPP和SPPF)的推理速度,并通过代码示例展示了如何进行性能分析。
79 3
|
5月前
|
机器学习/深度学习 计算机视觉
【机器学习】YOLOv10与YOLOv8分析
【机器学习】YOLOv10与YOLOv8分析
1165 6
|
5月前
|
计算机视觉
【YOLOv10训练教程】如何使用YOLOv10训练自己的数据集并且推理使用
【YOLOv10训练教程】如何使用YOLOv10训练自己的数据集并且推理使用
|
6月前
|
机器学习/深度学习 算法 PyTorch
基于Pytorch用GAN生成手写数字实例(附代码)
基于Pytorch用GAN生成手写数字实例(附代码)
150 0
|
6月前
|
机器学习/深度学习 数据采集 PyTorch
PyTorch搭建卷积神经网络(ResNet-50网络)进行图像分类实战(附源码和数据集)
PyTorch搭建卷积神经网络(ResNet-50网络)进行图像分类实战(附源码和数据集)
222 1
|
算法 PyTorch 调度
ResNet 高精度预训练模型在 MMDetection 中的最佳实践
作为最常见的骨干网络,ResNet 在目标检测算法中起到了至关重要的作用。许多目标检测经典算法,如 RetinaNet 、Faster R-CNN 和 Mask R-CNN 等都是以 ResNet 为骨干网络,并在此基础上进行调优。同时,大部分后续改进算法都会以 RetinaNet 、Faster R-CNN 和 Mask R-CNN 为 baseline 进行公平对比。
906 0
ResNet 高精度预训练模型在 MMDetection 中的最佳实践
|
人工智能 并行计算 计算机视觉
|
机器学习/深度学习 自然语言处理 PyTorch
【Pytorch神经网络实战案例】33 使用BERT模型实现完形填空任务
案例:加载Transformers库中的BERT模型,并用它实现完形填空任务,即预测一个句子中缺失的单词。
561 0