iOS CoreImage专题(二) —— 进阶

来源:互联网 发布:c语言数据类型怎么用 编辑:程序博客网 时间:2024/05/16 13:43

  • 前言
  • 管理线程安全
  • 滤镜链
  • 使用转场效果
  • 图像中的人脸检测
    • 人脸检测
    • 获取人脸和人脸的特征所在的矩形界限
  • 自动图像增强
    • 自动图像增强滤镜
    • 使用自动增强滤镜
  • 总结

前言

上一章节我们简单的介绍了CoreImage以及其滤镜的简单使用,包括对输出图像的渲染。这一部分我们将使用更高级一些的技巧:滤镜链、图像转场、人脸检测、自动图像增强等。

管理线程安全

在我们开始之前,先看一个比较重要的东西,那就是线程安全。

CIContext和CIImage对象都是不可变的,也就是说他们可以安全地共享给多个线程。多线程可以使用相同的GPU或者CPU下的CIContext对象来渲染CIImage对象。然而,这种情况并不能用于CIFilter对象,因为它是可变的。一个CIFilter对象不能安全地共享给多个线程。如果你的app是多线程的,每个线程必须创建它自己的CIFilter对象。否则你的app可能会产生一些意料之外的效果。

滤镜链

你可以创建一个滤镜链来获得一种amazing的效果。创建滤镜链就是把一个滤镜的的输出图像作为下一个滤镜的输入图像。我们来看看如何在一个图像上应用多个滤镜 – 变暗(CIGloom)以及凹凸变形(CIBumpDistortion)。
变暗的滤镜通过削弱一个图像的强光部分来让其变暗。下面的代码创建了一个滤镜然后把刚才调整色调的输出图像作为变暗滤镜的输入图像。这就是把滤镜连成一串,简单吧。

    CIFilter *gloom = [CIFilter filterWithName:@"CIGloom"];    [gloom setDefaults];    // 1    [gloom setValue: result forKey: kCIInputImageKey];    [gloom setValue: @25.0f forKey: kCIInputRadiusKey]; // 2    [gloom setValue: @0.75f forKey: kCIInputIntensityKey]; // 3    result = [gloom valueForKey: kCIOutputImageKey]; // 4

1. 设置默认参数。在OS X中必须要手动调用才能设置默认参数,iOS则不需要,因为在iOS中在调用filterWithName的时候已经自动设置好了。
2. 设置输入半径为25。输入半径指定了效果的范围,可以从0变化到100,其默认值为10。之前提到了你能在代码中通过一个滤镜的属性字典来找到最大值、最小值和默认值。
3. 设置输入强度为0.75。输入强度是一个标量值,它指定了原图和输出图的线性混合。最小值为0,最大值为1,默认值为1。
4. 获取输出图像,而此时并有绘制图像。

代码到这里只是请求了一个输出图像而并没有绘制图像,下图展示了如果你在这时就绘制图像(在处理了色调和变暗后)的话图像将会显示成什么样子。

凹凸变形滤镜(CIBumpDistortion)在原图中一个指定的点创建一个凸起效果。下面的代码将展示如何创建、设置以及将其应用在之前的滤镜(变暗滤镜)的输出图像上。凹凸变形需要三个参数:指定效果的位置,效果的半径以及输入比例。

    CIFilter *bumpDistortion = [CIFilter filterWithName:@"CIBumpDistortion"]; //1    [bumpDistortion setDefaults]; //2    [bumpDistortion setValue: result forKey: kCIInputImageKey];    [bumpDistortion setValue: [CIVector vectorWithX:200 Y:150]                      forKey: kCIInputCenterKey]; //3    [bumpDistortion setValue: @100.0f forKey: kCIInputRadiusKey]; //4    [bumpDistortion setValue: @3.0f forKey: kCIInputScaleKey]; //5    result = [bumpDistortion valueForKey: kCIOutputImageKey];
  1. 通过滤镜名称创建滤镜
  2. 同样的需要在OS X中设置为默认值(iOS中则不需要)
  3. 设置效果的中心点在图像中的位置
  4. 设置凹凸半径为100像素
  5. 设置输入比例为3。输入比例指定了效果的方向和数量。默认值为-0.5。取值范围是-10.0到10.0。设置为0则表示没有效果。负值创建一个向外凸起的效果,正值则创建向内凸起的效果。

最终的渲染图像:

使用转场效果

