学习在iOS中处理图像和创建酷炫的效果!
欢迎来到本系列教程的第二节,iOS中的图像!
在本系列的第一节,我们学会了如何访问和修改图像的原始像素值。
在本系列的第二节或者说最终节中,你将学习如何使用其他的库来执行同样的任务:Core Graphics, Core Image 和GPUImage。你将学习它们各自的优点和缺点,这样你就可以针对你的情况做出更好的选择。
本教程从上一节结束的地方开始。如果你没有项目文件,你可以在这里下载它。
如果你在第一节中表现得很好,你要好好享受这一节!既然你理解了工作原理,你将充分理解这些库进行图像处理是多么的简单。
超级SpookCam之Core Graphics版本
Core Graphics是Apple基于Quartz 2D绘图引擎的绘图API。它提供了底层API,如果你熟悉OpenGL可能会觉得它们很相似。
如果你曾经重写过视图的-drawRect:函数,你其实已经与Core Graphics交互过了,它提供了很多绘制对象、斜度和其他很酷的东西到你的视图中的函数。
这个网站已经有大量的Core Graphics教程,比如这个和这个。所以,这本教程中,我们将关注于如何使用Core Graphics来做一些基本的图像处理。
在开始之前,我们需要熟悉一个概念Graphics Context。
概念:Graphics Contexts是OpenGl和Core Graphics的核心概念,它是渲染中最常见的类型。它是一个持有所有关于绘制信息的全局状态对象。
在Core Graphics中,包括了当前的填充颜色,描边颜色,变形,蒙版,在哪里绘制等。在iOS中,还有其他不同类型的context比如PDF context,它可以让你绘制一个PDF文件。
在本教程中,你只会使用到Bitmap context,它可以绘制位图。
在-drawRect:函数中,你会发现你可以直接调用UIGraphicsGetCurrentContext()来使用context。系统被设置为你可以直接在视图上绘制被渲染的图像。
在-drawRect:函数外,通常没有图形context可用。你可以像第一个项目中一样用CGContextCreate()创建,或者你可以使用UIGraphicsBeginImageContext()和UIGraphicsGetCurrentContext()抓取创建的context。
这叫做离屏-渲染,意思是你不是在任何地方直接绘制,而是在离屏缓冲区渲染。
在Core Graphics中,你可以获得context中的UIImage然后把它显示在屏幕上。使用OpenGL,你可以直接把这个缓冲区与当前渲染在屏幕中的交换,然后直接显示它。
使用Core Graphics处理图像利用了在缓冲区渲染图像的离屏渲染,它从context抓取图像,并适用任何你想要的效果。
好了,概念介绍完了,是时候变一些代码的魔术了!添加下面的新函数到ImageProcessor.m中:
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
|
- (UIImage *)processUsingCoreGraphics:(UIImage*)input {
CGRect imageRect = {CGPointZero,input.size};
NSInteger inputWidth = CGRectGetWidth(imageRect);
NSInteger inputHeight = CGRectGetHeight(imageRect);
// 1) Calculate the location of Ghosty
UIImage * ghostImage = [UIImage imageNamed:@
"ghost.png"
];
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);
CGRect ghostRect = {ghostOrigin, ghostSize};
// 2) Draw your image into the context.
UIGraphicsBeginImageContext(input.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);
CGAffineTransform flipThenShift = CGAffineTransformTranslate(flip,0,-inputHeight);
CGContextConcatCTM(context, flipThenShift);
CGContextDrawImage(context, imageRect, [input CGImage]);
CGContextSetBlendMode(context, kCGBlendModeSourceAtop);
CGContextSetAlpha(context,0.5);
CGRect transformedGhostRect = CGRectApplyAffineTransform(ghostRect, flipThenShift);
CGContextDrawImage(context, transformedGhostRect, [ghostImage CGImage]);
// 3) Retrieve your processed image
UIImage * imageWithGhost = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 4) Draw your image into a grayscale context
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
context = CGBitmapContextCreate(nil, inputWidth, inputHeight,
8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaNone);
CGContextDrawImage(context, imageRect, [imageWithGhost CGImage]);
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage * finalImage = [UIImage imageWithCGImage:imageRef];
// 5) Cleanup
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CFRelease(imageRef);
return
finalImage;
}
|
这个函数内容可真够多的,让我们一点一点分析它。
1) 计算Ghosty的位置
1
2
3
4
5
6
|
UIImage * ghostImage = [UIImage imageNamed:@
"ghost.png"
];
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);
CGRect ghostRect = {ghostOrigin, ghostSize};
|
创建一个新的CGContext。
像前面讨论的,这里创建了一个“离屏”(“off-screen”)的context。还记得吗?CGContext的坐标系以左下角为原点,相反的UIImage使用左上角为原点。
有趣的是,如果你使用UIGraphicsBeginImageContext()来创建一个context,系统会把坐标翻转,把原点设为左上角。因此,你需要变换你的context把它翻转回来,从而使CGImage能够进行正确的绘制。
如果你直接在这个context中绘制UIImage,你不需要执行变换,坐标系统将会自动匹配。设置这个context的变换将影响所有你后面绘制的图像。
2) 把你的图像绘制到context中
1
2
3
4
5
6
7
8
9
10
|
UIGraphicsBeginImageContext(input.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);
CGAffineTransform flipThenShift = CGAffineTransformTranslate(flip,0,-inputHeight);
CGContextConcatCTM(context, flipThenShift);
CGContextDrawImage(context, imageRect, [input CGImage]);
CGContextSetBlendMode(context, kCGBlendModeSourceAtop);
CGContextSetAlpha(context,0.5);
CGRect transformedGhostRect = CGRectApplyAffineTransform(ghostRect, flipThenShift);
CGContextDrawImage(context, transformedGhostRect, [ghostImage CGImage]);
|
在绘制完图像后,你context的alpha值设为了0.5。这只会影响后面绘制的图像,所以本次绘制的输入图像使用了全alpha。
你也需要把混合模式设置为kCGBlendModeSourceAtop。
这里为context设置混合模式是为了让它使用之前的相同的alpha混合公式。在设置完这些参数之后,翻转幽灵的坐标然后把它绘制在图像中。
3) 取回你处理的图像
1
2
|
UIImage * imageWithGhost = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
|
为了把你的图像转换成黑白的,你将创建一个使用灰度(grayscale)色彩的新的CGContext。它将把所有你在context中绘制的图像转换成灰度的。
因为你使用CGBitmapContextCreate()来创建了这个context,坐标则是以左下角为原点,你不需要翻转它来绘制CGImage。
4) 绘制你的图像到一个灰度(grayscale)context中
1
2
3
4
5
6
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
context = CGBitmapContextCreate(nil, inputWidth, inputHeight,
8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaNone);
CGContextDrawImage(context, imageRect, [imageWithGhost CGImage]);
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage * finalImage = [UIImage imageWithCGImage:imageRef];
|
取回你最终的图像。
为什么你不可以使用UIGraphicsGetImageFromCurrentImageContext()呢,因为你没有把当前的图形context设置为灰度context。
因此,你需要自己创建它。你需要使用CGBitmapContextCreateImage()来渲染context中的图像。
5) 清理
1
2
3
4
|
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CFRelease(imageRef);
return
finalImage;
|
在最后,你需要释放所有你创建的对象。然后 – 完成了!
内存使用:当执行图像处理时,密切关注内存使用情况。像在第一节中讨论的一样,一个8M像素的图像占用了高达32M的内存。尽量避免在内存中同一时间保持同一图像的多个复制。
注意到为什么我们第二次需要释放context而第一次不需要了吗?这是因为第一次时,你使用UIGraphicsGetCurrentImageContext()获取了context。这里的关键词是‘get’。
‘Get’意味着你获取了当前context的引用,你并不持有它。
在第二次中,你调用了CGBitmapContextCreateImage(),Create意味着你持有这个对象,并需要管理它的生命周期。这也是你为什么需要释放imageRef的原因,因为你是通过CGBitmapContextCreateImage()创建它的。
干得漂亮!现在,替换processImage中的第一行:调用这个新的函数替换掉processUsingPixels::
1
|
UIImage * outputImage = [self processUsingCoreGraphics:inputImage];
|
生成和运行一下。你应该能看到和之前一样的输出。
好灵异!你可以在这里下载到本节中完整项目的代码。
在这个简单的例子中,使用Core Graphics看起来好像不比直接操作像素更简单。
然而,想象一个更复杂的操作,比如旋转图像。在像素操作中,这需要相当复杂的数学。
但是,使用Core Graphics,你只需要在绘制图像前给context设置一个旋转的变换就可以了。因为,你处理的内容越复杂,你使用Core Graphics则能节省更多的时间。
介绍完了两种方法,下面还有两种方法。下一个:Core Image!
超超SpookCam之Core Image版本
这个网站也已经有大量好的Core Image教程,比如IOS 6中的这个。我们也在我们的iOS教程系列中有很多关于Core Image的章节。
在本教程中,你将看到有很多关于Core Image与其他几种方法对比的讨论。
Core Image是Apple的图像处理的解决方案。它避免了所有底层的像素操作方法,转而使用高级别的滤镜替代了它们。
Core Image最好的部分在于它对比操作原始像素或Core Graphics有着极好的性能。这个库使用CPU和GPU混合处理提供接近实时的性能。
Apple还提供了巨大的预先制作的滤镜库。在OSX中,你甚至可以使用Core Image Kernel Language创建你自己的滤镜,它跟OpenGL中的着色语言GLSL很相似。在写本教程时,你还不能在iOS中制作你自己的Core Image滤镜(只支持Mac OS X)。
它还有一些比Core Graphics更好的效果。正如你在代码中看到的,你用Core Graphics来充分利用Core Image。
添加这个新函数到ImageProcessor.m中:
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
|
- (UIImage *)processUsingCoreImage:(UIImage*)input {
CIImage * inputCIImage = [[CIImage alloc] initWithImage:input];
// 1. Create a grayscale filter
CIFilter * grayFilter = [CIFilter filterWithName:@
"CIColorControls"
];
[grayFilter setValue:@(0) forKeyPath:@
"inputSaturation"
];
// 2. Create your ghost filter
// Use Core Graphics for this
UIImage * ghostImage = [self createPaddedGhostImageWithSize:input.size];
CIImage * ghostCIImage = [[CIImage alloc] initWithImage:ghostImage];
// 3. Apply alpha to Ghosty
CIFilter * alphaFilter = [CIFilter filterWithName:@
"CIColorMatrix"
];
CIVector * alphaVector = [CIVector vectorWithX:0 Y:0 Z:0.5 W:0];
[alphaFilter setValue:alphaVector forKeyPath:@
"inputAVector"
];
// 4. Alpha blend filter
CIFilter * blendFilter = [CIFilter filterWithName:@
"CISourceAtopCompositing"
];
// 5. Apply your filters
[alphaFilter setValue:ghostCIImage forKeyPath:@
"inputImage"
];
ghostCIImage = [alphaFilter outputImage];
[blendFilter setValue:ghostCIImage forKeyPath:@
"inputImage"
];
[blendFilter setValue:inputCIImage forKeyPath:@
"inputBackgroundImage"
];
CIImage * blendOutput = [blendFilter outputImage];
[grayFilter setValue:blendOutput forKeyPath:@
"inputImage"
];
CIImage * outputCIImage = [grayFilter outputImage];
// 6. Render your output image
CIContext * context = [CIContext contextWithOptions:nil];
CGImageRef outputCGImage = [context createCGImage:outputCIImage fromRect:[outputCIImage extent]];
UIImage * outputImage = [UIImage imageWithCGImage:outputCGImage];
CGImageRelease(outputCGImage);
return
outputImage;
}
|
我们看一下这个代码跟之前的函数有多大区别。
使用Core Image,你设置了大量的滤镜来处理你的图像 – 你使用了CIColorControls滤镜来设置灰度,CIColorMatrix和CISourceAtopCompositing来设置混合,最后把它们连接在一起。
现在,让我们浏览一遍这个函数来学习它的每一个步骤。
1.创建CIColorControls滤镜,设置它的inputSaturation值为0。你可能记得,饱和度是HSV颜色空间的一个通道。这里的0表示了灰度。
2.创建一个和输入图像一样大小的填充的幽灵图像。
3.创建CIColorMatrix滤镜,设置它的alphaVector值为[0 0 0.5 0]。这将给幽灵的alpha值增加0.5。
4.创建CISourceAtopCompositing滤镜来进行alpha混合。
5.合并你的滤镜来处理图像。
6.渲染输出CIImage到CGImage,创建最终的UIImage。记得在后面释放你的内存。
这个方法使用了一个叫做-createPaddedGhostImageWithSize:的帮助函数,它使用Core Graphics创建了输入图像25%大小缩小版的填充的幽灵。你自己能实现这个函数吗?
自己试一下。如果你被卡住了,请看下面的解决方案:
解决方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
- (UIImage *)createPaddedGhostImageWithSize:(CGSize)inputSize {
UIImage * ghostImage = [UIImage imageNamed:@
"ghost.png"
];
CGFloat ghostImageAspectRatio = ghostImage.size.width / ghostImage.size.height;
NSInteger targetGhostWidth = inputSize.width * 0.25;
CGSize ghostSize = CGSizeMake(targetGhostWidth, targetGhostWidth / ghostImageAspectRatio);
CGPoint ghostOrigin = CGPointMake(inputSize.width * 0.5, inputSize.height * 0.2);
CGRect ghostRect = {ghostOrigin, ghostSize};
UIGraphicsBeginImageContext(inputSize);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect inputRect = {CGPointZero, inputSize};
CGContextClearRect(context, inputRect);
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);
CGAffineTransform flipThenShift = CGAffineTransformTranslate(flip,0,-inputSize.height);
CGContextConcatCTM(context, flipThenShift);
CGRect transformedGhostRect = CGRectApplyAffineTransform(ghostRect, flipThenShift);
CGContextDrawImage(context, transformedGhostRect, [ghostImage CGImage]);
UIImage * paddedGhost = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return
paddedGhost;
}
|
最后,替换掉processImage中的第一行: 来调用你的新的函数:
1
|
UIImage * outputImage = [self processUsingCoreImage:inputImage];
|
现在生成并运行。这次,你应该看到相同的幽灵图像。
你可以在这里下载到本节项目的所有代码。
Core Image提供了大量的滤镜,你可以使用它们来创建几乎任何你想要的效果。它是你处理图像时的好伙伴。
现在到了最后一个解决方案,也是本教程中附带的唯一的第三方选项:GPUImage。
大型超超SpookCam之GPUImage版本
GPUImage是一个活跃的iOS上基于GPU的图像处理库。它在这个网站中的十佳iOS库中赢得了一席之地!
GPUImage隐藏了在iOS中所有需要使用OpenGL ES的复杂的代码,并用极其简单的接口以很快的速度处理图像。GPUImage的性能甚至在很多时候击败了Core Image,但是Core Image仍然在很多函数中有优势。
在开始学习GPUImage之前,你需要把它包含到你的项目中。这可以使用Cocoapods在项目中生成静态库或直接嵌入源码来完成。
项目应用已经包含一个建立在外部的静态框架。你可以根据下面的步骤简单的把它复制到项目中:
说明:
在命令行中运行build.sh。生成的库和头文件将会被放在build/Release-iphone。
你也可以通过修改build.sh中的IOSSDK_VER变量来修改iOS SDK的版本(你可以通过使用xcodebuild -showsdks来查看所有可用的版本)。
你可以通过下面来自Github仓库的说明把源代码嵌入你的项目:
说明:
拖拽GPUImage.xcodeproj文件到你Xcode项目中来把框架嵌入到你的项目中。
然后,到应用程序的target添加GPUImage为一个target依赖。
从GPUImage框架新产品文件夹中拖拽libGPUImage.a库到你应用程序target中的Link Binary With Librariesbuild phase。
GPUImage需要链接一些其他框架到你的应用程序,所以你需要添加如下的相关库到你的应用程序target:
CoreMedia
CoreVideo
OpenGLES
AVFoundation
QuartzCore
然后你需要找到框架的头文件。在你项目的build设置中,设置Header Search Paths的相对路径为你应用程序中框架/子文件夹中的GPUImage源文件目录。使Header Search Paths是递归的。
添加GPUImage到你的项目中后,一定要在ImageProcessor.m中包含头文件。
如果你想包含静态的框架,使用#import GPUImage/GPUImage.h。如果你想直接在项目中包含它,使用#import “GPUImage.h”。
添加新的处理函数到ImageProcessor.m中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
- (UIImage *)processUsingGPUImage:(UIImage*)input {
// 1. Create the GPUImagePictures
GPUImagePicture * inputGPUImage = [[GPUImagePicture alloc] initWithImage:input];
UIImage * ghostImage = [self createPaddedGhostImageWithSize:input.size];
GPUImagePicture * ghostGPUImage = [[GPUImagePicture alloc] initWithImage:ghostImage];
// 2. Set up the filter chain
GPUImageAlphaBlendFilter * alphaBlendFilter = [[GPUImageAlphaBlendFilter alloc] init];
alphaBlendFilter.mix = 0.5;
[inputGPUImage addTarget:alphaBlendFilter atTextureLocation:0];
[ghostGPUImage addTarget:alphaBlendFilter atTextureLocation:1];
GPUImageGrayscaleFilter * grayscaleFilter = [[GPUImageGrayscaleFilter alloc] init];
[alphaBlendFilter addTarget:grayscaleFilter];
// 3. Process & grab output image
[grayscaleFilter useNextFrameForImageCapture];
[inputGPUImage processImage];
[ghostGPUImage processImage];
UIImage * output = [grayscaleFilter imageFromCurrentFramebuffer];
return
output;
}
|
嘿!它看来很明确。这是它的具体内容:
创建GPUImagePicture对象;再次使用-createPaddedGhostImageWithSize:为一个工具。这时GPUImage会把图像纹理上传到GPU内存。
创建和链接你将要使用的滤镜。这种链接与Core Image中的滤镜链接不同,它类似于管道。在你完成后,它看起来是这样的:
GPUImageAlphaBlendFilter接受两个输入,在这种情况下为顶部和底部的图像,纹理的位置很重要。-addTarget:atTextureLocation: 设置纹理为正确的输入(位置)。
在链中的最后一个滤镜调用-useNextFrameForImageCapture然后对两个输入调用-processImage 。这可以确保滤镜知道你想要从中抓取图像然后持有它。
最后,替换processImage的第一行代码: 来调用新的函数:
1
|
UIImage * outputImage = [self processUsingGPUImage:inputImage];
|
就是这样。生成并运行。幽灵看起来和往常一样好!
正如你看到的,GPUImage很容易操作。你也可以在GLSL里制作你自己的着色器并创建你自己的滤镜。查看这里的GPUImage文档来更多的学习如何使用本框架。
在这里下载本节项目中的所有代码。
下一步?
恭喜!你已经用四种不同方式实现了SpookCam。这里是所有的下载链接:
SpookCam-Starter
SpookCam-Pixel
SpookCam-CoreGraphics
SpookCam-CoreImage
SpookCam-GPUImage
当然,除本教程外还有很多其他有趣的图像处理概念:
内核和卷积。内核与图像采样滤镜协同工作。例如,模糊滤镜。
图像分析。有时候你需要对图像进行深入的分析,例如你想进行人脸识别。Core Image为这个过程提供了CIDetector类。
最后但同样重要的,没有图像处理教程没有提及OpenCV就结束了。
OpenCV是所有图像处理事实上的库,它还有一个iOS的build!然后,它还远远不是轻量级的。这更多用于技术领域,比如特性跟踪。在这里学习更多的OpenCV知识。
这个网站也有OpenCV教程。
下一步是选择一种方法来开始创建你自己革命性的自拍app。不要停止学习!
我真心希望你能喜欢本教程。如果你有任何疑问或意见,请在下面的论坛留言告诉我们。
注: 图片出自于Free Range Stock,由Roxana Gonzalez拍摄。