yolov5 opencv dnn

1 源码地址

  1. yolov5官网还提供的dnn、tensorrt推理链接
  2. opencv c++ github代码

2 运行github代码

2.1 推理条件

实现推理code中作者的yolov5s.onnx

  • windows 10
  • Visual Studio 2019
  • Nvidia GeForce GTX 1070
  • opencv 4.5.5、opencv4.7.0 (注意 4.7.0代码不适用,如果要使用opencv4.7.0来进行推理,可能会出现下面的问题图1 problem中的问题)(但是,如果添加了之后,4.7.0的推理速度会比4.5.5的速度慢了不少)
    请添加图片描述 图 1 p r o b l e m 图1 problem 1problem
    解决方法请添加图片描述 图 1 s o l v t i o n 图1 solvtion 1solvtion

2.2 python部署(因为python比较简单就直接介绍了)

一、直接用VScode打开代码
二、然后在终端输入命令
python python/yolo.py
请添加图片描述
三、结果如下图所示
请添加图片描述

2.3 c++部署

当然不管是使用opencv dnn的cpu还是gpu都得创建相应的环境,这里先不做介绍,以后有时间再介绍。
一、使用VS2019创建一个新的项目,这里不做过多赘述
二、该项目搭建公共的opencv属性,或者使用已搭建好的公共的opencv455属性(以opencv455为例,添加其它的也是这样的,例如opencv455_cuda等)
这里如果使用公共的opencv455属性,只需要在新建的c++空项目中使用以下步骤即可添加
1、 “属性管理器”——>“鼠标右键点击Release|x64”——>“添加现有属性表(E)”
在这里插入图片描述
2、 选在对应的已创建好的属性表,然后点击“打开”
在这里插入图片描述
3、添加后的结果
在这里插入图片描述
三、将code中的下列文件复制到新建的项目中的repos/Project4/Project4中,如下图所示
在这里插入图片描述
四、将code中的cpp/yolo.cpp添加到新建项目的源文件中,添加过程和结果如下图所示
在这里插入图片描述
结果
在这里插入图片描述
五、使用x64进行Release,结果如下图所示
1、opencv4.5.5推理结果
在这里插入图片描述
2、opencv4.7.0推理结果
在这里插入图片描述

六、yolo.cpp的代码在这(本人应该没做改动吧,忘记了)

#include <fstream>

#include <opencv2/opencv.hpp>

/*下文所有注释全是自我理解*/

/*加载classes.txt*/
std::vector<std::string> load_class_list()
{
    std::vector<std::string> class_list;  // 该行代码可以理解为申明一个可变容量的字符串数组class_list
    std::ifstream ifs("config_files/classes.txt");
    std::string line;
    while (getline(ifs, line))
    {
        class_list.push_back(line);
    }
    return class_list;
}

void load_net(cv::dnn::Net& net, bool is_cuda)
{
    auto result = cv::dnn::readNet("config_files/yolov5s.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);
        // 经过验证,这里必须去掉FP16,才能使用cuda加速,就是yolov5导出的是half onnx也不行,opencv只能读取16,但是还是按照32运行的
    }
    else
    {
        std::cout << "Running on CPU\n";
        result.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
        result.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
    }
    net = result;
}

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; // NMS中的score阈值(一般来说,这个应该是conf * class的阈值)
const float NMS_THRESHOLD = 0.4;   // NMS中的IoU阈值
const float CONFIDENCE_THRESHOLD = 0.4; // conf阈值 (class, conf, x, y, w, h)

struct Detection
{
    int class_id;
    float confidence;
    cv::Rect box;
};

