logo头像

小玉的技术博客

iOS图片处理之位图图像原图修改

前言

本文来源于网络,笔者在学习时参考该文章,特此记录。

想象一张最好的生活自拍照。它是很高大尚滴并且以后会有用武之地。转发,票选将会使你获得成千上万份的关注,因为它确实很酷很帅。现在,如果你有什么办法,可以让它看起来更加的高大尚。。。

这就是图形图像处理要做到的!它可以让你的照片带上更多的特殊效果,比如修改颜色,与其它的图片进行合成等等。

在这两部分教程中,你需要先弄明白一些图形图像处理的基础知识。接着,你可以利用如下四个流行的图形图像处理方法编写一个实现“幽灵图像过滤器”的程序:
1:位图图像原图修改
2:使用Core Graphics库
3:使用Core Image库
4:使用GPUImage库的第三部分

在图形图像处理教程的第一节,主要讲解位图图像原图的修改。一但你明白基本的图形处理方法,那么其它的相关内容你也会较容易的弄明白。在教程的第二部分,主要介绍另外的三种修改图像方法。

本教程假设你拥有关于IOS系统和Object-C的基础,但在开始本教程前不需要拥有任何关于图形图像处理的知识。

开始
在开始写代码之前,先理解一些关于图形图像处理的基本概念很是需要。所以,先别急,放轻松,让我们在最短的时间里去了解一下图形图像的内部工作原理。

第一件事情,看一下我们本教程中的新朋友…幽灵!

不要怕,幽灵不是真的鬼魂。实际上,它只是一张图像。简单来说,它就是由一堆1和0组成的。这样说听上去会更好一些。

什么是图形图像
一张图像就是像素点的集合,每一个像素都是一个单独,明了的颜色。图像一般情况下都存储成数组,你可以把他们相像成2维数组。

这一张是缩放版本的幽灵,被放大后:

图像中这些小的“方块”就是像素,每一像素只表示一种颜色。当成百上千万的像素集体到一起后,就构成了图形图像。

如何用字节来表示颜色
表示图形的方式有许多种。在本教程中使用的是最简单的:32位RGBA模式。

如同它的名字一样,32位RGBA模式会将一个颜色值存储在32位,或者4个字节中。每一个字节存储一个部分或者一个颜色通道。这4个部分分别是:

~ R代表红色

~ G代表绿色

~ B代表蓝色

~ A代表透明度

正如你所知道的,红,绿和蓝是所有颜色的基本颜色集。你几乎可以使用他们创建搭配出任何想要的颜色。

由于使用8位表示每一种颜色值,那么使用32位RGBA模式实际上可以创建出不透明的颜色的总数是256256256种,已经接近17亿种。惊叹,那是好多好多好多的颜色!

alpha通道与其它的不同。你可以把它当成透明的东西,就像UIView的alpah属性。

透明颜色意味着没有任何的颜色,除非在它的后面有另外一种颜色;它的主要功能就是要告诉图像处理这个像素的透明度是多少,于是,就会有多少颜色值穿透过它而显示出来。

你将会通过本节后面的内容更新深入的了解。

总结一下,一个图形就是像素的集体,并且每一个像素只能表示一种颜色。本节,你已经了解了32位RGBA模式。

提示:你有没有想过,位图的结构组成?一张位图就是一张2D的地图,每一块就是一个像素!像素就是地图的每一块。哈哈!

现在你已经了解了用字节表示颜色的基础了。不过在你开始着手写代码前,还有三个以上的概念需要你了解。

颜色空间
使用RGB模式表示颜色是颜色空间的一个例子。它只是众多存储颜色方法中的一种。另外一种颜色空间是灰阶空间。像它的名字一样,所有的图形都只有黑和白,只需要保存一个值来表示这种颜色。

下面这种使用RGB模式表示的颜色,人类的肉眼是很难识别的。

Red: 0 Green:104 Blue:55

你认为RGB值为[0,104,55]会产生一种什么颜色?

认真的思考一下,你也许会说是一种蓝绿色或者绿色,但那是错的。原来,你所看到的是深绿色。

