NSArray *faces = [faceDetector featuresInImage:image];
从该图片中检测到的每一张面孔都在数组 faces 中保存着一个 CIFaceFeature 实例。这个实例中保存着这张面孔的所处的位置和宽高,除此之外,眼睛和嘴的位置也是可选的。
另一方面,OpenCV 也提供了一套物体检测功能,经过训练后能够检测出任何你需要的物体。该库为多个场景自带了可以直接拿来用的检测参数,如人脸、眼睛、嘴、身体、上半身、下半身和笑脸。检测引擎由一些非常简单的检测器的级联组成。这些检测器被称为 Haar 特征检测器,它们各自具有不同的尺度和权重。在训练阶段,决策树会通过已知的正确和错误的图片进行优化。关于训练与检测过程的详情可参考此原始论文。当正确的特征级联及其尺度与权重通过训练确立以后,这些参数就可被加载并初始化级联分类器了:
// 正面人脸检测器训练参数的文件路径
NSString *faceCascadePath = [[NSBundle mainBundle] pathForResource:@”haarcascade_frontalface_alt2”
ofType:@”xml”];
const CFIndex CASCADE_NAME_LEN = 2048;
char CASCADE_NAME = (char ) malloc(CASCADE_NAME_LEN);
CFStringGetFileSystemRepresentation( (CFStringRef)faceCascadePath, CASCADE_NAME, CASCADE_NAME_LEN);
CascadeClassifier faceDetector;
faceDetector.load(CASCADE_NAME);
这些参数文件可在 OpenCV 发行包里的 data/haarcascades 文件夹中找到。
在使用所需要的参数对人脸检测器进行初始化后,就可以用它进行人脸检测了:
cv::Mat img;
vector faceRects;
double scalingFactor = 1.1;
int minNeighbors = 2;
int flags = 0;
cv::Size minimumSize(30,30);
faceDetector.detectMultiScale(img, faceRects,
scalingFactor, minNeighbors, flags
cv::Size(30, 30) );
检测过程中,已训练好的分类器会用不同的尺度遍历输入图像的每一个像素,以检测不同大小的人脸。参数 scalingFactor 决定每次遍历分类器后尺度会变大多少倍。参数minNeighbors 指定一个符合条件的人脸区域应该有多少个符合条件的邻居像素才被认为是一个可能的人脸区域;如果一个符合条件的人脸区域只移动了一个像素就不再触发分类器,那么这个区域非常可能并不是我们想要的结果。拥有少于minNeighbors 个符合条件的邻居像素的人脸区域会被拒绝掉。如果minNeighbors 被设置为 0,所有可能的人脸区域都会被返回回来。参数flags 是 OpenCV 1.x 版本 API 的遗留物,应该始终把它设置为 0。最后,参数minimumSize 指定我们所寻找的人脸区域大小的最小值。faceRects 向量中将会包含对img 进行人脸识别获得的所有人脸区域。识别的人脸图像可以通过cv::Mat 的 () 运算符提取出来,调用方式很简单:cv::Mat faceImg = img(aFaceRect)。
不管是使用 CIDetector 还是 OpenCV 的 CascadeClassifier,只要我们获得了至少一个人脸区域,我们就可以对图像中的人进行识别了。
人脸识别
OpenCV 自带了三个人脸识别算法:Eigenfaces,Fisherfaces 和局部二值模式直方图 (LBPH)。如果你想知道它们的工作原理及相互之间的区别,请阅读 OpenCV 的详细文档。
针对于我们的 demo app,我们将采用 LBPH 算法。因为它会根据用户的输入自动更新,而不需要在每添加一个人或纠正一次出错的判断的时候都要重新进行一次彻底的训练。
要使用 LBPH 识别器,我们也用 Objective-C++ 把它封装起来。这个封装中暴露以下函数:
(FJFaceRecognizer )faceRecognizerWithFile:(NSString )path;
(NSString )predict:(UIImage)img confidence:(double *)confidence;
(void)updateWithFace:(UIImage )img name:(NSString )name;
像下面这样用工厂方法来创建一个 LBPH 实例:
(FJFaceRecognizer )faceRecognizerWithFile:(NSString )path {
FJFaceRecognizer *fr = [FJFaceRecognizer new];
fr->_faceClassifier = createLBPHFaceRecognizer();
fr->_faceClassifier->load(path.UTF8String);
return fr;
}
预测函数可以像下面这样实现:
(NSString )predict:(UIImage)img confidence:(double *)confidence {
cv::Mat src = [img cvMatRepresentationGray];
int label;
self->_faceClassifier->predict(src, label, *confidence);
return _labelsArray[label];
}
请注意,我们要使用一个类别方法把 UIImage 转化为 cv::Mat。此转换本身倒是相当简单直接:使用CGBitmapContextCreate 创建一个指向cv::Image 中的 data 指针所指向的数据的CGContextRef。当我们在此图形上下文中绘制此UIImage 的时候,cv::Image 的data 指针所指就是所需要的数据。更有趣的是,我们能对一个 Objective-C 类创建一个 Objective-C++ 的类别,并且确实管用。
另外,OpenCV 的人脸识别器仅支持整数标签,但是我们想使用人的名字作标签,所以我们得通过一个 NSArray 属性来对二者实现简单的转换。