iOS 端基于OpenCV实现人脸识别
一、OpenCV的编译
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉和机器学习软件库,它可以运行在大多数的主流操作系统上,其中包括iOS。
在OpenCV官网有提供已经编译好的 iOS 端 framework 下载,但是当前版本(4.5.1)OpenCV提供的二进制包中是不包括人脸识别等扩展模块的,这需要我们自己手动编译。
1、环境准备
编译OpenCV iOS版 需要确保电脑中已经安装以下软件:
- Xcode
- Xcode Command Line Tools
- cmake
- Python
Xcode 可以在App Store下载,安装完成后在终端执行 xcode-select --install
来安装 Xcode Command Line Tools。
然后可以通过 Home Brew 安装 cmake :brew install cmake
, 至于 如何安装Home Brew 可以参考Home Brew 官网的教程。
Mac OS 已经自带了Python2.7 所有我们不需要安装Python。
2、下载代码
首先到 https://github.com/opencv/opencv 下载最新的OpenCV代码,建议不要克隆整个仓库,因为这样会花费较多的时间,下载最新的代码zip包即可。
然后到 https://github.com/opencv/opencv_contrib下载最新的扩展包代码,注意应该和前面下载的代码保持版本号一致。
将下载的代码放到同一个文件夹方便操作,例如:
./OpenCV
├── opencv-4.5.1
└── opencv_contrib-4.5.1
3、编译
OpenCV已经为我们写好了编译用的工具脚本,放在opencv-4.5.1/platforms/ios/
目录下,其中有一个 build_framework.py,它就是编译iOS 端 framework 的工具脚本。
在终端执行这个工具脚本(注意将python文件的路径换为你电脑上的真实路径)
# 切换到你的工作目录,编译后的文件将输出到这个目录
cd <yourWorkSpace>
# 执行编译脚本, --contrib 参数指定为我们下周的扩展包的目录
python path/to/build_framework.py --contrib path/to/opencv_contrib iOS
我们通过 --contrib 参数指定了OpenCV的扩展包目录,这样就能将扩展包中的功能一起编译到最终的 framework 中。
最后的ios 参数指定了输出路径为当前目录的ios目录。
如果你不想包含扩展包中的全部扩展,可以用 --without 参数来排除特定的扩展,更多的编译参数可以查看 build_framework.py 中if __name__ == "__main__":
这一行代码后面的参数解析代码,可以知道还有哪些参数可以使用。
如果一切顺利,你将在工作目录的 ios文件夹中找到编译好的 opencv2.framework 文件,这就是我们需要的库文件。
二、基于OpenCV实现人脸识别
实现最基础的人脸识别主要分为以下几步 :
- 录入
- 通过摄像头采集样本照片
- 通过人脸检测技术,从照片中提取人脸
- 将人脸数据做灰度处理
- 将处理后的人脸数据交给OpenCV自带的人脸识别器进行训练
- 识别
- 通过摄像头采集样本照片
- 通过人脸检测技术,从照片中提取人脸
- 将人脸数据做灰度处理
- 将处理后的数据交给训练好的识别器进行对比得到相似度
首先将编译得到的framework导入到Xcode的iOS 项目中。然后在需要的地方 #import <opencv2/opencv2.h>
就可以开始使用了
1、图像采集
OpenCV 为我们提供了方便的封装:CvVideoCamera2,可以很容易的获取到摄像头数据。
// 初始化一个 CvVideoCamera2 对象,ParentView参数是一个UIView 对象,用来展示摄像头预览。
CvVideoCamera2 *videoCamera = [[CvVideoCamera2 alloc] initWithParentView:self];
// 设置使用的摄像头,这里设置为前置摄像头
videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionFront;
// 设置图像分辨率
videoCamera.defaultAVCaptureSessionPreset = AVCaptureSessionPreset640x480;
// 设置视频的方向。这里为竖屏方向展示
videoCamera.defaultAVCaptureVideoOrientation = AVCaptureVideoOrientationPortrait;
// 设置采集帧率
videoCamera.defaultFPS = 30;
// 是否灰度
videoCamera.grayscaleMode = NO;
// 是否使用预览
videoCamera.useAVCaptureVideoPreviewLayer = YES;
// 设置代理,回传采集到的数据
videoCamera.delegate = self;
然后实现 CvVideoCameraDelegate2 代理协议
在 - (void*)processImage:(Mat *)image;
回调方法中处理捕获到的图像。系统将以你设置的帧率回传摄像头捕捉到的图像(例如这里的30帧每秒)。
其中参数 Mat * image 是OpenCV的图片对象。它可以和UIImage CGImage相互转换。
转换方式请查看 <opencv2/Mat+Converters.h>
2、人脸检测
OpenCV
OpenCV提供了基础的人脸检测工具 CascadeClassifier。
// 初始化一个检测工具
CascadeClassifier * classifier = [[CascadeClassifier alloc] initWithFilename:filePath];
这里需要一个filePath参数,它指定了图像分类器的预设,这里我们需要传入一个人脸检测是预设。文件在OpenCV的源代码里:
opencv-4.5.1/data/haarcascades_cuda/haarcascade_frontalface_alt2.xml
这里有好几种人脸检测是预设,你可以自行选择。
将这个文件导入Xcode工程,然后获取到它的路径传入到 CascadeClassifier的初始化参数里即可。
接下来进行人脸检测
[classifier detectMultiScale:image objects:resultData];
这里有两个参数,第一个参数是需要进行检测的图片,也就是刚才摄像头捕捉到的图片,第二个参数是一个 NSMutableArray<Rect2i *> *
类型的数组,用来保存检测结果,当检测到图片中的人脸之后,人脸在图片中的坐标,将会保存在这个数组中。
CoreImage
除了OpenCV之外,苹果也提供了方便的人脸检测工具。它就是包含在CoreImage模块中的 CIDetector 类。
// CIDetector 的配置
NSDictionary *detectorOptions = @{
CIDetectorAccuracy:CIDetectorAccuracyHigh, // 设置检测精确度
CIDetectorEyeBlink:@(YES), // 是否检测眨眼
CIDetectorTracking:@(YES) // 是否追踪人脸
};
// 初始化一个 CIDetector
CIDetector* faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context: nil options:detectorOptions];
使用 CIDetector 检测人脸我们需要将Mat 转换成 CIImage 类型。
// 先转换成CGImage
CGImageRef cgImg = [image toCGImage];
// 再转换成 CIImage
CIImage *ciImage = [CIImage imageWithCGImage:cgImg];
然后检测人脸:
NSArray<CIFeature *> *features = [faceDetector featuresInImage:ciImage];
返回的数组就是检测到的人脸,里面的 CIFeature ,是检测到的物体的一些属性,它是一个抽象类。
因为 CIDetector 不止可以检测人脸。如果数组里的对象是CIFaceFeature 则检测到了人脸。
我们可以对人脸进行很多的判断。
for (CIFeature *feature in features) {
if ([feature isKindOfClass:CIFaceFeature.class]) {
CIFaceFeature *faceFeature = (CIFaceFeature *)feature;
// 判断是否检测到了嘴巴,是否检测到了左右眼,是否有追踪ID,如果在创建CIDetector 时允许了人脸追踪,则相同的人脸会有相同的trackingID。
if (faceFeature.hasMouthPosition && faceFeature.hasLeftEyePosition && faceFeature.hasRightEyePosition && faceFeature.hasTrackingID) {
// faceFeature.bounds 属性保存了人脸的位置信息。
// 处理图像,处理图像的部分,比如裁剪和缩放灰度等,ios原生也能处理,你也可以选择用Opencv处理。
}
}
}
3、图像处理
现在我们拿到了人脸在图片中的位置,下一步就是将人脸裁剪下来,并做统一的大小处理。
使用 Mat 的- (instancetype)initWithMat:(Mat*)mat rect:(Rect2i*)roi;
初始化方法可以将人脸从原始图片中裁剪下来。
// faceRect 参数为上一步检测人脸时得到的数组中的数据。
Mat * face = [[Mat alloc] initWithMat:image rect:faceRect];
裁剪出来的人脸图片大小不一,不适合进行识别,接下来对图片进行统一大小的缩放,这里需要一点c++代码了,请确保你写代码的文件名是.mm 结尾的,这样这个文件才支持 c++ 和 Objective-C 的混编。
// 获取原生C++的mat
cv::Mat nativeFace = face.nativeRef;
// 创建要缩放的目标尺寸
cv::Size destSize = cv::Size(128,128);
// 用于保存缩放后的图片的原生 c++ mat
cv::Mat newNativeFace;
// 缩放
cv::resize(nativeFace, newNativeFace, destSize);
// 转换成OC对象
Mat * newFace = [Mat fromNative:newNativeFace];
这样,我们就得到了大小统一都是 128x128 的图像。
接下来对图像进行灰度处理,将图片都转成灰色。
为了方便,我们给Mat 类加一个扩展,依然是c++ 与 Objective-C 混编:
// Mat+gray_img.h
@interface Mat(gray_img)
-(Mat *)grayMat;
@end
// Mat+gray_img.mm
@implementation Mat (gray_img)
-(Mat *)grayMat {
cv::Mat cvMat = self.nativeRef;
if (cvMat.channels() == 1) { // 如果已经只有一个灰度通道,直接返回
return [Mat fromNative:cvMat];
} else { // 处理图片,只保留灰度通道
cv::Mat grayMat = cv::Mat(cvMat.rows, cvMat.cols, CV_8UC1);
cv::cvtColor(cvMat, grayMat, COLOR_BGR2GRAY);
return [Mat fromNative:grayMat];
}
}
@end
我们需要多收集一些人脸数据,大概几十张就可以了,越多的话训练速度越慢,供后面使用。
4、训练人脸识别器
通过上面的步骤,我们已经拿到了我们需要的人脸数据。
接下来就是人脸识别。OpenCV提供了三种基础的人脸识别算法。分别是LBPHFaceRecognizer、EigenFaceRecognizer、FisherFaceRecognizer。
他们的用法基本相同,这里以 LBPHFaceRecognizer举例。
创建一个识别器,依然是c++与Objective-C混编:
LBPHFaceRecognizer* recognizer = [[LBPHFaceRecognizer alloc] initWithNativePtr: cv::face::LBPHFaceRecognizer::create()];
这样初始化出来的识别器是空白的,我们需要使用图片来训练它。
封装一个这样的方法:
/// 训练识别器
/// @param image 训练用的图片
/// @param label 图片的标签,和图片一一对应
-(void)train:(NSArray<Mat *> *)image label:(NSArray<NSNumber *> * )label {
std::vector<cv::Mat> datas; // 将OC的数组转成c++的数组
for (Mat *item in image) {
datas.push_back(item.nativeRef);
}
std::vector<int> labels;// 将OC的数组转成c++的数组
for (NSNumber *n in label) {
labels.push_back(int(n.integerValue));
}
// 训练
self.recognizer.nativePtrFaceRecognizer->update(datas, labels);
// 训练完成后可以将训练的结果保存到文件中。这样下次使用就不需要再训练了,直接从文件读取。
// [self.recognizer write:filePath];
// 从文件读取之前的训练结果
// [self.recognizer read:filePath];
}
确保label和image的数量一致且顺序一一对应。
假设我们采集了30张用户的人脸照片,我们把它标记为100
则图片数组里应该有30 张图片 label数组里有30个100,且顺序要对,为了提高识别的精准度,我们还可以将一些提前准备好的其他人的脸加入到训练集合里,方便识别器更好的区分不同的人脸。比如加入30张其他的人脸,并将它们标记为除了100以外的其他标签。
这样,最终传入进行训练的图片有60张,前30张为用户的人脸图片,后30张为其他人的人脸图片,label也有60个,前30个为100,后30个为其他人对应的标签。
5、识别人脸
经过训练,我们得到了一个可用的识别器。
接下来进行人脸识别,前几步和之前的1~3部是一样的。从摄像头采集图像,裁剪,然后做灰度处理,将处理后的图片交给识别器进行识别。
使用函数- (void*)predict:(Mat*)src label:(int*)label confidence:(double*)confidence;
进行识别
这里有三个参数:
Mat* src 是需要识别的图片
int* label 是一个int指针,用来接收识别到的标签
double* confidence 是一个double指针,用来接收识别出来的相似度,越小相似度越高
[self.recognizer predict:image label:label confidence:confidence];
至此,我们就完成了简单的基于OpenCV的人脸识别。
三、总结
OpenCV是一个强大的计算机视觉库,里面提供了很多强大的工具。我们用过OpenCV提供的方便的工具实现了一个基本的人脸识别程序,这只是最基础的人脸识别,如果要应用到生产上,还有很多细节需要优化。
比如:
-
活体检测
如何确保是真人而不是照片
-
识别率较低
OpenCV提供的几种人脸识别算法都比较老了,在被识别的照片和训练时的照片角度光线差距较大时识别率普遍不高
-
用户交互
优化用户在录入人脸和识别人脸时的交互体验