「这是我参加2022初次更文挑战的第5天,活动详情检查:2022初次更文挑战」

本案例的意图是理解如何用Metal实现LUT色彩查找表滤镜,经过将色彩值存储在一张表中,在需求的时分经过索引在这张表上找到对应的色彩值,将原有色值替换成查找表中的色值;

总结便是一种针对色彩空间的办理和转化技能,LUT 便是一个 RGB 组合到另一个 RGB 组合的映射关系表;


Demo

  • HarbethDemo地址
  • iDay每日共享文档地址

实操代码

// LUT查找滤镜
let filter = C7LookupTable.init(image: R.image("lut_abao"))
// 计划1:
let dest = BoxxIO.init(element: originImage, filter: filter)
ImageView.image = try? dest.output()
dest.filters.forEach {
  NSLog("%@", "\($0.parameterDescription)")
}
// 计划2:
ImageView.image = try? originImage.make(filter: filter)
// 计划3:
ImageView.image = originImage ->> filter

实现原理

  • 过滤器

这款滤镜采用并行核算编码器规划.compute(kernel: "C7LookupTable"),参数因子[intensity]

对外开放参数

  • intensity: 强度,其实便是调整mix混合平均值。
/// LUT映射滤镜
public struct C7LookupTable: C7FilterProtocol {
    public let lookupImage: C7Image?
    public let lookupTexture: MTLTexture?
    public var intensity: Float = 1.0
    public var modifier: Modifier {
        return .compute(kernel: "C7LookupTable")
    }
    public var factors: [Float] {
        return [intensity]
    }
    public var otherInputTextures: C7InputTextures {
        return lookupTexture == nil ? [] : [lookupTexture!]
    }
    public init(image: C7Image?) {
        self.lookupImage = image
        self.lookupTexture = image?.cgImage?.mt.newTexture()
    }
    public init(name: String) {
        self.init(image: R.image(name))
    }
}
  • 着色器

1、用蓝色值核算正方形的方位,得到quad1和quad2;
2、依据赤色值和绿色值核算对应方位在整个纹理的坐标,得到texPos1和texPos2;
3、依据texPos1和texPos2读取映射结果newColor1和newColor2,再用蓝色值的小数部分进行mix操作;

kernel void C7LookupTable(texture2d<half, access::write> outputTexture [[texture(0)]],
                          texture2d<half, access::read> inputTexture [[texture(1)]],
                          texture2d<half, access::sample> lookupTexture [[texture(2)]],
                          constant float *intensity [[buffer(0)]],
                          uint2 grid [[thread_position_in_grid]]) {
    const half4 inColor = inputTexture.read(grid);
    const half blueColor = inColor.b * 63.0h; // 蓝色部分[0, 63] 共64种
    // 经过蓝色核算两个方格quad1,quad2
    half2 quad1;
    quad1.y = floor(floor(blueColor) / 8.0h);
    quad1.x = floor(blueColor) - (quad1.y * 8.0h);
    half2 quad2;
    quad2.y = floor(ceil(blueColor) / 8.0h);
    quad2.x = ceil(blueColor) - (quad2.y * 8.0h);
    const float A = 0.125;
    const float B = 0.5 / 512.0;
    const float C = 0.125 - 1.0 / 512.0;
    float2 texPos1; // 核算色彩(r,b,g)在第一个正方形中对应方位
    texPos1.x = A * quad1.x + B + C * inColor.r;
    texPos1.y = A * quad1.y + B + C * inColor.g;
    float2 texPos2;
    texPos2.x = A * quad2.x + B + C * inColor.r;
    texPos2.y = A * quad2.y + B + C * inColor.g;
    constexpr sampler quadSampler(mag_filter::linear, min_filter::linear);
    const half4 newColor1 = lookupTexture.sample(quadSampler, texPos1);
    const half4 newColor2 = lookupTexture.sample(quadSampler, texPos2);
    const half4 newColor = mix(newColor1, newColor2, fract(blueColor));
    const half4 outColor = half4(mix(inColor, half4(newColor.rgb, inColor.a), half(*intensity)));
    outputTexture.write(outColor, grid);
}

1、经过蓝色核算两个方格quad1,quad2

