分享

iOS图像处理第2部分:核心图形,核心图像,GPUImage

 最初九月雪 2016-06-07

学习在iOS中处理图像和创建酷炫的效果!

欢迎来到本系列教程的第二节,iOS中的图像!

在本系列的第一节,我们学会了如何访问和修改图像的原始像素值。

Header-250x250.png

在本系列的第二节或者说最终节中,你将学习如何使用其他的库来执行同样的任务: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];

生成和运行一下。你应该能看到和之前一样的输出。

55.png

好灵异!你可以在这里下载到本节中完整项目的代码。

在这个简单的例子中,使用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];

现在生成并运行。这次,你应该看到相同的幽灵图像。

56.png

你可以在这里下载到本节项目的所有代码。

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中的滤镜链接不同,它类似于管道。在你完成后,它看起来是这样的:

GPUFilterChain-283x320.png

GPUImageAlphaBlendFilter接受两个输入,在这种情况下为顶部和底部的图像,纹理的位置很重要。-addTarget:atTextureLocation: 设置纹理为正确的输入(位置)。

在链中的最后一个滤镜调用-useNextFrameForImageCapture然后对两个输入调用-processImage 。这可以确保滤镜知道你想要从中抓取图像然后持有它。

最后,替换processImage的第一行代码: 来调用新的函数:

1
UIImage * outputImage = [self processUsingGPUImage:inputImage];

就是这样。生成并运行。幽灵看起来和往常一样好!

57.png

正如你看到的,GPUImage很容易操作。你也可以在GLSL里制作你自己的着色器并创建你自己的滤镜。查看这里的GPUImage文档来更多的学习如何使用本框架。

在这里下载本节项目中的所有代码。

下一步?

恭喜!你已经用四种不同方式实现了SpookCam。这里是所有的下载链接:

当然,除本教程外还有很多其他有趣的图像处理概念:

内核和卷积。内核与图像采样滤镜协同工作。例如,模糊滤镜。

图像分析。有时候你需要对图像进行深入的分析,例如你想进行人脸识别。Core Image为这个过程提供了CIDetector类。

最后但同样重要的,没有图像处理教程没有提及OpenCV就结束了。

OpenCV是所有图像处理事实上的库,它还有一个iOS的build!然后,它还远远不是轻量级的。这更多用于技术领域,比如特性跟踪。在这里学习更多的OpenCV知识。

这个网站也有OpenCV教程

下一步是选择一种方法来开始创建你自己革命性的自拍app。不要停止学习!

我真心希望你能喜欢本教程。如果你有任何疑问或意见,请在下面的论坛留言告诉我们。

注: 图片出自于Free Range Stock,由Roxana Gonzalez拍摄。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多