【CV学习笔记】之ncnnFastDet多线程c++部署

news2025/1/18 6:57:32

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

在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};
    };

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);

2.4、接口实现代码 infer.cpp

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

struct Job {
  shared_ptr<promise<vector<TargetBox>>> pro;
  Mat input;
};

接口实现类

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);
};

下面为类中函数的实现:

//终止推理,在析构函数中调用,将线程的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");
}

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;
}

执行:

mkdir build && cd bulid
cmake .. && make -j
./multi_thead_infer

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

4、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/387138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

English Learning - L2 第2次小组纠音 [iː] [ɜː] [æ] 2023.3.1 周三

English Learning - L2 第2次小组纠音 [iː] [ɜː] [] 2023.3.1 周三共性问题分析前元音 [iː]中元音 [ɜː]前元音 []我的发音问题舌位找的不准纠音过程共性问题分析 前元音 [iː] 嘴角左右拉伸没有到位 解决方法&#xff1a; 嘴角是往耳后根的方向&#xff0c;微微上扬的角…

指针和数组面试题(逐题分析,完善你可能遗漏的知识)

人生不是一种享乐&#xff0c;而是一桩十分沉重的工作。 —— 列夫托尔斯泰 前言&#xff1a;之前我们就学习了数组和指针的知识。 数组&#xff1a;数组就是能够存放一组相同类型的元素&#xff0c;数组的大小取决于数组的元素个数和元素类型。 指针&#xff1a;…

Linux操作系统学习(进程等待)

文章目录进程等待进程等待的必要性如何进程等待waiwaitpid验证进程等待 ​ 我们知道fork函数可以创建一个子进程&#xff0c;而子进程通常是替父进程完成一些任务&#xff0c;而父进程在fork之后需要通过wait/waitpid等待子进程退出。这就是进程等待 进程等待的必要性 通过获…

Allegro如何导入第三方网表操作指导

Allegro如何导入第三方网表操作指导 在用Allegro做PCB设计的时候,除了支持第一方网表的导入,同样也是可以导入第三方网表的,第三方网表如下图 如何导入,具体操作如下 点击Setup点击User Preference

