Metal 系列教程(2)- Metal 實現(xiàn) LUT 濾鏡

Demo 地址已更新

https://github.com/Danny1451/MetalLutFilter

簡單濾鏡

在我們平時做圖像處理的過程中诵棵,最長做的就是改變整體圖像的某個顏色。
我們舉個例子昵宇,如果做一個將所有 RGB 中的 R 值改為原來的 0.5 倍造虏,根據(jù)上一個 wiki 里面所提到的御吞,一張圖表繪制的過程是先頂點 vertex 再 fragment,而 fragment 是負責繪制每個像素的顏色漓藕。

fragment float4 myFragmentShader(
                                VertexOut vertexIn [[stage_in]],
                            texture2d<float,access::sample>   inputImage   [[ texture(0) ]],
                                 sampler textureSampler [[sampler(0)]]
                             )
{
    float4 color = inputImage.sample(textureSampler, vertexIn.texCoords);
    return color;

}

所以就在這個 shader 里面將返回的 color 的 r 值乘上 0.5陶珠,就能夠?qū)崿F(xiàn)我們想要的效果。

return float4(color.r * 0.5 ,color.gba)

重新運行我們之前的 demo 享钞,我們的三角就有點綠了揍诽,說明我們的效果實現(xiàn)了。

ColorLUT

但是上面是理想情況栗竖,一般圖片的處理會復(fù)雜的多暑脆。
假設(shè)我們的圖片是 1280 * 720 像素,那么就會進行 921600 的浮點運算狐肢,對每個像素的 r 值乘以 0.5添吗。
如果圖片小的話,對 GPU 的計算來說并沒有什么壓力份名,但是當圖片更大并且數(shù)量更多的時候碟联,就是會影響 GPU 計算的速度了。

look up table
顧名思義就是查找表僵腺,而 ColorLUT 就是顏色查找表鲤孵。

所以引入了查詢表,把對應(yīng)的變換完的像素存起來辰如,用的時候只要進行一次查詢操作就可以普监,這樣的操作會比之前的查表操作快的多,特別是在負載的顏色運算的情況下琉兜。

但是要把所有顏色的變換都存儲起來凯正,假設(shè)是 RGB24 ,一個是 8 * 3 24位呕童,RGB 每個顏色都是 0-255漆际,所有一共有 16777216 個顏色的變換淆珊,全存下來就是 256* 256* 256* 24 / 8 / 1024/ 1024 = 48 mb夺饲,如果每個濾鏡都是 48 mb 的話,那圖片處理軟件里面那么多濾鏡,app 的大小不得沒邊了往声?

所以為了解決這一問題就有了 ColorLut 這樣的標準濾鏡圖片擂找,默認的是如下的圖片,512*512 浩销,代表著所有顏色的變換贯涎,若不在圖片中的顏色就去對應(yīng)的差值:

這是一張標準顏色的圖,rbg 都是原來的顏色慢洋,所以對這張圖片進行顏色的調(diào)整塘雳,然后得到一張新的 lut 圖片,新的圖片加上修改后的 lut 圖片濾鏡就可以查詢到對應(yīng)的顏色該怎么替換普筹,從而的到新的圖片败明。

下面我們來解釋下上面的這張圖片和如何使用:
首先觀察一下這個圖片

  • 8*8 的方塊組成
  • 整體上看每個方塊左上角從左上往右下由黑變藍
  • 單獨每個個方塊的右上角是紅色為主
  • 單獨每個個方塊的左下角是綠色為主

上述的信息有沒有給你一點點啟示呢?
我們在簡化一點
顏色是 r g b 三個值太防,都以歸一化的值表示( 1 代表 255 )妻顶。

  • 整體對每個小方塊而言,從左上往右下 b 從 0 到 1 蜒车,是 z 字型的順序
  • 單獨對每個小方塊而言讳嘱,從左到右 r 從 0 到 1,代表 x
  • 單獨對每個小方塊而言酿愧,從上到下 g 從 0 到 1沥潭,代表 y

所以得到 0,0,1 的純藍色對應(yīng)的位置就是 (7 * 64 , 7 * 64),右下角的那個方塊嬉挡。

現(xiàn)在讓我們通過個例子叛氨,來演示一遍查詢的過程。

假設(shè)我們現(xiàn)在需要獲取的顏色是 (0.4棘伴,0.6寞埠,0.2) 都采用歸一化坐標

  • 首先我們確定用哪個方塊 b = 0.2 * 63 = 12.6 即 (4,1)那個方塊
  • r = 0.4 * 63 = 25.6焊夸,g = 0.6 * 63 = 37.8 轉(zhuǎn)換到大坐標(4 * 64 + 25.6, 1*64 + 37.8)
  • 前三步得到的都是浮點數(shù)仁连,但是我們?yōu)V鏡的圖像的像素都是固定的,不存在小數(shù)
  • 對于 r,g 最后將的到的坐標再轉(zhuǎn)換為歸一化坐標阱穗,( (4 * 64 + 25.6)/512, (1*64 + 37.8)/512),通過取樣器 sampler 插值取出精確顏色值
  • 對于 b 我們可以通過對下一個方塊 (5,1)再進行取色饭冬,再把兩個顏色混合得到最后的顏色

Metal 圖像處理

在上一篇中,我們提到 CommandBuffer 有三種 Encoder 揪阶。

  • MTLRenderCommandEncoder 渲染 3D 編碼器
  • MTLComputeCommandEncoder 計算編碼器
  • MTLBlitCommandEncoder 位圖復(fù)制編碼器 拷貝 buffer texture 同時也能生成 mipmap

之前的 demo 是簡單的對圖像進行繪制昌抠,用的是 MTLRenderCommandEncoder 的 Encoder。
這次我們對圖片添加濾鏡鲁僚,用到的是 MTLComputeCommandEncoder 炊苫,通過 GPU 的計算能力裁厅,來為我們實現(xiàn)查詢 lut,并混合顏色的操作侨艾。

簡而言之执虹,相比之前的渲染操作,是輸入圖片的 texture 就能渲染出來了唠梨,濾鏡我們需要做的是有個處理的方法袋励,我們給 GPU 輸入原始圖片 texture 和 lut 圖片的 texture , GPU 返回給我們一個新的添加完濾鏡的圖片 texture当叭,我們把這個 texture 再給我們之前的渲染的 Encoder茬故,就會在三角中繪制一張我們加過濾鏡之后的圖片了。

