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 ¶m_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 ¶m_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 ¶m_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指的是classInferImpl,第三个参数指的是传引用,因为我们在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){//如果加载模型失败,则返回falsepro.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_设置为falseif (!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为1for (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++多线程编程的知识,并应用到实际项目中。