1 源码地址

github: opencv c++

2 运行github代码

2.1 推理条件

  • windows 10
  • Visual Studio 2019
  • Nvidia GeForce GTX 1070
  • opencv4.7.0 (opencv4.5.5在别的地方看到不支持yolov8的推理,所以只使用opencv4.7.0)

2.2 c++部署

先将源码复制到下图位置中
在这里插入图片描述

环境和代码的大致步骤跟yolov5 opencv dnn部署 github代码一样,环境部署好之后

选择“release x64”
在这里插入图片描述
在选择编译环境之后,还需要对文件路径进行修改

"Resources/classes.txt" -> "YOLO_cpp/Resurces/classes.txt"

另外还需要特别需要修改的一个地方是load_net,如果不修改这个代码,使用cpu可以运行程序,使用gpu可以运行程序,但是gpu运行的速度特别慢。

void load_net(cv::dnn::Net& net, bool is_cuda)
{
    auto result = cv::dnn::readNet("YOLO_cpp/Resources/yolov8s.onnx");

    if (is_cuda)
    {
        std::cout << "Attempty to use CUDA\n";
        result.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
        result.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA_FP16);  // 这里一定要修改为下面的这行
        // result.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA)
    }
    else
    {
        std::cout << "Running on CPU\n";
        result.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
        result.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
    }
    net = result;
}

然后直接运行yolov8.cpp,不用做任何修改

2.3 c++推理结果

在这里插入图片描述

3 部署custom yolov8

3.1 推理条件

同2.1

3.2 导出yolov8模型

yolov8版本: version = ‘8.0.110’
首先将default.yaml中的一些配置修改以下,将只修改的部分贴上去,注意下面的batch一定要设置为1

task: detect  # YOLO task, i.e. detect, segment, classify, pose
mode: export  # YOLO mode, i.e. train, val, predict, export, track, benchmark

# Train settings -------------------------------------------------------------------------------------------------------
# model: C:\Users\HUST\Desktop\yolov8_ultralytics\ultralytics\models\v8\yolov8.yaml # path to model file, i.e. yolov8n.pt, yolov8n.yaml
model: C:\Users\Administrator\Desktop\yolov8_ultralytics\runs\detect\yolov8n\weights\best.pt # path to model file, i.e. yolov8n.pt, yolov8n.yaml
data: C:\Users\Administrator\Desktop\yolov8_ultralytics/ultralytics/datasets/custom.yaml # path to data file, i.e. coco128.yaml
weights: yolov8n.pt
epochs: 1  # number of epochs to train for
patience: 50  # epochs to wait for no observable improvement for early stopping of training
batch: 1  # number of images per batch (-1 for AutoBatch)

default.yaml中的export部分的配置也需要修改

# Export settings ------------------------------------------------------------------------------------------------------
format: onnx  # format to export to
keras: False  # use Keras
optimize: False  # TorchScript: optimize for mobile
int8: False  # CoreML/TF INT8 quantization
dynamic: False  # ONNX/TF/TensorRT: dynamic axes
simplify: False  # ONNX: simplify model
opset: 12 # ONNX: opset version (optional)
workspace: 4  # TensorRT: workspace size (GB)
nms: False  # CoreML: add NMS

然后直接运行ultralytics/yolo/engine/exporter.py
在这里插入图片描述
测试一下导出的best.onnx可不可用,直接正常的val即可
在这里插入图片描述
将best.onnx模型放入netron中,onnx的输入和输出如下图1所示
在这里插入图片描述
在这里插入图片描述 图 1 图1 1

3.3 c++部署

先将源码复制到下图位置中
在这里插入图片描述

环境和代码的大致步骤跟yolov5 opencv dnn部署 github代码一样

由于源码中使用的输入尺寸如图2是640 * 480的,我导出模型时使用的模型的输入如图1是640 * 640,所以需要对尺寸的那一部分需要进行修改,修改为640 * 640

const float INPUT_WIDTH = 640.0;
const float INPUT_HEIGHT = 640.0;
const float SCORE_THRESHOLD = 0.45;
const float NMS_THRESHOLD = 0.5;
const float CONFIDENCE_THRESHOLD = 0.25;

在这里插入图片描述
在这里插入图片描述 图 2 图2 2在进行修改之后,就可以直接运行yolo.cpp

3.4 c++推理结果

yolov8_deploy_fire

4 github代码介绍

4.1 yolov8与yolov5对比

SCORE_THRESHOLD, NMS_THRESHOLD的阈值选择,由于yolov8是free anchor,因此yolov8的输出没有conf,只有xyxy+cls_score,因此,是nms直接将cls_socre中最大的且大于SCORE_THRESHOLD当作conf处理。因此在github代码中post_process函数中已经不需要CONFIDENCE_THRESHOLD

yolov8

const std::vector<cv::Scalar> colors = { cv::Scalar(255, 255, 0), cv::Scalar(0, 255, 0), cv::Scalar(0, 255, 255), cv::Scalar(255, 0, 0) };

