概述
本文将详细介绍如何在Linux环境下部署MTCNN模型进行人脸检测,并使用NCNN框架进行推理。
1. CMake的安装与配置
下载CMake源码
前往CMake官网下载,找到适合您系统的最新版本tar.gz文件链接,或者直接通过wget下载:CMake官方下载页面https://cmake.org/download/
cd ~
wget https://github.com/Kitware/CMake/releases/download/v3.x.x/cmake-3.x.x.tar.gz
请将3.x.x
替换为您想要安装的具体版本号。
解压并进入解压后的目录
tar -xzvf cmake-3.x.x.tar.gz
cd cmake-3.x.x
编译与安装
-
配置编译选项:使用bootstrap脚本进行配置:
./bootstrap
-
编译:使用所有可用的核心进行并行编译:
make -j$(nproc)
-
安装:将CMake安装到系统中:
sudo make install
-
刷新共享库缓存(如果需要):
sudo ldconfig
安装完成后,再次运行以下命令验证安装是否成功:
cmake --version
配置环境变量(可选)
如果您选择自定义安装路径(例如/usr/local/bin
以外的路径),可能需要手动配置环境变量以确保系统能够找到新安装的CMake。
编辑~/.bashrc
或~/.zshrc
文件(取决于您使用的shell),添加以下行:
export PATH=/path/to/cmake/bin:$PATH
其中/path/to/cmake/bin
是您指定的CMake安装路径下的bin
目录。
保存文件后,运行以下命令使更改生效:
source ~/.bashrc # 对于Bash用户
# 或者
source ~/.zshrc # 对于Zsh用户
2. Protobuf的安装
更新系统软件包
首先,更新您的系统软件包列表,确保所有现有的包都是最新的:
sudo apt-get update
sudo apt-get upgrade
安装依赖项
安装构建Protobuf所需的各种工具和库:
sudo apt-get install autoconf automake libtool curl make g++ unzip
下载Protobuf源码
您可以从GitHub上克隆官方Protobuf仓库,或者直接下载特定版本的压缩包。这里我们使用Git进行操作:
cd ~
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
编译Protobuf
创建并进入构建目录
为了保持源代码目录的整洁,建议在一个新的目录中进行编译:
cd ~/protobuf
mkdir -p build && cd build
使用CMake配置项目
CMake是一个跨平台的构建系统生成器,支持多种IDE和构建工具。
-
配置CMake:
cmake .. -DCMAKE_BUILD_TYPE=Release
-
编译Protobuf:
make -j$(nproc)
-
安装Protobuf:
sudo make install sudo ldconfig # 刷新共享库缓存
验证安装
检查版本信息
验证Protobuf是否正确安装,并检查其版本号:
protoc --version
您应该看到类似如下的输出:
libprotoc 3.x.x
其中3.x.x
是具体的版本号。
配置环境变量(可选)
如果希望在任何位置都能直接运行protoc
命令,而无需指定完整路径,可以将Protobuf的bin目录添加到系统的PATH
环境变量中。编辑~/.bashrc
或~/.zshrc
文件,根据您的shell类型,添加以下行:
export PATH=$PATH:/usr/local/bin
保存文件后,运行以下命令使更改生效:
source ~/.bashrc # 对于Bash用户
# 或者
source ~/.zshrc # 对于Zsh用户
3. OpenCV库的安装与配置
更新系统软件包
首先,更新您的系统软件包列表:
sudo apt-get update
sudo apt-get upgrade
安装依赖项
安装构建OpenCV所需的各种工具和库:
sudo apt-get install build-essential cmake git pkg-config libgtk-3-dev
sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev
sudo apt-get install python3-dev python3-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev
sudo apt-get install libdc1394-22-dev libopenblas-dev liblapack-dev gfortran
sudo apt-get install libprotobuf-dev protobuf-compiler
下载OpenCV源码
您可以通过Git克隆OpenCV的GitHub仓库来获取最新的稳定版本:
cd ~
git clone https://github.com/opencv/opencv.git
cd opencv
git checkout 4.x # 替换为所需的版本号
# 克隆contrib仓库(可选)
cd ~
git clone https://github.com/opencv/opencv_contrib.git
cd opencv_contrib
git checkout 4.x # 确保与主仓库版本一致
编译OpenCV
创建并进入构建目录
cd ~/opencv
mkdir -p build && cd build
使用CMake配置项目
运行CMake以配置构建选项。这里我们指定一些常用的选项,例如启用Python支持、设置安装路径等。如果您不需要这些功能或使用了不同的路径,请相应地调整命令。
cmake -D CMAKE_BUILD_TYPE=Release \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
-D BUILD_opencv_python3=ON \
-D PYTHON3_EXECUTABLE=$(which python3) \
-D PYTHON3_INCLUDE_DIR=$(python3 -c "from distutils.sysconfig import get_python_inc; print(get_python_inc())") \
-D PYTHON3_PACKAGES_PATH=$(python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") ..
编译
make -j$(nproc)
这可能需要一些时间,具体取决于您的硬件性能。
安装
完成编译后,使用以下命令安装OpenCV到系统中:
sudo make install
sudo ldconfig
验证安装
查找头文件和库文件
安装完成后,OpenCV的头文件通常位于/usr/local/include/opencv4/
,而库文件则位于/usr/local/lib/
。
您可以检查这些位置是否包含必要的文件:
ls /usr/local/include/opencv4/
ls /usr/local/lib/
4. ncnn库在Linux环境下的编译
更新系统软件包
首先,更新您的系统软件包列表,确保所有现有的包都是最新的:
sudo apt-get update
sudo apt-get upgrade
安装依赖项
安装构建NCNN所需的各种工具和库:
sudo apt-get install build-essential cmake git libprotobuf-dev protobuf-compiler
sudo apt-get install libvulkan-dev vulkan-utils # 如果需要Vulkan支持
libprotobuf-dev
和 protobuf-compiler
是用于处理模型文件(如 .param
和 .bin
文件)的必要依赖项。
下载NCNN源码
您可以通过Git克隆NCNN的GitHub仓库来获取最新的稳定版本:
cd ~
git clone https://github.com/Tencent/ncnn.git
cd ncnn
如果您想要特定的版本,可以切换到对应的分支或标签:
git checkout <branch_or_tag_name>
例如,切换到最新稳定版:
git checkout master
编译NCNN
创建并进入构建目录
为了保持源代码目录的整洁,建议在一个新的目录中进行编译:
mkdir -p build && cd build
使用CMake配置项目
运行CMake以配置构建选项。这里我们指定一些常用的选项,例如启用Vulkan支持、设置安装路径等。如果您不需要这些功能或使用了不同的路径,请相应地调整命令。
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DNCNN_VULKAN=ON \ # 如果你希望使用Vulkan加速,请启用此选项
-DNCNN_PROTOBUF_USE_SYSTEM=ON \
-DProtobuf_DIR=/usr/lib/x86_64-linux-gnu/pkgconfig \ # 手动指定 Protobuf 的路径
-DProtobuf_INCLUDE_DIR=/usr/include \
-DProtobuf_LIBRARY=/usr/lib/x86_64-linux-gnu/libprotobuf.so \
-DProtobuf_PROTOC_EXECUTABLE=/usr/bin/protoc
编译
使用所有可用的核心进行并行编译:
make -j$(nproc)
这可能需要一些时间,具体取决于您的硬件性能。
安装
完成编译后,使用以下命令安装NCNN到系统中:
sudo make install
sudo ldconfig # 刷新共享库缓存
默认情况下,头文件会被安装到 /usr/local/include/ncnn
,库文件会被安装到 /usr/local/lib
。
验证安装
查找头文件和库文件
安装完成后,NCNN的头文件通常位于 /usr/local/include/ncnn
,而库文件则位于 /usr/local/lib
。
您可以检查这些位置是否包含必要的文件:
ls /usr/local/include/ncnn/
ls /usr/local/lib/
5. MTCNN源码
ncnn框架实现的mtcnn主要包含两个核心代码文件mtcnn.h,mtcnn.cpp
mtcnn.h代码如下:
#pragma once
#ifndef __MTCNN_NCNN_H__
#define __MTCNN_NCNN_H__
#include <ncnn/net.h>
#include <string>
#include <vector>
#include <time.h>
#include <algorithm>
#include <map>
#include <iostream>
using namespace std;
struct Bbox
{
float score;
int x1;
int y1;
int x2;
int y2;
bool exist;
float area;
float ppoint[10];
float regreCoord[4];
};
class MTCNN {
public:
MTCNN(const string& model_path);
MTCNN(const std::vector<std::string> param_files, const std::vector<std::string> bin_files);
~MTCNN();
void configure_ncnn(ncnn::Net& net, int num_threads);
void SetMinFace(int minSize);
void detect(ncnn::Mat& img_, std::vector<Bbox>& finalBbox);
private:
void generateBbox(ncnn::Mat score, ncnn::Mat location, vector<Bbox>& boundingBox_, float scale);
void nms(vector<Bbox>& boundingBox_, const float overlap_threshold, string modelname = "Union");
void refine(vector<Bbox>& vecBbox, const int& height, const int& width, bool square);
void PNet();
void RNet();
void ONet();
ncnn::Net Pnet, Rnet, Onet;
ncnn::Mat img;
const float nms_threshold[3] = { 0.5f, 0.7f, 0.7f };
const float mean_vals[3] = { 127.5, 127.5, 127.5 };
const float norm_vals[3] = { 0.0078125, 0.0078125, 0.0078125 };
const int MIN_DET_SIZE = 12;
std::vector<Bbox> firstBbox_, secondBbox_, thirdBbox_;
int img_w, img_h;
private://部分可调参数
const float threshold[3] = { 0.8f, 0.8f, 0.6f };
int minsize = 20;
const float pre_facetor = 0.709f;
};
#endif //__MTCNN_NCNN_H__
mtcnn.cpp代码如下:
#include "mtcnn.h"
bool cmpScore(Bbox lsh, Bbox rsh) {
if (lsh.score < rsh.score)
return true;
else
return false;
}
bool cmpArea(Bbox lsh, Bbox rsh) {
if (lsh.area < rsh.area)
return false;
else
return true;
}
MTCNN::MTCNN(const std::string& model_path) {
std::vector<std::string> param_files = {
model_path + "/det1.param",
model_path + "/det2.param",
model_path + "/det3.param"
};
std::vector<std::string> bin_files = {
model_path + "/det1.bin",
model_path + "/det2.bin",
model_path + "/det3.bin"
};
// 配置多线程
int num_threads = 4; // 设置线程数
configure_ncnn(Pnet, num_threads);
configure_ncnn(Rnet, num_threads);
configure_ncnn(Onet, num_threads);
// 加载模型
Pnet.load_param(param_files[0].data());
Pnet.load_model(bin_files[0].data());
Rnet.load_param(param_files[1].data());
Rnet.load_model(bin_files[1].data());
Onet.load_param(param_files[2].data());
Onet.load_model(bin_files[2].data());
}
MTCNN::~MTCNN(){
Pnet.clear();
Rnet.clear();
Onet.clear();
}
void MTCNN::configure_ncnn(ncnn::Net& net, int num_threads) {
ncnn::Option opt;
opt.num_threads = num_threads; // 设置线程数
opt.use_vulkan_compute = false; // 如果不使用 Vulkan,设置为 false
net.opt = opt;
}
void MTCNN::SetMinFace(int minSize){
minsize = minSize;
}
void MTCNN::generateBbox(ncnn::Mat score, ncnn::Mat location, std::vector<Bbox>& boundingBox_, float scale){
const int stride = 2;
const int cellsize = 12;
//score p
float *p = score.channel(1);//score.data + score.cstep;
//float *plocal = location.data;
Bbox bbox;
float inv_scale = 1.0f/scale;
for(int row=0;row<score.h;row++){
for(int col=0;col<score.w;col++){
if(*p>threshold[0]){
bbox.score = *p;
bbox.x1 = round((stride*col+1)*inv_scale);
bbox.y1 = round((stride*row+1)*inv_scale);
bbox.x2 = round((stride*col+1+cellsize)*inv_scale);
bbox.y2 = round((stride*row+1+cellsize)*inv_scale);
bbox.area = (bbox.x2 - bbox.x1) * (bbox.y2 - bbox.y1);
const int index = row * score.w + col;
for(int channel=0;channel<4;channel++){
bbox.regreCoord[channel]=location.channel(channel)[index];
}
boundingBox_.push_back(bbox);
}
p++;
//plocal++;
}
}
}
void MTCNN::nms(std::vector<Bbox> &boundingBox_, const float overlap_threshold, string modelname){
if(boundingBox_.empty()){
return;
}
sort(boundingBox_.begin(), boundingBox_.end(), cmpScore);
float IOU = 0;
float maxX = 0;
float maxY = 0;
float minX = 0;
float minY = 0;
std::vector<int> vPick;
int nPick = 0;
std::multimap<float, int> vScores;
const int num_boxes = boundingBox_.size();
vPick.resize(num_boxes);
for (int i = 0; i < num_boxes; ++i){
vScores.insert(std::pair<float, int>(boundingBox_[i].score, i));
}
while(vScores.size() > 0){
int last = vScores.rbegin()->second;
vPick[nPick] = last;
nPick += 1;
for (std::multimap<float, int>::iterator it = vScores.begin(); it != vScores.end();){
int it_idx = it->second;
maxX = (std::max)(boundingBox_.at(it_idx).x1, boundingBox_.at(last).x1);
maxY = (std::max)(boundingBox_.at(it_idx).y1, boundingBox_.at(last).y1);
minX = (std::min)(boundingBox_.at(it_idx).x2, boundingBox_.at(last).x2);
minY = (std::min)(boundingBox_.at(it_idx).y2, boundingBox_.at(last).y2);
//maxX1 and maxY1 reuse
maxX = ((minX-maxX+1)>0)? (minX-maxX+1) : 0;
maxY = ((minY-maxY+1)>0)? (minY-maxY+1) : 0;
//IOU reuse for the area of two bbox
IOU = maxX * maxY;
if(!modelname.compare("Union"))
IOU = IOU/(boundingBox_.at(it_idx).area + boundingBox_.at(last).area - IOU);
else if(!modelname.compare("Min")){
IOU = IOU/((boundingBox_.at(it_idx).area < boundingBox_.at(last).area)? boundingBox_.at(it_idx).area : boundingBox_.at(last).area);
}
if(IOU > overlap_threshold){
it = vScores.erase(it);
}else{
it++;
}
}
}
vPick.resize(nPick);
std::vector<Bbox> tmp_;
tmp_.resize(nPick);
for(int i = 0; i < nPick; i++){
tmp_[i] = boundingBox_[vPick[i]];
}
boundingBox_ = tmp_;
}
void MTCNN::refine(vector<Bbox> &vecBbox, const int &height, const int &width, bool square){
if(vecBbox.empty()){
cout<<"Bbox is empty!!"<<endl;
return;
}
float bbw=0, bbh=0, maxSide=0;
float h = 0, w = 0;
float x1=0, y1=0, x2=0, y2=0;
for(vector<Bbox>::iterator it=vecBbox.begin(); it!=vecBbox.end();it++){
bbw = (*it).x2 - (*it).x1 + 1;
bbh = (*it).y2 - (*it).y1 + 1;
x1 = (*it).x1 + (*it).regreCoord[0]*bbw;
y1 = (*it).y1 + (*it).regreCoord[1]*bbh;
x2 = (*it).x2 + (*it).regreCoord[2]*bbw;
y2 = (*it).y2 + (*it).regreCoord[3]*bbh;
if(square){
w = x2 - x1 + 1;
h = y2 - y1 + 1;
maxSide = (h>w)?h:w;
x1 = x1 + w*0.5 - maxSide*0.5;
y1 = y1 + h*0.5 - maxSide*0.5;
(*it).x2 = round(x1 + maxSide - 1);
(*it).y2 = round(y1 + maxSide - 1);
(*it).x1 = round(x1);
(*it).y1 = round(y1);
}
//boundary check
if((*it).x1<0)(*it).x1=0;
if((*it).y1<0)(*it).y1=0;
if((*it).x2>width)(*it).x2 = width - 1;
if((*it).y2>height)(*it).y2 = height - 1;
it->area = (it->x2 - it->x1)*(it->y2 - it->y1);
}
}
void MTCNN::PNet(){
firstBbox_.clear();
float minl = img_w < img_h? img_w: img_h;
float m = (float)MIN_DET_SIZE/minsize;
minl *= m;
float factor = pre_facetor;
vector<float> scales_;
while(minl>MIN_DET_SIZE){
scales_.push_back(m);
minl *= factor;
m = m*factor;
}
for (size_t i = 0; i < scales_.size(); i++) {
int hs = (int)ceil(img_h*scales_[i]);
int ws = (int)ceil(img_w*scales_[i]);
ncnn::Mat in;
resize_bilinear(img, in, ws, hs);
ncnn::Extractor ex = Pnet.create_extractor();
//ex.set_num_threads(2);
ex.set_light_mode(true);
ex.input("data", in);
ncnn::Mat score_, location_;
ex.extract("prob1", score_);
ex.extract("conv4-2", location_);
std::vector<Bbox> boundingBox_;
generateBbox(score_, location_, boundingBox_, scales_[i]);
nms(boundingBox_, nms_threshold[0]);
firstBbox_.insert(firstBbox_.end(), boundingBox_.begin(), boundingBox_.end());
boundingBox_.clear();
}
}
void MTCNN::RNet(){
secondBbox_.clear();
int count = 0;
for(vector<Bbox>::iterator it=firstBbox_.begin(); it!=firstBbox_.end();it++){
ncnn::Mat tempIm;
copy_cut_border(img, tempIm, (*it).y1, img_h-(*it).y2, (*it).x1, img_w-(*it).x2);
ncnn::Mat in;
resize_bilinear(tempIm, in, 24, 24);
ncnn::Extractor ex = Rnet.create_extractor();
//ex.set_num_threads(2);
ex.set_light_mode(true);
ex.input("data", in);
ncnn::Mat score, bbox;
ex.extract("prob1", score);
ex.extract("conv5-2", bbox);
if ((float)score[1] > threshold[1]) {
for (int channel = 0; channel<4; channel++) {
it->regreCoord[channel] = (float)bbox[channel];//*(bbox.data+channel*bbox.cstep);
}
it->area = (it->x2 - it->x1)*(it->y2 - it->y1);
it->score = score.channel(1)[0];//*(score.data+score.cstep);
secondBbox_.push_back(*it);
}
}
}
void MTCNN::ONet(){
thirdBbox_.clear();
for(vector<Bbox>::iterator it=secondBbox_.begin(); it!=secondBbox_.end();it++){
ncnn::Mat tempIm;
copy_cut_border(img, tempIm, (*it).y1, img_h-(*it).y2, (*it).x1, img_w-(*it).x2);
ncnn::Mat in;
resize_bilinear(tempIm, in, 48, 48);
ncnn::Extractor ex = Onet.create_extractor();
//ex.set_num_threads(2);
ex.set_light_mode(true);
ex.input("data", in);
ncnn::Mat score, bbox, keyPoint;
ex.extract("prob1", score);
ex.extract("conv6-2", bbox);
ex.extract("conv6-3", keyPoint);
if ((float)score[1] > threshold[2]) {
for (int channel = 0; channel < 4; channel++) {
it->regreCoord[channel] = (float)bbox[channel];
}
it->area = (it->x2 - it->x1) * (it->y2 - it->y1);
it->score = score.channel(1)[0];
for (int num = 0; num<5; num++) {
(it->ppoint)[num] = it->x1 + (it->x2 - it->x1) * keyPoint[num];
(it->ppoint)[num + 5] = it->y1 + (it->y2 - it->y1) * keyPoint[num + 5];
}
thirdBbox_.push_back(*it);
}
}
}
void MTCNN::detect(ncnn::Mat& img_, std::vector<Bbox>& finalBbox_){
img = img_;
img_w = img.w;
img_h = img.h;
img.substract_mean_normalize(mean_vals, norm_vals);
PNet();
//the first stage's nms
if(firstBbox_.size() < 1) return;
nms(firstBbox_, nms_threshold[0]);
refine(firstBbox_, img_h, img_w, true);
//second stage
RNet();
if(secondBbox_.size() < 1) return;
nms(secondBbox_, nms_threshold[1]);
refine(secondBbox_, img_h, img_w, true);
//third stage
ONet();
if(thirdBbox_.size() < 1) return;
refine(thirdBbox_, img_h, img_w, true);
nms(thirdBbox_, nms_threshold[2], "Min");
finalBbox_ = thirdBbox_;
}
6. Linux下进行推理
在Linux下配置完Opencv和ncnn的环境后编写简单的main.cpp进行模型的推理,代码如下:
#include "mtcnn.h"
#include <opencv2/opencv.hpp>
#include <chrono>
using namespace cv;
int main()
{
std::string model_path = "./models"; //根据模型权重所在位置修改路径
MTCNN mm(model_path);
mm.SetMinFace(20);
cv::VideoCapture video("./video/video.mp4"); //根据测试视频所在位置修改路径
if (!video.isOpened()) {
std::cerr << "failed to load video" << std::endl;
return -1;
}
std::vector<Bbox> finalBbox;
cv::Mat frame;
// 记录开始时间
auto start = std::chrono::high_resolution_clock::now();
do {
finalBbox.clear();
video >> frame;
if (!frame.data) {
std::cerr << "Capture video failed" << std::endl;
break;
}
ncnn::Mat ncnn_img = ncnn::Mat::from_pixels(frame.data, ncnn::Mat::PIXEL_BGR2RGB, frame.cols, frame.rows);
mm.detect(ncnn_img, finalBbox);
for (vector<Bbox>::iterator it = finalBbox.begin(); it != finalBbox.end(); it++) {
if ((*it).exist) {
cv::rectangle(frame, cv::Point((*it).x1, (*it).y1), cv::Point((*it).x2, (*it).y2), cv::Scalar(0, 0, 255), 2, 8, 0);
}
}
} while (1);
// 释放资源
video.release();
// 记录结束时间
auto end = std::chrono::high_resolution_clock::now();
// 计算持续时间
std::chrono::duration<double> duration = end - start;
// 输出结果(秒)
std::cout << "Time taken: " << duration.count() << " seconds" << std::endl;
return 0;
}
测试项目的目录结构如下:
mtcnn/
├── Makefile
├── video
│ └── video.mp4
├── include/
│ └── mtcnn.h
├── src/
│ ├── mtcnn.cpp
│ └── main.cpp
└── models
├── det1.bin
├── det1.param
├── det2.bin
├── det2.param
├── det3.bin
└── det3.param
ncnn架构的mtcnn模型权重下载链接如下:
ncnn架构的mtcnn模型权重下载https://download.csdn.net/download/m0_57010556/90433089Makefile的内容如下:
# 编译器
CXX = g++
# 编译选项
CXXFLAGS = -Wall -I./include -O2 -fopenmp `pkg-config --cflags opencv4`
# 目标可执行文件名
TARGET = face_detection
# 源文件目录
SRCDIR = src
# 头文件目录
INCDIR = include
# 链接库路径
OPENCV_LIBS = `pkg-config --libs opencv4`
OPENCV_CFLAGS = `pkg-config --cflags opencv4`
NCNN_CFLAGS = -I/home/ncnn/build/install/include
NCNN_LIBS = -L/home/ncnn/build/install/lib -lncnn
# 找到所有源文件
SOURCES := $(wildcard $(SRCDIR)/*.cpp)
# 生成目标文件列表
OBJECTS := $(patsubst $(SRCDIR)/%.cpp, %.o, $(SOURCES))
# 默认目标
all: $(TARGET)
# 链接目标文件生成可执行文件
$(TARGET): $(OBJECTS)
$(CXX) $(CXXFLAGS) $^ -o $@ $(OPENCV_LIBS) $(NCNN_LIBS) -lpthread -ldl -lgomp
# 规则:从源文件生成目标文件
%.o: $(SRCDIR)/%.cpp
$(CXX) $(CXXFLAGS) $(OPENCV_CFLAGS) $(NCNN_CFLAGS) -c $< -o $@
# 清理生成的文件
clean:
rm -f $(OBJECTS) $(TARGET)
.PHONY: all clean
编译和运行
在项目目录下运行以下命令来编译和运行你的程序:
编译
make
这将编译 src/mtcnn.cpp
和 src/main.cpp
并生成可执行文件 face_detection
。
运行
./face_detection
你应该会看到输出:
Capture video failed
Time taken: 22.336 seconds
此时说明在linux下模型推理成功