【工程應用九】再談基於離散夾角餘弦相似度指標的形狀匹配優化(十六角度量化+指令集加速+目標只有部分在圖像內的識別+最小外接矩形識別重疊等)

  繼去年上半年一鼓作氣研究了幾種不同的模版匹配算法後,這個方面的工作基本停滯了有七八個月沒有去碰了,因爲感覺已經遇到了瓶頸,無論是速度還是效率方面,以當時的理解感覺都到了頂了。年初,公司業務慘淡,也無心向佛,總要找點事情做一做,充實下自己,這裏選擇了前期一直想繼續研究的基於離散夾角餘弦相似度指標的形狀匹配優化。 

  在前序的一些列文章裏,我們也描述了我從linemod模型裏抽取的一種相似度指標用於形狀匹配,個人取名爲離散夾角餘弦,其核心是將傳統的基於梯度點積相似度的的指標進行了離散化:

  傳統的梯度點積計算公式如下:

    

  對於任意的兩個點,通過各自的梯度方向,按照上述公式可計算出他們的相似度。

  那麼離散夾角餘弦的區別是,不直接計算兩個梯度方向的餘弦,而是提前進行一些定點化,比如,早期的linemod就是把360度角分爲了8份,每份45度,這樣[0,45]的梯度方向標記爲0,[45,90]的梯度方向標記爲1,依次類推,直到[315, 360]之間的梯度方向標記爲7,這樣共有8個標記,然後提前構建好8個標記之間的一個得分表,比如,下面這樣的一個表:

                              

  這個表的意思也很簡單,就是描述不同標記之間的得分,比如兩個點的梯度方向,都爲3或者4或者5,那麼他們的得分就比較高,可以取8,如果一個爲1,一個爲6,則得分就只有2, 一個爲0,一個爲5,則得分爲0(即完全相反的兩個方向)。       

  很明顯,角度量化的越細,則得到的結果越和傳統的梯度點積結果越接近。但是計算量可能也就越大,

  關於這個過程,我去年的版本也有弄過8角度、16角度及32角度,個人覺得,在目前的計算機框架下,16角度應該是既能滿足進度要求,又能在速度方面更爲完美的一個選擇。

  這裏記錄下最近對基於16角度離散餘弦夾角指標的形狀匹配的進一步優化過程。

  一、核心的優化策略

  通過前面的描述,我們知道,這種方法的得分是通過查表獲取的,而且,在大部分的計算中,是沒有涉及到浮點計算的,我們通過適當的構造表的內容,可以通過簡單的整數類型的加減乘除來得到最後的得分。這個的好處有很多,其中一個就是精度問題,在基於梯度點積的計算中,如果採用float類型來累計計算結果,通常或多或少的存在某些情況下的精度丟失,而且還不好定位哪裏有問題。還有個好處就是真的可以加速,當然這個的加速主要是通過一個很特殊,但是又很有效的SSE指令集語句實現的,這個指令就是_mm_shuffle_epi8,其原型及相關介紹如下:

    __m128i _mm_shuffle_epi8 (__m128i a, __m128i b)

   這是個很牛逼的東西,如果我們把a看成一個16個元素的字節數組,b也是一個16個元素的字節數據,則簡單的理解他就是實現下述功能:

    dst[i] = a[b[i]];  即一個16個元素的查表功能。

  很明顯,b[i]要在0到15之間纔有效,否則,就查不到元素了,但是該指令還有比較隱藏的功能是,當b[i]大於15時,dst就返回0了。

  爲什麼說這個指令牛逼,我們看我們前面說的這個獲取離散夾角餘弦的過程,對於兩個等面積的區域,假定一個區域的量化後的離散角度標記保存在QuantizedAngleT內存中,另外一個保存在QuantizedAngle內存中,他們的寬和高分別爲ValidW和ValidH,則這兩個區域按照前面定義的標準,其得分可用下述代碼表示(這裏SimilarityLut是16角度離散的):

