基于OpenCV的iOS图像处理

OpenCV for iOS

关于图片处理


随着科技的发展,AI、机器学习、AR、VR等已经逐渐走进生活,模式识别、图像捕捉、图片拼接等已经成为其中的重要环节。因此,图像处理技术在未来会被移动端广泛使用。其中,有很多C++的库的应用普遍,常用的有:OpenCV、FreeImage、CImg和CxImage。

关于这几个库的特点和优缺点可以参考图像识别四大图像库比较

关于OpenCV


简介

OpenCV (Open Source Computer Vision Library)是一个在BSD许可下发布的开源库,因此它是免费提供给学术和商业用途。有C++、C、Python和Java接口,支持Windows、Linux、MacOS、iOS和Android等系统。OpenCV是为计算效率而设计的,而且密切关注实时应用程序的发展和支持。该库用优化的C/C++编写,可以应用于多核处理。在启用OpenCL的基础上,它可以利用底层的异构计算平台的硬件加速。

——opencv.org

OpenCV的模块

官方文档中我们可以看到其包含模块以及对iOS的支持情况。

  • core:简洁的核心模块,定义了基本的数据结构,包括稠密多维数组 Mat 和其他模块需要的基本函数。
  • imgproc:图像处理模块,包括线性和非线性图像滤波、几何图像转换 (缩放、仿射与透视变换、一般性基于表的重映射)、颜色空间转换、直方图等等。
  • video:视频分析模块,包括运动估计、背景消除、物体跟踪算法。
  • calib3d:包括基本的多视角几何算法、单体和立体相机的标定、对象姿态估计、双目立体匹配算法和元素的三维重建。
  • features2d:包含了显著特征检测算法、描述算子和算子匹配算法。
  • objdetect:物体检测和一些预定义的物体的检测 (如人脸、眼睛、杯子、人、汽车等)。
  • ml:多种机器学习算法,如 K 均值、支持向量机和神经网络。
  • highgui:一个简单易用的接口,提供视频捕捉、图像和视频编码等功能,还有简单的 UI 接口 (iOS 上可用的仅是其一个子集)。
  • gpu:OpenCV 中不同模块的 GPU 加速算法 (iOS 上不可用)。
  • ocl:使用 OpenCL 实现的通用算法 (iOS 上不可用)。
  • 一些其它辅助模块,如 Python 绑定和用户贡献的算法。

我们可以利用OpenCV在iOS上做什么

基于OpenCV,iOS应用程序可以实现很多有趣的功能,也可以把很多复杂的工作简单化。一般可用于:

  • 对图片进行灰度处理(官方示例)

  • 人脸识别,即特征跟踪(官方示例)

  • 训练图片特征库(可用于模式识别)

  • 提取特定图像内容(根据需求还原有用图像信息)

……

导入OpenCV


opencv目前分为两个版本系列:opencv2.4.x和opencv3.x。

导入项目的两种方式:

1.从官网下载框架,引入工程。

  1. 前往OpenCV官网OpenCV中文官网下载相关iOS版本framework文件,从项目引入,

  2. 导入OpenCV依赖库

  • libc++.tbd

  • AVFoundation.framework

  • CoreImage.framework

  • QuartzCore.framework

  • Accelerate.framework

  • CoreVideo.framework

  • CoreMedia.framework

  • AssetsLibrary.framework

  1. 引入相关头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14

#import <opencv2/opencv.hpp>



#import <opencv2/imgproc/types_c.h>



#import <opencv2/imgcodecs/ios.h>



#import <opencv2/highgui/highgui_c.h>

注:使用OpenCV的类必须支持C++的编译环境,把.m文件改为.mm即可。

2.使用CocoaPods安装。

很简单。

1
2

pod 'OpenCV'

OpenCV的简单使用


处理图片可以创建一个UIImage的分类,OpenCV图像处理的相关代码都可以在这个类中实现。

代码可见笔者Github项目地址

图像灰度处理

1.在.h文件中声明两个类

1
2
3
4
5
6

@property (nonatomic, readonly) cv::Mat CVMat;



@property (nonatomic, readonly) cv::Mat CVGrayscaleMat;

2.声明Mat与UIImage互相转换以及灰度处理并返回UIImage对象的外部方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

/**



cv::Mat --> UIImage







@return UIImage



*/



+ (UIImage *)imageWithCVMat:(const cv::Mat&)cvMat;



/**



UIImage --> cv::Mat







@param image image



@return cv::Mat



*/



+ (cv::Mat)cvMatFromUIImage:(UIImage *)image;



/**



UIImage --> cv::Mat (gray)



@param image image



@return cv::Mat



*/



+ (cv::Mat)cvMatGrayFromUIImage:(UIImage *)image;

3.在.m中实现相关方法