我們延續(xù)之前的 demo蚁鳖,Device 和 CommandQueue 均牢,CommandBuff,默認都已經(jīng)有了我們在之前的渲染的 Encoder 之前增加一個 Compute 的 Encoder才睹。

  1. 每個 Encoder 都需要一個 PipelineState 負責鏈接 Shader 的方法
    這里新建個 ComputePipelineState 徘跪,對應(yīng)的 shader 方法稍后介紹。

     id<MTLLibrary> library = [device newDefaultLibrary];
    id<MTLFunction> function = [library newFunctionWithName:@"image_filiter"];
    
    self.computeState = [device newComputePipelineStateWithFunction:function error:nil];
    
    
  2. 配置資源琅攘,原始圖片和 lut 圖片垮庐。

    下面是 UIImage 轉(zhuǎn)換為 Texture 的一種方法,通過 CGContext 繪制坞琴。

    - (void)setLutImage:(UIImage *)lutImage{
    _lutImage = lutImage;
    
    CGImageRef imageRef = [_lutImage CGImage];
    
    // Create a suitable bitmap context for extracting the bits of the image
    NSUInteger width = CGImageGetWidth(imageRef);
    NSUInteger height = CGImageGetHeight(imageRef);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    uint8_t *rawData = (uint8_t *)calloc(height * width * 4, sizeof(uint8_t));
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * width;
    NSUInteger bitsPerComponent = 8;
    CGContextRef bitmapContext = CGBitmapContextCreate(rawData, width, height,
                                                       bitsPerComponent, bytesPerRow, colorSpace,
                                                       kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    
  
    CGContextDrawImage(bitmapContext, CGRectMake(0, 0, width, height), imageRef);
    CGContextRelease(bitmapContext);
    
    MTLRegion region = MTLRegionMake2D(0, 0, width, height);
    [self.lutTexture replaceRegion:region mipmapLevel:0 withBytes:rawData bytesPerRow:bytesPerRow];
    
    free(rawData);
    }
  1. 配置可配參數(shù)哨查,如濾鏡的混合度,返回等等剧辐。
    這里我新建了一個 struct 寒亥,代表了添加濾鏡的返回和強度。通過 bytes 可以把相應(yīng)的配置傳到 shader 中去荧关。
typedef struct
{

    UInt32 clipOriginX;
    UInt32 clipOriginY;
    UInt32 clipSizeX;
    UInt32 clipSizeY;
    Float32 saturation;
    bool changeColor;
    bool changeCoord;

    }ImageSaturationParameters;

  1. 配置 Encoder

    將上述的組件都組裝起來溉奕,sourceTexture 為輸入的圖片 texture ,destinationTexture 為將要寫入的圖片 texture忍啤,
    self.lutTexture 為輸入的濾鏡圖片 texture加勤,分為對應(yīng)為 texture 的 0,1同波,2 輸入源鳄梅。
    把參數(shù)配置,作為 bytes 傳入 shader 中未檩。

    ImageSaturationParameters params;
    params.clipOriginX = floor(self.filiterRect.origin.x);
    params.clipOriginY = floor(self.filiterRect.origin.y);
    params.clipSizeX = floor(self.filiterRect.size.width);
    params.clipSizeY = floor(self.filiterRect.size.height);

    params.saturation = self.saturation;
    params.changeColor = self.needColorTrans;
    params.changeCoord = self.needCoordTrans;
    
    
    id<MTLComputeCommandEncoder> encoder = [commandBuffer computeCommandEncoder];
    [encoder pushDebugGroup:@"filter"];
    [encoder setLabel:@"filiter encoder"];
   
    [encoder setComputePipelineState:self.computeState];
    [encoder setTexture:sourceTexture atIndex:0];
    [encoder setTexture:destinationTexture atIndex:1];
    
    if (self.lutTexture == nil) {
        NSLog(@"lut == nil");
        [encoder setTexture:sourceTexture atIndex:2];
    }else{
        [encoder setTexture:self.lutTexture atIndex:2];
    }
    
    [encoder setSamplerState:self.samplerState atIndex:0];

    [encoder setBytes:&params length:sizeof(params) atIndex:0];

    ```
    
5. threadgroups 
    在 Compute encoder 中戴尸,為了提高計算的效率,每個圖片都會分為一個小的單元送到 GPU 進行并行處理冤狡,分多少組和每個組的單元大小都是由 Encder 來配置的孙蒙。
    
    ![](http://upload-images.jianshu.io/upload_images/838133-b6971554cc1422cd?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    為了盡可能地發(fā)揮 GPU 計算最大的效率项棠,可以通過如下方式來配置:
    
NSUInteger wid = self.computeState.threadExecutionWidth;
NSUInteger hei = self.computeState.maxTotalThreadsPerThreadgroup / wid;

MTLSize threadsPerGrid = {(sourceTexture.width + wid - 1) / wid,(sourceTexture.height + hei - 1) / hei,1};
MTLSize threadsPerGroup = {wid, hei, 1};


[encoder dispatchThreadgroups:threadsPerGrid
threadsPerThreadgroup:threadsPerGroup];

```
  1. Shader
    這里也就是核心的計算邏輯,和之前渲染不同的是马篮,它既不是 vertex ,也不是 fragment怜奖,而是新的 kernel 修飾的浑测,具體的如下,其實就是上面的解釋 lut 的代碼版本歪玲,如果你能理解上面的 lut 坐標的定位的迁央,那么下面的相關(guān)代碼也不存在問題。
    同時下面代碼還增加了一個是否是需要添加濾鏡的范圍的判斷滥崩,可以看到取樣器是可以復(fù)用的岖圈,不同 texture 都可以使用同一個取樣器。
    可以看到 image_filiter 函數(shù)有 6 個輸入值钙皮,從上網(wǎng)上分別為配置參數(shù)蜂科,原圖 texture,寫入的目標 texture短条,濾鏡的 texture导匣,采樣器,執(zhí)行時的位置(這個參數(shù)返回的是在之前配置的 threadgroup 中計算出來的茸时,位于整個圖像中的位置贡定,不是歸一化的值,直接取樣即可獲取對應(yīng)位置的顏色)
//check the point in pos
bool checkPointInRect(uint2 point,uint2 origin, uint2 rect){
    return point.x >= origin.x &&
    point.y >= origin.y &&
    point.x <= (origin.x + rect.x) &&
    point.y <= (origin.y + rect.y);
}
kernel void image_filiter(constant ImageSaturationParams *params [[buffer(0)]],
                          texture2d<half, access::sample> sourceTexture [[texture(0)]],
                          texture2d<half, access::write> targetTexture [[texture(1)]],
                          texture2d<half, access::sample> lutTexture [[texture(2)]],
                          sampler samp [[sampler(0)]],
                          uint2 gridPos [[thread_position_in_grid]]){

    
    float2 sourceCoord = float2(gridPos);
    half4 color = sourceTexture.sample(samp,sourceCoord);
    
    
    float blueColor = color.b * 63.0;
    
    int2 quad1;
    quad1.y = floor(floor(blueColor) / 8.0);
    quad1.x = floor(blueColor) - (quad1.y * 8.0);
    
    int2 quad2;

    quad2.y = floor(ceil(blueColor) / 8.0);
    quad2.x = ceil(blueColor) - (quad2.y * 8.0);
    
    half2 texPos1;
    texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
    texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
    
    half2 texPos2;
    texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
    texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
    
    
    half4 newColor1 = lutTexture.sample(samp,float2(texPos1.x * 512 ,texPos2.y * 512));
    half4 newColor2 = lutTexture.sample(samp,float2(texPos2.x * 512,texPos2.y * 512 ));
  
    half4 newColor = mix(newColor1, newColor2, half(fract(blueColor)));
    
    
    half4 finalColor = mix(color, half4(newColor.rgb, color.w), half(params->saturation));
    
 
    uint2 destCoords = gridPos + params->clipOrigin;
    
    
    uint2 transformCoords = destCoords;
    
    //transform coords for y
    if (params->changeCoord){
        transformCoords = uint2(destCoords.x, sourceTexture.get_height() - destCoords.y);
    }
    //transform color for r&b
    half4 realColor = finalColor;
    if (params->changeColor){
        realColor = half4(finalColor.bgra);
    }
    
    if(checkPointInRect(transformCoords,params->clipOrigin,params->clipSize))
    {
        targetTexture.write(realColor, transformCoords);
        
    }else{
        
        targetTexture.write(color,transformCoords);
    }
}

7.計算
在上述步驟都配置完成之后可都,就可以 encode 了缓待。

    [encoder endEncoding];

在執(zhí)行上述步驟之后,我們就得到了一個添加完濾鏡之后的 destinationTexture渠牲,將該 texture 傳給之前的渲染流程旋炒,我們就可以獲得一個帶濾鏡效果的三角形了!

對比下原圖

通過 Metal System Trace 根據(jù) label 可以明顯的看到签杈,在我們的 render 之前多了一個 Compute 的 encoder国葬。

總結(jié)

上面是利用 ComputeEncoder 來實現(xiàn)的圖像處理工作,其實通過 ComputeEncoder 能將一些復(fù)雜的數(shù)學計算轉(zhuǎn)移到 GPU 上執(zhí)行芹壕,如機器學習需要的大量的矩陣運算等汇四。
總體的流程還是和之前的 Render 相同,唯一不同的可能是多了 threadgroup 的配置踢涌,

參考:

wiki - Colour_look-up_table
Metal Programming Guide
使用CIColorCube快速製作濾鏡

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末通孽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子睁壁,更是在濱河造成了極大的恐慌背苦,老刑警劉巖互捌,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異行剂,居然都是意外死亡秕噪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門厚宰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腌巾,“玉大人,你說我怎么就攤上這事铲觉〕候” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵撵幽,是天一觀的道長灯荧。 經(jīng)常有香客問我,道長盐杂,這世上最難降的妖魔是什么逗载? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮链烈,結(jié)果婚禮上撕贞,老公的妹妹穿的比我還像新娘。我一直安慰自己测垛,他們只是感情好捏膨,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著食侮,像睡著了一般号涯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锯七,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天链快,我揣著相機與錄音,去河邊找鬼眉尸。 笑死域蜗,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的噪猾。 我是一名探鬼主播霉祸,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼袱蜡!你這毒婦竟也來了丝蹭?” 一聲冷哼從身側(cè)響起陌僵,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤淑际,失蹤者是張志新(化名)和其女友劉穎蒙兰,沒想到半個月后炊昆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡贱田,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年缅茉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片男摧。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蔬墩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出彩倚,到底是詐尸還是另有隱情筹我,我是刑警寧澤扶平,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布帆离,位于F島的核電站,受9級特大地震影響结澄,放射性物質(zhì)發(fā)生泄漏哥谷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一麻献、第九天 我趴在偏房一處隱蔽的房頂上張望们妥。 院中可真熱鬧,春花似錦勉吻、人聲如沸监婶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惑惶。三九已至,卻和暖如春短纵,著一層夾襖步出監(jiān)牢的瞬間带污,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工香到, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鱼冀,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓悠就,卻偏偏與公主長得像千绪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子梗脾,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容