int Score = 0;
for (int Y = 0; Y < ValidH; Y++)
{
    int Index = Y * ValidW;
    for (int X = 0; X < ValidW; X++)
    {
        Score += SimilarityLut[QuantizedAngleT[Index + X] * 16 + QuantizedAngle[Index + X]];
        //    Score += SimilarityLut[QuantizedAngleT[Index + X],  QuantizedAngle[Index + X]];
    }
}

  如果把SimilarityLut看成是二維的數組,上面註釋掉的得分可能看起來更爲清晰。

  實際上,我們在進行模版匹配的時候大部分都是在進行這樣的得分計算,因此,如果上面的過程能夠提速,那麼整體將提速很多。

  通常,查表的算法是無法進行指令集優化的(AVX2的gather雖然有一定效果,但是弄的不好會適得其反),但是,正是因爲我們本例的特殊性,使得這個查表反而更有利於算法的性能提高。

  注意到前面有說過我們在進行16角度量化時,量化的標記範圍是[0,15],意味着什麼,這個正好是_mm_shuffle_epi8 指令裏參數b的有效範圍啊。

  但是仔細看上面的SimilarityLut表,他由兩個變量確定索引,這就有點麻煩了,解決的辦法是換位思考,我們能不能固定其中的一個呢,這個就要結合我們的實際應用了。

  在形狀匹配中,我們提取了很多特徵點,然後需要使用這些特徵點對圖像中有效區域範圍的目標進行得分統計,也就是說任何一個位置,都要計算所有特徵點的得分,並計總和,一個簡單的表示爲:

for (int Y = 0; Y < ValidH; Y++)
{
    int Index = Y * ValidW;
    for (int X = 0; X < ValidW; X++)
    {
        int b = 圖像位置對應的量化值
        for (int Z = 0; Z < Template.PointAmount; Z++)
        {
            int a = 模版位置對應的量化值
            Score += SimilarityLut16[a, b];
        }
    }
}

  這種情況a,b在每次獨立的小循環中還是變化的,一樣無法使用指令集。

  不過,如果我們調換下循環的順序,改爲以下方式:

for (int Z = 0; Z < Template.PointAmount; Z++)
{
    int a = 模版位置對應的量化值
    for (int Y = 0; Y < ValidH; Y++)
    {
        int Index = Y * ValidW;
        for (int X = 0; X < ValidW; X++)
        {
            int b = 圖像位置對應的量化值
            Score += SimilarityLut16[a, b];
        }
    }
}

  此時,a在內部循環裏是不變的,我們通過a之可以定位到SimilarityLut16的a*16地址處,並加載16字節內容,作爲查找表的內容,而b值也可以一次性加載16個字節,作爲查找的索引,這樣一次性就能獲得16個位置的得分了。

  還有一點,我們在算法裏有個最小對比度的東西, 這個東西是用來加快速度的,即梯度值小於這個數據,我們不要這個點參與到匹配中,即此時這個點的得分是0,爲了標記這樣的點,我們需要再原圖的量化值裏增加不在[0,15]範圍內的東西,一旦有這個東西存在,我們的普通C代碼裏就需要添加類似這樣的代碼了:

