在咱們平時作圖像處理的過程當中,最長作的就是改變總體圖像的某個顏色。
咱們舉個例子,若是作一個將全部 RGB 中的 R 值改成原來的 0.5 倍,根據上一個 wiki 裏面所提到的,一張圖表繪製的過程是先頂點 vertex 再 fragment,而 fragment 是負責繪製每一個像素的顏色。ios
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,就可以實現咱們想要的效果。bash
return float4(color.r * 0.5 ,color.gba)app
從新運行咱們以前的 demo ,咱們的三角就有點綠了,說明咱們的效果實現了。機器學習
可是上面是理想狀況,通常圖片的處理會複雜的多。
假設咱們的圖片是 1280 * 720 像素,那麼就會進行 921600 的浮點運算,對每一個像素的 r 值乘以 0.5。
若是圖片小的話,對 GPU 的計算來講並無什麼壓力,可是當圖片更大而且數量更多的時候,就是會影響 GPU 計算的速度了。ide
look up table
顧名思義就是查找表,而 ColorLUT 就是顏色查找表。函數
因此引入了查詢表,把對應的變換完的像素存起來,用的時候只要進行一次查詢操做就能夠,這樣的操做會比以前的查表操做快的多,特別是在負載的顏色運算的狀況下。post
可是要把全部顏色的變換都存儲起來,假設是 RGB24 ,一個是 8 3 24位,RGB 每一個顏色都是 0-255,全部一共有 16777216 個顏色的變換,全存下來就是 256 256 256 24 / 8 / 1024/ 1024 = 48 mb,若是每一個濾鏡都是 48 mb 的話,那圖片處理軟件裏面那麼多濾鏡,app 的大小不得沒邊了?學習
因此爲了解決這一問題就有了 ColorLut 這樣的標準濾鏡圖片,默認的是以下的圖片,512*512 ,表明着全部顏色的變換,若不在圖片中的顏色就去對應的差值:ui
這是一張標準顏色的圖,rbg 都是原來的顏色,因此對這張圖片進行顏色的調整,而後獲得一張新的 lut 圖片,新的圖片加上修改後的 lut 圖片濾鏡就能夠查詢到對應的顏色該怎麼替換,從而的到新的圖片。編碼
下面咱們來解釋下上面的這張圖片和如何使用:
首先觀察一下這個圖片
上述的信息有沒有給你一點點啓示呢?
咱們在簡化一點
顏色是 r g b 三個值,都以歸一化的值表示( 1 表明 255 )。
因此獲得 0,0,1 的純藍色對應的位置就是 (7 64 , 7 64),右下角的那個方塊。
如今讓咱們經過個例子,來演示一遍查詢的過程。
假設咱們如今須要獲取的顏色是 (0.4,0.6,0.2) 都採用歸一化座標
在上一篇中,咱們提到 CommandBuffer 有三種 Encoder 。
以前的 demo 是簡單的對圖像進行繪製,用的是 MTLRenderCommandEncoder 的 Encoder。
此次咱們對圖片添加濾鏡,用到的是 MTLComputeCommandEncoder ,經過 GPU 的計算能力,來爲咱們實現查詢 lut,並混合顏色的操做。
簡而言之,相比以前的渲染操做,是輸入圖片的 texture 就能渲染出來了,濾鏡咱們須要作的是有個處理的方法,咱們給 GPU 輸入原始圖片 texture 和 lut 圖片的 texture , GPU 返回給咱們一個新的添加完濾鏡的圖片 texture,咱們把這個 texture 再給咱們以前的渲染的 Encoder,就會在三角中繪製一張咱們加過濾鏡以後的圖片了。
咱們延續以前的 demo,Device 和 CommandQueue ,CommandBuff,默認都已經有了咱們在以前的渲染的 Encoder 以前增長一個 Compute 的 Encoder。
每一個 Encoder 都須要一個 PipelineState 負責連接 Shader 的方法
這裏新建個 ComputePipelineState ,對應的 shader 方法稍後介紹。
id<MTLLibrary> library = [device newDefaultLibrary];
id<MTLFunction> function = [library newFunctionWithName:@"image_filiter"];
self.computeState = [device newComputePipelineStateWithFunction:function error:nil];複製代碼
配置資源,原始圖片和 lut 圖片。
下面是 UIImage 轉換爲 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);
}複製代碼
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,分爲對應爲 texture 的 0,1,2 輸入源。
把參數配置,做爲 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];複製代碼
threadgroups
在 Compute encoder 中,爲了提升計算的效率,每一個圖片都會分爲一個小的單元送到 GPU 進行並行處理,分多少組和每一個組的單元大小都是由 Encder 來配置的。
爲了儘量地發揮 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];複製代碼
//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];複製代碼
在執行上述步驟以後,咱們就獲得了一個添加完濾鏡以後的 destinationTexture,將該 texture 傳給以前的渲染流程,咱們就能夠得到一個帶濾鏡效果的三角形了!
對比下原圖
經過 Metal System Trace 根據 label 能夠明顯的看到,在咱們的 render 以前多了一個 Compute 的 encoder。
#總結
上面是利用 ComputeEncoder 來實現的圖像處理工做,其實經過 ComputeEncoder 能將一些複雜的數學計算轉移到 GPU 上執行,如機器學習須要的大量的矩陣運算等。
整體的流程仍是和以前的 Render 相同,惟一不一樣的多是多了 threadgroup 的配置,
##參考:
wiki - Colour_look-up_table
Metal Programming Guide
使用CIColorCube快速製做濾鏡