Metal每日分享,LUT查找濾鏡效果

本案例的目的是理解如何用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.
  • 濾鏡部分大致分爲以下幾個模塊:
    • Blend:圖像融合技術
    • Blur:模糊效果
    • Pixel:圖像的基本像素顏色處理
    • Effect:效果處理
    • Lookup:查找表過濾器
    • Matrix: 矩陣卷積濾波器
    • Shape:圖像形狀大小相關
    • Visual: 視覺動態特效
    • MPS: 系統 MetalPerformanceShaders.

最後

  • 關於LUT查找濾鏡介紹與設計到此爲止吧。
  • 慢慢再補充其他相關濾鏡,喜歡就給我點個星🌟吧。
  • 濾鏡Demo地址,目前包含100+種濾鏡,同時也支持CoreImage混合使用。
  • 再附上一個開發加速庫KJCategoriesDemo地址
  • 再附上一個網絡基礎庫RxNetworksDemo地址
  • 喜歡的老闆們可以點個星🌟,謝謝各位老闆!!!

✌️.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章