if (QuantizedAngle[Index + X] != 255) Score += SimilarityLut[QuantizedAngleT[Index + X] * 16 + QuantizedAngle[Index + X]];

  這裏我使用了255這個不在[0,15]範圍內的數字來表示這個點不需要參與匹配計算。本來說,如果剛剛那條_mm_shuffle_epi8指令,只是純粹的實現[0,15]索引之間的查表,那有了這個東西,這個指令又就沒法用了,但是恰恰這個指令能實現在索引不在0到15之間,可以返回其他值,而這個其他值恰好又是0,你們說是多麼的巧合和神奇啊。所以,這一切都是爲這個指令準備的。我們看看其他shuffle指令,包括_mm_shuffle_epi32,_mm_shuffle_ps,都沒有這個附帶的功能。

  其實這裏還是要交代一點,這個算法,如果遇到那種不能用指令集的機器,或者說用純C語言去實現,效率就比較低了,因爲C語言裏只能直接查表,而且還要有那個判斷。

  二、特徵點數量的展開即貪婪度參數的捨棄

  linemod裏使用8角度的特徵,其兩個特徵之間的得分最大值爲8,其內部使用了16位的加法,所以其最大的特徵點數量爲8096個,當模型較大時,往往會超出這個數量的特徵點,特別是在最後面基層金字塔的時候,一種方案就是我們限制特徵點的數量,並對特徵點進行合適的提取,這也不失爲一種方案。 那想要完美呢,就必須還得是上32位的加法。這裏就存在一個問題,精度和速度如何同時保證,畢竟在SSE指令集的世界裏,16位的加法是要比32位的加法快的。

  一種解決方案就是,對特徵點進行分區,我們按照可能超出16位能表達的最大範圍時特徵點的數量爲區間大小,對特徵點按順序進行多區間劃分,在每個小區間裏還是用16位的指令集加法,完成一次後,把臨時結果在加到32位的數據裏。這樣就精度和速度都能兼顧了,只不過代碼又複雜了一些。

  當我們嘗試了這麼多努力後,我們發現無論是頂層的得分計算,還是後續的每層的局部更新,其速度都變得飛快,這個時候我們又想到了一個貪婪度參數,這個東西,在論文有提到,可以提前結束循環,加快速度。可是,我也想嘗試把這個東西加入到我這個算法的過程裏,我發現他會破壞我整體的節奏,最終我們選擇捨棄了這個參數,核心理由如下:

  1、提前結束循環,是需要進行判斷的,而且是每次都要判斷,特別是對於後期的局部更新判斷,因爲大部分候選點都是能滿足最小得分要求的,這部分的判斷一般來說,基本上就無法滿足了,也就是純粹的多了這些無效判斷。

  2、因爲我們使用了_mm_shuffle_epi8指令,一次性可以處理16個位置的得分,也就是這個粒度是16個像素,而如果使用SSE進行判斷,也只有當16個位置都不滿足最小得分要求後,纔可以跳出循環,這個在很多情況下也是得不償失的。

  三、目標只有部分在圖像內的識別

   有些情況下,被識別的目標只有局部在圖像範圍內,而我們也期望能識別他,這個功能,我知道早期版本的halcon是沒有的,他只能識別那些特徵點完全在圖像內的目標(不是模版圖像邊緣)。我早期版本也麼有這個功能,後期有做過一些擴展,擴展的方法是通過擴大原始圖像合適的範圍,同時爲了避免不增加新的邊緣信息,擴展的部分都是用了邊界的像素值。這樣做在大部分情況下是能夠解決問題的,不過其實也隱藏的一些不合理的地方,這些擴展的部分在細節上還是會產生額外的邊緣的,只是不怎麼明顯。因此,這個版本,我也考慮了幾個優化,在內部直接實現了邊緣的擴展。

  這裏有幾個技巧。

  1、原圖的金字塔圖像還是不動。

  2、計算原圖每層金字塔圖像的角度量化值時,對這個量化值進行擴展,擴展的部分的量化值填充前面說的那個不在[0,15]之間的無效值,比如這裏是255,這樣,這些區域的得分就是0。

  3、爲了能保證在極端情況下這些部分在圖像的目標能被識別,擴展的大小要以特徵有效值的外接矩形的對角線長度爲基準,再進行適當的擴展(這個擴展也有特別要求)。

  4、計算完成後,座標值要進行相應的調整。

  通過這種方式,可在內部實現缺失目標的識別,而且在內存佔用、速度等方面也有一定的優勢。

   四、最小外接矩形識別重疊

  halcon有說過其maxoverlap參數是通過計算特徵點的最小外接矩形之間的重疊來實現的,在我以前的版本中,這個功能是通過其他的簡易方法來搞定的。這個主要是以前搞不定最小外接矩形的計算,年初,恰好從opencv裏翻譯可扣取了一些代碼,起重工就有最小外接矩形的獲取,這個需要通過計算凸包以前其他一些複雜的東西搞定,我沒有看懂原理,只是直接扣取了代碼,不過CV的代碼繞來繞去,扣的我也是頭暈腦脹,總算搞出來了。

  那麼這裏其實也有蠻多的細節和可選方案,我列舉如下:

  1、在創建特徵時,計算好每個旋轉後的特徵的最小外接矩形(勾選了預生成模型數據)。

  2、在最後確定的底層金字塔裏所有的候選點出計算每個特徵點對應的外接矩形。

  3、只計算底層金字塔0角度是特徵單的最小外接矩形,然後其他底層金字塔的最小外接矩形用他旋轉得到。

  我們實際考慮啊,方案一對創建模型不友好,方案二實際測試對運行的效率產生了不良影響,方案3最好,基本不耗時,而且對精度的影響也非常有限,所以可以選擇方案3。

   五、其他的一些我未公開的討論的課題

  1、16角度SimilarityLut的值如何設計,其實在halcon裏有個metric參數,他有三種選擇,使用極性、忽略全局極性、忽略局部極性。如果想前面給出的那種8角度的SimilarityLut,是隻能實現使用極性和忽略全局極性的。這裏適當擴展,就可以實現三個都有了,而且對是速度提升還有好處。

  2、5*5局部得分過程的特別優化,尤其是如何高效的加載每行5個字節,並拼接成合適的形式,使得能快速的使用指令集。

  3、也可以使用8*8的局部區域(非對稱的局部更新),這樣方便使用指令集,但是因爲數量變大,還是沒有優化後的5*5快。

  4、在最頂層,計算候選點時,可以直接計算,也可以考慮使用ResponseMaps,其中,測試表明還是使用ResponseMaps快一點。

  5、還是候選點的選擇問題,在最頂層,目前我還是用的某個角度下的二維得分結果中選擇得分大於最小得分要求,同時是5*5領域的最大值作爲候選點,這種方式留下的候選點還是有很多的,對於只有旋轉的匹配,是否可以考慮在3D(X方向、Y方向以及角度方向)的空間裏,選得分大於最小得分要求且是5*5*5領域的最大值呢,這樣候選點肯定會少很多,但是代碼的編寫似乎變得困難了很多,還有佔用內存問題。那如果擴展到多尺度的匹配,或者是各項異性的匹配,那就要在4D和5D空間搜索最大值了,這個感覺就更爲複雜了。

  6、另外,再有頂層金字塔向下層金字塔迭代更新的過程中的候選點的捨棄問題,也值得探討,到底是隻根據得分是否滿足需求,還是根據一些物理空間或者角度方面的特性做些特別的優化和捨棄呢,而且這種捨棄行爲本身有的時候可能會帶來性能的下降,因爲在搜索那些目標可以捨棄時,需要一個循環,當候選目標有幾千個的時候,兩重這樣的循環會對對後續候選點變少帶來的性能加速起到反向作用,這個甚至會超過候選點變少帶來的加速。所以   目前我這個版本爲了穩定性,只是對得到的重複的點做了捨棄。

  7、原本再想一個優化,即我們的特徵點的保存順序問題,現在只有0角度的特徵點的保存順序是X及Y方向都是由小向大方向座標排列,這樣訪問的時候和圖像的內存佈局方向性一致,按理說cache  miss要小一些。但是,如果按照順序把0角度的特徵點旋轉後,得到新的位置信息,一般來說肯定不是按照這樣的順序排列的了,所以訪問時隨機性就差了一些,那是否我再計算前把這些點再按0角度時那樣做個排序後,有利於算法的性能呢,我做了實際的測試,應該說基本沒啥區別吧。

  六、結論

   綜合以上各種優化手段後,目前經過測試,速度較以前有很大的提高,而且和基於傳統的梯度點積的方法比較,速度更具有優勢,而且精度也不逞多讓。我們測試在2500*2000的灰度中查找 300 * 150的多目標,大約耗時11ms, 傳統的方式結合貪婪度耗時也需要18ms。這個時間我測試和halcon的比較已經非常接近了。當然這種比較還是要看具體的測試圖。

  從算法精度上看,怎麼上定位也是很準確的,在執行過程中,佔用的內存也不大,因此,個人覺得這個方法不失爲一個優質的算子。

  本文測試DEMO鏈接:https://files.cnblogs.com/files/Imageshop/QuantizedOrientations_Matching.rar?t=1710810282&download=true

   如果想時刻關注本人的最新文章,也可關注公衆號或者添加本人微信:  laviewpbt

                             

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章