/*这个作用是将需要预测的图片都变为正方形图片,以左上角对齐,将短的边都填充0*/
cv::Mat format_yolov5(const cv::Mat& source) {
    int col = source.cols;
    int row = source.rows;
    int _max = MAX(col, row);
    cv::Mat result = cv::Mat::zeros(_max, _max, CV_8UC3);
    source.copyTo(result(cv::Rect(0, 0, col, row)));
    return result;
}

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);  // 将预测图片resize到640,并将所有的像素都归一化
    net.setInput(blob);
    std::vector<cv::Mat> outputs;  // 预测框作为一个矩阵保存在Mat,这个可变数组永远只有一个Mat,因为一个Mat即可保存所有的预测框,类似于图片[1, H, W],所以下文中outputs[0].data是所有预测框的地址
    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; // 其实就是COCO的class + conf + xywh
    const int rows = 25200;    // pre_box的数量小于25200

    std::vector<int> class_ids;
    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;

    // 原始的NMS使用的非极大值抑制,并不是yolov5中的多分类非极大值抑制
    for (int i = 0; i < rows; ++i) {

        float confidence = data[4];
        if (confidence >= CONFIDENCE_THRESHOLD) {

            float* classes_scores = data + 5;
            cv::Mat scores(1, className.size(), CV_32FC1, classes_scores); // 将classes_scores转化为一个Mat格式的数据
            cv::Point class_id;
            double max_class_score;
            minMaxLoc(scores, 0, &max_class_score, 0, &class_id); // 将scores中的最大的值以及其id分别赋给max_class_score, class_id
            if (max_class_score > SCORE_THRESHOLD) {

                confidences.push_back(confidence);

                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 += 85;  // 这个是cv::Mat中的每一个[x, y, w, h, conf, class]的首地址,一个预测框的首地址都加85[xywh+conf+class]即[5+80]后就是下一个预测框的首地址

    }

    std::vector<int> nms_result;
    cv::dnn::NMSBoxes(boxes, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, nms_result);  // 将NMS筛选之后的索引返回给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);
    }
}

int main(int argc, char** argv)
{

    std::vector<std::string> class_list = load_class_list();

    cv::Mat frame;
    cv::VideoCapture capture("sample.mp4");
    if (!capture.isOpened())
    {
        std::cerr << "Error opening video file\n";
        return -1;
    }

    //bool is_cuda = argc > 1 && strcmp(argv[1], "cuda") == 0;
    bool is_cuda = true;

    cv::dnn::Net net;
    load_net(net, is_cuda);

    auto start = std::chrono::high_resolution_clock::now();
    int frame_count = 0;
    float fps = -1;
    int total_frames = 0;

    while (true)
    {
        capture.read(frame);
        if (frame.empty())
        {
            std::cout << "End of stream\n";
            break;
        }

        std::vector<Detection> output;
        detect(frame, net, output, class_list);

        frame_count++;
        total_frames++;

        int detections = output.size();

        for (int i = 0; i < detections; ++i)
        {

            auto detection = output[i];
            auto box = detection.box;
            auto classId = detection.class_id;
            const auto color = colors[classId % colors.size()];
            cv::rectangle(frame, box, color, 3);

            cv::rectangle(frame, cv::Point(box.x, box.y - 20), cv::Point(box.x + box.width, box.y), color, cv::FILLED);
            cv::putText(frame, class_list[classId].c_str(), cv::Point(box.x, box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
        }

        if (frame_count >= 30)
        {

            auto end = std::chrono::high_resolution_clock::now();
            fps = frame_count * 1000.0 / std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

            frame_count = 0;
            start = std::chrono::high_resolution_clock::now();
        }

        if (fps > 0)
        {

            std::ostringstream fps_label;
            fps_label << std::fixed << std::setprecision(2);
            fps_label << "FPS: " << fps;
            std::string fps_label_str = fps_label.str();

            cv::putText(frame, fps_label_str.c_str(), cv::Point(10, 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2);
        }

        cv::imshow("output", frame);

        if (cv::waitKey(1) != -1)
        {
            capture.release();
            std::cout << "finished by user\n";
            break;
        }
    }

    std::cout << "Total frames: " << total_frames << "\n";

    return 0;
}

3 部署custom yolov5

3.1 c++部署

环境和代码的大致步骤跟2中的一样

在将所有前置布置好了之后,运行yolo.cpp的时候可能会出现下图中的问题。
在这里插入图片描述
这个是由于yolov5 v6.1版本的问题,可以参考github源码中的issue的解决方案。当然,也可以按照下面的进行代码进行修改。

#include <fstream>

#include <opencv2/opencv.hpp>

std::vector<std::string> load_class_list()
{
    std::vector<std::string> class_list;
    std::ifstream ifs("./config_files/classes_fire.txt");
    std::string line;
    while (getline(ifs, line))
    {
        class_list.push_back(line);
    }
    return class_list;
}

void load_net(cv::dnn::Net &net, bool is_cuda)
{
    auto result = cv::dnn::readNet("./config_files/yolov5n.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);
    }
    else
    {
        std::cout << "Running on CPU\n";
        result.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
        result.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
    }
    net = result;
}

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;

struct Detection
{
    int class_id;
    float confidence;
    cv::Rect box;
};

cv::Mat format_yolov5(const cv::Mat &source) {
    int col = source.cols;
    int row = source.rows;
    int _max = MAX(col, row);
    cv::Mat result = cv::Mat::zeros(_max, _max, CV_8UC3);
    source.copyTo(result(cv::Rect(0, 0, col, row)));
    return result;
}

// 所有的代码修改都在这个函数中
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.5.5的时候注释掉,使用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);
    }
}