转场效果通常被用于幻灯片的切换或者在视频中从一个场景切换到另一个场景。这种效果是随着时间的推移而渲染的,所以你要设置一个计时器。接下来要讲的是如何设置计时器,你将会学会如何设置拷贝机(copy machine)转场滤镜(CICopyMachine)并将其应用于两张图片。拷贝机转场将创建一根光条,就像你在拷贝机或者图像扫描器上看到的那样。这根光条在源图像上从左扫到右,扫过的地方将变成目标图像。下图展示了从一个滑雪靴到滑雪者的转场中,这个滤镜在作用前、作用时和作用后看起来是怎样的。

你需要通过以下步骤来实现一个转场滤镜的效果:
1. 创建一个CIImage对象用来转场。
2. 设置并调度一个计时器。
3. 创建一个CIContext对象。
4. 创建一个CIFilter对象,将用来应用于图像。
5. 在OS X中,需要设置滤镜的默认值。
6. 设置滤镜参数。
7. 设置源图像和目标图像。
8. 计算时间。
9. 应用滤镜。
10. 绘制结果。
11. 重复8-10步直到转场完成。

你会发现这些步骤中的好几步都跟用一个普通滤镜处理图像一样。不同之处在于使用了计时器来在整个转场的过程中重复地绘制效果。
在下面的代码中,我们将封装一个UIView,在他的initWithFrame:方法里面获取两张图片(boots.png和skier.png)然后把它们设置为源图像和目标图像。使用了一个计时器,它每1/30秒重复一次。thumbnailWidth和thumbnailHeight两个变量用来限制被渲染的图像是如何显示到视图上的。
这个例子在官方例子的基础上作了修改,因为官方的例子是基于OS X的CoreImage方法来呈现的,一些方法在iOS中并不存在。

@interface CICopyMachineView (){    CGFloat thumbnailWidth;    CGFloat thumbnailHeight;    NSTimeInterval base;    CIFilter * transition;    CIContext * context;}@property (nonatomic, strong) CIImage * sourceImage;@property (nonatomic, strong) CIImage * targetImage;@end@implementation CICopyMachineView- (instancetype)initWithFrame:(CGRect)frame{    self = [super initWithFrame:frame];    if (self) {        NSTimer    *timer;        NSURL      *url;        thumbnailWidth  = CGRectGetWidth(frame);        thumbnailHeight = CGRectGetHeight(frame);        url   = [NSURL fileURLWithPath: [[NSBundle mainBundle]                                         pathForResource: @"boots" ofType: @"png"]];        [self setSourceImage: [CIImage imageWithContentsOfURL: url]];        url   = [NSURL fileURLWithPath: [[NSBundle mainBundle]                                         pathForResource: @"skier" ofType: @"png"]];        [self setTargetImage: [CIImage imageWithContentsOfURL: url]];        timer = [NSTimer scheduledTimerWithTimeInterval: 1.0/30.0                                                 target: self                                               selector: @selector(timerFired:)                                               userInfo: nil                                                repeats: YES];        base = [NSDate timeIntervalSinceReferenceDate];        [[NSRunLoop currentRunLoop] addTimer: timer                                     forMode: NSDefaultRunLoopMode];        [[NSRunLoop currentRunLoop] addTimer: timer                                     forMode: UITrackingRunLoopMode];    }    return self;}

创建一个转场滤镜就和创建普通滤镜一样,使用filterWithName:方法来创建滤镜。然后它会自动调用setDefaults来对所有输入参数进行初始化。按例使用thumbnail变量来指定效果的中心。在这个例子中效果的中心被设置到了图像的中心上,而效果的中心被设置为图像的中心并不是必须的。