const float INPUT_WIDTH = 640.0;
const float INPUT_HEIGHT = 640.0;
const float SCORE_THRESHOLD = 0.45;
const float NMS_THRESHOLD = 0.5;
const float CONFIDENCE_THRESHOLD = 0.25;  // 不需要这个, free anchor没有conf,是将cls_score当作conf

Mat post_process(Mat& input_image, vector<Mat>& outputs, const vector<string>& class_name)
{
    // Initialize vectors to hold respective outputs while unwrapping     detections.
    vector<int> class_ids;
    vector<float> confidences;
    vector<Rect> boxes;

    int rows = outputs[0].size[2];
    int dimensions = outputs[0].size[1];

    outputs[0] = outputs[0].reshape(1, dimensions);
    cv::transpose(outputs[0], outputs[0]);

    float* data = (float*)outputs[0].data;

    // Resizing factor.
    float x_factor = input_image.cols / INPUT_WIDTH;
    float y_factor = input_image.rows / INPUT_HEIGHT;

    // Iterate through  detections.
    //cout << "num detections  : " << rows << " " << dimensions << endl;
    for (int i = 0; i < rows; ++i)
    {
        float* classes_scores = data + 4;

        cv::Mat scores(1, class_name.size(), CV_32FC1, classes_scores);
        cv::Point class_id;
        double maxClassScore;

        minMaxLoc(scores, 0, &maxClassScore, 0, &class_id);  // 这里选取cls_score中的max

        if (maxClassScore > SCORE_THRESHOLD)  // 只保留大于SCORE_THRESHOLD的预测框
        {
            confidences.push_back(maxClassScore);
            class_ids.push_back(class_id.x);

            float x = data[0];
            float y = data[1];
            float w = data[2];
            float h = data[3];

            int left = int((x - 0.5 * w) * x_factor);
            int top = int((y - 0.5 * h) * y_factor);

            int width = int(w * x_factor);
            int height = int(h * y_factor);

            boxes.push_back(cv::Rect(left, top, width, height));
        }

        data += dimensions;
    }
    // Perform Non-Maximum Suppression and draw predictions.
    vector<int> indices;
    NMSBoxes(boxes, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, indices);
    //cout << "num detections finally : "<< indices.size() <<endl;
    for (int i = 0; i < indices.size(); i++)
    {
        int idx = indices[i];
        Rect box = boxes[idx];
        int left = box.x;
        int top = box.y;
        int width = box.width;
        int height = box.height;
        // Draw bounding box.
        rectangle(input_image, Point(left, top), Point(left + width, top + height), BLUE, 3 * THICKNESS);
        // Get the label for the class name and its confidence.
        string label = format("%.2f", confidences[idx]);
        label = class_name[class_ids[idx]] + ":" + label;
        // Draw class labels.
        draw_label(input_image, label, left, top);
    }
    return input_image;
}

yolov5

const std::vector<cv::Scalar> colors = {cv::Scalar(255, 255, 0), cv::Scalar(0, 255, 0), cv::Scalar(0, 255, 255), cv::Scalar(255, 0, 0)};

const float INPUT_WIDTH = 640.0;
const float INPUT_HEIGHT = 640.0;
const float SCORE_THRESHOLD = 0.2;
const float NMS_THRESHOLD = 0.4;
const float CONFIDENCE_THRESHOLD = 0.4;