int main(int argc, char **argv)
{

    std::vector<std::string> class_list = load_class_list();

    cv::Mat frame;
    cv::VideoCapture capture("sample_fire2.mp4");
    // 如果想要将结果保存为视频
    /*
    cv::VideoWriter writer;
    int coder = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
    double fps_w = 25.0;//设置视频帧率
    std::string filename = "fire.avi";//保存的视频文件名称
    writer.open(filename, coder, fps_w, cv::Size(640, 360));//创建保存视频文件的视频流 Size(640, 360)是smaple_fire2.mp4的分辨率
    */
    if (!capture.isOpened())
    {
        std::cerr << "Error opening video file\n";
        return -1;
    }
	// 因为是window系统,且直接使用VStudio运行代码的,如果想使用cuda,直接将is_cuda = true即可
    bool is_cuda = argc > 1 && strcmp(argv[1], "cuda") == 0;
    cv::dnn::Net net;
    load_net(net, is_cuda);

    auto start = std::chrono::high_resolution_clock::now();
    int frame_count = 0;
    float fps = -1;
    int total_frames = 0;

    while (true)
    {
        capture.read(frame);
        if (frame.empty())
        {
            std::cout << "End of stream\n";
            break;
        }

        std::vector<Detection> output;
        detect(frame, net, output, class_list);

        frame_count++;
        total_frames++;

        int detections = output.size();

        for (int i = 0; i < detections; ++i)
        {

            auto detection = output[i];
            auto box = detection.box;
            auto classId = detection.class_id;
            const auto color = colors[classId % colors.size()];
            cv::rectangle(frame, box, color, 3);

            cv::rectangle(frame, cv::Point(box.x, box.y - 20), cv::Point(box.x + box.width, box.y), color, cv::FILLED);
            cv::putText(frame, class_list[classId].c_str(), cv::Point(box.x, box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
        }

        if (frame_count >= 30)
        {

            auto end = std::chrono::high_resolution_clock::now();
            fps = frame_count * 1000.0 / std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

            frame_count = 0;
            start = std::chrono::high_resolution_clock::now();
        }

        if (fps > 0)
        {

            std::ostringstream fps_label;
            fps_label << std::fixed << std::setprecision(2);
            fps_label << "FPS: " << fps;
            std::string fps_label_str = fps_label.str();

            cv::putText(frame, fps_label_str.c_str(), cv::Point(10, 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2);
        }

        cv::imshow("output", frame);
        //  writer.write(frame);  // 如果想要将结果保存为视频

        if (cv::waitKey(1) != -1)
        {
            capture.release();
            // writer.release();  // 如果想要将结果保存为视频
            std::cout << "finished by user\n";
            break;
        }
    }

    std::cout << "Total frames: " << total_frames << "\n";

    return 0;
}

3.2 c++ 推理结果

opencv 4.5.5
yolov5 v6.1 导出的是yolov5n.onnx

yolov5_deploy_fire

opencv 4.7.0
yolov5 v6.1 导出的是yolov5n.onnx

yolov5_deploy_fire2

4 参考链接

  • 代码参考链接
  1. https://github.com/doleron/yolov5-opencv-cpp-python
  2. https://github.com/Hexmagic/ONNX-yolov5/tree/master
  3. https://github.com/yzy12-max/yolov5_deploy(这个是理论参考链接2中对应的仓库)
  • 理论参考链接
  1. https://github.com/ultralytics/yolov5/issues/251
  2. https://blog.csdn.net/weixin_41311686/article/details/128421801(这个的是另外的代码推理解析部分,值得一看)
  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值