Core Image框架详细解析(十四) —— 创建自定义滤波器 Creating Custom Filters(一)

版本记录

版本号 时间
V1.0 2018.01.29

前言

Core Image是IOS5中新加入的一个框架,里面提供了强大高效的图像处理功能,用来对基于像素的图像进行操作与分析。还提供了很多强大的滤镜,可以实现你想要的效果,下面我们就一起解析一下这个框架。感兴趣的可以参考上面几篇。
1. Core Image框架详细解析(一) —— 基本概览
2. Core Image框架详细解析(二) —— Core Image滤波器参考
3. Core Image框架详细解析(三) —— 关于Core Image
4. Core Image框架详细解析(四) —— Processing Images处理图像(一)
5. Core Image框架详细解析(五) —— Processing Images处理图像(二)
6. Core Image框架详细解析(六) —— 图像中的面部识别Detecting Faces in an Image(一)
7. Core Image框架详细解析(七) —— 自动增强图像 Auto Enhancing Images
8. Core Image框架详细解析(八) —— 查询系统中的过滤器 Querying the System for Filters
9. Core Image框架详细解析(九) —— 子类化CIFilter:自定义效果的配方 Subclassing CIFilter: Recipes for Custom Effects(一)
10. Core Image框架详细解析(十) —— 子类化CIFilter:自定义效果的配方 Subclassing CIFilter: Recipes for Custom Effects(二)
11. Core Image框架详细解析(十一) —— 获得最佳性能 Getting the Best Performance
12. Core Image框架详细解析(十二) —— 使用反馈处理图像 Using Feedback to Process Images
13. Core Image框架详细解析(十三) —— 在写一个自定义滤波器之前你需要知道什么?

Creating Custom Filters - 创建自定义滤波器

如果Core Image提供的过滤器没有提供您需要的功能,您可以编写自己的过滤器。您可以将过滤器作为应用程序项目的一部分,或者(仅在macOS中),可以将一个或多个过滤器作为独立的映像单元进行打包。图像单元使用NSBundle类,并允许App托管外部插件过滤器。

以下部分提供了有关如何创建和使用自定义过滤器和图像单元的详细信息:


Expressing Image Processing Operations in Core Image - 在Core Image中表达图像处理操作

Core Image的工作原理是内核(即每像素处理程序)被写为一个计算,其中输出像素使用反映射到内核的输入图像的相应像素来表示。 虽然可以用这种方式来表示大多数像素计算,但是也有一些比其他处理更自然的图像处理操作,即使不是不可能,也是很困难的。 在编写滤镜之前,您可能需要考虑图像处理操作是否可以用Core Image表示。 例如,计算直方图很难被描述为对源图像的逆映射。


Creating a Custom Filter - 创建自定义滤波器

本节介绍如何创建具有Objective-C部分和内核部分的Core Image过滤器。按照本节中的步骤,您将创建一个CPU可执行的过滤器。您可以按照Packaging and Loading Image Units中的说明将此过滤器和其他过滤器(如果需要)打包为图像单位。或者,您可以简单地使用您自己的应用程序中的过滤器。有关详细信息,请参阅使Using Your Own Custom Filter

Core Image提供了三种基于内核的滤镜:彩色滤镜,扭曲滤镜和通用滤镜color filters, warp filters, and general filters。通用滤波器包括一个GPU内核例程,可以修改像素颜色和像素位置。但是,如果您设计的滤镜只能修改像素颜色,或者在不修改像素的情况下更改图像几何体,则创建颜色或扭曲滤镜可使Core Image在各种iOS和Mac硬件上提供更好的滤镜性能。有关详细信息,请参阅CIColorKernelCIWarpKernel类的参考文档。

本节中的通用过滤器假设感兴趣区域(ROI)和定义域重合。如果你想写一个过滤器这个假设是不正确的,请确保你也阅读Supplying an ROI Function。在创建自己的自定义过滤器之前,请确保您了解Core Image坐标空间。请参阅 Building a Dictionary of Filters

要创建自定义CPU可执行过滤器,请执行以下步骤:

下面的章节将详细介绍每个步骤,以使用浊度去除过滤器haze removal filter为例。 除雾过滤器的效果是调整图像的亮度和对比度,并对其进行锐化。 此滤镜对于校正通过轻雾或朦胧拍摄的图像非常有用,这通常是从飞机拍摄图像时的情况。 图9-1显示了使用浊度去除过滤器处理之前和之后的图像。 使用过滤器的应用程序提供滑块,使用户能够调整过滤器的输入参数。

Figure 9-1 An image before and after processing with the haze removal filter

1. Write the Kernel Code - 编写内核代码

执行每像素处理的代码驻留在扩展名为.cikernel的文件中。您可以在此文件中包含多个内核例程。如果你想让你的代码模块化,你也可以包含其他例程。您可以使用OpenGL着色语言(glslang)的子集和Core Image扩展来指定内核。有关该语言的允许元素的信息,请参阅Core Image Kernel Language Reference

内核例程签名必须返回一个向量(vec4),其中包含将源像素映射到目标像素的结果。Core Image为每个像素调用一次内核例程。请记住,您的代码无法从像素到像素积累知识。编写代码时一个好的策略就是尽可能多地从实际的内核中移动不变的计算,并把它放在过滤器的Objective-C部分。

Listing 9-1显示了一个雾霾移除过滤器的内核例程。列表中的每一行代码都有详细的解释。 (在Kernel Routine ExamplesImage Unit Tutorial中有其他像素处理例程。

// Listing 9-1  A kernel routine for the haze removal filter

kernel vec4 myHazeRemovalKernel(sampler src,             // 1
                     __color color,
                    float distance,
                    float slope)
{
    vec4   t;
    float  d;
 
    d = destCoord().y * slope  +  distance;              // 2
    t = unpremultiply(sample(src, samplerCoord(src)));   // 3
    t = (t - d*color) / (1.0-d);                         // 4
 
    return premultiply(t);                               // 5
}

代码如下:

    1. 接受四个输入参数并返回一个向量。在声明过滤器的接口时,必须确保声明与在内核中指定的相同数量的输入参数。内核必须返回一个vec4数据类型。
    1. 根据目标坐标的y值和斜率和距离输入参数计算一个值。destCoord例程(由Core Image提供)返回当前正在计算的像素在工作空间坐标中的位置。
    1. 在应用与src关联的任何变换矩阵之后,获取采样器src中与当前输出像素关联的采样器空间中的像素值。回想一下,Core Image使用颜色组件与预乘alpha值。在处理之前,您需要对从采样器收到的颜色值进行无预处理。
    1. 通过应用除雾公式计算输出矢量,该公式包含颜色的斜率、距离计算和调整。
    1. 根据需要返回一个vec4向量。内核在返回结果之前执行预乘操作,因为Core Image使用带有预乘alpha值的颜色分量。

关于采样器和采样坐标空间的一些说明:您为向自定义内核提供采样而设置的采样器可以包含滤波器计算所需的任何值,而不仅仅是颜色值。 例如,采样器可以提供数值表的值,其中x和y值分别由红色和绿色分量,高度字段等表示的向量字段。 这意味着您可以在采样器中存储最多具有四个分量的矢量值字段。 为了避免过滤器客户端的混淆,最好提供说明何时不使用向量的文档。 当您使用不提供颜色的采样器时,可以绕过Core Image通常通过提供nil色彩空间执行的色彩校正。

2. Use Quartz Composer to Test the Kernel Routine - 使用Quartz Composer来测试内核例程

Quartz Composer是一个易于使用的开发工具,您可以使用它来测试内核例程。

下面看一下下载Quartz Composer

  • 打开Xcode。
  • 选择Xcode > Open Developer Tool > More Developer Tools...,选择这个项目将带你到developer.apple.com
  • 登录到developer.apple.com。然后您应该看到苹果开发者网站的下载。
  • 下载包含Quartz Composer的Xcode包的Graphics Tools

Quartz Composer提供了一个patch-Core Image Filter,您可以在其中放置内核例程。您只需打开Core Image Filter修补程序的Inspector,然后粘贴或输入代码到文本字段中,如图9-2所示。

Figure 9-2 The haze removal kernel routine pasted into the Settings pane

输入代码后,补丁的输入端口将根据内核函数的原型自动创建,如图9-3所示。 补丁总是有一个输出端口,它代表了内核产生的结果图像。

图9-3所示的简单组合使用Image Importer修补程序导入图像文件,通过内核处理它,然后使用Billboard修补程序在屏幕上显示结果。 您的内核可以使用多个图像,或者如果它生成输出,则可能不需要任何输入图像。

您构建的用于测试内核的组合可能比图9-3中显示的更复杂。 例如,您可能想要将内核例程与其他内置的Core Image过滤器或其他内核例程链接起来。 Quartz Composer提供了很多其他的patches,可以在测试内核例程的过程中使用。

Figure 9-3 A Quartz Composer composition that tests a kernel routine

3. Declare an Interface for the Filter - 声明过滤器的接口

过滤器的.h文件包含指定过滤器输入的接口,如Listing 9-2所示。 除雾内核有四个输入参数:源,颜色,距离和斜率。 过滤器的接口也必须包含这些输入参数。 输入参数必须与过滤器指定的顺序相同,并且数据类型必须在两者之间兼容。

注意:确保在输入参数的名称前加上input,如Listing 9-2所示。

// Listing 9-2  Code that declares the interface for a haze removal filter

@interface MyHazeFilter: CIFilter
{
    CIImage   *inputImage;
    CIColor   *inputColor;
    NSNumber  *inputDistance;
    NSNumber  *inputSlope;
}
@end 

4. Write an Init Method for the CIKernel Object - 为CIKernel对象编写一个Init方法

过滤器的实现implementation文件包含一个方法,该方法使用.cikernel文件中指定的内核例程初始化Core Image内核对象(CIKernel).cikernel文件可以包含多个内核例程。 列表后面会出现每行代码的详细说明。

// Listing 9-3  An init method that initializes the kernel

static CIKernel *hazeRemovalKernel = nil;
 
- (id)init
{
    if(hazeRemovalKernel == nil)                                         // 1
    {
        NSBundle    *bundle = [NSBundle bundleForClass: [self class]];   // 2
        NSString    *code = [NSString stringWithContentsOfFile: [bundle
                                pathForResource: @"MyHazeRemoval"
                                ofType: @"cikernel"]];                   // 3
        NSArray     *kernels = [CIKernel kernelsWithString: code];       // 4
        hazeRemovalKernel = kernels[0];                                  // 5
    }
    return [super init];
}

下面解释下代码的作用:

    1. 检查CIKernel对象是否已经被初始化。
    1. 返回动态加载CIFilter类的包。
    1. 返回从指定路径的文件名创建的字符串,在本例中为MyHazeRemoval.cikernel文件。
    1. code参数指定的字符串中创建一个CIKernel对象。 标记为内核的.cikernel文件中的每个例程都返回到内核kernels数组中。 这个例子在.cikernel文件中只有一个内核,所以这个数组只包含一个项目。
    1. hazeRemovalKernel设置为内核kernels数组中的第一个内核。 如果.cikernel文件包含多个内核,那么也可以在这个例程中初始化这些内核。

5. Write a Custom Attributes Method - 编写自定义属性方法

customAttributes方法允许过滤器的客户端获取过滤器属性,例如输入参数,默认值以及最小值和最大值。 (请参阅CIFilter Class Reference以获取属性的完整列表。)过滤器不需要提供有关属性的任何信息,但是如果属性不存在,过滤器必须以合理的方式运行。

通常,这些是您的customAttributes方法将返回的属性:

  • 输入和输出参数
  • 您提供的每个参数的属性类(必需)
  • 每个参数的最小值,最大值和默认值(可选)
  • 其他适当的信息,如滑块的最小值和最大值(可选)

Listing 9-4显示了Haze过滤器的customAttributes方法。输入参数inputDistanceinputSlope分别具有最小值,最大值,滑块最小值,滑块最大值,默认值和标识值集。滑块的最小值和最大值用于设置图9-1所示的滑块。 inputColor参数有一个默认值集。

// Listing 9-4  The customAttributes method for the Haze filter

- (NSDictionary *)customAttributes
{
    return @{
        @"inputDistance" :  @{
            kCIAttributeMin       : @0.0,
            kCIAttributeMax       : @1.0,
            kCIAttributeSliderMin : @0.0,
            kCIAttributeSliderMax : @0.7,
            kCIAttributeDefault   : @0.2,
            kCIAttributeIdentity  : @0.0,
            kCIAttributeType      : kCIAttributeTypeScalar
            },
        @"inputSlope" : @{
            kCIAttributeSliderMin : @-0.01,
            kCIAttributeSliderMax : @0.01,
            kCIAttributeDefault   : @0.00,
            kCIAttributeIdentity  : @0.00,
            kCIAttributeType      : kCIAttributeTypeScalar
            },
         kCIInputColorKey : @{
         kCIAttributeDefault : [CIColor colorWithRed:1.0
                                               green:1.0
                                                blue:1.0
                                               alpha:1.0]
           },
   };
}

6. Write an Output Image Method - 编写一个输出图像方法

outputImage方法为每个输入图像(或图像蒙版)创建一个CISampler对象,创建一个CIFilterShape对象(如果适用),并应用内核方法。Listing 9-5显示了haze removal filteroutputImage方法。代码首先要设置一个采样器来从输入图像中获取像素。由于此过滤器只使用一个输入图像,因此代码只设置一个采样器。

该代码调用CIFilterapply:arguments:options:方法来生成一个CIImage对象。 apply方法的第一个参数是包含haze移除内核函数的CIKernel对象。 (请参阅Write the Kernel Code。)回想一下,除雾内核函数有四个参数:采样器,颜色,距离和斜率。这些参数作为接下来的四个参数传递给Listing 9-5中的apply:arguments:options:方法。 apply方法的其余参数指定控制Core Image如何评估函数的选项(键值对)。您可以传递三个键之一:kCIApplyOptionExtentkCIApplyOptionDefinitionkCIApplyOptionUserInfo。本示例使用kCIApplyOptionDefinition键指定输出图像的定义域(DOD)。有关这些键的描述,请参阅CIFilter Class Reference,以及有关使用apply:arguments:options:方法的更多信息。

最后一个参数nil指定选项列表的结尾。

Listing 9-5  A method that returns the image output from a haze removal filter

- (CIImage *)outputImage
{
    CISampler *src = [CISampler samplerWithImage: inputImage];
 
    return [self apply: hazeRemovalKernel, src, inputColor, inputDistance,
        inputSlope, kCIApplyOptionDefinition, [src definition], nil];
}

Listing 9-5是一个简单的例子。 您的outputImage方法的实现需要根据您的过滤器进行调整。 如果你的过滤器需要循环不变计算,你可以将它们包含在outputImage方法中而不是内核中。

7. Register the Filter - 注册过滤器

理想情况下,无论您是打算将过滤器分发给其他人,还是仅在您自己的应用中使用过滤器,都将过滤器打包为图像单元。 如果您打算将此过滤器打包为图像单元,则将使用Packaging and Loading Image Units中介绍的CIPlugInRegistration协议注册过滤器。 您可以跳过本节的其余部分。

注意:将自定义过滤器打包为图像单元可以促进模块化编程和代码可维护性。

如果由于某种原因,您不想将过滤器打包为一个图像单元(不推荐),则需要使用Listing 9-6中所示的CIFilter类的注册方法注册过滤器。 initialize方法调用registerFilterName:constructor:classAttributes:,您只应注册显示名称(kCIAttributeFilterDisplayName)和过滤器类别(kCIAttributeFilterCategories)。 所有其他的过滤器属性应该在customAttributes方法中指定。 (请参阅Write a Custom Attributes Method)。

过滤器名称是用于创建haze removal filter的字符串。 指定的构造函数对象实现了filterWithName:方法(请参阅Write a Method to Create Instances of the Filter)。 过滤器类属性被指定为一个NSDictionary对象。 显示名称 - 您在用户界面上显示的内容 - 此滤镜是Haze Remover

// Listing 9-6  Registering a filter that is not part of an image unit

+ (void)initialize
{
    [CIFilter registerFilterName: @"MyHazeRemover"
                     constructor: self
                 classAttributes:
     @{kCIAttributeFilterDisplayName : @"Haze Remover",
       kCIAttributeFilterCategories : @[
               kCICategoryColorAdjustment, kCICategoryVideo,
               kCICategoryStillImage, kCICategoryInterlaced,
               kCICategoryNonSquarePixels]}
     ];
}

8. Write a Method to Create Instances of the Filter - 编写一个方法来创建过滤器的实例

如果您打算只在您自己的应用程序中使用此过滤器,那么您将需要实现本节中所述的filterWithName:方法。 如果打算将此过滤器打包为供第三方开发人员使用的图像单元,则可以跳过本节,因为打包的过滤器可以使用CIFilter类提供的filterWithName:方法。

Listing 9-7中显示的filterWithName:方法在请求时创建过滤器的实例

// Listing 9-7  A method that creates instance of a filter

+ (CIFilter *)filterWithName: (NSString *)name
{
    CIFilter  *filter;
    filter = [[self alloc] init];
    return filter;
}

按照这些步骤创建过滤器之后,您可以在自己的应用程序中使用过滤器。 有关详细信息,请参阅Using Your Own Custom Filter,如果您想将过滤器或一组过滤器用作其他应用程序的插件,请参阅Packaging and Loading Image Units

后记

本篇已结束,后面更精彩~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容