在 app 内利用各种图形算法可以对图片进行一些变换,这样的效果也称为“滤镜”,滤镜效果大致可以分为以下几类:
- 独立像素点变换,包括亮度、对比、饱和度、色调、灰色化、分离RGB通道等
- 像素卷积变换,包括边缘检测、浮雕化、模糊、锐化
- 仿射矩阵变换。包括缩放、旋转、倾斜、扭曲、液化等
- 多图像合成
其中最简单的就是进行独立像素点变换,利用 LUT 技术还可以提供给设计师灵活的方式来自定义各种滤镜效果。
1.LUT 技术
1.1 LUT 技术简介
LUT 是 LookUpTable 的简称,也称作颜色查找表技术,它可以分为一维 LUT(1DLUT) 和 三维 LUT(3DLUT)。简单来说,LUT 就是一个 RGB 组合到 RGB 组合的映射,对于一维 LUT,假设映射关系为 LUT1,则
LUT(R1) = R2
LUT(G1) = G2
LUT(B1) = B2
其中 R1、G1、B1 为原像素值,R2、G2、B2 为映射像素值,可以看出 1DLUT 的映射颜色值的每一个分量仅与其原始像素值的分量有关,用图像表示如下
对于 3DLUT,假设其映射关系为 LUT3,则
LUT(R1, G1, B1) = (R2, G2, B2)
3DLUT 相比于 1DLUT 能够实现全立体的色彩空间控制,非常适合用于精确的颜色控制工作,它的示意图如下
可以简单做一个计算,如果 RGB 三个分量分别可以取 256 种值的话,那么 3DLUT 技术就可以包含 256X256X256 种情况,大约占 48MB 空间,这样一个 3DLUT 映射关系的数据量有些庞大,通常会采取采样方式来降低数据量,例如可以对每一个分量按照每 4 个变化值为间距,进行 64 次采样,获得一个 64X64X64 大小的映射关系表,对于不在表内的颜色值进行内插法获得其相似结果。
那么获得了 LUT 映射表以后,如何对任意一张图片进行滤镜变换呢。我们可以遍历图片的像素点,对于每一个像素点,获得其 RGB 组合,在 LUT 表格中查找此 RGB 组合及其对应的 RGB 映射值,然后用 RGB 映射值替换原图的像素点,就可以完成滤镜变换了。
1.2 3DLUT 数据存储方式
3DLUT 是一个三维颜色空间体,通过下面的方式可以将其数据压入一张二维图片中。这里以一张 64X64X64 数据量的 LUT 图为例,它的大小是 512X512
它在横竖方向上分成了 8X8 一共 64 个小方格,每一个小方格内的 B 分量为一个定值,总共就表示了 B 分量的 64 种可能值。同时对于每一个小方格,横竖方向又各自分为 64 个小格,横向小格的 R 分量依次增加,纵向小格的 G 分量依次增加,通过放大图片可以看到如下细节
这样就将所有数据都存储到一张 LUT 图中了,从图中也可以看出色值随着 RGB 分量变化而变化的情况。
上面所展示的 LUT 图是一张特殊的 LUT 图,因为它的映射关系最简单,原始 RGB 颜色是什么,映射 RGB 颜色就是什么,这样的 LUT 图我们可以将其作为 LUT 参照图,设计师将想实现的滤镜效果分别作用于 LUT 参照图上,可以生成 LUT 滤镜图,其可能情况如下图所示
通过对比 LUT 参照图和 LUT 滤镜图,就能获知任何原始 RGB 色值的映射颜色值是多少了。
2. LUT 滤镜变换过程实现
iOS 中与图像处理有关的框架大致有以下几个:CoreImage,Metal,OpenGL-ES,第三方框架 GPUImage 等,它们都可以实现 LUT 映射。下面分点阐述。
2.1 CoreImage
CoreImage 是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了 180 种)。CoreImage 实现 LUT 有两种方式:
- CIColorCube 过滤器
- CIKernel
2.1.1 CIColorCube 过滤器
CIColorCube 接受一个 LUT 映射颜色矩阵作为输入参数,对于输入图片进行色值映射,具体实现如下
- 获取 LUT 图的 bitmap
+ (unsigned char *)createRGBABitmapFromImage:(CGImageRef)image
{
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
unsigned char *bitmap;
NSInteger bitmapSize;
NSInteger bytesPerRow;
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
bytesPerRow = width * 4;
bitmapSize = bytesPerRow * height;
bitmap = malloc(bitmapSize);
if (bitmap == NULL) {
return NULL;
}
colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
free(bitmap);
return NULL;
}
context = CGBitmapContextCreate (bitmap,
width,
height,
8,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);
CGColorSpaceRelease(colorSpace);
if (context == NULL) {
free(bitmap);
}
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
CGContextRelease(context);
return bitmap;
}
- 生成 CIColorCube 所需的 inputCubeData
+ (CIFilter *)filterWithLUTImage:(UIImage *)image dimension:(NSInteger)n
{
NSInteger width = CGImageGetWidth(image.CGImage);
NSInteger height = CGImageGetHeight(image.CGImage);
NSInteger row = height/n;
NSInteger column = width/n;
if ((width % n != 0) || (height % n != 0) || (row * column != n)) {return nil;}
unsigned char *bitmap = [self createRGBABitmapFromImage:image.CGImage];
if (!bitmap) {return nil;}
NSInteger z = 0;
NSUInteger size = n * n * n * sizeof(float) * 4; // 所有像素点的 rgba 值个数 64 * 64 * 64 * 4
float *data = malloc(size); // 存储空间
NSInteger bitmapOffest = 0;
for (NSInteger rowIndex = 0; rowIndex < row; rowIndex++) {
for (NSInteger y = 0; y < n; y++) {
NSInteger originalZ = z;
for (NSInteger columnIndex = 0; columnIndex < column; columnIndex++) {
for (NSInteger x = 0; x < n; x++) {
double r = (unsigned int)bitmap[bitmapOffest];
double g = (unsigned int)bitmap[bitmapOffest + 1];
double b = (unsigned int)bitmap[bitmapOffest + 2];
double a = (unsigned int)bitmap[bitmapOffest + 3];
NSInteger dataOffset = (z * n * n + y * n + x) * 4; // 在大存储空间中的偏移,z 从 0 开始,z 偏移一个,总共偏移 64 * 64 个点,y 偏移一个,总共偏移 64 个点,加上 x 个点,乘以 rgba 的 4 个点
// 存储值
data[dataOffset] = r / 255.0;
data[dataOffset + 1] = g / 255.0;
data[dataOffset + 2] = b / 255.0;
data[dataOffset + 3] = a / 255.0;
bitmapOffest += 4; // 偏移 4 个点
}
z++;
}
z = originalZ; // 每一行遍历完,z 恢复到行头所属块的 index
}
z += column;
}
free(bitmap); // 释放位图
CIFilter *filter = [CIFilter filterWithName:@"CIColorCube"];
[filter setValue:[NSData dataWithBytesNoCopy:data length:size freeWhenDone:YES] forKey:@"inputCubeData"];
[filter setValue:[NSNumber numberWithInteger:n] forKey:@"inputCubeDimension"];
return filter;
}
这里读取的时候就是按照 3DLUT 存储方式来读取到一个存储空间里的。
- 封装并使用
将上述过程封装为一个 Category,传入原图,得到处理后的图片
CIImage *image = [[CIImage alloc] initWithImage:self.mediaAsset.assetImage];
CIFilter *filter = [CIFilter filterWithLUTImage:[UIImage imageNamed:@"lookup_yth002"] dimension:64];
[filter setValue:image forKey:@"inputImage"];
CIImage *outputImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:[outputImage extent]];
UIImage *result = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);