本案例的目的是理解如何用Metal實現LUT顏色查找表濾鏡,通過將顏色值存儲在一張表中,在需要的時候通過索引在這張表上找到對應的顏色值,將原有色值替換成查找表中的色值;
總結就是一種針對色彩空間的管理和轉換技術,LUT 就是一個 RGB 組合到另一個 RGB 組合的映射關係表;
Demo
效果圖
實操代碼
// 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,則取第7,8個方格,他們不在同一行
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圖是一張512×512大小的圖片,分爲64個8×8的小區域,每個小區域對應一個B值(0 ~ 255,間隔4),小區域內的每個像素點對應一組R和G值(0 ~ 255,間隔爲4)。
使用時,獲取原圖某個像素點的值,通過顏色查找,替換爲對應的濾鏡顏色值。
從圖可以看出:
- 8x8的方塊組成
- 整體上看每個方塊左上角從左上往右下由黑變藍
- 單獨每個方塊的右上角是紅色爲主
- 單獨每個方塊的左下角是綠色爲主
這是一個64x64x64顆粒度的LUT設計,總的方格大小爲512x512,8x8=64個方格,所以每個方格大小爲64x64;
64個方格,每個方格大小爲64x64,所以叫做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: 方格大小爲512x512,位置爲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的數據量大概是3x2^10bit,3D LUT就是(3x210)3bit
由此可以看出3D LUT的數據量比1D LUT多了一個指數級,所以3D LUT的精度比1D LUT高了很多,因爲3D LUT的數據量太大,所以是通過列舉節點的方式進行數據存儲;
參考文章:https://www.jianshu.com/p/f054464e1b40
備註: 在相機捕獲時實時渲染每一幀圖片的時候,就會有顯著的性能差別,尤其是 iPhone 8 Plus 相機捕獲的每一幀大小几乎都是最後幾種情況那麼大(4032x3024)
Harbeth功能清單
- 支持ios系統和macOS系統
- 支持運算符函數式操作
- 支持多種模式數據源 UIImage, CIImage, CGImage, CMSampleBuffer, CVPixelBuffer.
- 支持快速設計濾鏡
- 支持合併多種濾鏡效果
- 支持輸出源的快速擴展
- 支持相機採集特效
- 支持視頻添加濾鏡特效
- 支持矩陣卷積
- 支持使用系統 MetalPerformanceShaders.
- 支持兼容 CoreImage.
- 濾鏡部分大致分爲以下幾個模塊:
最後
- 關於LUT查找濾鏡介紹與設計到此爲止吧。
- 慢慢再補充其他相關濾鏡,喜歡就給我點個星🌟吧。
-
濾鏡Demo地址,目前包含
100+
種濾鏡,同時也支持CoreImage混合使用。 - 再附上一個開發加速庫KJCategoriesDemo地址
- 再附上一個網絡基礎庫RxNetworksDemo地址
- 喜歡的老闆們可以點個星🌟,謝謝各位老闆!!!
✌️.