YOLOv4 libtorch推理【附代码】

目标检测中libtorch推理与图像分类和分割不同,目标检测需要对边界框进行解码。

本项目支持CPU、GPU推理,剪枝后的模型也可用进行推理。

环境配置可见另一篇文章:

使用TorchScript和libtorch进行模型推理[附C++代码]_爱吃肉的鹏的博客-CSDN博客_libtorch模型推理


环境

windows 10

pytorch:1.7.0(低版本应该也可以)

libtorch 1.7 Debug版

cuda 10.2

VS 2017

英伟达 1650 4G


目录

大致推理过程为:

1.model定义:

 2.image2tensor

 3.获得输出张量output

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

5.用confidence和NMS进行筛选

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

 完整代码


 

大致推理过程为:

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 input;
input.emplace_back(tensor_image);

auto outputs = model.forward(input).toTuple();
// 提取三个head
std::vector 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 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 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 nms_output;
	at::Tensor output = prediction[0];
	std::tuple 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 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>boxes, std::vectorclass_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);
	}

};


YOLOv4 libtorch推理【附代码】_第1张图片


 完整代码

github:

YINYIPENG-EN/YOLOv4_libtorch · GitHubSupport CPU and GPU inference, and the pruned model can also be used for inference. - YINYIPENG-EN/YOLOv4_libtorchhttps://github.com/YINYIPENG-EN/YOLOv4_libtorch.git

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

你可能感兴趣的:(libtorch,深度学习,人工智能,python)