本章节是这个专题最后一篇文章,学完这里,你应该能非常熟练的掌握CoreImage的使用技巧了。
在上一章节中我们介绍了如何实现一些高级技巧,包括滤镜链的实现、转场效果、人脸检测等。
这一章节我们将介绍如何通过子类化一个CIFilter实例来封装自定义的滤镜效果。
你可以将一个滤镜的输出图像作为另一个滤镜的输入图像来创建各种自定义效果,你想链接多少个滤镜都可以。当你这样创建一种效果后,如果你想多次使用这种效果,就可以考虑子类化一个CIFilter来把这种效果封装成一个滤镜。
接下来我们将展示CoreImage如何通过子类化CIFilter来创建CIColorInvert滤镜,它还描述了将多个滤镜链起来的配方以获取这个有趣的效果。跟着我们的代码来进行操作,你应该能够将这个例子进行扩展,以创建自己的各种好玩的内建滤镜组合。
当你子类化一个CIFilter的时候你可以修改已存在的滤镜,通过代码设置它们的预设值或者将它们建成一个滤镜链。CoreImage也通过这样做来实现了一些内置滤镜。
要子类化一个滤镜你需要进行下面的操作:
- 用属性来声明滤镜的输入参数,并且这些属性的名字必须以input作为开头,比如inputImage。
- 如果有必要的话重写setDefaults方法。在iOS中,一个CIFilter被创建出来后,setDefaults方法会自动被调用,所以可以在里面设置一些默认值。
- 重写outputImage方法。
由CoreImage提供的CIColorInvert滤镜是由CIColorMatrix滤镜变异而来。就像它的名字描述的那样,CIColorInvert滤镜将一些向量提供给CIColorMatrix以让输入图像实现反色。跟着接下来的代码做并学习,你就可以封装自己的滤镜了。
在头文件中按照我们的实现步骤,需要提供一个用来接收输入参数的属性。
因为CoreImage已经自带了CIColorInvert类,所以我们这个例子中的类换了个名字。
@interface CIColorInvertFilter : CIFilter
@property (retain, nonatomic) CIImage *inputImage;
@end
在.m文件中,因为我们不需要设置默认值,所以没必要重写setDefaults方法。
@implementation CIColorInvertFilter
- (CIImage *) outputImage
{
CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix" keysAndValues:
kCIInputImageKey, _inputImage,
@"inputRVector", [CIVector vectorWithX: -1 Y:0 Z:0],
@"inputGVector", [CIVector vectorWithX:0 Y:-1 Z:0 ],
@"inputBVector", [CIVector vectorWithX: 0 Y:0 Z:-1],
@"inputBiasVector", [CIVector vectorWithX:1 Y:1 Z:1],
nil];
return filter.outputImage;
}
@end
我们再来看一个自定义滤镜的实现。色度键滤镜将从源图像中移除一种或者一个范围的颜色然后将源图像和一张背景图像进行混合。
创建步骤如下:
- 创建一个数据的立体映射用来映射你想要移除的颜色,所以它们是透明的。
- 使用CIColorCube滤镜以及立体映射来移除你想要移除的颜色。
- 使用UISourceOverCompositing滤镜来将处理后的图像和一张背景图片进行混合。
接下来我们将进行这些步骤的实现。
色立方是一个三维颜色查找表。CoreImage滤镜CIColorCube将颜色值作为输入,然后将查找表应用于这些值,比如在查找表中,绿色对应透明色,那么CIColorCube就会将图像中所有绿色改为透明色。
CIColorCube默认的查找表是一个单位矩阵 – 意味着它对于给定的数据不做任何处理。然而,我们这个配方需要将所有的绿色从图像中移除。(当然你可以移除你想要的任何颜色)
你将通过把绿色映射为透明色来移除所有的绿色。“绿色”实际上包括一个颜色范围,最简单粗暴的方法就是将图像中的颜色值从RGBA转换为HSV。在HSV中,色调代表了一个围绕圆柱中心轴的角度,在这样的表示下,你可以将颜色想象是一个饼状的东西,然后简单地移除掉这块饼中代表了色度键的颜色的那一块。
为了移除绿色,你需要定义一个最小和最大角度,纯绿色的值对应到了120°,最大值和最小值应该围绕这个值来设置。只要碰到绿色,你就把它的透明度设为0。
立体映射数据必须先预乘透明度,所以创建立体映射的最后一步就是用你计算的透明度值(如果是绿色的色调透明度值就为0,否则为1)乘以RGB值。下面的代码展现了如何创建该滤镜配方需要的的色立方。
在下面的例子中,我们设置了最小角度为90,最大角度为140来移除图像中的绿色部分。在过程中使用了RGB颜色空间转换到HSV颜色空间的函数,函数的实现在后面列出来了。
- (CIImage *)outputImage
{
CGFloat minHueAngle = 90.f;
CGFloat maxHueAngle = 140.f;
// Allocate memory
const unsigned int size = 64;
NSUInteger cubeDataSize = size * size * size * 4 * sizeof(float);
float *cubeData = (float *)malloc (cubeDataSize);
float rgb[3], hsv[3], *c = cubeData;
// Populate cube with a simple gradient going from 0 to 1
for (int z = 0; z < size; z++){
rgb[2] = ((double)z)/(size-1); // Blue value
for (int y = 0; y < size; y++){
rgb[1] = ((double)y)/(size-1); // Green value
for (int x = 0; x < size; x ++){
rgb[0] = ((double)x)/(size-1); // Red value
// Convert RGB to HSV
// You can find publicly available rgbToHSV functions on the Internet
RGBtoHSV(rgb[0],rgb[1],rgb[2] ,&hsv[0],&hsv[1],&hsv[2]);
// Use the hue value to determine which to make transparent
// The minimum and maximum hue angle depends on
// the color you want to remove
float alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle) ? 0.0f:1.0f;
// Calculate premultiplied alpha values for the cube
c[0] = rgb[0] * alpha;
c[1] = rgb[1] * alpha;
c[2] = rgb[2] * alpha;
c[3] = alpha;
c += 4; // advance our pointer into memory for the next color value
}
}
}
// Create memory with the cube data
NSData *data = [NSData dataWithBytesNoCopy:cubeData
length:cubeDataSize
freeWhenDone:YES];
CIFilter *colorCube = [CIFilter filterWithName:@"CIColorCube"];
[colorCube setValue:@(size) forKey:@"inputCubeDimension"];
// Set data for cube
[colorCube setValue:data forKey:@"inputCubeData"];
[colorCube setValue:_inputImage forKey:kCIInputImageKey];
CIImage *result = [colorCube valueForKey:kCIOutputImageKey];
return result;
}
RGB转HSV的函数:
static void RGBtoHSV( float r, float g, float b, float *h, float *s, float *v )
{
float min, max, delta;
min = MIN( r, MIN( g, b ));
max = MAX( r, MAX( g, b ));
*v = max; // v
delta = max - min;
if( max != 0 )
*s = delta / max; // s
else {
// r = g = b = 0 // s = 0, v is undefined
*s = 0;
*h = -1;
return;
}
if( r == max )
*h = ( g - b ) / delta; // between yellow & magenta
else if( g == max )
*h = 2 + ( b - r ) / delta; // between cyan & yellow
else
*h = 4 + ( r - g ) / delta; // between magenta & cyan
*h *= 60; // degrees
if( *h < 0 )
*h += 360;
}
更多的自定义滤镜效果请参考我的这篇博客。
CoreImage提供了很多可选项来创建图像、上下文和渲染内容。你如何完成一项任务取决于:
- 你的app需要执行一项任务的频率
- 你的app是使用静态图像还是视频图像
- 你是否需要支持实时的处理和分析
- 颜色的保真度对你的用户是否重要
你应该通过最佳性能实践以确保你的app能够高效的运行。
按照下面的方式来获取最佳性能实践:
- 不要每次渲染都创建一个CIContext对象。上下文中保存了大量的状态信息,重用它们会更加高效。
- 评估一下你的app是否需要颜色管理。如果你不需要就不要使用它。
- 当你使用GPU的上下文来渲染CIImage对象时尽量避免有CoreAnimation动画在进行。如果你希望它们同时进行,那么就使用CPU来渲染。
- 保证图像不要超过CPU和GPU的限制。在CoreImage中图像尺寸限制对于CIContext对象来说在CPU下合GPU下是不同的。在iOS中可以使用inputImageMaximumSize和outputImageMaximumSize来获得限制的大小。
- 尽可能使用小图像。性能是由输出像素的数量来度量的。你可以让CoreImage渲染到一个小的视图、纹理、或者帧缓冲区。允许CoreAnimation提升一级来显示尺寸。使用CoreGraphics或ImageI/O框架的函数来进行剪裁或降质,比如CGImageCreateWithImageInRect或者CGImageSourceCreateThumbnailAtIndex函数。
- UIImageView这个类最好用来显示静态图像。如果你的App要获得最佳性能,请使用更底层的API。
- 避免在CPU和GPU之间进行没必要的纹理转换。
- 在将内容缩放因子应用到源图像之前,将其渲染到一个和源图像大小相同的矩形中。
- 考虑使用能达到近似于算法滤镜效果的简单滤镜。比如CIColorCube能产生与CISepiaTone近似的输出,并且更高效。
- 利用iOS6.0以后的对YUV图像的支持。摄像头的像素缓冲本身就是YUV格式的而大多数图像处理算法则期望的是RGBA数据,它们之间的相互转换将造成一定成本。CoreImage支持从CVPixelBuffer对象中读取YUV数据然后再应用合适的颜色变换。
NSDictionary * options = @{ (id)kCVPixelBufferPixelFormatTypeKey :
@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) };
默认情况下,CoreImage中所有滤镜都适用于线性光照颜色空间(light-linear color space),这就提供了最精准和一致的结果。转换到sRGB和从sRGB转换将会增加滤镜的复杂度,并且需要CoreImage应用下面的方程:
rgb = mix(rgb.0.0774, pow(rgb*0.9479 + 0.05213, 2.4), step(0.04045, rgb))
rgb = mix(rgb12.92, pow(rgb*0.4167) * 1.055 - 0.055, step(0.00313, rgb))
请考虑在以下情况禁止颜色管理:
- 你的app需要绝对的最高性能。
- 夸张操作(exaggerated manipulations)后用户并不能注意到质量的不同。
要禁止颜色管理,将kCIImageColorSpace这个键的值设为null。如果你使用的是EAGL上下文,你还需要在创建EAGL上下文的时候把上下文颜色空间设为null。
这是我们这个专题的终章。我们用了两个具体的例子来讲述了如何通过子类化CIFilter来实现自定义滤镜效果并封装。我专门写了一篇另一篇博客来讲述如何实现素描滤镜效果,在这篇博客。
到此为止我们走完了整个CoreImage框架,希望大家在自己的App开发中能熟练使用强大的CoreImage,整个专题的demo我整理到了这个git仓库中更多技巧(比如如何与OpenGL交互、使用EAGL上下文进行渲染)请参考苹果官方文档。