生成cv::Mat对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

- (cv::Mat)CVMat {



CGColorSpaceRef colorSpace = CGImageGetColorSpace(self.CGImage);



CGFloat cols = self.size.width;



CGFloat rows = self.size.height;







cv::Mat cvMat(rows,cols,CV_8UC(4)); // 8 bits per component,4 channels







CGContextRef contextRef = CGBitmapContextCreate(cvMat.data, // Pointer to backing data



cols, // Width of bitmap



rows, // Height of bitmap



8, // Bits per conponent



cvMat.step[0], // Bytes per row



colorSpace, // Colorspace



kCGImageAlphaNoneSkipLast |



kCGBitmapByteOrderDefault);// Bitmap info flags







CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), self.CGImage);



CGContextRelease(contextRef);







return cvMat;



}

生成灰度cv::Mat对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

- (cv::Mat)CVGrayscaleMat {



CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();



CGFloat cols = self.size.width;



CGFloat rows = self.size.height;







cv::Mat cvMat = cv::Mat(rows,cols,CV_8SC1); // 8 bits per conponpent,1 channel







CGContextRef contextRef = CGBitmapContextCreate(cvMat.data, // Pointer to backing data



cols, // Width of bitmap



rows, // Height of bitmap



8, // Bits of bitmap



cvMat.step[0], //Bytes per row



colorSpace, // Colorspace



kCGImageAlphaNone |



kCGBitmapByteOrderDefault);// Bitmap info flags







CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), self.CGImage);



CGContextRelease(contextRef);



CGColorSpaceRelease(colorSpace);







return cvMat;



}

cv::Mat –> UIImage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

+ (UIImage *)imageWithCVMat:(const cv::Mat &)cvMat {



return [[UIImage alloc] initWithCVMat:cvMat];



}



+ (cv::Mat)cvMatFromUIImage:(UIImage *)image {



CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);



CGFloat cols = image.size.width;



CGFloat rows = image.size.height;







cv::Mat cvMat(rows, cols, CV_8UC4); // 8 bits per component, 4 channels (color channels + alpha)







CGContextRef contextRef = CGBitmapContextCreate(cvMat.data, // Pointer to data



cols, // Width of bitmap



rows, // Height of bitmap



8, // Bits per component



cvMat.step[0], // Bytes per row



colorSpace, // Colorspace



kCGImageAlphaNoneSkipLast |



kCGBitmapByteOrderDefault); // Bitmap info flags







CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);



CGContextRelease(contextRef);







return cvMat;



}

UIimage –> Gray cv::Mat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

+ (cv::Mat)cvMatGrayFromUIImage:(UIImage *)image {



CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);



CGFloat cols = image.size.width;



CGFloat rows = image.size.height;







cv::Mat cvMat(rows, cols, CV_8UC1); // 8 bits per component, 1 channels







CGContextRef contextRef = CGBitmapContextCreate(cvMat.data, // Pointer to data



cols, // Width of bitmap



rows, // Height of bitmap



8, // Bits per component



cvMat.step[0], // Bytes per row



colorSpace, // Colorspace



kCGImageAlphaNoneSkipLast |



kCGBitmapByteOrderDefault); // Bitmap info flags







CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);



CGContextRelease(contextRef);







return cvMat;



}

4.在控制器中调用UIImage+OpenCV相关代码,实现图片灰度处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28



UIImage *image = [UIImage imageNamed:@"icon.jpg"];



cv::Mat inputMat = [UIImage cvMatFromUIImage:image];



cv::Mat greyMat;



cv::cvtColor(inputMat, greyMat, CV_BGR2GRAY);



//cv::Mat greyMat = [UIImage cvMatGrayFromUIImage:image];



UIImage *greyImage = [UIImage imageWithCVMat:greyMat];



self.imageView.image = greyImage;

5.效果

原图

处理后

人脸识别

关于人脸识别的实现,可以参考基于OpenCV的人脸识别。这是ObjC中国上一篇译文,作者是国外大牛,这片博客写得非常详尽。

我的Demo中不含有拍照部分,直接对一张图片中的人脸进行识别,其实现如下:

1.创建一个Objective-C++的类FaceRecognition(即把.m文件.mm文件,支持Objective-C与C++混编)

2.引入头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16



#import <opencv2/opencv.hpp>



#import <opencv2/imgproc/types_c.h>



#import <opencv2/imgcodecs/ios.h>



#import "UIImage+OpenCV.h"

3.对图片进行处理转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124



