1. 参考博客和代码、模型仓库:
1.1. 【C++随记】collect2: error: ld returned 1 exit status错误分析与解决
1.2. Visual Studio 2022新建 cmake 工程测试 tensorRT 自带样例 sampleOnnxMNIST
1.3.报错:ModuleNotFoundError: No module named ‘tensorrt’
1.4. 推荐使用:Amirstan_plugin - 助力TensorRT高效能优化
1.5. 利用TensorRT实现神经网络提速(读取ONNX模型并运行)
1.6. Ubuntu下安装CUDA
1.7. 【Python】成功解决ModuleNotFoundError: No module named ‘packaging’
1.8. pytorch模型(.pth)转tensorrt模型(.engine)的几种方式
1.9. pytorch模型(.pth)转tensorrt模型(.engine)几种方式
1.10. 【Debug】TensorRT报错汇总
1.11. ace-recognition-cpp-tensorrt
1.12. arcFace 模型下载地址
1.13. retinaFace 模型下载地址
1.14. https://gitcode.com/gh_mirrors/am/amirstan_plugin/overview?utm_source=artical_gitcode&index=bottom&type=card&webUrl&isLogin=1
2. 下载代码( github 仓库地址:https://github.com/nghiapq77/face-recognition-cpp-tensorrt)
3. 修改代码:
face-recognition-cpp-tensorrt 仓库是一个功能完备的项目,包括请求、人脸检测、人脸识别( 特征向量计算 )、保存数据库、人脸特征向量相似度计算,由于本人只是想快速验证一下使用 arcFace 计算人脸特征向量是否靠谱,再加上本人对 c++ 不是很熟悉,不想花费大量的时间用在环境安装上,所以我对代码做了精简,只摘出了使用 arcface 计算人脸特征向量的部分,以下是我摘出的 arcface 部分的完整代码:
3.1. 代码结构:
arcface_test/conversion/torch2trt.py // 未修改cpp/arcface.cppcommon.cppinclude/arcface.hcommon.hface_images/xxxmodels/backbone_ir50_asia.pth // 下载地址:https://github.com/ZhaoJ9014/face.evoLVe#model-zoo( xxx仓库的 readme 中也有下载链接引导,我下载的是IR-50 Private Asia Face Data 亚洲人脸的) )test.cppCMakeLists.txt
3.2. arcface.cpp:
#include "arcface.h"void getCroppedFaces(cv::Mat frame, std::vector<struct Bbox> &outputBbox, int resize_w, int resize_h, std::vector<struct CroppedFace> &croppedFaces) {croppedFaces.clear();for (std::vector<struct Bbox>::iterator it = outputBbox.begin(); it != outputBbox.end(); it++) {cv::Rect facePos(cv::Point((*it).y1, (*it).x1), cv::Point((*it).y2, (*it).x2));cv::Mat tempCrop = frame(facePos);struct CroppedFace currFace;cv::resize(tempCrop, currFace.faceMat, cv::Size(resize_h, resize_w), 0, 0, cv::INTER_CUBIC);currFace.face = currFace.faceMat.clone();currFace.x1 = it->x1;currFace.y1 = it->y1;currFace.x2 = it->x2;currFace.y2 = it->y2;croppedFaces.push_back(currFace);}
}int ArcFaceIR50::classCount = 0;ArcFaceIR50::ArcFaceIR50(TRTLogger gLogger, const std::string engineFile, int frameWidth, int frameHeight, std::string inputName, std::string outputName,std::vector<int> inputShape, int outputDim, int maxBatchSize, int maxFacesPerScene, float knownPersonThreshold) {m_frameWidth = static_cast<const int>(frameWidth);m_frameHeight = static_cast<const int>(frameHeight);assert(inputShape.size() == 3);m_INPUT_C = static_cast<const int>(inputShape[0]);m_INPUT_H = static_cast<const int>(inputShape[1]);m_INPUT_W = static_cast<const int>(inputShape[2]);m_OUTPUT_D = static_cast<const int>(outputDim);m_INPUT_SIZE = static_cast<const int>(m_INPUT_C * m_INPUT_H * m_INPUT_W * sizeof(float));m_OUTPUT_SIZE = static_cast<const int>(m_OUTPUT_D * sizeof(float));m_maxBatchSize = static_cast<const int>(maxBatchSize);m_embed = new float[m_OUTPUT_D];croppedFaces.reserve(maxFacesPerScene);m_embeds = new float[maxFacesPerScene * m_OUTPUT_D];m_knownPersonThresh = knownPersonThreshold;// load engine from .engine file or create new engineloadEngine(gLogger, engineFile);// create stream and pre-allocate GPU buffers memorypreInference(inputName, outputName);
}void ArcFaceIR50::loadEngine(TRTLogger gLogger, const std::string engineFile) {if (fileExists(engineFile)) {std::cout << "[INFO] Loading ArcFace Engine...\n";std::vector<char> trtModelStream_;size_t size{0};std::ifstream file(engineFile, std::ios::binary);if (file.good()) {file.seekg(0, file.end);size = file.tellg();file.seekg(0, file.beg);trtModelStream_.resize(size);file.read(trtModelStream_.data(), size);file.close();}nvinfer1::IRuntime *runtime = nvinfer1::createInferRuntime(gLogger);assert(runtime != nullptr);m_engine = runtime->deserializeCudaEngine(trtModelStream_.data(), size);assert(m_engine != nullptr);m_context = m_engine->createExecutionContext();assert(m_context != nullptr);} else {throw std::logic_error("Cant find engine file");}
}void ArcFaceIR50::preInference() {// Engine requires exactly IEngine::getNbBindings() number of buffers.assert(m_engine->getNbBindings() == 2);// In order to bind the buffers, we need to know the names of the input and// output tensors. Note that indices are guaranteed to be less than IEngine::getNbBindings()inputIndex = m_engine->getBindingIndex("input");outputIndex = m_engine->getBindingIndex("output");// Create GPU buffers on devicecudaMalloc(&buffers[inputIndex], m_maxBatchSize * m_INPUT_SIZE);cudaMalloc(&buffers[outputIndex], m_maxBatchSize * m_OUTPUT_SIZE);// Create streamcudaStreamCreate(&stream);
}void ArcFaceIR50::preInference(std::string inputName, std::string outputName) {// Engine requires exactly IEngine::getNbBindings() number of buffers.assert(m_engine->getNbBindings() == 2);// In order to bind the buffers, we need to know the names of the input and// output tensors. Note that indices are guaranteed to be less than IEngine::getNbBindings()inputIndex = m_engine->getBindingIndex(inputName.c_str());outputIndex = m_engine->getBindingIndex(outputName.c_str());// Create GPU buffers on devicecudaMalloc(&buffers[inputIndex], m_maxBatchSize * m_INPUT_SIZE);cudaMalloc(&buffers[outputIndex], m_maxBatchSize * m_OUTPUT_SIZE);// Create streamcudaStreamCreate(&stream);
}void ArcFaceIR50::preprocessFace(cv::Mat &face, cv::Mat &output) {cv::cvtColor(face, face, cv::COLOR_BGR2RGB);face.convertTo(face, CV_32F);face = (face - cv::Scalar(127.5, 127.5, 127.5)) * 0.0078125;std::vector<cv::Mat> temp;cv::split(face, temp);for (int i = 0; i < temp.size(); i++) {output.push_back(temp[i]);}
}void ArcFaceIR50::preprocessFaces() {for (int i = 0; i < croppedFaces.size(); i++) {cv::cvtColor(croppedFaces[i].faceMat, croppedFaces[i].faceMat, cv::COLOR_BGR2RGB);croppedFaces[i].faceMat.convertTo(croppedFaces[i].faceMat, CV_32F);croppedFaces[i].faceMat = (croppedFaces[i].faceMat - cv::Scalar(127.5, 127.5, 127.5)) * 0.0078125;std::vector<cv::Mat> temp;cv::split(croppedFaces[i].faceMat, temp);for (int i = 0; i < temp.size(); i++) {m_input.push_back(temp[i]);}croppedFaces[i].faceMat = m_input.clone();m_input.release();}
}void ArcFaceIR50::doInference(float *input, float *output) {// DMA input batch data to device, infer on the batch asynchronously, and DMA output back to hostcudaMemcpyAsync(buffers[inputIndex], input, m_INPUT_SIZE, cudaMemcpyHostToDevice, stream);m_context->enqueueV2(buffers, stream, nullptr);cudaMemcpyAsync(output, buffers[outputIndex], m_OUTPUT_SIZE, cudaMemcpyDeviceToHost, stream);cudaStreamSynchronize(stream);
}void ArcFaceIR50::doInference(float *input, float *output, int batchSize) {// Set input dimensionsm_context->setBindingDimensions(inputIndex, nvinfer1::Dims4(batchSize, m_INPUT_C, m_INPUT_H, m_INPUT_W));// DMA input batch data to device, infer on the batch asynchronously, and DMA output back to hostcudaMemcpyAsync(buffers[inputIndex], input, batchSize * m_INPUT_SIZE, cudaMemcpyHostToDevice, stream);m_context->enqueueV2(buffers, stream, nullptr);cudaMemcpyAsync(output, buffers[outputIndex], batchSize * m_OUTPUT_SIZE, cudaMemcpyDeviceToHost, stream);cudaStreamSynchronize(stream);
}void ArcFaceIR50::addEmbedding(const std::string className, float embedding[]) {classNames.push_back(className);std::copy(embedding, embedding + m_OUTPUT_D, m_knownEmbeds + classCount * m_OUTPUT_D);classCount++;
}void ArcFaceIR50::addEmbedding(const std::string className, std::vector<float> embedding) {classNames.push_back(className);std::copy(embedding.begin(), embedding.end(), m_knownEmbeds + classCount * m_OUTPUT_D);classCount++;
}void ArcFaceIR50::initKnownEmbeds(int num) { m_knownEmbeds = new float[num * m_OUTPUT_D]; }void ArcFaceIR50::forward(cv::Mat frame, std::vector<struct Bbox> outputBbox) {getCroppedFaces(frame, outputBbox, m_INPUT_W, m_INPUT_H, croppedFaces);preprocessFaces();if (m_maxBatchSize < 2) {for (int i = 0; i < croppedFaces.size(); i++) {doInference((float *)croppedFaces[i].faceMat.ptr<float>(0), m_embed);std::copy(m_embed, m_embed + m_OUTPUT_D, m_embeds + i * m_OUTPUT_D);}std::cout << "---------------------------------------------------" << std::endl;std::cout << "[";for (int i = 0; i < m_OUTPUT_D; ++i) {std::cout << m_embed[i] << ",";}std::cout << "]" << std::endl;std::cout << "---------------------------------------------------" << std::endl;} else {int num = croppedFaces.size();int end = 0;for (int beg = 0; beg < croppedFaces.size(); beg = beg + m_maxBatchSize) {end = std::min(num, beg + m_maxBatchSize);cv::Mat input;for (int i = beg; i < end; ++i) {input.push_back(croppedFaces[i].faceMat);}doInference((float *)input.ptr<float>(0), m_embed, end - beg);std::copy(m_embed, m_embed + (end - beg) * m_OUTPUT_D, m_embeds + (end - beg) * beg * m_OUTPUT_D);}std::cout << "---------------------------------------------------" << std::endl;std::cout << "[";for (int i = 0; i < m_OUTPUT_D; ++i) {std::cout << m_embed[i] << ",";}std::cout << "]" << std::endl;std::cout << "---------------------------------------------------" << std::endl;}
}std::tuple<std::vector<std::string>, std::vector<float>> ArcFaceIR50::getOutputs(float *output_sims) {/*Get person corresponding to maximum similarity score based on cosine similarity matrix.*/std::vector<std::string> names;std::vector<float> sims;for (int i = 0; i < croppedFaces.size(); ++i) {int argmax = std::distance(output_sims + i * classCount, std::max_element(output_sims + i * classCount, output_sims + (i + 1) * classCount));float sim = *(output_sims + i * classCount + argmax);std::string name = classNames[argmax];names.push_back(name);sims.push_back(sim);}return std::make_tuple(names, sims);
}void ArcFaceIR50::visualize(cv::Mat &image, std::vector<std::string> names, std::vector<float> sims) {for (int i = 0; i < croppedFaces.size(); ++i) {float fontScaler = static_cast<float>(croppedFaces[i].x2 - croppedFaces[i].x1) / static_cast<float>(m_frameWidth);cv::Scalar color;if (sims[i] >= m_knownPersonThresh)color = cv::Scalar(0, 255, 0);elsecolor = cv::Scalar(0, 0, 255);cv::rectangle(image, cv::Point(croppedFaces[i].y1, croppedFaces[i].x1), cv::Point(croppedFaces[i].y2, croppedFaces[i].x2), color, 2, 8, 0);cv::putText(image, names[i] + " " + std::to_string(sims[i]), cv::Point(croppedFaces[i].y1 + 2, croppedFaces[i].x2 - 3), cv::FONT_HERSHEY_DUPLEX,0.1 + 2 * fontScaler, color, 1);}
}void ArcFaceIR50::resetEmbeddings() {classCount = 0;classNames.clear();
}ArcFaceIR50::~ArcFaceIR50() {// Release stream and bufferscudaStreamDestroy(stream);cudaFree(buffers[inputIndex]);cudaFree(buffers[outputIndex]);
}
3.3. common.cpp:
#include "common.h"bool fileExists(const std::string &name) {std::ifstream f(name.c_str());return f.good();
}void getFilePaths(std::string rootPath, std::vector<struct Paths> &paths) {DIR *dir;struct dirent *entry;std::string postfix = ".jpg";if ((dir = opendir(rootPath.c_str())) != NULL) {while ((entry = readdir(dir)) != NULL) {std::string class_path = rootPath + "/" + entry->d_name;DIR *class_dir = opendir(class_path.c_str());struct dirent *file_entry;while ((file_entry = readdir(class_dir)) != NULL) {std::string name(file_entry->d_name);if (name.length() >= postfix.length() && 0 == name.compare(name.length() - postfix.length(), postfix.length(), postfix))if (file_entry->d_type != DT_DIR) {struct Paths tempPaths;tempPaths.className = std::string(entry->d_name);tempPaths.absPath = class_path + "/" + name;paths.push_back(tempPaths);}}}closedir(dir);}
}
3.4. arcface.h:
#ifndef ARCFACE_H
#define ARCFACE_H#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <tuple>#include "common.h"struct CroppedFace {cv::Mat face;cv::Mat faceMat;int x1, y1, x2, y2;
};void getCroppedFaces(cv::Mat frame, std::vector<struct Bbox> &outputBbox, int resize_w, int resize_h, std::vector<struct CroppedFace> &croppedFaces);class ArcFaceIR50 {public:ArcFaceIR50(TRTLogger gLogger, const std::string engineFile, int frameWidth, int frameHeight, std::string inputName, std::string outputName,std::vector<int> inputShape, int outputDim, int maxBatchSize, int maxFacesPerScene, float knownPersonThreshold);~ArcFaceIR50();void preprocessFace(cv::Mat &face, cv::Mat &output);void doInference(float *input, float *output);void doInference(float *input, float *output, int batchSize);void addEmbedding(const std::string className, float embedding[]);void addEmbedding(const std::string className, std::vector<float> embedding);void forward(cv::Mat image, std::vector<struct Bbox> outputBbox);std::tuple<std::vector<std::string>, std::vector<float>> getOutputs(float *output_sims);void resetEmbeddings();void initKnownEmbeds(int num);void visualize(cv::Mat &image, std::vector<std::string> names, std::vector<float> sims);std::vector<struct CroppedFace> croppedFaces;static int classCount;private:void loadEngine(TRTLogger gLogger, const std::string engineFile);void preInference();void preInference(std::string inputName, std::string outputName);void preprocessFaces();int m_frameWidth, m_frameHeight, m_INPUT_C, m_INPUT_H, m_INPUT_W, m_OUTPUT_D, m_INPUT_SIZE, m_OUTPUT_SIZE, m_maxBatchSize, m_maxFacesPerScene;float m_knownPersonThresh;cv::Mat m_input;float *m_embed, *m_embeds, *m_knownEmbeds, *m_outputs;std::vector<std::string> classNames;nvinfer1::ICudaEngine *m_engine;nvinfer1::IExecutionContext *m_context;cudaStream_t stream;void *buffers[2];int inputIndex, outputIndex;
};#endif // ARCFACE_H
3.5. common.h:
#ifndef COMMON_H
#define COMMON_H#include "NvInfer.h"
// #include "cuda_runtime_api.h"
#include <cublasLt.h>
#include <dirent.h>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>struct Bbox {int x1, y1, x2, y2;float score;
};struct Paths {std::string absPath;std::string className;
};bool fileExists(const std::string &name);
void getFilePaths(std::string rootPath, std::vector<struct Paths> &paths);class TRTLogger : public nvinfer1::ILogger {public:void log(nvinfer1::ILogger::Severity severity, const char *msg) noexcept override {switch (severity) {case Severity::kINTERNAL_ERROR:std::cerr << "INTERNAL_ERROR: ";break;case Severity::kERROR:std::cerr << "ERROR: ";break;case Severity::kWARNING:std::cerr << "WARNING: ";break;case Severity::kINFO:std::cerr << "INFO: ";break;case Severity::kVERBOSE:std::cerr << "VERBOSE: ";break;default:std::cerr << "UNKNOWN: ";break;}std::cerr << msg << std::endl;}
};#endif // COMMON_H
3.6. test.cpp:
#include <opencv2/imgcodecs.hpp>
#include <opencv2/opencv.hpp>#include "arcface.h"
#include "common.h"int main(int argc, char* argv[]) {TRTLogger gLogger;int videoFrameWidth = 640;int videoFrameHeight = 480;std::vector<int> recInputShape = {3, 112, 112};int recOutputDim = 512;std::string recEngineFile = "/mnt/d/code/c_code/arcface_test/face-recognition-cpp-tensorrt/arcface_test/conversion/arcface-ir50_asia-112x112-b1-fp16.engine";std::cout << "recEngineFile = " << recEngineFile << std::endl;int maxFacesPerScene = 4;float knownPersonThreshold = 0.65;int recMaxBatchSize = 1;std::string recInputName = "input";std::string recOutputName = "output";std::vector<struct Bbox> outputBbox;outputBbox.reserve(maxFacesPerScene);ArcFaceIR50 recognizer(gLogger,recEngineFile,videoFrameWidth,videoFrameHeight,recInputName,recOutputName,recInputShape,recOutputDim,recMaxBatchSize,maxFacesPerScene,knownPersonThreshold);std::string face_image_path = argv[1];std::cout << "face_image_path = " << face_image_path << std::endl;cv::Mat frame = cv::imread( face_image_path );// frame 应该是表示使用 opencv 加载的一张图片,即mat// frame = cv::imdecode(data, cv::IMREAD_UNCHANGED);int height = frame.size[0];int width = frame.size[1];std::cout << "Image: " << frame.size() << std::endl;if (frame.empty()){throw "Empty image";}if ((height != recInputShape[1]) || (width != recInputShape[2])) {std::cout << "Resizing input to " << recInputShape[1] << "x" << recInputShape[2] << "\n";cv::resize(frame, frame, cv::Size(recInputShape[1], recInputShape[2]));}std::cout << "Getting embedding...";Bbox bbox;bbox.x1 = 0;bbox.y1 = 0;bbox.x2 = recInputShape[1];bbox.y2 = recInputShape[2];bbox.score = 1;outputBbox.push_back(bbox);recognizer.forward(frame, outputBbox);
}
3.7. CMakeLists.txt( 请自行修改你的 cuda、tensorrt 等路径 ):
cmake_minimum_required (VERSION 3.8) project("test")include_directories( "/soft/TensorRT-8.6.1.6/include" "/usr/local/cuda/include""/mnt/d/code/c_code/arcface_test/face-recognition-cpp-tensorrt/arcface_test/include")link_directories( "/soft/TensorRT-8.6.1.6/lib""/usr/local/cuda/lib64""/soft/opencv-3.4.6/build/lib")link_libraries( nvinfer nvinfer_pluginnvonnxparsernvparserscudnncublascudartopencv_coreopencv_imgproc opencv_highguiopencv_imgcodecs)# 将源代码添加到此项目的可执行文件。
add_executable("test" "/mnt/d/code/c_code/arcface_test/face-recognition-cpp-tensorrt/arcface_test/test.cpp" "/mnt/d/code/c_code/arcface_test/face-recognition-cpp-tensorrt/arcface_test/cpp/arcface.cpp" "/mnt/d/code/c_code/arcface_test/face-recognition-cpp-tensorrt/arcface_test/cpp/common.cpp")
3.8. cuda、tensorrt、opencv 版本说明:
截止到 2024年8月28日,该仓库中 readme --》Requirements 显示的该项目依赖的 cuda、tensorrt、opencv 的版本如下:
CUDA 11.3TensorRT 8.2.2.1OpenCV 4.5.5
但是本人的 cuda、tensorrt、opencv 版本如下所示:
CUDA 11.2TensorRT 8.6.1OpenCV xxx
最后证明也是可以编译通过并且成功计算人脸特征向量的,但是如果你的 tensorrt 是7.x 版本就放弃吧,因为 tensort8 相较于 Tensorrt7单单 API上就做了很多调整。
3.9. cmake 、make 执行生成了可执行文件 test,但是现在还没有 .engine 文件,需要先试用 python 脚本生成 .engine 文件方可进行测试。
4. 生成 .engine 文件:
4.1. cd conversion
4.2. python torch2trt.py( 如果成功,会生成 arcface-ir50_asia-112x112-b1-fp16.engine 文件 )
4.3. 报错以及解决方案:
大部分错都可以百度很快的解决( 比如缺 Python 依赖什么的 ),这里只列举一个花了我很久才解决的,就是需要安装 torch2trt_dynamic( ps:整个过程中一定要有耐心,不要砸桌子,根据环境的不同,需要陆陆续续安装很多依赖,而安装每一个依赖可能都很费劲,加油吧 ),以下是安装步骤:
git clone https://github.com/grimoire/torch2trt_dynamic.gitcd torch2trt_dynamicpython setup.py install
安装过程确实很顺利,但是执行 python torch2trt.py 的时候报错:
File "/usr/local/lib/python3.8/dist-packages/torch2trt_dynamic/converters/__init__.py", line 37, in <module>from .Conv2d import convert_Conv2d
ModuleNotFoundError: No module named 'torch2trt_dynamic.converters.Conv2d'
我的 torch2trt_dynamic 安装到了 /usr/local/lib/python3.8/dist-packages/torch2trt_dynamic 目录下,我进入 /usr/local/lib/python3.8/dist-packages/torch2trt_dynamic/converters 下发现确实没有 Conv2d.py,倒是有个conv2d.py,我将
conv2d.py 改名为 Conv2d.py,再次运行 python torch2trt.py 发现不报刚才那个错了,但是会报一个好像是循环依赖的问题,看来应该是没有 Conv2d.py 导致的。这个错在全网搜不到直接的解决方案,都是说 torch 版本的问题,其实是因为 torch2trt_dynamic/torch2trt_dynamic/converters 下面同时存在 Conv2d.py、conv2d.py 导致的,不知道为什么 git clone 以后 Conv2d.py就没了(可能配置下大小写敏感可以可决吧,我没试),我是直接下载的 .zip 源码包,存在大小写问题的 .py 文件如下:
'torch2trt_dynamic/converters/Conv2d.py''torch2trt_dynamic/converters/conv2d.py''torch2trt_dynamic/converters/Identity.py''torch2trt_dynamic/converters/identity.py''torch2trt_dynamic/converters/Linear.py''torch2trt_dynamic/converters/linear.py''torch2trt_dynamic/converters/ReLU.py''torch2trt_dynamic/converters/relu.py''torch2trt_dynamic/converters/ReLU6.py''torch2trt_dynamic/converters/relu6.py'
所以需要重新安装 torch2trt_dynamic,我也不知道 python setup.py install 对应的卸载命令是什么,我是直接将安装路径( 例如我的是 /usr/local/lib/python3.8/dist-packages )下的 torch2trt_dynamic 直接干掉了( ps:还要删除该目录下其余几个 torch2trt_dynamic 前缀的文件或文件夹 )。重新安装 torch2trt_dynamic 后,执行 python torch2trt.py 就成功生成了 arcface-ir50_asia-112x112-b1-fp16.engine( 可能会报几个小错,不过我遇到的都是一百度一大堆,很好解决 )
5. 测试 arcFace 的人脸识别( 计算特征向量 ):
我在 face_images 下准备了几张明星的人脸照片:
执行 build 下的 ./test ../face_images/yaoming_0001.png 输出512维度的坐标:
最后我将这三个明星的人脸特征向量保存到了 milvus 向量数据库中,度量方式使用欧式距离,查询出的 topk还算可以,比如使用甄子丹的人脸特征向量查询结果如下: