Core Image【2】—— 自定义 Filter

栏目: 编程工具 · 发布时间: 6年前

内容简介:Core Image 系列,目前的文章如下:PS:如果想了解 Core Image 相关,建议按序阅读,前后有依赖。

Core Image 系列,目前的文章如下:

  • Core Image【1】—— 概述
  • Core Image【2】—— 自定义 Filter
  • Core Image【3】—— 2017 新特性
  • Core Image【4】—— 2018 新特性

PS:

如果想了解 Core Image 相关,建议按序阅读,前后有依赖。

对应源码,见最末链接。

前言

本文主要讲解 Core Image 自定义滤镜部分的内容,包括如何使用自定义 Filter,如何编写 kernel,QC 工具介绍,注意点以及一些开发技巧。

在这之前,我默认你了解 Core Image 的基本原理以及使用方式。如果没有,我建议你花点时间看看我的上一篇文章 Core Image【1】—— 概述,它介绍 Core Image 相关基础概念、使用方式、注意点以及和其他图像处理方案的对比,想必会有所收获。

现在,开始吧~

自定义 Filter 流程

自定义的 Filter 和系统内置的各种 CIFilter,使用起来方式是一样的。我们唯一要做的,就是实现一个符合规范的 CIFilter 的子类,然后该怎么用怎么用。

这里总结起来就3步:

  • 编写 CIKernel:使用 CIKL,自定义滤镜效果。
  • 加载 CIKernel:CIFilter 读取编写好的 CIKernel。
  • 设置参数:设置 CIKernel 需要的输入参数以及 DOD 和 ROI。

不难看出,这些操作都是围绕 CIKernel 展开的,那么,它是什么? CIKL,DOD,ROI 又是什么鬼? Core Image【2】—— 自定义 Filter

先撇开这些麻烦的东西,我们先这样简单的认为:

  • CIKernel 是我们 Filter 对应的脚本,它描述 Filter 的具体工作原理。
  • CIKL (Core Image Kernel Language)是编写 CIKernel 的语言。
  • DOD,ROI 当做普通的参数处理。

弄清了这些,我们再来看具体操作过程。

拿一个图片翻转效果举例,效果如下:

Core Image【2】—— 自定义 Filter

1. 编写 CIKernel

File —> New —> File —> Empty, 创建一个名为 MirrorX.cikernel 的文件。

编辑 .cikernel 文件,比如:

kernel vec2 mirrorX ( float imageWidth ) 
{
      // 获取待处理点的位置
      vec2 currentVec = destCoord();
    // 返回最终显示位置
      return vec2 ( imageWidth - currentVec.x , currentVec.y ); 
}

PS:这个 kernel 如果有不懂的,可以先跳过。下文会重点说明。

2. 加载 CIKernel

File —> New —> File —> Cocoa Touch Clas,新建一个继承自 CIFilter 的类,比如 MirrorXFilter

MirrorXFilter.m 中,添加如下代码:

static CIKernel *customKernel = nil;

- (instancetype)init {

    self = [super init];
    if (self) {
        if (customKernel == nil)
        {
            NSBundle *bundle = [NSBundle bundleForClass: [self class]];
            NSURL *kernelURL = [bundle URLForResource:@"MirrorX" withExtension:@"cikernel"];

            NSError *error;
            NSString *kernelCode = [NSString stringWithContentsOfURL:kernelURL
                                                            encoding:NSUTF8StringEncoding error:&error];
            if (kernelCode == nil) {
                NSLog(@"Error loading kernel code string in %@\n%@",
                      NSStringFromSelector(_cmd),
                      [error localizedDescription]);
                abort();
            }

            NSArray *kernels = [CIKernel kernelsWithString:kernelCode];
            customKernel = [kernels objectAtIndex:0];
        }
    }
    return self;
}

这段代码很简单,重写 init 方法,主要就是读取 .cikernel 文件中代表 CIKernel 的字符串(当然, CIKernel 也可以直接写在 NSString 里头,免去文件读取这步),然后使用 kernelsWithString

方法获取到真正的 CIKernel 对象。

+ (nullable NSArray<CIKernel *> *)kernelsWithString:(NSString *)string  NS_AVAILABLE(10_4, 8_0);

至此,CIKernel 加载完毕。

3. 设置参数

MirrorXFilter.m 中,添加需要的成员变量。

@interface MirrorXFilter () {
    CIImage  *inputImage;
}

这里只需要一个成员变量, inputImage 表示我们的输入图片。

之后,就是设置参数,传入 kernel 中。

// 使用
- (CIImage *)outputImage
{
    CGFloat inputWidth = inputImage.extent.size.width;
    CIImage *result = [customKernel applyWithExtent: inputImage.extent roiCallback: ^( int index, CGRect rect ) {
        return rect;
    } inputImage: inputImage arguments: @[@(inputWidth)]];
    return result;
}

这里只需要重写 outputImage 方法即可。

extent用于返回 CIImage 对象对应的 bounds,通过它可以拿到图片的宽度。

/* Return a rect the defines the bounds of non-(0,0,0,0) pixels */
@property (NS_NONATOMIC_IOSONLY, readonly) CGRect extent;

然后通过 applyWithExtent 来设置对应的参数。

- (nullable CIImage *)applyWithExtent:(CGRect)extent
                          roiCallback:(CIKernelROICallback)callback
                           inputImage:(CIImage*)image
                            arguments:(nullable NSArray<id> *)args;

这里有4个参数。

  • extent,也就是之前提到的 DOD,暂且略过。
  • callback,也就是之前提到的 ROI,暂且略过。
  • image,缺省的 inputImage,传入我们的成员变量 inputImage 即可。
  • args,输入参数数组,与 CIKernel 中定义的一一对应。这里只有一个 inputWidth。

PS:这里可能有同学会有疑惑,为什么 inputImage 可以缺省,inputWidth 就需要传入呢。这里暂且不纠结,下面会详细说明~

如此,一个自定义 Filter 就完成了。简单吧~

Core Image【2】—— 自定义 Filter

4. 使用

至于使用上,则和普通的 CIFilter 基本一致。

#import "MirrorXFilter.h"

// 1. 将UIImage转换成CIImage
CIImage *ciImage = [[CIImage alloc] initWithImage:self.imageView.image];

// 2. 创建滤镜
self.filter = [[MirrorXFilter alloc] init];
// 设置相关参数
[self.filter setValue:ciImage forKey:@"inputImage"];

// 3. 渲染并输出CIImage
CIImage *outputImage = [self.filter outputImage];

// 4. 获取绘制上下文
self.context = [CIContext contextWithOptions:nil];

// 5. 创建输出CGImage
CGImageRef cgImage = [self.context createCGImage:outputImage fromRect:[outputImage extent]];
UIImage *image = [UIImage imageWithCGImage:cgImage];
// 6. 释放CGImage
CGImageRelease(cgImage);

如此,我们便可得到翻转后的图片。

5. 更多

当然,如果你是一个完美主义者,我觉得你还还可以做更多~

- (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]
           },
   };
}

可以为自定义的 Filter 添加对应的参数描述,以及默认值,范围限制等。

这不是必须的,但却是可取的。至于如何设置,可以参考 CIFilter 对应的 attributes 属性,或者参照上面这个例子。

另外,iOS 9之后,引入了 registerFilterName , 你可以通过重写 + (CIFilter *)filterWithName: (NSString *)name; ,然后外部使用的时候,跟 CIFilter 一模一样。

/** Publishes a new filter called 'name'.

 The constructor object 'anObject' should implement the filterWithName: method.
 That method will be invoked with the name of the filter to create.
 The class attributes must have a kCIAttributeFilterCategories key associated with a set of categories.
 @param   attributes    Dictionary of the registration attributes of the filter. See below for attribute keys.
*/
+ (void)registerFilterName:(NSString *)name
               constructor:(id<CIFilterConstructor>)anObject
           classAttributes:(NSDictionary<NSString *,id> *)attributes NS_AVAILABLE(10_4, 9_0);

不过需要 iOS 9以上才支持,另外一般用于打包成 Image Units 给他人使用。

正常情况下应该是用不到。如果真有这个需求,可以参考这篇文章: Packaging and Loading Image Units

至此,自定义 Filter 的流程就算走完了,我们很容易就可以配置好需要的环境。

然而,真正的自定义部分,才刚刚开始!

Core Image【2】—— 自定义 Filter

DOD & ROI

1. DOD

DOD ( domain of definition ) ,简单来说就是 Filter 处理后,输入的图片区域。

一般来说,Filter 操作都是基于原图,添加上效果,但是并不会改变图片的大小,显示区域。所以一般与原图的一致即可。

CGRect dod = inputImage.extent;

但是针对形变类的 Filter,则需要根据输出图片大小,设置正确的 DOD。

2. ROI

ROI ( region of interest ),在一定的时间内特别感兴趣的区域,即当前处理区域。

可以简单的理解为:当前处理区域对应于原图中的哪个区域。

ROI 的定义如下:

/* Block callback used by Core Image to ask what rectangles of a kernel's input images
 * are needed to produce a desired rectangle of the kernel's output image.
 *
 * 'index' is the 0-based index specifying which of the kernel's input images is being queried.
 * 'destRect' is the extent rectangle of kernel's output image being queried.
 *
 * Returns the rectangle of the index'th input image that is needed to produce destRect.
 * Returning CGRectNull indicates that the index'th input image is not needed to produce destRect.
 * The returned rectangle need not be contained by the extent of the index'th input image.
 */
typedef CGRect (^CIKernelROICallback)(int index, CGRect destRect);

CIKernelROICallback 在 Core Image 内部进行处理的时候,会多次调用。

index表示输入图片的下标,顺序和 kernel 中的入参顺序一致,从0开始。

destRect表示输出图片的区域。 也就是我们先前设置的 DOD。

那,我们为什么要显示设置 ROI 呢 ?

因为输入图片中,参与处理的实际区域,Core Image 是无法知道的,我们需要显式的告诉 CI 这个区域。

这么讲可能有点难以理解,下面我们看两个具体的例子。

先看一个旋转的例子。

Core Image【2】—— 自定义 Filter

这里就是进行了 x,y 互换操作。很容易得到我们的 DOD:

CGRect dod = CGRectMake(inputImage.extent.origin.y, inputImage.extent.origin.x, inputImage.extent.size.height, inputImage.extent.size.width);

// e.g.
// 原图片extent (0, 0, 200, 300)
// 旋转后的输出图片 (0, 0, 300, 200),也就是 DOD

那 ROI 应该怎么设置呢 ?我们之前说过,ROI 计算就是计算当前处理区域对应于原图中的哪个区域。

也就是一个逆向过程。

假如,A:输入图片中的某点 B:输出图片中的某点。那么 ROI 计算可以理解成 ROI(B)= A。

理解好这点,我们不难写出这个操作对应的 ROI:

CIKernelROICallback callback = ^(int index, CGRect rect) {
    return CGRectMake(rect.origin.y, rect.origin.x, rect.size.height, rect.size.width);
};

另外,当输入图片不止一个的时候,则需要根据 index 来做区别。因为这里的 rect 每次都是返回 DOD ,而不是当前图片的 extent。

CIKernel 介绍

终于到了本文最重要的部分了,CIKernel 介绍!

Core Image【2】—— 自定义 Filter

在此之前,我们先了解下它的一些背景知识。

CIKernel 需要使用 Core Image Kernel Language (CIKL) 来编写,CIKL 是 OpenGL Shading Language (GLSL) 的子集,如果你之前有过 OpenGL 着色器编写的经验,这部分你会感觉格外亲切。CIKL 集成了 GLSL 绝大部分的参数类型和内置函数,另外它还添加了一些适应 Core Image 的参数类似和函数。

一个 kernel 的处理过程,可以用下面伪代码表示:

for i in 1 ... image.width
    for j in 1 ... image.height
        New_Image[i][j] = CustomKernel(Current_Image[i][j])
    end
end

也就是说,每个需要处理的 fragment 都会调用一次 kernel 相关操作,每次操作的目的就是返回当前 fragment 对应的结果 fragment,这里 fragment 可以理解为像素点。

所以我们的 kernel,应该是针对一个点,而不是一张图片。

Core Image 内置了3种适用于不同场景的 Kernel,可以根据实际需求来选择。

  • CIColorKernel:用于处理色值变化的 Filter。
  • CIWarpKernel:用于处理形变的 Filter。
  • CIKernel:通用。

CIColorKernel,CIWarpKernel 是官方推荐使用的。某个 Filter,在使用它们能实现的情况下,应该使用它们,即使是一个 CIKernel 拆分成多个 CIColorKernel 以及 CIWarpKernel,也应该用这种方式。因为 Core Image 内部对这两张 Kernel 做了优化。

当然,它们的使用时有限制的。目的一定要很纯粹,比如 CIColorKernel 只能处理色值上的变化。否则就算定义为 CIColorKernel,如果实现上涉及了其他 CIColorKernel 不允许的操作,Core Image 也会当做普通的 CIFilter 处理。

另外,kernel 的入参只支持下面这么几种:

Kernel routine input parameter Object
sampler CISampler
__table sampler CISampler
__color CIColor
float NSNumber
vec2, vec3, or vec4 CIVector

简单说明一下:

  • sampler:可以理解成纹理,或者图片。外部以 CIImage 形式传入。
  • __table sampler:表示颜色查找表(lookup table),虽然它也是图片,但是添加该声明可以避免被修改。外部以 CIImage 形式传入。
  • __color:表示颜色。外部以 CIColor 形式传入。
  • float:kernel 内部处理都是 float 类型。外部以 NSNumber 形式传入。
  • vecN:表示一个多元向量。比如 vec2 可以表示一个点,vec4 可以表示一个色值。外部以 CIVector 形式传入。

至于 kernel 中可以使用的函数,那就太多了。这里不一一枚举,在下面的具体讲解中,会说明几个常用的。如果想了解更多,可以参考 Core Image Kernel Language Reference ,以及 OpenGL ES Shading Language Reference

下面我会通过一个 Demo,讲解这三种 Kernel 的具体用法。

1. CIColorKernel

首先看下官方的定义:

/*
 * CIColorKernel is an object that encapsulates a Core Image Kernel Language
 * routine that processes only the color information in images.
 *
 * Color kernels functions are declared akin to this example:
 *   kernel vec4 myColorKernel (__sample fore, __sample back, vec4 params)
 *
 * The function must take a __sample argument for each input image.
 * Additional arguments can be of type float, vec2, vec3, vec4, or __color.
 * The destination pixel location is obtained by calling destCoord().
 * The kernel should not call sample(), sampleCoord(), or samplerTransform().
 * The function must return a vec4 pixel color.
 */
NS_CLASS_AVAILABLE(10_11, 8_0)
@interface CIColorKernel : CIKernel

很重要的一点: processes only the color information in images ,它只处理图片的颜色信息。

所以在使用它之前,一定要确保该 Filter 只涉及颜色处理。

CIKL 的语法和大多数 C 阵营一样,变量,运算符,控制结构,函数等都大同小异,所以它的学习成本是很低的。

真正的核心应该是: 如果用这样的语言来实现这个滤镜,也就是我们经常说的算法。

下面我们以一个 Vignette 来实际讲解一下。

它的效果如下所示:

Core Image【2】—— 自定义 Filter

2016101796011vignette_demo.gif

不难看出,Vignette 滤镜,它实际上就是一个FOV(Field of View) 的效果,即视野中央看的最清楚,清晰程度与到中心距离呈反比,与人类的视觉是类似的。

Core Image【2】—— 自定义 Filter

所以针对图片上的每个像素点 A,经过 Vignette 滤镜处理后得到的 B,应该满足:

Vignette(A)= A * Darken = B; 而 Darken 的计算依赖 A 与中心点的距离。

如此,我们可以很容易的写出对应的 kernel:

kernel vec4 vignetteKernel(__sample image, vec2 center, float radius, float alpha)
{
    // 计算出当前点与中心的距离
    float distance = distance(destCoord(), center) ;
    // 根据距离计算出暗淡程度
    float darken = 1.0 - (distance / radius * alpha);
    // 返回该像素点最终的色值
    image.rgb *= darken;

    return image.rgba;
}

和 C 语言的一样,函数需要具备:

  • 返回类型:vec4
  • 函数名:vignetteKernel
  • 参数列表:__sample image, vec2 center, float radius, float alpha)
  • 函数体:{}中的具体实现

有所不同的,kernel 函数需要带上 kernel 关键字,与其它普通函数做区分。一个 .cikernel 文件中,允许包括多个函数,甚至是多个 kernel 函数,不过 函数调用要出现在函数定义之后

另外,这里有个特别的参数类型, __sample ,和之前讲的 sampler 有所不同。因为这里我们使用的是 CIColorKernel ,在得到高效性能的同时,也有一定的局限性。因为只是处理图片当前位置的颜色信息,所以 __sample 提供的 rgba 变量足够了,无法获取一些其它的信息。

比如在 CIKernel 中,可以通过 sample() 等函数获取其它位置的色值,而在 CIColorKernel 中,无法使用 sample(), sampleCoord() 以及 samplerTransform() 。

下面逐行解释这个 kernel。

// 计算出当前点与中心的距离
float distance = distance(destCoord(), center) ;

destCoord

  • varying vec2 destCoord ()

    返回当前正在处理的像素点所处坐标。(working space coordinates)

这里使用的 CIKL 内置的函数 destCoord,它返回的坐标是基于 working space 的。所谓 working space,即工作空间,它的取值范围对应图片实际大小。比如 inputImage 的大小为 300 * 200,那么 destCoord() 返回坐标的取值范围在 (0, 0) - (300, 200)。

distance

  • float distance (vec2 p0, vec2 p1)

    计算向量p0,p1之间的距离

如此便能很容易得到当前点与中心的距离。

// 根据距离计算出暗淡程度
float darken = 1.0 - (distance / radius * alpha);

之后根据清晰程度与到中心距离呈反比这一原理,结合外部控制的 alpha 变量,计算出暗淡程度。

// 返回该像素点最终的色值
image.rgb *= darken;
return image.rgba;

这里之前提到, __sample 有个 rgba 变量,通过它可以获取到当前处理点的色值。

在 CIKL 中,vec4 的任何一个分量都可以单独获取,也可以组合获取,例如 image.aimage.rrgg 等,都是可行的。

CIColorKernel 是针对色值的处理,所以它的返回值必须是一个代表色值的 vec4 类型变量。

至此,这个 vignetteKernel 就分析完毕了。很简单吧~

2. CIWarpKernel

同样,先看下文档定义:

/*
 * CIWarpKernel is an object that encapsulates a Core Image Kernel Language
 * function that processes only the geometry of an image.
 *
 * Warp kernels functions are declared akin to this example:
 *   kernel vec2 myWarpKernel (vec4 params)
 *
 * Additional arguments can be of type float, vec2, vec3, vec4.
 * The destination pixel location is obtained by calling destCoord().
 * The kernel should not call sample(), sampleCoord(), or samplerTransform().
 * The function must return a vec2 source location.
 */
NS_CLASS_AVAILABLE(10_11, 8_0)
@interface CIWarpKernel : CIKernel

同样,它也有很重要一点: processes only the geometry of an image 。它只处理图片的几何形状。

所谓的改变几何形状,也就是形变,把原本放置在 A 处的点,用 B 处的点去填充,或者反过来,把原本 B 处的点,挪到 A 处去,也是一样的。

它可以用这个表达式表示: Warp(A)= B;

所以它和之前的 CIColorKernel 不同,它的返回值是 vec2,代表点的坐标。另外它只允许传入一张图片,所以这里的 inputImage 缺省了。

同样的,在 CIWarpKernel 中,无法使用 sample(), sampleCoord() 以及 samplerTransform() 。

下面以一个马赛克,像素化(Pixellate)的例子来讲解。它的效果如下:

Core Image【2】—— 自定义 Filter

2016101762677pixellate_demo.gif

马赛克,比较简单的一种算法是按照固定的间隔取像素点,将图片分割成一些小块,然后每个小块内选择一个像素点,然后把这个区域全部用这个像素点填充即可。这里的每个小块,称作晶格,晶格越大,马赛克效果越好。

依照这个简单算法,我们可以很容易的写出对应的 kernel:

kernel vec2 pixellateKernel(float radius)
{
    vec2 positionOfDestPixel, centerPoint;
    // 获取当前点坐标
    positionOfDestPixel = destCoord();
    // 获取对应晶格内的中心像素点
    centerPoint.x = positionOfDestPixel.x - mod(positionOfDestPixel.x, radius * 2.0) + radius;
    centerPoint.y = positionOfDestPixel.y - mod(positionOfDestPixel.y, radius * 2.0) + radius;

    return centerPoint;
}

同样的,先是获取到当前处理点的坐标,positionOfDestPixel。

// 获取对应晶格内的中心像素点
centerPoint.x = positionOfDestPixel.x - mod(positionOfDestPixel.x, radius * 2.0) + radius;
centerPoint.y = positionOfDestPixel.y - mod(positionOfDestPixel.y, radius * 2.0) + radius;

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数学规划

数学规划

黄红选 / 清华大学出版社 / 2006-3 / 45.00元

《数学规划》以数学规划为对象,从理论、算法和计算等方面介绍,分析和求解常见的最优化问题的一些方法,全书共分8章,其中第l章介绍了数学规划的实例、模型以及在分析最优化问题时所涉及的基础知识,第2章至第8章分别讨论了凸分析、线性规划、无约束优化、约束优化、多目标规划、组合优化和整数规划以及全局优化等七个方面的内容,此外,书中每章的最后一节给出了一些习题,书末列出了参考文献和索引。《数学规划》可作为应用......一起来看看 《数学规划》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器