+ (UIImage *)convertImage: (UIImage *)image {



// 初始化一个图片的二维矩阵cvImage



cv::Mat cvImage;







// 将图片UIImage对象转为Mat对象



cvImage = [UIImage cvMatFromUIImage:image];







if (!cvImage.empty()) {



cv::Mat gray;







// 进一步将图片转为灰度显示



cv::cvtColor(cvImage, gray, CV_RGB2GRAY);







// 利用搞死滤镜去除边缘



cv::GaussianBlur(gray, gray, cv::Size(5, 5), 1.2, 1.2);







// 计算画布



cv::Mat edges;



cv::Canny(gray, edges, 0, 50);







// 使用白色填充



cvImage.setTo(cv::Scalar::all(225));







// 修改边缘颜色



cvImage.setTo(cv::Scalar(0,128,255,255),edges);







// 将Mat转换为UIImage



return [UIImage imageWithCVMat:cvImage];



}



return nil;



}

4.读取图片中人脸的相关数据并存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

+ (NSArray*)facePointDetectForImage:(UIImage*)image{



static cv::CascadeClassifier faceDetector;



static dispatch_once_t onceToken;



dispatch_once(&onceToken, ^{



// 添加xml文件



NSString* cascadePath = [[NSBundle mainBundle]



pathForResource:@"haarcascade_frontalface_alt2"



ofType:@"xml"];



faceDetector.load([cascadePath UTF8String]);



});



cv::Mat faceImage;



faceImage = [UIImage cvMatFromUIImage:image];



// 转为灰度



cv::Mat gray;



cvtColor(faceImage, gray, CV_BGR2GRAY);







// 检测人脸并储存



std::vector<cv::Rect>faces;



faceDetector.detectMultiScale(gray, faces,1.1,2,CV_HAAR_FIND_BIGGEST_OBJECT,cv::Size(30,30));







NSMutableArray *array = [NSMutableArray array];



for(unsigned int i= 0;i < faces.size();i++)



{



const cv::Rect& face = faces[i];



float height = (float)faceImage.rows;



float width = (float)faceImage.cols;



CGRect rect = CGRectMake(face.x/width, face.y/height, face.width/width, face.height/height);



[array addObject:[NSNumber valueWithCGRect:rect]];



}



return [array copy];



}

5.检测人脸并在图片上人脸部分添加红色矩形线框以标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

+ (UIImage*)faceDetectForImage:(UIImage*)image {



static cv::CascadeClassifier faceDetector;



static dispatch_once_t onceToken;



dispatch_once(&onceToken, ^{







NSString* cascadePath = [[NSBundle mainBundle]



pathForResource:@"haarcascade_frontalface_alt"



ofType:@"xml"];



faceDetector.load([cascadePath UTF8String]);



});







cv::Mat faceImage;



faceImage = [UIImage cvMatFromUIImage:image];







// 转为灰度



cv::Mat gray;



cvtColor(faceImage, gray, CV_BGR2GRAY);







// 检测人脸并储存



std::vector<cv::Rect>faces;



faceDetector.detectMultiScale(gray, faces,1.1,2,0,cv::Size(30,30));







// 在每个人脸上画一个红色四方形



for(unsigned int i= 0;i < faces.size();i++)



{



const cv::Rect& face = faces[i];



cv::Point tl(face.x,face.y);



cv::Point br = tl + cv::Point(face.width,face.height);



// 四方形的画法



cv::Scalar magenta = cv::Scalar(255, 0, 0, 255);



cv::rectangle(faceImage, tl, br, magenta, 2, 2, 0);



}



return [UIImage imageWithCVMat:faceImage];



}

6.运行效果

Face Recognition

Objective-C与C++混编


很多地方需要用到Objective-C与C++混编,来解决一些对象的传递转换问题。

字符串的转换

在C++中,字符串对象为char *,而在Objective-C中字符串对象为NSString,在编程中常常需要在二者之间互相转换。

1.NSString转换为char *

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52



/**



NSString --> char *







@param string NSString (Objective-C)



@return char * (C++)



*/



char * string2Char(NSString *string) {



const char *charString = [string UTF8String];



char *result = new char[strlen(charString) + 1];



strcpy(result, charString);



// delete[] result;



return result;



}

2.char *转换为NSString

1
2

NSString *OCString = [NSString stringWithUTF8String:cppString];

储存cv::Mat图片对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60



/**



Write image to Document



@param imageName image name



@param img cv::Mat



@return if complete



*/



bool writeImage2Document(const char *imageName, cv::Mat img) {



UIImage *stitchedImage = [[UIImage alloc] initWithCVMat:img];



NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];



NSString *imagePath = [docPath stringByAppendingString:[NSString stringWithFormat:@"/%@",[NSString stringWithUTF8String:imageName]]];



// 将UIImage对象转换成NSData对象



NSData *data = UIImageJPEGRepresentation(stitchedImage, 0);



[data writeToFile:imagePath atomically:YES];



return true;



}

总结


OpenCV实在是太强大了,笔者决定潜心研究,暂不总结,所以没有总结。