1. 程式人生 > >Vulkan移植GpuImage(一)高斯模糊與自適應閾值

Vulkan移植GpuImage(一)高斯模糊與自適應閾值

## 自適應閾值效果圖 [demo](https://github.com/xxxzhou/aoce/tree/master/samples/vulkanextratest) ![avatar](https://pic2.zhimg.com/80/v2-ea3ec0303ba7c9d4f82ab4db0a3afe11_720w.jpg) 這幾天抽空看了下GpuImage的filter,移植了高斯模糊與自適應閾值的vulkan compute shader實現,一個是基本的影象處理,一個是組合基礎影象處理聚合,算是比較有代表性的二種. ## [高斯模糊實現與優化](https://github.com/xxxzhou/aoce/tree/master/code/aoce_vulkan_extra) 大部分模糊效果主要是卷積核的實現,相應值根據公式得到. ```c++ int ksize = paramet.blurRadius * 2 + 1; if (paramet.sigma <= 0) { paramet.sigma = ((ksize - 1) * 0.5 - 1) * 0.3 + 0.8; } double scale = 1.0f / (paramet.sigma * paramet.sigma * 2.0); double cons = scale / M_PI; double sum = 0.0; std::vector karray(ksize * ksize); for (int i = 0; i < ksize; i++) { for (int j = 0; j < ksize; j++) { int x = i - (ksize - 1) / 2; int y = j - (ksize - 1) / 2; karray[i * ksize + j] = cons * exp(-scale * (x * x + y * y)); sum += karray[i * ksize + j]; } } sum = 1.0 / sum; for (int i = ksize * ksize - 1; i >= 0; i--) { karray[i] *= sum; } ``` 其中對應compute shader程式碼. ```glsl #version 450 layout (local_size_x = 16, local_size_y = 16) in;// gl_WorkGroupSize layout (binding = 0, rgba8) uniform readonly image2D inTex; layout (binding = 1, rgba8) uniform image2D outTex; layout (binding = 2) uniform UBO { int xksize; int yksize; int xanchor; int yanchor; } ubo; layout (binding = 3) buffer inBuffer{ float kernel[]; }; void main(){ ivec2 uv = ivec2(gl_GlobalInvocationID.xy); ivec2 size = imageSize(outTex); if(uv.x >
= size.x || uv.y >= size.y){ return; } vec4 sum = vec4(0); int kInd = 0; for(int i = 0; i< ubo.yksize; ++i){ for(int j= 0; j< ubo.xksize; ++j){ int x = uv.x-ubo.xanchor+j; int y = uv.y-ubo.yanchor+i; // REPLICATE border x = max(0,min(x,size.x-1)); y = max(0,min(y,size.y-1)); vec4 rgba = imageLoad(inTex,ivec2(x,y)) * kernel[kInd++]; sum = sum + rgba; } } imageStore(outTex, uv, sum); } ``` 這樣一個簡單的高斯模糊就實現了,結果就是我在用Redmi 10X Pro在攝像頭1080P下使用21的核長是不到一楨的處理速度. 高斯模糊的優化都有現成的講解與實現,其一就是[影象處理中的卷積核分離](https://zhuanlan.zhihu.com/p/81683945),一個m行乘以n列的高斯卷積可以分解成一個1行乘以n列的行卷積,計算複雜度從原來的O(k^2)降為O(k),其二就是用shared區域性視訊記憶體減少訪問紋理視訊記憶體的操作,注意這塊容量非常有限,如果不合理分配,能並行的組就少了.考慮到Android平臺,使用packUnorm4x8/unpackUnorm4x8優化區域性視訊記憶體佔用. 其核分成一列與一行,具體相應實現請看VkSeparableLinearLayer類的實現,由二個compute shader組合執行. ```c++ int ksize = paramet.blurRadius * 2 + 1; std::vector karray(ksize); double sum = 0.0; double scale = 1.0f / (paramet.sigma * paramet.sigma * 2.0); for (int i = 0; i < ksize; i++) { int x = i - (ksize - 1) / 2; karray[i] = exp(-scale * (x * x)); sum += karray[i]; } sum = 1.0 / sum; for (int i = 0; i < ksize; i++) { karray[i] *= sum; } rowLayer->updateBuffer(karray); updateBuffer(karray); ``` 其glsl主要邏輯實現來自opencv裡opencv_cudafilters模組裡cuda程式碼改寫,在這隻貼filterRow的實現,filterColumn的實現和filterRow類似,有興趣的朋友可以自己翻看. ```glsl #version 450 layout (local_size_x = 16, local_size_y = 16) in;// gl_WorkGroupSize layout (binding = 0, rgba8) uniform readonly image2D inTex; layout (binding = 1, rgba8) uniform image2D outTex; layout (binding = 2) uniform UBO { int xksize; int anchor; } ubo; layout (binding = 3) buffer inBuffer{ float kernel[]; }; const int PATCH_PER_BLOCK = 4; const int HALO_SIZE = 1; // 共享塊,擴充左邊右邊HALO_SIZE(分為左邊HALO_SIZE,中間自身*PATCH_PER_BLOCK,右邊HALO_SIZE) shared uint row_shared[16][16*(PATCH_PER_BLOCK+HALO_SIZE*2)];//vec4[local_size_y][local_size_x] // 假定1920*1080,gl_WorkGroupSize(16,16),gl_NumWorkGroups(120/4,68),每一個執行緒寬度要管理4個 // 核心的最大寬度由HALO_SIZE*gl_WorkGroupSize.x決定 void main(){ ivec2 size = imageSize(outTex); uint y = gl_GlobalInvocationID.y; if(y >= size.y){ return; } // 紋理正常範圍的全域性起點 uint xStart = gl_WorkGroupID.x * (gl_WorkGroupSize.x*PATCH_PER_BLOCK) + gl_LocalInvocationID.x; // 每個執行緒組填充HALO_SIZE*gl_WorkGroupSize個數據 // 填充每個左邊HALO_SIZE,需要注意每行左邊是沒有紋理資料的 if(gl_WorkGroupID.x > 0){//填充非最左邊塊的左邊 for(int j=0;j
self.luminance --> self.boxBlur --> self.adaptiveThreshold --> output self.luminance --> self.adaptiveThreshold } } } ``` 可以看到實現不復雜,根據輸入圖片得到亮度,然後boxBlur,然後把亮度圖與blur後的亮度圖交給adaptiveThreshold處理就完成了,原理很簡單,但是要求層可以內部加入別的處理層以及多輸入,當初設計時使用Graph計算圖時就考慮過多輸入多輸出的問題,這個是支援的,內部層加入別的處理層,這是圖層組合能力,這個我當初設計是給外部使用者用的,在這稍微改動一下,也是比較容易支援內部層類組合. ```c++ void VkAdaptiveThresholdLayer::onInitGraph() { VkLayer::onInitGraph(); // 輸入輸出 inFormats[0].imageType = ImageType::r8; inFormats[1].imageType = ImageType::r8; outFormats[0].imageType = ImageType::r8; // 這幾個節點新增在本節點之前 pipeGraph->
addNode(luminance.get())->addNode(boxBlur->getLayer()); // 更新下預設UBO資訊 memcpy(constBufCpu.data(), ¶met.offset, conBufSize); } void VkAdaptiveThresholdLayer::onInitNode() { luminance->getNode()->addLine(getNode(), 0, 0); boxBlur->getNode()->addLine(getNode(), 0, 1); getNode()->setStartNode(luminance->getNode()); } ``` 和別的處理層一樣,不同的是新增這個層時,根據onInitNode設定Graph如何自動連線前後層. 相應的luminance/adaptiveThreshold以及專門顯示只有一個通道層的影象處理大家有興趣自己翻看,比較簡單就不貼了. 有興趣的可以在[samples/vulkanextratest](https://github.com/xxxzhou/aoce/blob/master/samples/vulkanextratest)裡,PC平臺修改Win32.cpp,Android平臺修改Android.cpp檢視不同效果.後續有時間完善android下的UI使之檢視不同