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指的是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++多线程编程的知识,并应用到实际项目中。