iOS-OpenCV笔记:实现简单的人脸识别(一)
前言:人脸识别技术已经逐渐进入到人们的生活中,并广泛运用到了移动端,包括手机人脸解锁,金融领域人脸验证,美颜相机人脸动态添加表情和美化等。
今天介绍的是一个开源的计算机视觉和机器学习库:OpenCV。通过这篇文我们将了解它的基础知识,并学习如何在 iOS 设备上实现基于OpenCV的人脸检测与人脸识别。
一、OpenCV 概述:
-
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,因此它在学术和商业上都是免费的。它具有C ++,Python和Java接口,并支持Windows,Linux,Mac OS,iOS和Android。
-
OpenCV旨在提高计算效率并强调实时应用程序。用优化的C / C ++编写,该库可以利用多核处理。通过OpenCL启用,它可以利用底层异构计算平台的硬件加速。
-
OpenCV在世界各地采用,拥有超过4.7万的用户社区,预计下载量超过1400万。使用范围从交互式艺术到矿山检查,在网上拼接地图或通过高级机器人学习工具。
优势:
计算机视觉市场巨大而且持续增长,且这方面没有标准API,而标准的API将简化计算机视觉程序和解决方案的开发,OpenCV致力于成为这样的标准API。
OpenCV通过优化的C代码的编写,对其执行速度带来了可观的提升,并且可以通过购买Intel的IPP高性能多媒体函数库(Integrated Performance Primitives)得到更快的处理速度。
下图为 OpenCV 与当前其他主流视觉函数库的性能比较。
性能比较.jpg应用领域:
二、在 iOS 上集成 OpenCV 库
集成 OpenCV 到工程有以下三种方式:
- 使用Cocopods进行管理依赖 pod ''OpenCV''
- 在官网OpenCV-iOS framework直接下载编译好的库
- 从 GitHub 拉下源码,编译成framework,导入工程中
下面介绍的是第3种,我编译的版本为 OpenCV-3.4.1,参考:
OpenCV_iOS 安装官方文档
我的运行环境:
- macOS High Sierra Version: 10.13.3
- Xcode Version: 9.2
必需软件包
- CMake 2.8.8或更高:CMake 官方安装教程
注意:CMake必须安装,不然后面的编译会报错
如果安装CMake时出现:
CMake Error at cmake_install.cmake:36 (file): file cannot create directory: /usr/local/doc/cmake-3
原因:没有权限,命令前加Sudo
解决:sudo make install
也可以使用Homebrew,在终端中输入:“brew install cmake”,自动安装 CMake。
- Xcode 4.2或更高版本
从Git仓库获取最新的OpenCV
启动Git客户端从这里克隆OpenCV存储库
在MacOS中,可以在终端中使用以下命令完成:
cd〜/ <my_working _directory>
git clone https://github.com/opencv/opencv.git
使用CMake和命令行从源代码构建OpenCV
- 为Xcode制作符号链接,让OpenCV构建脚本查找编译器,头文件等。
cd /
sudo ln -s /Applications/Xcode.app/Contents/Developer DeveloperDeveloper
- 构建OpenCV框架:
cd〜/ <my_working_directory>
python opencv/platforms/ios/build_framework.py ios
如果一切正常,几十分钟后你会得到/<my_working_directory> /ios/opencv2.framework。(因为要编译支持的 CPU 架构 armv7,armv7s,i386,x86_64,arm64,所以很慢)
将编译完成的库导入工程
-
将opencv2.framework添加到你的Xcode项目中。
我编译的版本是:OpenCV-3.4.1
-
添加需要用到的依赖库:
opencv2
Accelerate
AssetsLibrary
AVFoundation
CoreGraphics
CoreImage
CoreMedia
CoreVideo
QuartzCore
AVFoundation
- 修改 Build Settings
- 'Build Options' --> 'Enable Bitcode'修改为NO
- 'Other Warning Flags'增加条目'-Wno-documentation',以屏蔽一些注释产生的 warning
- 修改项目prefix.pch 文件(若没有的话,自己生成一个),将文件内容修改成如下所示
#import <Availability.h>
#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#endif
5.将调用 OpenCV 的文件后缀修改为 .mm。
三、OpenCV 库的介绍
下面通过 OpenCV 官方文档 来了解它的API
1. OpenCV 主要模块
OpenCV 的 API 是用 C++ 编写的,具有模块化结构,由不同的模块组成,这些模块中包含范围极为广泛的各种方法,从底层的图像颜色空间转换到高层的机器学习工具。
以下是其最重要的模块:
1521713887508.jpg- core:简洁的核心模块,定义了基本的数据结构,包括稠密多维数组 Mat 和其他模块需要的基本函数。
- imgproc:图像处理模块,包括线性和非线性图像滤波、几何图像转换 (调整大小、仿射与透视变换、通用基于表的重新映射)、颜色空间转换、直方图等等。
- video:视频分析模块,包括运动估计、背景消除和对象跟踪算法。
- calib3d:包括基本的多视角几何算法、单体和立体相机校准、对象姿态估计、双目立体匹配算法和元素的三维重建。
- features2d:包含了显著特征检测算法、描述算子和算子匹配算法。
- objdetect:物体检测和一些预定义的物体的检测 (例如,脸、眼睛、杯子、人、汽车等)。
- ml:多种机器学习算法,如 K 均值、支持向量机和神经网络。
- highgui:一个简单易用的接口,提供视频捕捉、图像和视频编码等功能,还有简单的 UI 接口 (iOS 上可用的仅是其一个子集)。
- gpu:OpenCV 中不同模块的 GPU 加速算法 (iOS 上不可用)。
- ocl:使用 OpenCL 实现的通用算法 (iOS 上不可用)。
- 一些其它辅助模块,如FLANN和Google测试包装器, Python 绑定和用户贡献的算法。
2. cv命名空间
所有的OpenCV类和函数都被放置在cv命名空间中。因此, OpenCV 的类的前面会有个 cv:: 前缀,就像 cv::Mat、 cv::Algorithm 等等。
如果要从你的代码访问这些功能,需要使用 cv::
声明,或着指令:using namespace cv
,例如:
#include "opencv2/core/core.hpp"
...
cv::Mat H = cv::findHomography(points1, points2, CV_RANSAC, 5);
...
或者
#include "opencv2/core/core.hpp"
using namespace cv;
...
Mat H = findHomography(points1, points2, CV_RANSAC, 5 );
...
一些当前或未来的OpenCV外部名称可能与STL或其他库冲突。在这种情况下,使用显式名称空间说明符来解决名称冲突:
Mat a(100, 100, CV_32F);
randu(a, Scalar::all(1), Scalar::all(std::rand()));
cv::log(a, a);
a /= std::log(2.);
3. 自动内存管理
OpenCV自动处理所有内存。
- 首先,
std::vector
,Mat
,以及其他数据结构,在被函数和方法调用时,析构函数会在有需要的时候释放潜在的内存缓存。 - 这意味着析构函数并不总是像缓冲区那样释放缓冲区的
Mat
。他们考虑到有可能的数据共享。 - 析构函数会递减 与矩阵数据缓冲区关联的引用计数器。当且仅当引用计数器达到零时,即当没有其他结构引用同一缓冲区时,缓冲区才被释放。
- 同样,当一个
Mat
实例被复制时,实际的数据不会被真正复制。相反,引用计数器会增加,内存会存在另一个具有相同数据的所有者。 - 当然还有一种
Mat::clone
的方法可以创建矩阵数据的完整副本。
请看下面的例子:
// 创建一个大小8Mb的矩阵
Mat A(1000, 1000, CV_64F);
// 创建一个相同的矩阵;
// 这是一个即时操作,无论矩阵大小如何。
Mat B = A;
// 将A的第3行创建另一个矩阵; 任何一个的数据都没有被复制
Mat C = B.row(3);
// 现在创建矩阵的单独副本
Mat D = B.clone();
// 将B的第5行复制到C,即复制A的第5行,到A的第3行.
B.row(5).copyTo(C);
// 现在让A和D共享数据; 之后,A 的修改版本仍然由B和C引用.
A = D;
// 现在使B成为一个空矩阵(它没有引用内存缓冲区),
// 但修改后的A版本仍然会被C引用,
// 尽管C只是原来A的一行
B.release();
// 最后,制作C的完整副本。因此,大修改后的
// 矩阵将被解除分配,因为它不被任何人引用
C = C.clone();
你会发现使用Mat
和其他基本结构很简单。但是,如果不考虑自动内存管理,创建高级类甚至用户数据类型又如何呢?对于他们来说,OpenCV提供了Ptr<>
类似于std::shared_ptr
C ++ TR1 的模板类。所以,而不是使用普通的指针:
T * ptr = new T (...);
你可以使用:
Ptr <T> ptr = new T (...);
也就是说,Ptr<T> ptr
封装了一个指向 T
的实例和引用计数器相关的指针。详情请参阅 Ptr 说明。
4. 基础类和操作
OpenCV 包含几百个类。为简便起见,我们只看几个基础的类和操作,进一步阅读请参考全部文档。
cv::Mat
cv::Mat
是 OpenCV 的核心数据结构,用来表示任意 N 维矩阵。因为图像只是 2 维矩阵的一个特殊场景,所以也是使用 cv::Mat
来表示的。也就是说,cv::Mat
将是你在 OpenCV 中用到最多的类。
一个 cv::Mat
实例的作用就像是图像数据的头,其中包含着描述图像格式的信息。图像数据只是被引用,并能为多个 cv::Mat
实例共享。OpenCV 使用类似于 ARC 的引用计数方法,以保证当最后一个来自 cv::Mat
的引用也消失的时候,图像数据会被释放。图像数据本身是图像连续的行的数组 (对 N 维矩阵来说,这个数据是由连续的 N-1 维数据组成的数组)。使用 step[]
数组中包含的值,图像的任一像素地址都可通过下面的指针运算得到:
uchar *pixelPtr = cvMat.data + rowIndex * cvMat.step[0] + colIndex * cvMat.step[1]
每个像素的数据格式可以通过 type()
方法获得。除了常用的每通道 8 位无符号整数的灰度图 (1 通道,CV_8UC1
) 和彩色图 (3 通道,CV_8UC3
),OpenCV 还支持很多不常用的格式,例如 CV_16SC3
(每像素 3 通道,每通道使用 16 位有符号整数),甚至 CV_64FC4
(每像素 4 通道,每通道使用 64 位浮点数)。
cv::Algorithm
Algorithm
是 OpenCV 中实现的很多算法的抽象基类,包括将在我们的 demo 工程中用到的 FaceRecognizer
。它提供的 API 与苹果的 Core Image 框架中的 CIFilter
有些相似之处。创建一个 Algorithm
的时候使用算法的名字来调用 Algorithm::create()
,并且可以通过 get()
和 set()
方法来获取和设置各个参数,这有点像是键值编码。另外,Algorithm
从底层就支持从/向 XML 或 YAML 文件加载/保存参数的功能。
四、人脸检测与识别
1. 人脸检测
从 iPhone 的摄像头获取视频流,对它持续进行人脸检测。
OpenCV 的 highgui 模块中有个类,CvVideoCamera,它把 iPhone 的摄像机抽象出来,让我们的 app 通过一个代理函数 - (void)processImage:(cv::Mat&)image 来获得视频流。CvVideoCamera 实例如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.videoCamera = [[CvVideoCamera alloc] initWithParentView:_imageView];
self.videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionFront;
self.videoCamera.defaultAVCaptureSessionPreset = AVCaptureSessionPreset640x480;
self.videoCamera.defaultAVCaptureVideoOrientation = AVCaptureVideoOrientationPortrait;
self.videoCamera.defaultFPS = 30;
self.videoCamera.grayscaleMode = NO;
self.videoCamera.delegate = self;
}
#pragma mark - Protocol CvVideoCameraDelegate
- (void)processImage:(cv::Mat &)image {
// Do some OpenCV stuff with the image
cv::Mat image_copy;
cvtColor(image, image_copy, cv::COLOR_BGR2GRAY);
// invert image
bitwise_not(image_copy, image_copy);
//Convert BGR to BGRA (three channel to four channel)
cv::Mat bgr;
cvtColor(image_copy, bgr, cv::COLOR_GRAY2BGR);
cvtColor(bgr, image, cv::COLOR_BGR2BGRA);
}
OpenCV 提供了一套物体检测功能,经过训练后能够检测出任何你需要的物体。关于训练与检测过程的详情可参考此原始论文。
这些训练好的参数文件可在 OpenCV 发行包里的 data/haarcascades 文件夹中找到,将它导入工程就可以使用。
// 正面人脸检测器训练参数的文件路径
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);
在使用所需要的参数对人脸检测器进行初始化后,就可以用它进行人脸检测了:
cv::Mat img;
vector<cv::Rect> 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) );
2. 人脸识别
OpenCV 自带了三个人脸识别算法:Eigenfaces,Fisherfaces 和局部二值模式直方图 (LBPH)。如果你想知道它们的工作原理及相互之间的区别,请阅读 OpenCV 的详细文档。
后面的demo采用的是 LBPH 算法。它会根据用户的输入自动更新,而不需要在每添加一个人或纠正一次出错的判断的时候都要重新进行一次彻底的训练。
-
注意:
OpenCV-3.X 之后的版本不再包含 face.hpp,因此无法调用 Ptr<FaceRecognizer>类的 createLBPHFaceRecognizer() 函数进行人脸识别,你需要下载额外的扩展模块包,地址:opencv_contrib
下载完后将 opencv 与 opencv_contrib 合并,输入控制台:
$ cd <opencv_build_directory>
$ cmake -DOPENCV_EXTRA_MODULES_PATH=<opencv_contrib>/modules <opencv_source_directory>
$ make -j5
$ sudo make install
然后在 opencv/platforms/ios 文件下找到编译脚本 build_framework.py 打开,添加 contrib 的路径和修改默认配置,如下:
1. 找到:
class Builder:
def __init__(self, opencv, contrib, dynamic, bitcodedisabled, exclude, targets):
self.opencv = os.path.abspath(opencv)
//添加这一行:contrib的路径
self.contrib = os.path.abspath(contrib)
2. 找到:
parser.add_argument('--contrib', metavar='DIR', default=none, help...
将 default=none,改成 default=folder
最后再一次执行脚本 build_framework.py 编译,将contrib加入:
python opencv/platforms/ios/build_framework.py --contrib opencv_contrib ios_contrib
opencv2.framework的大小从250MB变成407MB,因为上面的方式是将 opencv_contrib 的所有模块一起构建,如果你不想所有的模块,官方还提供构建可选项,详情请到官方文档了解,这里就不过多介绍了。
3. 真机实现效果如下: IMG_1605.jpg
IMG_1609.png IMG_1607.png详细代码已上传到我的GitHub:
注:Demo不包含opencv2.framework,我手动编译的 Opencv+Contrib 库版本为 3.4.1,大约407MB上传不了,Git上传单个文件只允许<100MB,所以你可以在这个地址下载我编译好的库:Opencv+Contrib-3.4.1,如有遇到问题,请留言。
总结
本文主要是介绍 OpenCV 的基础知识,以及如何将它集成到 iOS 的项目中。通过学习和参考一些优秀的文章和代码,初步实现了一个简单的人脸识别功能。其具体的算法实现原理和更多有趣的功能还需要我去长时间研究,之后我还会继续写几篇文章记录对 OpenCV 的学习。