当前位置:   article > 正文

【CV学习笔记】ncnn+FastDet多线程c++部署_部署fastestdet

部署fastestdet

1、前言

ncnn是一款非常高效易用的深度学习推理框架,支持各种神经网络模型,如pytorch、tensorflow、onnx等,以及多种硬件后端,如x86、arm、riscv、mips、vulkan等。
ncnn项目地址:https://github.com/Tencent/ncnn
FastDet是设计用来接替yolo-fastest系列算法,相比于业界已有的轻量级目标检测算法,无论是速度还是参数量都要小,适用于嵌入式上的推理,当然精度还是差一些。但是这不重要,本文只是借用FastDet来实现多线程推理,如果有需要,理论上可以移植到任何模型以及平台。
FastDet项目链接:https://github.com/dog-qiuqiu/FastestDet
在实际项目中,单线程推理是一个稳定但是比较低效的方式,尤其是在多个模型对同一张图片进行推理时,因此就需要对设计多线程来进行优化,通过学习,现在也是掌握了多线程操作中的一些知识点:
多线程推理学习链接:https://shouxieai.com/solution/trt/integ-1.12-multithread
本文代码链接:https://github.com/Rex-LK/tensorrt_learning/tree/main/sideline_learn/ncnn_multi_thread
本文完整模型以及源代码百度云链接: https://pan.baidu.com/s/1f0gHxPRP3KrppnSOqF5ZIw?pwd=5fxe 提取码: 5fxe

2、推理代码详解

2.1、代码架构简介

下载ncnn代码,运行如下命令。

cd ncnn
mkdir build && cd build
cmake .. && make -j
make install
  • 1
  • 2
  • 3
  • 4

在build/install 目录下面会需要的出现 bin、lib、include三个文件夹。

2.2、fastdet推理代码fastdet.h

下载fastdet代码,只需要代码中的example/ncnn里面的模型以及推理文件,且本项目已经将该文件进行了简单的封装,便于多线程推理时进行调用,fastdet推理的头文件如下:

class FastDet
    {
	
    public:
    	// 构造函数中初始化模型
        FastDet(int input_width, int input_height, std::string param_path,
                std::string model_path);
        ~FastDet();
    	// 预处理
        void prepare_input(cv::Mat img);
    	// 执行推理
        void infrence(std::string inputName, std::string outputName, int num_threads);
    	// 后处理
        void postprocess(int img_width, int img_height, int class_num, float thresh);

    public:
        static const char *class_names[];
        std::vector<TargetBox> target_boxes;
        std::vector<TargetBox> nms_boxes;

    private:
        // 模型
        ncnn::Net net_;
        int input_width_;
        int input_height_;
        ncnn::Mat input_;
        ncnn::Mat output_;
        const float mean_vals_[3] = {0.f, 0.f, 0.f};
        const float norm_vals_[3] = {1 / 255.f, 1 / 255.f, 1 / 255.f};
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
2.3、接口类代码infer.hpp

在代码自己使用或者给其他人使用时,最好的办法是给一个简单的接口函数,无须担心函数内部发生什么变化,只要获得对应的结果即可。因此,这里原作者设计了一个十分简介的接口类infer.hpp,便于推理函数的使用。

// 接口类,使用时会用到多态的思想,即父类指针指向子类对象,使用者只会看到父类的commit函数,而无须关系子类中的函数做了什么。
class Infer
{
public:
    virtual std::shared_future<std::vector<fastdet::TargetBox>> commit(
        cv::Mat &input) = 0;
};
// 构造推力器的函数
std::shared_ptr<Infer> create_infer(const std::string &param_path,
                                    const std::string &model_path);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
2.4、接口实现代码 infer.cpp

首先构建一个任务结构体,表示输入一张图片以及推理完毕后返回对应的结果。

struct Job {
  shared_ptr<promise<vector<TargetBox>>> pro;
  Mat input;
};
  • 1
  • 2
  • 3
  • 4

接口实现类

class InferImpl : public Infer
{
public:
    virtual ~InferImpl() { stop(); }
    // 线程停止
    void stop();
    // 启动workerd的函数
    bool startup(const string &param_path, const string &model_path);
    // 输入图片并返回对应的推理结果
    virtual shared_future<vector<TargetBox>> commit(Mat &input) override;
    // 在worker内加载模型并推理
    void worker(promise<bool> &pro);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

下面为类中函数的实现:

//终止推理,在析构函数中调用,将线程的running状态设为false,并唤醒线程向下执行。
void InferImpl::stop()
{
    if (running_)
    {
        running_ = false;
        cv_.notify_one();
    }

    if (worker_thread_.joinable())
        worker_thread_.join();
}
// 启动推理线程
bool InferImpl::startup(const string &param_path, const string &model_path)
{
    param_path_ = param_path;
    model_path_ = model_path;
    running_ = true; // 启动后,运行状态设置为true

    // 线程传递promise的目的,是获得线程是否初始化成功的状态
    // 而在线程内做初始化,好处是,初始化跟释放在同一个线程内
    // 代码可读性好,资源管理方便
    promise<bool> pro;
    worker_thread_ = thread(&InferImpl::worker, this, std::ref(pro));
    /*
        注意:这里thread 一构建好后,worker函数就开始执行了
        第一个参数是该线程要执行的worker函数,第二个参数是this指的是class
       InferImpl,第三个参数指的是传引用,因为我们在worker函数里要修改pro。
     */
    return pro.get_future().get();
}
// 提交推理任务
shared_future<vector<TargetBox>> InferImpl::commit(Mat &input)
{
    Job job;
    job.input = input;
    job.pro.reset(new promise<vector<TargetBox>>());

    shared_future<vector<TargetBox>> fut =
        job.pro->get_future(); // 将fut与job关联起来
    {
        lock_guard<mutex> l(lock_);
        jobs_.emplace(std::move(job));
    }
    cv_.notify_one(); // 通知线程进行推理
    return fut;
}
// 加载模型、推理
void InferImpl::worker(promise<bool> &pro)
{
    // 加载模型
    fast_det_ =
        new FastDet(input_width_, input_height_, param_path_, model_path_);
    if (fast_det_ == nullptr)
    {
        //如果加载模型失败,则返回false
        pro.set_value(false);
        printf("Load model failed: %s\n", file_.c_str());
        return;
    }

    pro.set_value(true); // 这里的promise用来负责确认infer初始化成功了
    vector<Job> fetched_jobs;
    while (running_)
    {
        {
            unique_lock<mutex> l(lock_);
            cv_.wait(l, [&]()
                     { return !running_ || !jobs_.empty(); }); // 一直等着,cv_.wait(lock, predicate) // 如果 running不在运行状态
                                                               // 或者说 jobs_有东西 而且接收到了notify one的信号
			// 在调用析构函数时会将running_设置为false
            if (!running_)
                break; // 如果 不在运行 就直接结束循环

            for (int i = 0; i < batch_size && !jobs_.empty();
                 ++i)
            { // jobs_不为空的时候
                fetched_jobs.emplace_back(
                    std::move(jobs_.front())); // 就往里面fetched_jobs里塞东西
                jobs_
                    .pop(); // fetched_jobs塞进来一个,jobs_那边就要pop掉一个。(因为move)
            }
        }

        // 可以选择一次加载一批,并进行批处理
        // 本文设置的batchsize为1
        for (auto &job : fetched_jobs)
        {
            int img_width = job.input.cols;
            int img_height = job.input.rows;
            fast_det_->prepare_input(job.input);
            fast_det_->infrence(input_name_, output_name_, infer_thread_);
            fast_det_->postprocess(img_width, img_height, class_num, 0.65);
            job.pro->set_value(fast_det_->nms_boxes);
        }
        fetched_jobs.clear();
    }
    printf("Infer worker done.\n");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99

3、代码测试

fastdet推理代码为fastdet_test.cpp,使用单线程推理一张图片。多线程推理的代码为multi_thread_infer.cpp
多线程推理代码为:

int main()
{
    string param_path =
        "/home/rex/Desktop/ncnn_multi_thread/data/model/FastestDet.param";
    string model_path =
        "/home/rex/Desktop/ncnn_multi_thread/data/model/FastestDet.bin";
    auto infer = create_infer(
        param_path,
        model_path); // 创建及初始化 抖音网页短视频辅助讲解: 创建及初始化推理器
    if (infer == nullptr)
    {
        printf("Infer is nullptr.\n");
        return 0;
    }
    string img_path = "/home/rex/Desktop/ncnn_multi_thread/data/imgs/3.jpg";
    Mat img = cv::imread(img_path);
    auto fut = infer->commit(img);     // 将任务提交给推理器(推理器执行commit)
    vector<TargetBox> res = fut.get(); // 等待结果
    for (size_t i = 0; i < res.size(); i++)
    {
        TargetBox box = res[i];
        rectangle(img, cv::Point(box.x1, box.y1), cv::Point(box.x2, box.y2),
                  cv::Scalar(0, 0, 255), 2);
        // cv::putText(img, pred->class_names[box.category], cv::Point(box.x1,
        // box.y1),
        //             cv::FONT_HERSHEY_SIMPLEX, 0.75, cv::Scalar(0, 255, 0), 2);
    }
    cv::imwrite("result_test.jpg", img);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

执行:

mkdir build && cd bulid
cmake .. && make -j
./multi_thead_infer
  • 1
  • 2
  • 3

推理结果为:
在这里插入图片描述

4、总结

本文学习了ncnn的基本使用方式,希望后续能够学习到关于ncnn更加底层的知识了,同时利用多线程推理的方法优化的推理流程,从多线程的代码中学习到了许多关于c++多线程编程的知识,并应用到实际项目中。

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

闽ICP备14008679号