另外两种比较常见的颜色空间是HSV和YUV。

HSV,使用色调,饱和度和亮度来直观的存储颜色值。你可以把这三个部分这样来看:

·色调就是颜色
·饱和度就是这个颜色有多么的饱满
·值就是颜色的亮度有多亮

在这种颜色空间中,如果你发现自己并不知道HSV的值,那么通过它的三个值,可以很容易的相像出大概是什么颜色。

RGB和HSV颜色空间的区别是很容易理解的,请看下面的图像:

YUV是另外一种常见的颜色空间,电视机使用的就是这种方式。

最开始的时候,电视机只有灰阶空间一种颜色通道。后来,当彩色电影出现后,就有了2种通道。当然,如果你想在本教程中使用YUV,那么你需要去研究更多关于YUV和其它颜色空间的相关知识。

NOTE:同样的颜色空间,你也可以使用不同的方法表示颜色。比如16位RGB模式,可以使用5个字节存储R,6个字节存储G,5个字节存储B。

为什么用6个字节存储绿色,5个字节存储蓝色?这是一个有意思的问题,答案就是因为眼球。人类的眼球对绿色比较敏感,所以人类的眼球更空间分辨出绿色的颜色值变化。

坐标系统
既然一个图形是由像素构成的平面地图,那么图像的原点需要说明一下。通常原点在图像的左上角,Y轴向下;或者原点在图像的左下,Y轴向上。

没有固定的坐标系统,苹果在不同的地方可能会使用不同的坐标系。

目前,UIImage和UIView使用的是左上原点坐标,Core Image和Core Graphics使用的是左下原点坐标。这个概念很重要,当你遇到图像绘制倒立问题的时候你就知道了。

图形压缩
这是在你开始编写代码前的最后一个需要了解的概念了!原图的每一个像素都被存储在各自的内存中。

如果你使用一张8像素的图形做运算,它将会消耗810^6像素4比特/像素=32兆字节内存。关注一下数据!

这就是为什么会出现jpeg,png和其它图形格式的原因。这些都是图形压缩格式。

当GPU在绘制图像的时候,会使用大量内存把图像的原始尺寸进行解压缩。如果你的程序占用了过多的内存,那么操作系统会将进程杀死(程序崩溃)。所以请确定你的程序使用较大的图像进行过测试。

我需要一些行动…

关注一下像素
现在,你已经基础了解了图形图像的内部工作原理,已经可以开始编写代码喽。今天你将会开发一款改变自己照片的程序,叫做SpookCam,该程序会把一张幽灵的图像放到你的照片中!

下载工具包在xcode中打开该项目,编译并运行。在你的手机上会看到如下的图像:

在控制台,你会看到如下的输出:

当前的程序可以加载这张幽灵的图像,并得到图像的所有像素值,打印出每个像素的亮度值到日志中。

亮度值是神马?它就是红色,绿色和蓝色通过的平均值。

注意输出日志外围的亮度值都为0,这意味着他们代码的是黑色。然而,他们的透明度的值是0,所以它们是透明不可见的。为了证明这一点,试着将imageView的背景颜色设置成红色,然后再次编译并运行。

现在快速的浏览一下代码。ViewController.m 中使用 UIImagePickerController 来在相册中取得图像或者使用机机获得图像。

