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才睹。
-
每個 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];
-
配置資源琅攘,原始圖片和 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);
}
- 配置可配參數(shù)哨查,如濾鏡的混合度,返回等等剧辐。
這里我新建了一個 struct 寒亥,代表了添加濾鏡的返回和強度。通過 bytes 可以把相應(yīng)的配置傳到 shader 中去荧关。
typedef struct
{
UInt32 clipOriginX;
UInt32 clipOriginY;
UInt32 clipSizeX;
UInt32 clipSizeY;
Float32 saturation;
bool changeColor;
bool changeCoord;
}ImageSaturationParameters;
-
配置 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:¶ms 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];
```
- 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快速製作濾鏡