half2 quad1;
quad1.y = floor(floor(blueColor) / 8.0h);
quad1.x = floor(blueColor) - (quad1.y * 8.0h);
half2 quad2;
quad2.y = floor(ceil(blueColor) / 8.0h);
quad2.x = ceil(blueColor) - (quad2.y * 8.0h);
--------------
比方 inColor(0.4, 0.6, 0.2), 先确认第一个方格:
inColor.b = 0.2,blueColor = 0.2 * 63 = 12.6
即为第12个,第13个方格,可是咱们要核算它坐内行和列,
floor(12.6) = 12, floor(12 / 8.0h) = 1,即第一行;
floor(blueColor) - (quad1.y * 8.0h) = floor(12.6) - (1 * 8) = 4,即第4列;
同理能够算出第二个方格为第1行,第5//ceil 向下取整,ceil(12.6) = 13, 
处理跨行时核算问题,比方blueColor = 7.6,则取第78个方格,他们不在同一行

2、核算映射后色彩地点两个方格的方位的归一化纹理坐标

const float A = 0.125;
const float B = 0.5 / 512.0;
const float C = 0.125 - 1.0 / 512.0;
float2 texPos1; // 核算色彩(r,b,g)在第一个正方形中对应方位
texPos1.x = A * quad1.x + B + C * inColor.r;
texPos1.y = A * quad1.y + B + C * inColor.g;
float2 texPos2;
texPos2.x = A * quad2.x + B + C * inColor.r;
texPos2.y = A * quad2.y + B + C * inColor.g;
--------------
(quad1.x * 0.125)表明行归一化的坐标,
(quad1.y * 0.125)表明列归一化的坐标,总共8行,每一行的长度为1/8 = 0.125,总共8列,每一列的长度为1/8 = 0.125;
(inColor.r * 0.125)表明一个方格里赤色的方位,因为一个方格长度为0.125,r从0~1;绿色同理;
需求留心的是这里有个0.5/512 和 1.0/512;
0.5/512 是为了取点的中间值,一个点长度为1,总长度512,取点的中间值,即为0.5/512;
1.0/512 是因为核算texPos2.x时,单独关于一个方格来说,是从0~63,所以为63/512,即0.125 - 1.0 / 512;

3、核算映射后色彩

// 运用GPU采样器对纹理采样,取出LUT基准图上关于的 R G 色值
constexpr sampler quadSampler(mag_filter::linear, min_filter::linear);
const half4 newColor1 = lookupTexture.sample(quadSampler, texPos1);
const half4 newColor2 = lookupTexture.sample(quadSampler, texPos2);

4、混合色彩

// 线性取一个平均值,mix 办法依据 b 重量进行两个像素值的混合
const half4 newColor = mix(newColor1, newColor2, fract(blueColor));
// mix(x, y, a); 取x,y的线性混合,x(1-a)+ya
const half4 outColor = half4(mix(inColor, half4(newColor.rgb, inColor.a), half(*intensity))); 

LUT图介绍

LUT图是一张512512巨细的图片,分为64个88的小区域,每个小区域对应一个B值(0 ~ 255,间隔4),小区域内的每个像素点对应一组R和G值(0 ~ 255,间隔为4)。

运用时,获取原图某个像素点的值,经过色彩查找,替换为对应的滤镜色彩值。

Metal每日分享,LUT查找滤镜效果

从图能够看出:

  • 8×8的方块组成
  • 全体上看每个方块左上角从左上往右下由黑变蓝
  • 单独每个方块的右上角是赤色为主
  • 单独每个方块的左下角是绿色为主

这是一个64x64x64颗粒度的LUT规划,总的方格巨细为512×512,8×8=64个方格,所以每个方格巨细为64×64;

64个方格,每个方格巨细为64×64,所以叫做64x64x64颗粒度的规划。因为色彩值的规模为0~255,即256个取值,将256个取值归化到64;

从左上到右下(能够想作z方向),越来越蓝,蓝色值B从0~255,代表用来查找的B,即LUT(R1,G1,B1) = (R2,G2,B2)中的B1;
每一个方格里,从左往右(x方向),赤色值R从0~255,代表用来查找的R,即LUT(R1,G1,B1) = (R2,G2,B2)中的R1;
每一个方格里,从上往下(y方向),绿色值G从0~255,代表用来查找的G,即LUT(R1,G1,B1) = (R2,G2,B2)中的G1;