当它选定一张图像后,调用-setupWithImage:在这行中,输出了每一像素的亮度值到日志中。定位到ViewController.m中的logPixelsOfImage,查看方法中的开始部分:

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// 1.
CGImageRef inputCGImage = [image CGImage];
NSUInteger width = CGImageGetWidth(inputCGImage);
NSUInteger height = CGImageGetHeight(inputCGImage);
// 2.
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel *width;
NSUInteger bitsPerComponent = 8;
UInt32 * pixels;
pixels = (UInt32 *) calloc(height * width, sizeof(UInt32));
// 3.
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pixels, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
// 4.
CGContextDrawImage(context, CGRectMake(0,0, width, height), inputCGImage);
// 5. Cleanup
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
```
现在,让我们分段的来看一下:
1:第一部分:把UIImage对象转换为需要被核心图形库调用的CGImage对象。同时,得到图形的宽度和高度。
2:第二部分:由于你使用的是32位RGB颜色空间模式,你需要定义一些参数bytesPerPixel(每像素大小)和bitsPerComponent(每个颜色通道大小),然后计算图像bytesPerRow(每行有大)。最后,使用一个数组来存储像素的值。
3:第三部分:创建一个RGB模式的颜色空间CGColorSpace和一个容器CGBitmapContext,将像素指针参数传递到容器中缓存进行存储。在后面的章节中将会进一步研究核图形库。
4:第四部分:把缓存中的图形绘制到显示器上。像素的填充格式是由你在创建context的时候进行指定的。
5:第五部分:清除colorSpace和context.
NOTE:当你绘制图像的时候,设备的GPU会进行解码并将它显示在屏幕。为了访问本地数据,你需要一份像素的复制,就像刚才做的那样。
此时此刻,pixels存储着图像的所有像素信息。下面的几行代码会对pixels进行遍历,并打印:
```objc
// 1.
#define Mask8(x) ( (x) & 0xFF )
#define R(x) ( Mask8(x) )
#define G(x) ( Mask8(x >> 8 ) )
#define B(x) ( Mask8(x >> 16) )
NSLog(@"Brightness of image:");
// 2.
UInt32 * currentPixel = pixels;
for (NSUInteger j = 0; j < height; j++) {
for (NSUInteger i = 0; i < width; i++) {
// 3.
UInt32 color = *currentPixel;
printf("%3.0f ", (R(color)+G(color)+B(color))/3.0);
// 4.
currentPixel++;
}
printf("\n");
}
```
代码解释:
1:定义了一些简单处理32位像素的宏。为了得到红色通道的值,你需要得到前8位。为了得到其它的颜色通道值,你需要进行位移并取截取。
2:定义一个指向第一个像素的指针,并使用2for循环来遍历像素。其实也可以使用一个for循环从0遍历到width*height,但是这样写更容易理解图形是二维的。
3:得到当前像素的值赋值给currentPixel并把它的亮度值打印出来。
4:增加currentPixel的值,使它指向下一个像素。如果你对指针的运算比较生疏,记住这个:currentPixel是一个指向UInt32的变量,当你把它加1后,它就会向前移动4字节(32位),然后指向了下一个像素的值。
提示:还有一种非正统的方法就是把currentPiexl声明为一个指向8字节的类型的指针,比如char。这种方法,你每增加1,你将会移动图形的下一个颜色通道。与它进行位移运算,你会得到颜色通道的8位数值。
此时此刻,这个程序只是打印出了原图的像素信息,但并没有进行任何修改!下面将会教你如何进行修改。
SpookCame-原图修改
四种研究方法都会在本小节进行,你将会花费更多的时间在本节,因为它包括了图形图像处理的第一原则。掌握了这个方法你会明白其它库所做的。
在本方法中,你会遍历每一个像素,就像之前做的那个,但这次,将会对每个像素进行新的赋值。
这种方法的优点是容易实现和理解;缺点就是扫描大的图形和效果的时候会更复杂,不精简。
正如你在程序开始看到的,ImageProcessor类已经存在。将它应用到ViewController中,替换-setupWithImage,代码如下:
```objc
- (void)setupWithImage:(UIImage*)image {
UIImage * fixedImage = [image imageWithFixedOrientation];
self.workingImage = fixedImage;
// Commence with processing!
[ImageProcessor sharedProcessor].delegate = self;
[[ImageProcessor sharedProcessor] processImage:fixedImage];
}
```
注释掉 -viewDidLoad 中下面的代码:
// [self setupWithImage:[UIImage imageNamed:@"ghost_tiny.png"]];
现在,打开 ImageProcessor.m。如你所见,ImageProcessor 是单例模式,调用 -processUsingPixels 来加载图像,然后通过 ImageProcessorDelegate 返回输出。
-processsUsingPixels:是之前你所看到获得图形像素代码的一种复制品,如同inputImage。注意两个额外的宏A(x)和RGBAMake(r,g,b,a)的定义,用来方便处理。
编译,并运行。从相册(拍照)选择一张图片,它将会出现在屏幕上:
![](http://cdn.cocimg.com/cms/uploads/allimg/140812/4196_140812105248_1.png)
照片中的人看上去在放松,是时候把幽灵放进去了!
在processUsingPixels的返回语句前,添加如下代码,创建一个幽灵的CGImageRef对象。
UIImage * ghostImage = [UIImage imageNamed:@"ghost"];
CGImageRef ghostCGImage = [ghostImage CGImage];
现在,做一些数学运算来确定幽灵图像放在原图的什么位置。
```objc
CGFloat ghostImageAspectRatio = ghostImage.size.width / ghostImage.size.height;
NSInteger targetGhostWidth = inputWidth * 0.25;
CGSize ghostSize = CGSizeMake(targetGhostWidth, targetGhostWidth / ghostImageAspectRatio);
CGPoint ghostOrigin = CGPointMake(inputWidth * 0.5, inputHeight * 0.2);
```
以上代码会把幽灵的图像宽度缩小25%,并把它的原点设定在点ghostOrigin。
下一步是创建一张幽灵图像的缓存图,
```objc
NSUInteger ghostBytesPerRow = bytesPerPixel * ghostSize.width;
UInt32 * ghostPixels = (UInt32 *)calloc(ghostSize.width * ghostSize.height, sizeof(UInt32));
CGContextRef ghostContext = CGBitmapContextCreate(ghostPixels, ghostSize.width, ghostSize.height,
bit sPerComponent, ghostBytesPerRow, colorSpace,
kCG ImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextDrawImage(ghostContext, CGRectMake(0, 0, ghostSize.width, ghostSize.height),ghostCGImage);
```
上面的代码和你从inputImage中获得像素信息一样。不同的地方是,图像会被缩小尺寸,变得更小了。
现在已经到了把幽灵图像合并到你的照片中的最佳时间了。
合并:像前面提到的,每一个颜色都有一个透明通道来标识透明度。并且,你每创建一张图像,每一个像素都会有一个颜色值。
所以,如果遇到有透明度和半透明的颜色值该如何处理呢?
答案是,对透明度进行混合。在最顶层的颜色会使用一个公式与它后面的颜色进行混合。公式如下:
NewColor = TopColor * TopColor.Alpha + BottomColor * (1 - TopColor.Alpha)
这是一个标准的线性差值方程。
·当顶层透明度为1时,新的颜色值等于顶层颜色值。
·当顶层透明度为0时,新的颜色值于底层颜色值。
·最后,当顶层的透明度值是01之前的时候,新的颜色值会混合借于顶层和底层颜色值之间。
还可以用 premultiplied alpha的方法。
当处理成千上万像素的时候,他的性能会得以发挥。
好,回到幽灵图。
如同其它位图运算一样,你需要一些循环来遍历每一个像素。但是,你只需要遍历那些你需要修改的像素。
把下面的代码添加到processUsingPixels的下面,还是放在返回语句的前面:
```objc
NSUInteger offsetPixelCountForInput = ghostOrigin.y * inputWidth + ghostOrigin.x;
for (NSUInteger j = 0; j < ghostSize.height; j++) {
for (NSUInteger i = 0; i < ghostSize.width; i++) {
UInt32 * inputPixel = inputPixels + j * inputWidth + i + offsetPixelCountForInput;
UInt32 inputColor = *inputPixel;
UInt32 * ghostPixel = ghostPixels + j * (int)ghostSize.width + i;
UInt32 ghostColor = *ghostPixel;
// Do some processing here
}
}
```
通过对幽灵图像像素数的循环和offsetPixelCountForInput获得输入的图像。记住,虽然你使用的是2维数据存储图像,但在内存他它实际上是一维的。
下一步,添加下面的代码到注释语句 Do some processing here的下面来进行混合:
```objc
// Blend the ghost with 50% alpha
CGFloat ghostAlpha = 0.5f * (A(ghostColor) / 255.0);
UInt32 newR = R(inputColor) * (1 - ghostAlpha) + R(ghostColor) * ghostAlpha;
UInt32 newG = G(inputColor) * (1 - ghostAlpha) + G(ghostColor) * ghostAlpha;
UInt32 newB = B(inputColor) * (1 - ghostAlpha) + B(ghostColor) * ghostAlpha;
// Clamp, not really useful here :p
newR = MAX(0,MIN(255, newR));
newG = MAX(0,MIN(255, newG));
newB = MAX(0,MIN(255, newB));
*inputPixel = RGBAMake(newR, newG, newB, A(inputColor));
```
这部分有2点需要说明:
1:你将幽灵图像的每一个像素的透明通道都乘以了0.5,使它成为半透明状态。然后将它混合到图像中像之前讨论的那样。
2:clamping部分将每个颜色的值范围进行限定到0255之间,虽然一般情况下值不会越界。但是,大多数情况下需要进行这种限定防止发生意外的错误输出。
最后一步,添加下面的代码到 processUsingPixels 的下面,替换之前的返回语句:
// Create a new UIImage
CGImageRef newCGImage = CGBitmapContextCreateImage(context);
UIImage * processedImage = [UIImage imageWithCGImage:newCGImage];
return processedImage;
上面的代码创建了一张新的UIImage并返回它。暂时忽视掉内存泄露问题。编译并运行,你将会看到漂浮的幽灵图像:
![](http://cdn.cocimg.com/cms/uploads/allimg/140812/4196_140812105433_1.png)
好了,完成了,这个程序简直就像个病毒!
黑白颜色
最后一种效果。尝试自己实现黑白颜色效果。为了做到这点,你需要把每一个像素的红色,绿色,蓝色通道的值设定成三个通道原始颜色值的平均值,就像开始的时候输出幽灵图像所有像素亮度值那样。
在注释语句// create a new UIImage前添加上一步的代码 。
找到了吗?
```objc
// Convert the image to black and white
for (NSUInteger j = 0; j < inputHeight; j++) {
for (NSUInteger i = 0; i < inputWidth; i++) {
UInt32 * currentPixel = inputPixels + (j * inputWidth) + i;
UInt32 color = *currentPixel;
// Average of RGB = greyscale
UInt32 averageColor = (R(color) + G(color) + B(color)) / 3.0;
*currentPixel = RGBAMake(averageColor, averageColor, averageColor, A(color));
}
}
```
最后的一步就是清除内存。ARC不能代替你对CGImageRefsCGContexts进行管理。添加如下代码到返回语句之前。
```objc
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CGContextRelease(ghostContext);
free(inputPixels);
free(ghostPixels);

编译并运行,不要被结果吓到:

下面需要做的:

恭喜!你已经完成了自己的第一个图像处理程序。你可以在这里下载该工程的源代码。

还不错吧?你可以尝试修改一下循环中的代码创建自己想要的效果,尝试下实现下面的效果:

·尝试调换图像的红色和蓝色通道值
·提高图像的亮度10%
·作为进一步的挑战,尝试只使用基于像素的方法缩放幽灵的图像,下面是步骤:

1:使用幽灵图像的尺寸大小创建一个新的CGContext。
2:在原图像中得到你想要的并赋值到新的缓存图像中。
3:附加,尝试在像素之前进行计算并插入相似值像素点。如果你可以在四个像素间进行插入,你自己就已经实现 Bilinear scaling(双线性插值法)了!太牛了!

如果你已经完成了第一个项目,想必你对图形图像的处理已经有了基本的概念。现在你可以尝试使用更快更好的方法来实现相同的效果。

在下一章节中,你将会使用另外三个新的方法替换-processUsingPixels:完成相同的任务。一定要看丫!

同时,如果你对该章节有任何疑问和不解,请留言给我!

支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励