現把D到O的大部分濾鏡用vulkan的ComputeShader實現了,列舉其中一些有點特殊的說明.
GaussianBlurPosition 指定區域高斯模糊
沒有按照GPUImage裡的方式實現,按照類似GaussianSelectiveBlur方式實現,一張高斯模糊圖,一張原圖,二圖進行混合,這種實現方式更靈活(模糊半徑等引數),並且並不會降低效能(單獨的高斯模糊更容易優化).
SphereRefraction 環境對映
和GlassSphere一樣,主要就是球形座標系與UV座標系的轉化,剛開始完成後,我測試效果,發現圈外在閃爍,後面發現沒把圈外接0,其中withinSphere表示圈外圈內.
Halftone 半色調效果,類似新聞列印
這個類繼承PixellateFilter(馬賽克),順便也把這個實現了,他們引數一樣,實現主要是把一小快矩形範圍的UV統一成一個,然後處理.
HighPass 高通濾波,檢測影像畫素變動
實現這個類,首先要實現LowPass,這個濾境有點特殊,需要儲存上一楨的傳入影像,然後比較當前楨與上一楨.
我設定執行圖使用的有向無環圖,中間可以根據需求自動排除不可用節點並自動連結下層,所以不能有迴環線,從一層儲存一楨然後再拿回來使用,這樣構成迴環,所以我定義VkSaveFrameLayer層為bInput = true,當作輸入層,告訴外面不需要自動連線別層輸入,在相應介面手動指定別的層需要儲存的紋理.
最開始我直接呼叫vkCmdCopyImage,發現在Nsight顯示佔用0.3ms-0.7ms左右的時間,從邏輯上來說,應該不可能啊,這個copy應該比最簡單的運算層佔去的時間要更少才對,於是我測試了二種方案,對應引數bUserPipe,表示用不用管線,用管線控制到0.2ms內,用vkCmdCopyImage在0.3ms以上,以後找下資料看看是什麼問題.
LowPass有二個輸入,然後就是第一輸入節點是上層,第二個輸入節點是SaveFrameLayer,在LowPass執行前,SaveFrameLayer提供輸入,在LowPass後(混合前後楨,可以去掉類似摩爾紋的東東),把結果又儲存在SaveFrameLayer裡.
HighPass就是比較與LowPass的差異,可用來顯示變化大的畫素.
相應原始碼,有興趣可以自己檢視VkLowPassLayer
Histogram 直方圖
GPUImage通過回讀到CPU然後計算,這種方式直接放棄,檢視相應opencv cuda模組類似實現.
執行緒組定成256個,然後使用數值對應索引位置+1,結果就是索引上的數值表示對應值的個數.
有二種方式,儲存到臨時buffer,二是用原子集合(int/uint),這裡因為int滿足,使用原子集合.
儲存到臨時buffer,我在開始四通道時使用類似reduce2分二段,不用原子操作的方式,但是效果並不好,一是第一次把16*16塊的方式轉換成對應的一個個直方圖,模組並沒的縮小,導致第二塊把這一個直方圖通過for加在一起需要迴圈1920/16x1080/16(假設是1080P的圖),這個會花費超過2ms,這種方式就pass掉,我直接使用原子操作匯出四個圖然後再結合都比這個快.
可以看到,在這之前,要先vkCmdClearColorImage結果,幾乎不消費時間,我也是這樣理解的,但是為什麼vkCmdCopyImage會導致那麼大的時間等待了?
時間差不多在0.2ms左右,不過隨機的亮度圖,亮度比較分散,後面使用只有0,1的二種圖看看會不會影響這個時間.
一通道在0.18ms左右,四通道在0.52ms+0.01ms,比0.18*4快些.
直方圖的顯示可以使用VkAlphaShow2Layer顯示,沒有用GPUImage裡的顯示方式,其對應輸出分別為256x1x(R32UI/RGBA32UI)紋理,自己用glsl顯示各種需求應該更方便.要輸出到CPU的話,直接接一個VkOutputLayer也方便.
iOSBlur 一種特定模糊實現
把原來的幾個已經實現的層downSampling/saturation/gaussian blur/luminance range/upSampling組合一下輸出效果就是.
Kuwahara 實現類似油畫風格效果
在GPUImage上最開始就說了,只適合靜止影像,因為很耗效能.原理比較簡單,查詢當前畫素周邊四個方向區域方差最小的區域.
因其演算法特點,卷積分離沒有使用,只使用了區域性共享視訊記憶體優化,在PC 2070N卡1080P下,半徑5需要3.3ms,半徑10需要9.7ms.
測試手機Redmi 10X Pro 用半徑3在720P下非常流暢,可以滿足30fps執行.
PC平臺Vulkan運算層時間記錄裡可以看到,3x3時使用區域性共享視訊記憶體並不一定會佔優,但是隻要核大於3x3,使用區域性共享視訊記憶體的方案就一定會提速,核越大提速越多.
Kuwahara3x3因為演算法特性,需要4x4x4,與8x8相當,就算專門去掉重複的那幾行,也要讀取49個資料,相當於7x7,所以下面專門優化的Kuwahara3x3就沒用了.
Laplacian 使用拉普拉斯運算元查詢邊緣
比較常見,一個畫素與一個特定3x3矩陣的結果,單獨拿出來說,是因為在ubo中,直接使用mat3/mat4,在CPU中使用類似的的Mat3x3,Mat4x4對齊會有問題,並且我在Nsight檢視到對應的UBO裡放著正確的mat3對應資料,但是結果就是不對.最不容易出錯的方法就是在ubo中全使用單個float+std140表示類似vec3/vec4/mat3/mat4結構,在CPU端就不需要特殊處理結構,和ubo一樣順序就行,這樣最不容易出現CPU-GPU中的UBO對齊問題.
Lookup 顏色查詢表
這個在前面Vulkan移植GpuImage(三)從A到C的濾鏡已經說過,但是感覺由使用者自己來接一個輸入層不太方便,因此修改邏輯,內建一個輸入層,提供loadLookUp方法,由使用者提供三通道/四通道512x512x(3/4)的lookup影像資料就行.
下面的MissEtikate匯入lookup_miss_etikate影像資料就行,本框架現沒有讀各種影像格式的模組與第三方庫.
Median 中值濾波
開始和Kuwahara一樣,使用區域性共享視訊記憶體來優化,在結合上面文章裡的直方圖方式,但是結果並不好,詳細情況可以看PC平臺Vulkan運算層時間記錄記錄.
總的來說,優化了個寂寞,還是移植了GPUImage裡的3x3的實現,雖然核大會導致排序也是指數增長,但是這次優化明顯不成功,只能說是暫時可用大於3核的情況,後續找找更多的資料試試改進.
看了下相關opencv cuda裡的median裡的寫法,在前面會宣告一個影像元素*256的buffer?嗯,這個buffer是為了替換裡面單個執行緒uint hist[256]?在CS裡,一個執行緒宣告大堆疊資料會導致效能問題嗎?先暫停,opencv cuda這個演算法寫的太麻煩了,後面有時間研究.
MotionBlur 運動模糊
在沒看GPUImage實現前,我還在想HighPass就有點運動模糊的味道,但是其在GPUImage實現裡,就是簡單的給定一個方向,然後由這個方向模糊,並沒有前後楨的疊加比較,因其實現簡單,所以也移植了.
MotionDetector 運動檢測
這個倒是和HighPass很像,區別在於最後會聚合統計給CPU輸出,這個確實有必要,所以在本MotionDetector實現中,整合一個VkOutputLayer,並轉化輸出資料與GPUImage類似,可以看到運動的大小統計.
class VkMotionDetectorLayer : public VkLayer, public MotionDetectorLayer {
AOCE_LAYER_QUERYINTERFACE(VkMotionDetectorLayer)
private:
std::unique_ptr<VkLowPassLayer> lowLayer = nullptr;
std::unique_ptr<VkReduceLayer> avageLayer = nullptr;
std::unique_ptr<VkOutputLayer> outLayer = nullptr;
public:
VkMotionDetectorLayer();
virtual ~VkMotionDetectorLayer();
private:
void onImageProcessHandle(uint8_t* data, ImageFormat imageFormat,
int32_t outIndex);
protected:
virtual void onUpdateParamet() override;
virtual void onInitGraph() override;
virtual void onInitNode() override;
};
VkMotionDetectorLayer::VkMotionDetectorLayer(/* args */) {
glslPath = "glsl/motionDetector.comp.spv";
inCount = 2;
lowLayer = std::make_unique<VkLowPassLayer>();
avageLayer = std::make_unique<VkReduceLayer>(ReduceOperate::sum);
outLayer = std::make_unique<VkOutputLayer>();
paramet = 0.5f;
lowLayer->updateParamet(paramet);
outLayer->setImageProcessHandle(std::bind(
&VkMotionDetectorLayer::onImageProcessHandle, this, _1, _2, _3));
}
void VkMotionDetectorLayer::onImageProcessHandle(uint8_t* data,
ImageFormat imageFormat,
int32_t outIndex) {
if (onMotionEvent) {
vec4 motion = {};
memcpy(&motion, data, sizeof(vec2));
onMotionEvent(motion);
}
}
VkMotionDetectorLayer::~VkMotionDetectorLayer() {}
void VkMotionDetectorLayer::onUpdateParamet() {
lowLayer->updateParamet(paramet);
}
void VkMotionDetectorLayer::onInitGraph() {
VkLayer::onInitGraph();
pipeGraph->addNode(lowLayer->getLayer());
pipeGraph->addNode(avageLayer.get())->addNode(outLayer->getLayer());
}
void VkMotionDetectorLayer::onInitNode() {
lowLayer->addLine(this, 0, 1);
this->addLine(avageLayer.get());
setStartNode(this, 0);
setStartNode(lowLayer.get());
}
順便還通過CPU的輸出,查到了一個reduce2.comp裡divup誤寫導致的低階錯誤.
NobleCornerDetection Noble角點檢測
這個GPUImage中,實現方式和HarrisCornerDetection幾乎一樣,就是角點的選擇計算方式有點不同,會導致比HarrisCornerDetection多檢測很多角點出來.
Opening 開運算,先侵蝕後膨脹
和Closing一樣,由侵蝕與膨脹組合,專門提供一個類GroupLayer,本身不提供任何計算,只組合別的運算層.
中間沒有移植的濾鏡統計
-
FASTCornerDetection,其GPUImage裡並沒有實現,只是寫個宣告.
-
JFAVoronoi,看到其中需要CPU定迴圈多次GPU執行,其中UV與顏色對應相關程式碼暫時不理解,先放著,後期找更多資料確定移植方法.
-
LanczosResampling,GPUImage的實現好像和其原理差別有點大,應該是特化實現,暫不移植.
-
LineGenerator這個現在還沒想到如何能在GPGPU中高效畫多條線,普通的渲染管線方式倒是方便實現,後面查詢資料確定移植方法.
-
MosaicFilter沒找到具體顯示效果,看到顯示效果應該就可以馬上移植了.
-
直方圖的顯示,現框架實現的VkHistogramLayer裡會輸出分別為256x1x(R32UI/RGBA32UI)紋理,自己根據你的需求去寫相應glsl顯示更合適.
其中從D到O的處理,有一部分因為前面實現別的層時,有一些已經實現過,也不在本文裡說明,現在GPUImager移植進度大約有60%,相應的效果可以在vulkanextratest,win端修改Win32.cpp,android修改Android.cpp檢視對應平臺效果,等所有效果移植完成後會寫配套專門的UI介面檢視.