【抽水蓄能电站】基于粒子群优化算法的抽水蓄能电站的最佳调度方案研究(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…

Linux操作系统学习(进程替换)

文章目录进程替换进程替换是什么&#xff1f;替换的方法进程替换简易shell模拟进程替换 进程替换是什么&#xff1f; 如下图所示&#xff1a; ​ 进程替换就是&#xff0c;把进程B的代码和数据&#xff0c;替换正在执行的进程A的代码和数据在内存中的位置&#xff08;若代码…

etcd集群通过 Leader 写入数据,为什么K8s HA集群中讲每个 kube-apiserver 只和本机的 ETCD 通信

写在前面 对这个我不太明白&#xff0c;所有在 stackOverflow 的请教了大佬这里分享给小伙伴理解不足小伙伴帮忙指正 对每个人而言&#xff0c;真正的职责只有一个&#xff1a;找到自我。然后在心中坚守其一生&#xff0c;全心全意&#xff0c;永不停息。所有其它的路都是不完整…

spark sql(二)sql解析流程扩展

1、前言 通过前面的文章我们了解到&#xff0c;spark sql通过catalyst框架解析sql&#xff0c;而在将sql语句转变为可执行的任务过程中会将大的sql解析流程划分为未解析的逻辑计划、解析后的逻辑计划、优化后的逻辑计划、物理计划、可执行物理计划等阶段。大概的解析流程如下所…

Handler与线程

简介 Handler提供的种异步消息处理机制是&#xff1a;当它发出一个消息进入消息队列后&#xff0c;发送消息的函数立刻返回&#xff0c;接着主线程会逐个地从消息队列中把消息取出&#xff0c;然后对消息进行处理。明显&#xff0c;Handler发送消息和接收消息是异步进行的&…

三八送什么数码产品好?适合送礼的数码产品

数码产品是我们生活中比较常见到的物品&#xff0c;相比较于一般礼物的观赏性&#xff0c;它的实用性更强一些&#xff0c;所以如果你不知道送什么礼物给别人的话&#xff0c;数码产品也是不错的选择。 一、南卡小音舱蓝牙耳机 这个时代的女性&#xff0c;变得越来越自信了&am…

ChatGPT解答:根据使用者输入的字符串,自动判断规则,并给出各种正则表达式,用Python实现

ChatGPT解答&#xff1a; 根据使用者输入的字符串&#xff0c;自动判断规则&#xff0c;并给出各种正则表达式&#xff0c;用Python实现 根据输入的字符串&#xff0c;自动给出正则表达式 根据使用者输入的字符串&#xff0c;自动判断规则&#xff0c;并给出各种正则表达式&am…

JVM系统优化实践(7):垃圾回收器与垃圾回收算法

您好&#xff0c;我是湘王&#xff0c;这是我的CSDN博客&#xff0c;欢迎您来&#xff0c;欢迎您再来&#xff5e;上回说到了年轻代、老年代与数据计算的一个案例。接下来就先讲一讲年轻代和老年代的两个垃圾回收器&#xff1a;ParNew和CMS。和Serial垃圾回收器一样&#xff0c…

实战:yaml方式安装ingress-nginx-2023.3.2(测试成功)

实战&#xff1a;yaml方式安装ingress-nginx-2023.3.2(测试成功) 目录 文章目录实战&#xff1a;yaml方式安装ingress-nginx-2023.3.2(测试成功)目录实验环境实验软件1、安装过程2、第一个示例关于我最后最后实验环境 实验环境&#xff1a; 1、win10,vmwrokstation虚机&#x…

AI_News周刊:第四期

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 News 1.对抗“唤醒人工智能”马斯克招募团队开发 OpenAI 竞争对手 据两位直接了解这项工作的人士和另一位了解情况的人士透露&#xff0c;埃隆马斯克最近几周与人工智能研究人员接洽&#xff0c;商讨成…

详细分析什么是进程?如何理解进程状态?

什么是进程&#xff1f; 比较官方一点的回答是&#xff1a;当一个程序加载到内存的时候&#xff0c;就是一个进程。 但是这是不准确的回答&#xff0c;进程是怎么在内存中形成的&#xff0c;以及内存是如何管理进程的&#xff0c;是通过什么描述进程的&#xff1f;下面我们将…

Neo4j数据库部署配置

这里写目录标题一、neo4j图形数据库安装与部署1.1配置JDK运行环境&#xff08;注意jdk与neo4j版本对应&#xff09;1.2部署Neo4j&#xff08;注意jdk与neo4j版本对应&#xff09;二、数据库基本操作演示一、neo4j图形数据库安装与部署 1.1配置JDK运行环境&#xff08;注意jdk与…

centos安装rocketmq

centos安装rocketmq1 下载rocketmq二进制包2 解压二进制包3 修改broker.conf4 修改runbroker.sh和runserver.sh的JVM参数5 启动NameServer和Broker6 安装rockermq dashboard(可视化控制台)1 下载rocketmq二进制包 点击rocketmq二进制包下载地址&#xff0c;下载完成之后通过ft…

javaEE 初阶 — 数据链路层中的以太网数据帧

文章目录以太网帧格式1. MAC 地址2. MAC 地址是如何与 IP 地址相互配合的3. 以太网帧格式中的类型MTU&#xff08;了解&#xff09;以太网帧格式 数据链路层主要考虑的是相邻的两个结点之间的传输。 这里最知名的协议就是 以太网。 一个以太网数据帧有三个部分组成。帧头载荷…

【GlobalMapper精品教程】055:GM坐标转换器的巧妙使用

GM软件提供了一个简单实用的坐标转换工具,可以实现地理坐标和投影坐标之间的高斯正反算及多种转换计算。 文章目录 一、坐标转换器认识二、坐标转换案例1. 地理坐标←→地理坐标2. 地理坐标←→投影坐标三、在输出坐标上创建新的点四、其他转换工具的使用一、坐标转换器认识 …

653600-56-7,Ac4GaINAz,N-叠氮四酰化半乳糖用于PROTAC合成

基础产品数据&#xff1a;CAS号&#xff1a;653600-56-7中文名&#xff1a;N-叠氮四酰化半乳糖&#xff0c;叠氮修饰半乳糖英文名&#xff1a; Ac4GaINAzAc4GaINAz结构式&#xff08;Structural&#xff09;&#xff1a;详细产品数据&#xff1a;分子式&#xff1a;C16H22N4O10…