Vulkan移植GpuImage(一)高斯模糊與自適應閾值
阿新 • • 發佈:2021-03-12
## 自適應閾值效果圖 [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使之檢視不同