目录
引言:
环境构建(本文使用cmake,开发环境ubuntu22.04,IDE为clion)
项目文件构造
CMakeLists.txt编写
简单头文件
最初成员函数实现
add函数实现思路
search函数实现思路
main函数简单实现
思路
添加环境之后的main函数实现
需要创建的对象
如何获取add和search需要的数据
初步可运行的main
第一次运行:
添加和搜索同一张图片
不同图片测试
总结
引言:
在工业界,C++因其高性能、灵活性和对底层硬件的良好控制,一直是实现计算机视觉和机器学习任务的热门语言,尤其是在需要高性能计算的场景下。而FAISS(Facebook AI Similarity Search)作为一个高效的相似性搜索库,专为大规模特征向量的相似性搜索和聚类任务设计,尤其擅长处理高维向量空间中的近似最近邻搜索问题,这在人脸识别中尤为重要,因为人脸识别常常涉及到在大型特征数据库中快速查找最相似的人脸特征。
尽管Python因其实现简便快捷,也是机器学习和AI领域广泛使用的语言,但在对性能有严格要求的工业级应用中,C++与FAISS的组合由于其卓越的性能表现,成为追求极致效率和资源利用的首选。尤其是在处理大规模数据集和实时性要求高的应用场景下,C++的底层优化能力和FAISS的高效算法相结合,能够满足工业界对于人脸识别系统的高标准需求。今天我将从零开始,带着大家利用c++使用faiss库来实现一个人脸识别项目。在人脸识别之前还需要进行人脸检测和人脸特征提取,这部分会在以后的文章提到,后面会直接使用
环境构建(本文使用cmake,开发环境ubuntu22.04,IDE为clion)
项目文件构造
先在IDE中创建入下图所示的项目文件,include用来存放需要的头文件,src存放源文件
CMakeLists.txt编写
cmake_minimum_required(VERSION 3.20)# 设置CMake的最低版本要求为3.20(这个可以根据自己的情况来)project(AnnTest)# 定义一个名为AnnTest的项目set(CMAKE_CXX_STANDARD 17) # 设置C++源码的编译标准为C++17
set(CMAKE_THREAD_LIBS_INIT "-lpthread") # 设置线程库为pthread,用于支持多线程编程set(CMAKE_CHARACTER_SET utf-8)# 设置CMake的字符集为utf-8,用于处理非ASCII字符的源文件或输出
set(CMAKE_CUDA_ARCHITECTURES 75) # 针对CUDA编译设置,指定CUDA架构为7.5
set(CMAKE_CUDA_COMPILER /usr/local/cuda-11.8/bin/nvcc)# 设置CUDA编译器路径为CUDA 11.8的nvccfind_package(CUDA REQUIRED) # 查找并配置CUDA支持
find_library(CUDA_CUBLAS_LIBRARY NAMES libcublas.so.11 PATHS /usr/local/cuda/lib64)
# 查找名为libcublas.so.11的库文件,指定搜索路径为/usr/local/cuda/lib64set(SERVER_LIB/usr/local/lib/libfaiss.a/usr/local/faiss/lib/libfaiss_gpu.a) # 定义一个名为SERVER_LIB的变量,包含需要链接的FAISS库文件路径(这个一定要有)add_executable(AnnTest main.cpp) #添加一个可执行文件目标AnnTest,源文件为main.cpptarget_link_libraries(AnnTest PRIVATE ${SERVER_LIB})## 将之前定义的SERVER_LIB库链接到AnnTest目标上,链接类型为PRIVATE
不懂的可以看我后面的注释,。faiss库可以去官方网站上下载的,懒得下的可以点个赞私信我发你。
简单头文件
声明一个类(拷贝构造,析构函数和拷贝赋值先使用默认的)
#ifndef ANNTEST_ANN_H
#define ANNTEST_ANN_H
class ANN {
public:ANN();~ANN();
public:void add();bool search();void erase();
private:
};
#endif //ANNTEST_ANN_H
在上面的代码中我们先构建了最基本的三个功能成员函数,增加,搜索和删除人脸。下面我们会通过实现每一个函数需要的成员变量,来把这个类设计完整
最初成员函数实现
add函数实现思路
这里我的add函数要实现人脸的录入,也就是要调用faiss中录入人脸的API,我这里使用的有四种方式:faiss库中的IndexFlatL2,gpu::GpuIndexFlatL2,IndexFlatIP,gpu::GpuIndexFlatIP都有一个add函数用来添加人脸信息,由此添加四个成员变量(这四个成员都是类对象,使用指针来声明)。
#include <faiss/IndexFlat.h>
#include <faiss/gpu/GpuIndexFlat.h>
#include <memory>
class ANN {
public:ANN();~ANN();
public:void add();bool search();void erase();
private:std::unique_ptr<faiss::IndexFlatL2> euclideanIndex;std::unique_ptr<faiss::gpu::GpuIndexFlatL2> euclideanGPUIndex;std::unique_ptr<faiss::IndexFlatIP> cosineIndex;std::unique_ptr<faiss::gpu::GpuIndexFlatIP> cosineGPUIndex;
};
四个成员变量,IndexFlatL2表示使用欧氏距离,IndexFlatIP表示使用cos距离,带gpu表示使用gpu。 所以增加成员变量来判断是否使用GPU以及使用哪种计算方式
enum CalculatingMethod{Euclidean,Cosine
};
class ANN {
public:ANN();~ANN();
public:void add(std::unique_ptr<float[]> data);bool search();void erase();
private:CalculatingMethod method;bool useGPU = false;std::unique_ptr<faiss::IndexFlatL2> euclideanIndex;std::unique_ptr<faiss::gpu::GpuIndexFlatL2> euclideanGPUIndex;std::unique_ptr<faiss::IndexFlatIP> cosineIndex;std::unique_ptr<faiss::gpu::GpuIndexFlatIP> cosineGPUIndex;
};
然后在构造函数中根据不同的情况生成不同的类对象
ANN::ANN(int dimension,int kValue, float threshold, bool useGPU,int gpuDevice,CalculatingMethod method):
dimension(dimension),kValue(kValue), threshold(threshold), useGPU(useGPU),gpuDevice(gpuDevice) {if(this->method == Euclidean){if(this->useGPU){faiss::gpu::GpuIndexFlatConfig config;config.device = gpuDevice;faiss::gpu::StandardGpuResources gpuResources;this->euclideanGPUIndex = std::make_shared<faiss::gpu::GpuIndexFlatL2>(&gpuResources,this->dimension,config);}else{this->euclideanIndex = std::make_shared<faiss::IndexFlatL2>(this->dimension);}}else{if(this->useGPU){faiss::gpu::GpuIndexFlatConfig config;config.device = gpuDevice;faiss::gpu::StandardGpuResources gpuResources;this->cosineGPUIndex = std::make_shared<faiss::gpu::GpuIndexFlatIP>(&gpuResources,this->dimension,config);}else{this->cosineIndex = std::make_shared<faiss::IndexFlatIP>(this->dimension);}}}
dimension这里是我们要对比的特征向量的维度。
然后关于faiss的add函数
void add(idx_t, const float* x) override;
需要两个参数,第一个为要添加的数量,这里我默认为1,第一个为添加的数据。修改我们自己的add函数
void add(const std::unique_ptr<float[]>& data);
定义我们的最初的add函数
void ANN::add(const std::shared_ptr<float[]>& data) {if(this->method==Cosine){this->useGPU ? this->cosineGPUIndex->add(1,data.get()) : this->cosineIndex->add(1,data.get());}else{this->useGPU ? this->euclideanGPUIndex->add(1,data.get()) : this->euclideanIndex->add(1,data.get());}
}
search函数实现思路
为了验证我们最初的add函数是否能成功添加,我们需要在来一个最初的search函数,与add函数相同,在faiss库中也有相同的search API,
void search(idx_t n,const float* x,idx_t k,float* distances,idx_t* labels,const SearchParameters* params = nullptr) const override;
一个个来,n代表你要查询几个,一般我设为1,x为要查询的人脸信息,k为最相近的结果你要几个,distances用于接收查询结果中每个查询向量对应最近邻居的距离,大小最少要n*k,labels用于接收每个查询向量的最近邻居的索引或标识符。数组大小同样至少需要n*k,
params这是一个可选参数,允许用户指定额外的搜索参数,比如搜索时的GPU设备选择、搜索策略等。如果不提供(即保持默认值),则使用索引或库的默认设置
k的值我作为成员变量(我这里取5),distances和labels我们创建一个
bool ANN::search(const std::shared_ptr<float[]>& data) {std::vector<long> lables(this->kValue);std::vector<float> distances(this->kValue);if(this->method==Cosine){this->useGPU ? this->cosineGPUIndex->search(1,data.get(),this->kValue,distances.data(),lables.data()): this->cosineIndex->search(1,data.get(),this->kValue,distances.data(),lables.data());}else{this->useGPU ? this->euclideanGPUIndex->search(1,data.get(),this->kValue,distances.data(),lables.data()): this->euclideanIndex->search(1,data.get(),this->kValue,distances.data(),lables.data());}}
这里和上面的add很相似,然后我们说了,结果都在 distances和labels中,所以:
bool ANN::search(const std::shared_ptr<float[]>& data) {std::vector<long> lables(this->kValue);std::vector<float> distances(this->kValue);if(this->method==Cosine){this->useGPU ? this->cosineGPUIndex->search(1,data.get(),this->kValue,distances.data(),lables.data()): this->cosineIndex->search(1,data.get(),this->kValue,distances.data(),lables.data());}else{this->useGPU ? this->euclideanGPUIndex->search(1,data.get(),this->kValue,distances.data(),lables.data()): this->euclideanIndex->search(1,data.get(),this->kValue,distances.data(),lables.data());}for(int i=0;i<this->kValue;i++){long index = lables.at(i);float distance = distances.at(i);if(this->method==Cosine){if(index>=0&&distance>this->threshold){std::cout<<"查询到人脸,"<<"distance:"<<distance<<"index:"<<index<<std::endl;return true;}else{std::cout<<"未查询到人脸,"<<"distance:"<<distance<<"index:"<<index<<std::endl;return false;}}else{if(index>=0&&distance<this->threshold){std::cout<<"查询到人脸,"<<"distance:"<<distance<<"index:"<<index<<std::endl;return true;}else{std::cout<<"未查询到人脸,"<<"distance:"<<distance<<"index:"<<index<<std::endl;return false;}}}
}
根据计算方式的不同,对结果的判断也不同,判断到人脸后先直接打印数据即可,由此,最初的search函数写完了,模型里面没有数据时index=-1,所以加一层判断。
main函数简单实现
思路
先在有了增加和搜索的函数,我们要实现这个人脸的识别,无非就是,利用增加函数增加一个人脸,然后指定一个评分,再使用搜索函数,能搜索到人脸,则人脸识别成功,成功之后可以联动其它相关操作--比如开门。如下:
#include "ANN.h"
std::shared_ptr<ANN> ann;int main() {ann = std::make_shared<ANN>(5,0.6,1);
// ann->add();return 0;
}
上面的代码中定义了需要五个最相近的人脸,评分阈值取0.6(cos距离),使用GPU,但是,写到add函数的时候我们会发现,它需要我们传入一个人脸数据(这是一个抽象的说法,其实是一个float数组的智能指针对象),所以这里我们需要像之前提到的那样直接使用人脸检测和人脸特征提取的代码,修改一下cmakelists:
cmake_minimum_required(VERSION 3.20)
#project(test)
project(AnnTest)set(CMAKE_CXX_STANDARD 17)
set(CMAKE_THREAD_LIBS_INIT "-lpthread")set(CMAKE_CHARACTER_SET utf-8)
set(CMAKE_CUDA_ARCHITECTURES 75)
set(CMAKE_CUDA_COMPILER /usr/local/cuda-11.8/bin/nvcc)find_package(CUDA REQUIRED)
find_library(CUDA_CUBLAS_LIBRARY NAMES libcublas.so.11 PATHS /usr/local/cuda/lib64)find_package(Pugixml REQUIRED)set(MTCNN_SOURCESsrc/mtcnn/detector.cppinclude/mtcnn/face.hsrc/mtcnn/onet.cppsrc/mtcnn/pnet.cpp src/mtcnn/rnet.cppinclude/mtcnn/detector.h include/mtcnn/helpers.h include/mtcnn/onet.h include/mtcnn/pnet.h include/mtcnn/rnet.hsrc/FaceDetectionManager.cpp include/FaceANNThread.h src/FaceANNThread.cpp src/FaceInputHandler.cppinclude/MQTTClient.h src/MQTTClient.cpp src/MQTTClientManager.cpp src/AlgorithmHttpServer.cppinclude/AlgorithmHttpServer.h include/log.h include/g3sinks/LogRotate.h include/g3sinks/LogRotateUtility.hinclude/g3sinks/LogRotateWithFilter.h src/LogRotate.cpp src/LogRotateHelper.ipp src/LogRotateUtility.cppsrc/LogRotateWithFilter.cpp)list(APPEND CMAKE_PREFIX_PATH "/mnt/data/software/libtorch/share/cmake")
set(Torch_DIR /mnt/data/software/libtorch/share/cmake/Torch)
find_package(Torch REQUIRED)
message(STATUS "TORCH_LIBRARIES = ${TORCH_LIBRARIES}")
message(STATUS "TORCH_INCLUDES = ${TORCH_INCLUDE_DIRS}")find_package(g3log CONFIG REQUIRED)set(OpenCV_INCLUDE_DIRS "/mnt/data/software/opencv4")
message(STATUS "OpenCV_INCLUDE_DIRS = ${OpenCV_INCLUDE_DIRS}")
include_directories(${OpenCV_INCLUDE_DIRS})include_directories(${PROJECT_SOURCE_DIR}/include)include_directories(/usr/local/paho/include)
include_directories(${Torch_INCLUDE_DIRS})
link_directories(/mnt/data/software/Poco)
link_directories(/mnt/data/software/hkSDK/lib)
include_directories(/mnt/data/software/hkSDK/include)include_directories(/mnt/data/software/github/CppServer/include)
include_directories(/mnt/data/software/github/CppServer/modules/CppCommon/include)
include_directories(/mnt/data/software/github/CppServer/modules/CppCommon/modules/fmt/include/)
include_directories(/mnt/data/software/github/CppServer/modules/asio/asio/include/)
link_directories(/mnt/data/software/github/CppServer/bin/)
link_directories(/mnt/data/software/github/g3log/build)find_package(Poco REQUIRED Data)
message(STATUS "Poco_Data_LIBS = ${Poco}")set(SERVER_LIB/mnt/data/software/github/CppServer/bin/libasio.a/usr/local/lib/libfaiss.a/usr/local/faiss/lib/libfaiss_gpu.a/usr/lib/gcc/x86_64-linux-gnu/11/libgfortran.so/lib/x86_64-linux-gnu/libopenblas.so.0)
set(WEB_LIB/mnt/data/software/github/CppServer/bin/libasio.a)set(OpenCV_LIBS /mnt/data/software/github/opencv-4.8.0/build/lib/libopencv_img_hash.so.4.8.0/mnt/data/software/github/opencv-4.8.0/build/lib/libopencv_sfm.so.4.8.0/mnt/data/software/github/opencv-4.8.0/build/lib/libopencv_world.so.4.8.0/mnt/data/software/github/opencv-4.8.0/build/lib/libopencv_img_hash.so.408/mnt/data/software/github/opencv-4.8.0/build/lib/libopencv_sfm.so.408/mnt/data/software/github/opencv-4.8.0/build/lib/libopencv_world.so.408)add_executable(AnnTest main.cpp include/ANN.h src/ANN.cppinclude/faceRecognizer/FaceNetRecognizer.h include/FaceRecognition.hinclude/mtcnn/detector.h src/mtcnn/detector.cpp include/mtcnn/face.h include/mtcnn/onet.hinclude/mtcnn/pnet.h include/mtcnn/rnet.h src/mtcnn/onet.cpp src/mtcnn/pnet.cpp src/mtcnn/rnet.cppsrc/FaceRecognition.cpp include/faceRecognizer/FaceRecognizerFactory.hsrc/faceRecognizer/FaceNetRecognizer.cpp include/FacePreprocess.h src/faceCommon.cpp)
target_link_libraries(AnnTest PRIVATE ${TORCH_LIBRARIES} ${OpenCV_LIBS} ${SERVER_LIB} ${WEB_LIB} ${CUDA_CUBLAS_LIBRARY}-llapack -lblas -luuid -lssl -lcrypto-Wl,--copy-dt-needed-entries)
增加的东西可能有点多,主要环境是增加了libtorch和opencv,然后add_executable后面除了我们一开始写的那三个函数外,后面都是用来做人脸检测和特征提取的代码,最后项目的结构如下
这些增加的源码我会在有时间时更新文章来和大家一起写,如果有需要这段源码的同学可以点击人脸检测源码 。
添加环境之后的main函数实现
需要创建的对象
我们自己创建了一个ANN类,然后在我后续的代码中,还需要添加一个人脸检测类以及人脸特征类:如下
std::string modelPath = "/home/zlzg01/ly/AnnTest/bin/models";
std::shared_ptr<ANN> ann;
std::shared_ptr<FaceRecognition> globalFaceDetector = std::make_shared<FaceNetRecognizer>(modelPath);
std::shared_ptr<std::vector<Face>> faces = std::make_shared<std::vector<Face>>();
这些类在人脸检测的源码中都有具体的实现。
如何获取add和search需要的数据
这里globalFaceDetector为我们的检测器,大家暂时先记住使用它之前需要对它调用initDetection函数来初始化,然后它里面有一个getFaces方法,把我们的图片和face类作为参数放进去,就可以完成人脸的检测,face类中有一个getFeaturesData方法来获取人脸的特征值,这个就是我们自己实现的函数需要的参数,具体代码如下:
ann = std::make_shared<ANN>(512, 1, 0.6, 1, 0);
std::string path = "/yourPath/bb.jpg";
cv::Mat img = getImageMat(path);
std::string preLoadFile1 = modelPath + "/test_face1.jpg";
std::string preLoadFile2 = modelPath + "/test_face2.jpg";
bool success = globalFaceDetector->initDetection(preLoadFile1, preLoadFile2);
globalFaceDetector->getFaces(img, *faces);
auto features = faces->at(0).getFeaturesData();
ann->add(features);
ann->search(features);
上面的代码函数调用的流程和我说的一样,先创建我们的模型类,维度512,搜索1个最近的,阈值0.6,使用GPU,GPU使用第0块,getImageMat函数如下:
cv::Mat getImageMat(const std::string& path){std::string imagePath = path;return cv::imread(imagePath,cv::IMREAD_COLOR);
}
然后我们的检测模型一开始是需要做预热的所以
std::string preLoadFile1 = modelPath + "/test_face1.jpg";
std::string preLoadFile2 = modelPath + "/test_face2.jpg";
bool success = globalFaceDetector->initDetection(preLoadFile1, preLoadFile2)
初步可运行的main
把上面所说的内容整合一下,再导入后面加上的源码头文件代码如下:
//
// Created by zlzg01 on 24-6-20.
//
#include "ANN.h"
#include "mtcnn/face.h"
#include "FaceRecognition.h"
#include "faceCommon.h"
#include "faceRecognizer/FaceNetRecognizer.h"std::string modelPath = "/yourPath/bin/models";
std::shared_ptr<ANN> ann;
std::shared_ptr<FaceRecognition> globalFaceDetector = std::make_shared<FaceNetRecognizer>(modelPath);
std::shared_ptr<std::vector<Face>> faces = std::make_shared<std::vector<Face>>();
std::shared_ptr<std::vector<Face>> faces2 = std::make_shared<std::vector<Face>>();
cv::Mat getImageMat(const std::string& path){std::string imagePath = path;return cv::imread(imagePath,cv::IMREAD_COLOR);
}void cleanup() {// 显式释放共享指针ann.reset();globalFaceDetector.reset();
}int main() {try {ann = std::make_shared<ANN>(512, 5, 0.6, 1, 0);std::string path = "/yourPath/bb.jpg";std::string path2 = "/yourPath/download.jpg";cv::Mat img = getImageMat(path);cv::Mat img2 = getImageMat(path2);
// std::shared_ptr<cv::Mat> imgs = std::make_shared<cv::Mat>(img);std::string preLoadFile1 = modelPath + "/test_face1.jpg";std::string preLoadFile2 = modelPath + "/test_face2.jpg";bool success = globalFaceDetector->initDetection(preLoadFile1, preLoadFile2);if (!success) {int errorCode = globalFaceDetector->getErrorCode();std::string errorString = globalFaceDetector->getErrorString();}globalFaceDetector->getFaces(img, *faces);globalFaceDetector->getFaces(img2, *faces2);auto features = faces->at(0).getFeaturesData();auto features2 = faces2->at(0).getFeaturesData();ann->add(features);ann->search(features2);cleanup();return 0;}catch (const cv::Exception& e) {std::cerr << "OpenCV error: " << e.what() << std::endl;cleanup();return -1;} catch (const std::exception& e) {std::cerr << "Standard exception: " << e.what() << std::endl;cleanup();return -1;} catch (...) {std::cerr << "Unknown exception occurred." << std::endl;cleanup();return -1;}
}
除了ANN的头文件后面都是我们检测源码中会有的,main函数的内容和之前差不多,只是加上了异常处理的内容,然后使用了两张不同的图片,我的检测代码的思路是
- 先什么图片特征都不添加,直接进行检测,看检测结果
- 添加一张图片,检测同一张图片看结果
- 添加一张图片,检测另一张图片看结果
其实主要就是控制add函数的使用和add,search函数中的参数,
第一次运行:
ann = std::make_shared<ANN>(512, 5, 0.6, 1, 0);std::string path = "/yourPath/bb.jpg";std::string path2 = "/yourPath/download.jpg";cv::Mat img = getImageMat(path);cv::Mat img2 = getImageMat(path2);
// std::shared_ptr<cv::Mat> imgs = std::make_shared<cv::Mat>(img);std::string preLoadFile1 = modelPath + "/test_face1.jpg";std::string preLoadFile2 = modelPath + "/test_face2.jpg";bool success = globalFaceDetector->initDetection(preLoadFile1, preLoadFile2);if (!success) {int errorCode = globalFaceDetector->getErrorCode();std::string errorString = globalFaceDetector->getErrorString();}globalFaceDetector->getFaces(img, *faces);globalFaceDetector->getFaces(img2, *faces2);auto features = faces->at(0).getFeaturesData();auto features2 = faces2->at(0).getFeaturesData();//ann->add(features);ann->search(features);cleanup();return 0;
结果如下:
无结果是index会为-1.
添加和搜索同一张图片
ann->add(features);
ann->search(features);
结果
这里准确率直接给了1,有时候会是0.9999...,毕竟是同一张人脸图片
不同图片测试
ann->add(features);
ann->search(features2);
结果
这里是查询不到,然后准确率也给的很低
总结
到这里我们对模型的验证也初步完成了,有源码的同学也可以自己尝试使用更多的人脸来测试(源码获取点这里),在识别成功后就可以联动一些开门,拍照等操作了。然后大家也可能会发现一个问题,模型一开始录入了,但是程序重启之后又清零了,大家可以思考一下这个问题应该怎么解决,后面我会继续更新来和大家一起优化这个代码,然后大家对上面的代码有什么问题的可以下方留言(趁我现在还记得)。然后如果是缺少opencv和faiss环境自己懒得安装的,可以点赞后私信我。