在Android用vulkan完成藍綠幕扣像

天天不在發表於2021-02-07

效果圖(1080P處理)

在Android用vulkan完成藍綠幕扣像

因為攝像頭開啟自動曝光,畫面變動時,亮度變化導致扣像在轉動時如上。

原始碼地址vulkan_extratest

這個demo主要測試二點,一是測試ndk camera整合效果,二是本專案對接外部實現的vulkan層是否方便,用於以後移植GPUImage裡的實現。

我簡化了在android下vulkan與opengles紋理互通裡的處理,沒有vulkan視窗與交換鏈這些邏輯,只用到vulkan compute shader計算管線得到結果然後交換給opengl裡的紋理。

NDK Camera整合

主要參考 NdkCamera Sample的實現,然後封裝成滿足Aoce定義裝置介面。

說下遇到的坑。

  1. AIMAGE_FORMAT_YUV_420_888 可能是YUV420P,也可能是NV12,需要在AImageReader_ImageListener裡拿到image通過AImage_getPlanePixelStride裡的UV的plan是否為1來判斷是否為YUV420P,或者看data[u]-data[y]=1來看是否為NV12.具體可以看getVideoFrame的實現。

  2. AImageReader_new裡的maxImages比較重要,簡單理解為預先申請幾張圖,這個值越大,顯示越平滑。
    AImageReader_new如果不開執行緒,則影像處理加到這個執行緒裡,導致讀取影像變慢。開啟執行緒處理,
    我用的Redmi K10 pro,可以讀40003000,在AImageReader_ImageListener回撥不做特殊處理,如下錯誤。
    首先是Unable to acquire a lockedBuffer, very likely client tries to lock more than.
    可以看到,執行四次後報的,就是我設的maxImages,通過比對程式碼邏輯,應該是AImageReader_new讀四次後,我還沒處理完一楨,沒有AImage_delete,也就讀不了資料了.
    然後檢查 AImageReader_acquireNextImage 這個狀態,不對不讀,然後繼續引發讀取不可用記憶體問題,分析應該是處理資料的亂序執行緒AImage_delete可能釋放別的處理執行緒上的image,然後處理影像執行緒上加上lock_guard(mutex),不會引發問題,但是會導致每maxImages卡一下,可以理解,讀的執行緒快,處理的慢,後面想了下,直接讓thread.join,圖片讀取很大時慢(比不開執行緒要快很多,4000
    3000快二倍多,平均45ms),但是平滑的,暫時先這樣,後面看能不能直接拿AImage的harderbuffer去處理,讓處理速度追上讀取速度。

Chroma Key

如上所說,專案對接外部實現的vulkan層是否方便,在這重新生成一個模組aoce_vulkan_extra,在這我選擇UE4 Matting裡的邏輯來測試,因為這個邏輯非常簡單,也算讓我對手機的效能有個初步的瞭解。

首先把相關邏輯整理下,UE4上有相關節點,看下實現整理成glsl compute shader實現。

#version 450

// https://www.unrealengine.com/en-US/tech-blog/setting-up-a-chroma-key-material-in-ue4

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 (std140, binding = 2) uniform UBO {
    // 0.2 控制亮度的強度係數
    float lumaMask;
    float chromaColorX;
    float chromaColorY;
    float chromaColorZ;
    // 用環境光補受藍綠幕影響的畫素(簡單理解釦像結果要放入的環境光的顏色)
    float ambientScale;
    float ambientColorX;  
    float ambientColorY; 
    float ambientColorZ;   
    // 0.4
    float alphaCutoffMin;
    // 0.5
    float alphaCutoffMax;
    float alphaExponent;
    // 0.8
    float despillCuttofMax;
    float despillExponent;
} ubo;

const float PI = 3.1415926;

vec3 extractColor(vec3 color,float lumaMask){   
    float luma = dot(color,vec3(1.0f));
    // 亮度指數
    float colorMask = exp(-luma*2*PI/lumaMask);
    // color*(1-colorMask)+color*luma
    color = mix(color,vec3(luma),colorMask);
    // 生成基於亮度的飽和度圖    
    return color / dot(color,vec3(2.0));
}

void main(){
    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
    ivec2 size = imageSize(outTex);    
    if(uv.x >= size.x || uv.y >= size.y){
        return;
    }    
    vec3 inputColor = imageLoad(inTex,uv).rgb;   
    vec3 chromaColor = vec3(ubo.chromaColorX,ubo.chromaColorY,ubo.chromaColorZ);
    vec3 ambientColor = vec3(ubo.ambientColorX,ubo.ambientColorY,ubo.ambientColorZ);
    vec3 color1 = extractColor(chromaColor,ubo.lumaMask);
    vec3 color2 = extractColor(inputColor,ubo.lumaMask);
    vec3 subColor = color1 - color2;
    float diffSize = length(subColor);
    float minClamp = diffSize-ubo.alphaCutoffMin;
    float dist = ubo.alphaCutoffMax - ubo.alphaCutoffMin;
    // 扣像alpha
    float alpha= clamp(pow(max(minClamp/dist,0),ubo.alphaExponent),0.0,1.0);
    // 受扣像背景影響的顏色alpha
    float inputClamp = ubo.despillCuttofMax - ubo.alphaCutoffMin;
    float despillAlpha = 1.0f- clamp(pow(max(minClamp/inputClamp,0),ubo.despillExponent),0.0,1.0);
    // 亮度係數
    vec3 lumaFactor = vec3(0.3f,0.59f,0.11f);    
    // 新增環境光收益
    vec3 dcolor = inputColor*lumaFactor*ambientColor*ubo.ambientScale*despillAlpha;
    // 去除扣像背景
    dcolor -= inputColor*chromaColor*despillAlpha;
    dcolor += inputColor;    
    // 為了顯示檢視效果,後面遮蔽
    dcolor = inputColor*alpha + ambientColor*(1.0-alpha);
    imageStore(outTex,uv,vec4(dcolor,alpha)); 
}

這裡面程式碼最後倒數第二句實現混合背景時去掉,在這只是為了顯示檢視效果。

然後引用aoce_vulkan裡給的基類VkLayer,根據介面完成本身具體實現,相關VkChromKeyLayer的實現可以說是非常簡單,至少我認為達到我想要的方便。

還是一樣,先說遇到的坑,

  1. 開始在glsl中的UBO,我特意把一個float,vec3放一起,想當然的認為是按照vec4排列,這裡注意,vec3不管前後接什麼,大部分結構定義下,都至少佔vec4,所以後面為了和C++結構align一樣,全部用float.

  2. 層啟用/不啟用會導致整個運算graph重置,一般情況下,運算執行緒與結果輸出執行緒不在一起,在重置時,運算執行緒相關資源會重新生成,而此時輸出執行緒還在使用相關資源就會導致device lost錯誤,在這使用VkEvent用來表示是否在資源重置中。

然後就是與android UI層對接,android的UI沒怎麼用過,醜也就先這樣吧。

相關文章