void detect(cv::Mat &image, cv::dnn::Net &net, std::vector<Detection> &output, const std::vector<std::string> &className) {
    cv::Mat blob;

    auto input_image = format_yolov5(image);
    
    cv::dnn::blobFromImage(input_image, blob, 1./255., cv::Size(INPUT_WIDTH, INPUT_HEIGHT), cv::Scalar(), true, false);
    net.setInput(blob);
    std::vector<cv::Mat> outputs;
    // 添加代码,使得opencv4.7.0可以使用
    net.enableWinograd(false);
    net.forward(outputs, net.getUnconnectedOutLayersNames());

    float x_factor = input_image.cols / INPUT_WIDTH;
    float y_factor = input_image.rows / INPUT_HEIGHT;
    
    float *data = (float *)outputs[0].data;

    const int dimensions = 85;
    const int rows = 25200;
    const int max_wh = 768;  // 这个值是偏移量,这个酌情选择,不然太大会导致dnn:nms不工作
    // 添加代码
    int out_dim2 = outputs[0].size[2]; // 这里的是class+conf+xywh,相当于COCO的指标的85
    
    std::vector<int> class_ids;
    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;
    std::vector<cv::Rect> boxes_muti;

    for (int i = 0; i < rows; ++i) {
        // 添加代码
        int index = i * out_dim2; // 每一次循环索引都是下一个pre_box的初始位置
        float confidence = data[4 + index]; // 修改代码 这样读取的值就是下一个的pre_box的conf

        if (confidence >= CONFIDENCE_THRESHOLD) {
            // 修改代码 这样读取的值就是下一个的pre_box的class
            float * classes_scores = data + 5 + index;

            cv::Mat scores(1, className.size(), CV_32FC1, classes_scores);
            cv::Point class_id;
            double max_class_score;
            minMaxLoc(scores, 0, &max_class_score, 0, &class_id);
            max_class_score *= confidence;  // conf = obj_conf * cls_conf
            if (max_class_score > SCORE_THRESHOLD) {

                confidences.push_back(max_class_score);
                class_ids.push_back(class_id.x);
                // 修改代码,这样读取的值就是下一个的pre_box的xywh
                float x = data[0 + index];
                float y = data[1 + index];
                float w = data[2 + index];
                float h = data[3 + index];

                int left = int((x - 0.5 * w) * x_factor);
                int top = int((y - 0.5 * h) * y_factor);
                int width = int(w * x_factor);
                int height = int(h * y_factor);
                boxes.push_back(cv::Rect(left, top, width, height));

                // 实现多分类NMS,如果不需要实现,就直接删掉该部分
                // 在这里添加的是类似yolov5nms的class_id位置偏移
                int left_muti = int((x - 0.5 * w) * x_factor + class_id.x * max_wh);
                int top_muti = int((y - 0.5 * h) * y_factor + class_id.x * max_wh);
                int width_muti = int(w * x_factor + class_id.x * max_wh);
                int height_muti = int(h * y_factor + class_id.x * max_wh);
                boxes_muti.push_back(cv::Rect(left_muti, top_muti, width_muti, height_muti));
            }
        }
    }
    std::vector<int> nms_result;
    cv::dnn::NMSBoxes(boxes_muti, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, nms_result);
    for (int i = 0; i < nms_result.size(); i++) {
        int idx = nms_result[i];
        Detection result;
        result.class_id = class_ids[idx];
        result.confidence = confidences[idx];
        result.box = boxes[idx];
        output.push_back(result);
    }
}

4.2 blobFromImage 与 blobFromImages

blobFromImage是将单张图片转换成dnn可以推理的输入格式(1X3X640X640),使用下面代码可以打印出img经过cv::dnn::blobFromImage变换后的blob信息。
blobFromImages是将vector中的多张图片转化为dnn可以推理的输入格式(NXCX640X640)。

int main() {
	int INPUT_WIDTH = 640;
	int INPUT_HEIGHT = 640;
	cv::Mat blob;
	cv::Mat img = cv::imread("C:\\Users\\Administrator\\Desktop\\1.png", cv::IMREAD_COLOR);
	std::cout << "img shape: " << img.size << std::endl;  // 644 x 287 (W x H)
	std::cout << "img channels: " << img.channels() << std::endl;  // 打印通道数,彩色为3
	cv::dnn::blobFromImage(img, blob, 1. / 255., cv::Size(INPUT_WIDTH, INPUT_HEIGHT), cv::Scalar(), true, false);
	std::cout << "Blob shape: " << blob.size << std::endl;  // 1 x 3 x 640 x640 (N x C x W x H)
	std::cout << "Blob channels: " << blob.channels() << std::endl;  // 打印通道数, 为1

	const float* blob_data = reinterpret_cast<const float*>(blob.ptr<float>(0));
	int blob_size = blob.total() * blob.elemSize1();

	// 仅打印前10个元素
	for (int i = 0; i < std::min(blob_size, 100); ++i) {
		std::cout << blob_data[i] << " ";
	}
	std::cout << std::endl;
	
	//std::cout << "==Python风格==\n" << cv::format(img.rowRange(0, 1), cv::Formatter::FMT_PYTHON) << std::endl;
	cv::imshow("Image", img);
	cv::waitKey(0);
}

reshape的用法, cv::Mat::reshape(int cn, int rows = 0) constcn是新的通道数,rows是新的行数。

int main() {
	// 创建一个 1x3x640x640 的矩阵
	int sizes[] = { 1, 3, 640, 640 };
	cv::Mat blob(4, sizes, CV_32F); // 创建一个 1x3x640x640 的矩阵

	// 重塑为单通道矩阵
	cv::Mat reshaped1 = blob.reshape(1);  // 1 x 3 x 640 x 640
	std::cout << "Reshaped to 1 channel: " << reshaped1.size << std::endl;

	// 重塑为 3 个通道和 640 行的矩阵
	cv::Mat reshaped2 = blob.reshape(1, 3);  // 3 x 409600
	std::cout << "Reshaped to 1 channels and 3 rows: " << reshaped2.size << std::endl;

	cv::Mat reshaped3 = blob.reshape(1, std::vector<int>({ 3, 640, 640 })); // 3 x 640 x640
	std::cout << "Reshaped to 1 channels: " << reshaped3.size << std::endl;

	return 0;
}

5 参考链接

另外两个yolov8推理的代码参考链接

  1. https://github.com/hpc203/yolov8-face-landmarks-opencv-dnn
  2. https://github.com/UNeedCryDear/yolov8-opencv-onnxruntime-cpp
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