效果圖(1080P處理)
因為攝像頭開啟自動曝光,畫面變動時,亮度變化導致扣像在轉動時如上。
源碼地址vulkan_extratest
這個demo主要測試二點,一是測試ndk camera集成效果,二是本項目對接外部實現的vulkan層是否方便,用於以后移植GPUImage里的實現。
我簡化了在android下vulkan與opengles紋理互通里的處理,沒有vulkan窗口與交換鏈這些邏輯,只用到vulkan compute shader計算管線得到結果然后交換給opengl里的紋理。
NDK Camera集成
主要參考 NdkCamera Sample的實現,然后封裝成滿足Aoce定義設備接口。
說下遇到的坑。
-
AIMAGE_FORMAT_YUV_420_888 可能是YUV420P,也可能是NV12,需要在AImageReader_ImageListener里拿到image通過AImage_getPlanePixelStride里的UV的plan是否為1來判斷是否為YUV420P,或者看data[u]-data[y]=1來看是否為NV12.具體可以看getVideoFrame的實現。
-
AImageReader_new里的maxImages比較重要,簡單理解為預先申請幾張圖,這個值越大,顯示越平滑。AImageReader_new如果不開線程,則圖像處理加到這個線程里,導致讀取圖像變慢。打開線程處理,我用的Redmi K10 pro,可以讀4000x3000,在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的實現可以說是非常簡單,至少我認為達到我想要的方便。
還是一樣,先說遇到的坑,
-
開始在glsl中的UBO,我特意把一個float,vec3放一起,想當然的認為是按照vec4排列,這里注意,vec3不管前后接什么,大部分結構定義下,都至少占vec4,所以后面為了和C++結構align一樣,全部用float.
-
層啟用/不啟用會導致整個運算graph重置,一般情況下,運算線程與結果輸出線程不在一起,在重置時,運算線程相關資源會重新生成,而此時輸出線程還在使用相關資源就會導致device lost錯誤,在這使用VkEvent用來表示是否在資源重置中。
然后就是與android UI層對接,android的UI沒怎么用過,丑也就先這樣吧。