- (void)setupTransition{    CGFloat w = thumbnailWidth;    CGFloat h = thumbnailHeight;    CIVector *extent = [CIVector vectorWithX: 0  Y: 0  Z: w  W: h];    transition  = [CIFilter filterWithName: @"CICopyMachineTransition"];    // Set defaults on OS X; not necessary on iOS.    [transition setDefaults];    [transition setValue: extent forKey: kCIInputExtentKey];}

timerFired:方法将在每次计时器触发时刻到来时被回调。在这个方法中我们将获取一个矩形,它的宽高和视图宽高相等。接下来我们要设置一个渲染时刻。如果我们的CIContext对象还没有被创建,我们就创建一个(懒加载)。同样的,如果我们的转场滤镜还没有被创建,我们就创建一个。最后我们获取渲染后的输出图像,它是一个CGImageRef(指向CGImage结构体的指针),我们可以用它来渲染视图图层的内容,这样就能加以显示了。imageForTransition:方法每当渲染时刻到来时会被调用,它将滤镜应用于图像,并返回作用后的图像。

- (void)timerFired:(NSTimer *)timer{    CGRect  cg = self.bounds;    CGFloat t = 0.4 * ([NSDate timeIntervalSinceReferenceDate] - base);    if (context == nil) {        context = [CIContext contextWithOptions:nil];    }    if (transition == nil) {        [self setupTransition];    }    CIImage * image = [self imageForTransition: t + 0.1];    CGImageRef cgImage = [context createCGImage:image fromRect:cg];    self.layer.contents = (__bridge id)cgImage;    CGImageRelease(cgImage);}

imageForTransition:方法将基于当前渲染时刻指出谁才是真正的源图像谁才是真正的目标图像,这样我们就能让拷贝机的扫描效果循环地往返出现。如果你不想要循环效果,那么就删掉if-else代码块。

- (CIImage *)imageForTransition: (float)t{    // Remove the if-else construct if you don't want the transition to loop    if (fmodf(t, 2.0) < 1.0f) {        [transition setValue: _sourceImage  forKey: kCIInputImageKey];        [transition setValue: _targetImage  forKey: kCIInputTargetImageKey];    } else {        [transition setValue: _targetImage  forKey: kCIInputImageKey];        [transition setValue: _sourceImage  forKey: kCIInputTargetImageKey];    }    [transition setValue: @( 0.5 * (1 - cos(fmodf(t, 1.0f) * M_PI)) )                  forKey: kCIInputTimeKey];    CIFilter  *crop = [CIFilter filterWithName: @"CICrop"                                 keysAndValues:                       kCIInputImageKey, [transition valueForKey: kCIOutputImageKey],                       @"inputRectangle", [CIVector vectorWithX: 0  Y: 0                                                              Z: thumbnailWidth  W: thumbnailHeight],                       nil];    return [crop valueForKey: kCIOutputImageKey];}

最后我们在一个controller中调用这个view,就能看到炫酷的效果了。如果使用模拟器运行出现了一些卡帧,不用惊慌,那是因为我们的模拟器并没有被配置GPU,它只能使用CPU进行渲染,这会导致CPU占用率颇高,而我们的真机拥有GPU,可以相当高效地进行渲染。

#import "CICopyMachineViewController.h"#import "CICopyMachineView.h"@implementation CICopyMachineViewController- (void)viewDidLoad {    [super viewDidLoad];    CICopyMachineView * view = [[CICopyMachineView alloc] initWithFrame:CGRectMake(100, 100, 276, 208)];    [self.view addSubview:view];    self.view.backgroundColor = [UIColor whiteColor];}@end

图像中的人脸检测

CoreImage能够分析并发现图像中的人脸。它只进行人脸检测,而不是人脸识别。人脸检测是识别含有人脸特征的矩形,而人脸识别则是特定人脸的识别(这张脸属于谢耳朵,莱昂纳德等)。在CoreImage检测到人脸后,它可以提供脸部特征的信息,比如眼睛和嘴巴的位置。它还可以在视频中追踪一张指定的脸的位置。

知道一张图像里面的脸在哪些位置能让你进行一些其他的操作,如剪裁或调整人脸图像的图像品质(色调平衡,红眼校正等)。你也可以进行其他有趣的操作,比如:
· 匿名人脸滤镜配方,它展现了如何将一个像素化滤镜仅作用于图像中的人脸。
· 白晕人脸配方,它展现了如何在人脸周围加点光晕。

人脸检测

在下面的代码中,我们将使用CIDetector类来找到图像中的人脸。

CIContext *context = [CIContext contextWithOptions:nil]; //1    NSDictionary *opts = @{ CIDetectorAccuracy : CIDetectorAccuracyHigh }; //2    CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeFace                                              context:context                                              options:opts]; //3    opts = @{ CIDetectorImageOrientation :                  [[myImage properties] valueForKey:CIDetectorImageOrientation] }; // 4NSArray *features = [detector featuresInImage:myImage options:opts]; //5
  1. 创建一个上下文。当你创建一个检测器的时候可以传一个nil的上下文。
  2. 创建一个选项字典,它用来指定检测器的精度。你可以指定低精度或者高精度。低精度(CIDetectorAccuracyLow)执行得更快,而这个例子中的高精度将会比较慢。
  3. 创建一个人脸检测器。你唯一能创建的识别器类型就是人脸检测。
  4. 找到人脸,我们需要设置一个选项字典。让CoreImage知道图像的转向是相当重要的,如此一来检测器就知道它能在哪找到摆正的脸。大多数情况你都需要从图像中获取图像的转向,然后将它作为选项字典的值。
  5. 使用检测器来发现图像中的特征,该图像必须是一个CIImage对象。CoreImage将会返回一个CIFeature对象的数组,每个元素都代表了图像中的一张脸。

在你获得人脸的数组后,你就可以从中找到这些脸的特征,比如它们的眼睛和嘴巴在哪里。接下来我们就来看看如何读取人脸的各种特征。

获取人脸和人脸的特征所在的矩形界限

人脸特征包括:
· 左眼和右眼的位置。
· 嘴巴的位置
· 追踪ID和追踪边框数,CoreImage使用它们来在视频段落中追踪一张人脸。

当你通过CIDetector对象获取一个人脸特征的数组后,你就可以遍历出脸及脸部特征所在的边框。

for (CIFaceFeature *f in features)    {        NSLog(@"%@",NSStringFromCGRect(f.bounds));        if (f.hasLeftEyePosition)            NSLog(@"Left eye %g %g", f.leftEyePosition.x, f.leftEyePosition.y);        if (f.hasRightEyePosition)            NSLog(@"Right eye %g %g", f.rightEyePosition.x, f.rightEyePosition.y);        if (f.hasMouthPosition)            NSLog(@"Mouth %g %g", f.mouthPosition.x, f.mouthPosition.y);}

自动图像增强

CoreImage的自动增强特征分析了图像的直方图,人脸区域内容和元数据属性。接下来它将返回一个CIFilter对象的数组,每个CIFilter的输入参数已经被设置好了,这些设置能够自动去改善被分析的图像。

自动图像增强滤镜

下表列出了CoreImage用作自动图像增强的滤镜。这些滤镜将会解决在照片中被发现的那些常见问题。

滤镜 目的 CIRedEyeCorrection 修复由相机闪光造成的红眼,琥珀眼和白眼 CIFaceBalance 调整脸的颜色使其肤色看起来比较舒服 CIVibrance 增加图像饱和度,不改变肤色 CIToneCurve 调整图像对比度 CIHighlightShadowAdjust 调整阴影细节

使用自动增强滤镜

自动增强API只有两个方法:autoAdjustmentFilters以及autoAdjustmentFiltersWithOptions:。在大多数情况下,你会使用那个提供了选项字典的方法。

你可以设置这些选项:
· 图像转向,这对于CIRedEyeCorrection和CIFaceBalance滤镜来说相当关键,设置了图像转向就能让CoreImage正确的找到人脸。
· 是否只应用红眼校正。(设置kCIImageAutoAdjustEnhance为false)
· 是否应用除了红眼校正以外的所有滤镜。(设置kCIImageAutoAdjustRedEye为false)。

autoAdjustmentFiltersWithOptions:方法返回选项滤镜的数组,你将会把这些滤镜做成一个滤镜链,然后应用于被分析的图像。下面的代码首先创建了一个选项字典,然后获取图像的转向并将它设置为CIDetectorImageOrientation键的值。

CIImage * myImage = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"tbbt" ofType:@"png"]]];    id orientationProperty = [[myImage properties] valueForKey:(__bridge id)kCGImagePropertyOrientation];    NSDictionary *options = nil;    if (orientationProperty) {        options = @{CIDetectorImageOrientation : orientationProperty};    }    NSArray *adjustments = [myImage autoAdjustmentFiltersWithOptions:options];    for (CIFilter *filter in adjustments) {        [filter setValue:myImage forKey:kCIInputImageKey];        myImage = filter.outputImage;}

记住,输入参数的值已经被CoreImage设置好了,这样就能产生最佳结果。
你不必马上就去应用这些滤镜进行渲染,你可以把滤镜名称和参数值先存起来待会用。把这些参数保存起来稍后执行将会消除再次分析图像的成本。

总结

在这一章节中我们介绍了几种CoreImage的进阶技巧,我们整个专题的demo全部放到了这里:
github link for demo

在下一章节中,我们将基于上面的一些技巧来看看如何通过子类化一个CIFilter对象来封装我们的自定义滤镜。

0 0
原创粉丝点击