因为一个色彩重量是0~255,所以一个方格表明的蓝色规模为4,比方最左上的方格蓝色为0~4, 查找时,如果有某个像素的蓝色值在0~4之间,则一定是在第一个方格里查找其映射后的色彩;

Example:

  • 查找像素点归一化后的纯蓝色(0,0,1)的映射后的色彩;
  • 运用蓝色B定位方格数
n = 1(B值) * 63(总共64个方格,从第0个算起) = 63

Answer: 定位的方格n是第63个

  • 定位在方格里的方位,运用R,G定位方位x,y
x = 0(R值) * 63(每个方格巨细为 64 * 64) = 0
y = 0(G值) * 63(每个方格巨细为 64 * 64) = 0

Answer: 方格的(0,0)方位为要定位的x,y

  • 定位在整个图中方位
Py = floor(n/8) * 64 + y = 7 * 64 + 0 = 448;
Px = [n - floor(n/8)*8] * 64 + x = [63-7*8] * 64 + 0 = 448;
P1 = (448, 448)
其中floor(n/8)代表方位所内行,每一行的长度为64,y为方格里的G定位的方位;
[n - floor(n/8) * 8]代表方位地点列数,每一列的长度为64,x为方格里的R定位的方位;
floor为向下取整(处理跨行时核算问题),ceil为向上取整。比方2.3, floor(2.3) = 2; ceil(2.3) = 3;

Answer: 方格巨细为512×512,方位为P = (448, 448), 归一化后为(7/8, 7/8)
So: 色彩值(0, 0, 1)的方位确实在第63个方格的左上角;

查找办法

LUT分为1D和3D,本质的差异在于索引的输出所需求的索引数

用公式方法看看差异,先设置Ri、Gi、Bi为输入值,Ro、Go、Bo为输出值,LUT规范的转化办法为FuncLUT;

  • 1D LUT公式
    Ro = FuncLUT(Ri)
    Go = FuncLUT(Gi)
    Bo = FuncLUT(Bi)

从公式能够看出,各个数值之间独立

  • 3D LUT公式
    Ro = FuncLUT(Ri, Gi, Bi)
    Go = FuncLUT(Ri, Gi, Bi)
    Bo = FuncLUT(Ri, Gi, Bi)

在3D LUT中,数值之间会互相影响

从公式对比中咱们能够看出来,如果在色深为10位的体系中,1D LUT的数据量大概是3×2^10bit,3D LUT便是(3×2^10)^3bit

由此能够看出3D LUT的数据量比1D LUT多了一个指数级,所以3D LUT的精度比1D LUT高了许多,因为3D LUT的数据量太大,所以是经过罗列节点的办法进行数据存储;

参考文章:www.jianshu.com/p/f054464e1…

补白: 在相机捕获时实时烘托每一帧图片的时分,就会有显著的功能差别,尤其是 iPhone 8 Plus 相机捕获的每一帧巨细几乎都是最终几种状况那么大(4032×3024)

Harbeth功能清单

  • 支撑ios体系和macOS体系
  • 支撑运算符函数式操作
  • 支撑多种模式数据源 UIImage, CIImage, CGImage, CMSampleBuffer, CVPixelBuffer.
  • 支撑快速规划滤镜
  • 支撑合并多种滤镜效果
  • 支撑输出源的快速扩展
  • 支撑相机采集特效
  • 支撑视频添加滤镜特效
  • 支撑矩阵卷积
  • 支撑运用体系 MetalPerformanceShaders.
  • 支撑兼容 CoreImage.
  • 滤镜部分大致分为以下几个模块:
    • Blend:图画融合技能
    • Blur:含糊效果
    • Pixel:图画的基本像素色彩处理
    • Effect:效果处理
    • Lookup:查找表过滤器
    • Matrix: 矩阵卷积滤波器
    • Shape:图画形状巨细相关
    • Visual: 视觉动态特效
    • MPS: 体系 MetalPerformanceShaders.

最终

  • 关于LUT查找滤镜介绍与规划到此为止吧。
  • 慢慢再补充其他相关滤镜,喜爱就给我点个星吧。
  • 滤镜Demo地址,现在包含100+种滤镜,一起也支撑CoreImage混合运用。
  • 再附上一个开发加快库KJCategoriesDemo地址
  • 再附上一个网络基础库RxNetworksDemo地址
  • 喜爱的老板们能够点个星,谢谢各位老板!!!

✌️.