5月9日數據匹配圖論、匈牙利、KM算法,多目標跟蹤

修訂 :多目標跟蹤,即MOT(Multi-Object Tracking),也就是在一段視頻中同時跟蹤多個目標20200521

英文註解

無權二分圖(unweighted bipartite graph)的最大匹配(maximum matching)和完美匹配(perfect matching),以及用於求解匹配的匈牙利算法(Hungarian Algorithm)

背景知識

  • 圖論(來源於《數據結構》)

1.1基本概念:

圖論〔Graph Theory〕是數學的一個分支。它以圖爲研究對象。圖論中的圖是由若干給定的點及連接兩點的線所構成的圖形,這種圖形通常用來描述某些事物之間的某種特定關係,用點代表事物,用連接兩點的線表示相應兩個事物間具有這種關係

圖論是一種表示 “多對多” 的關係

在這裏插入圖片描述
圖論是一種表示 “多對多” 的關係

圖是由頂點和邊組成的:(可以無邊,但至少包含一個頂點)

  • 一組頂點:通常用 V(vertex) 表示頂點集合
  • 一組邊:通常用 E(edge) 表示邊的集合

圖可以分爲有向圖和無向圖,在圖中:

  • (v, w) 表示無向邊,即 v 和 w 是互通的
  • <v, w> 表示有向邊,該邊始於 v,終於 w

圖可以分爲有權圖和無權圖:

  • 有權圖:每條邊具有一定的權重(weight),通常是一個數字
  • 無權圖:每條邊均沒有權重,也可以理解爲權爲 1

圖又可以分爲連通圖和非連通圖:

  • 連通圖:所有的點都有路徑相連
  • 非連通圖:存在某兩個點沒有路徑相連

圖中的頂點有度的概念:

  • 度(Degree):所有與它連接點的個數之和
  • 入度(Indegree):存在於有向圖中,所有接入該點的邊數之和
  • 出度(Outdegree):存在於有向圖中,所有接出該點的邊數之和

1.2 圖的表示:

圖在程序中的表示一般有兩種方式:

1.2.1鄰接矩陣:

在 n 個頂點的圖需要有一個 n × n 大小的矩陣
在一個無權圖中,矩陣座標中每個位置值爲 1 代表兩個點是相連的,0 表示兩點是不相連的
在一個有權圖中,矩陣座標中每個位置值代表該兩點之間的權重,0 表示該兩點不相連
在無向圖中,鄰接矩陣關於對角線相等

1.2.2 鄰接鏈表:

對於每個點,存儲着一個鏈表,用來指向所有與該點直接相連的點
對於有權圖來說,鏈表中元素值對應着權重

例如在

無向無權圖中:
在這裏插入圖片描述
無向有權圖中:
在這裏插入圖片描述
可以看出在無向圖中,鄰接矩陣關於對角線對稱,而鄰接鏈表總有兩條對稱的邊

而在有向無權圖中:
在這裏插入圖片描述

鄰接矩陣和鏈表對比:
鄰接矩陣由於沒有相連的邊也佔有空間,因此存在浪費空間的問題,而鄰接鏈表則比較合理地利用空間
鄰接鏈表比較耗時,犧牲很大的時間來查找,因此比較耗時,而鄰接矩陣法相比鄰接鏈表法來說,時間複雜度低。

1.3 圖的遍歷:

1.3.1 深度優先遍歷:(Depth First Search, DFS)

基本思路:深度優先遍歷圖的方法是,從圖中某頂點 v 出發

1:訪問頂點 v
2:從 v 的未被訪問的鄰接點中選取一個頂點 w,從 w 出發進行深度優先遍歷
3:重複上述兩步,直至圖中所有和v有路徑相通的頂點都被訪問到

在這裏插入圖片描述

在這裏插入圖片描述

1.3.2 廣度優先搜索:(Breadth First Search, BFS)

廣度優先搜索,可以被形象地描述爲 “淺嘗輒止”,它也需要一個隊列以保持遍歷過的頂點順序,以便按出隊的順序再去訪問這些頂點的鄰接頂點。

實現思路:

1、頂點 v 入隊列
2、當隊列非空時則繼續執行,否則算法結束
3、出隊列取得隊頭頂點 v;訪問頂點 v 並標記頂點 v 已被訪問
4、查找頂點 v 的第一個鄰接頂點 col
5、若 v 的鄰接頂點 col 未被訪問過的,則 col 繼續
6、查找頂點 v 的另一個新的鄰接頂點 col,轉到步驟 5 入隊列,直到頂點 v 的所有未被訪問過的鄰接點處理完。轉到步驟 2

在這裏插入圖片描述

在這裏插入圖片描述

1.3.3 辯證理解

要理解深度優先和廣度優先搜索,首先要理解搜索步,一個完整的搜索步包括兩個處理

1、獲得當前位置上,有幾條路可供選擇
2、根據選擇策略,選擇其中一條路,並走到下個位置

相當於在漆黑的夜裏,你只能看清你站的位置和你前面的路,但你不知道每條路能夠通向哪裏。搜索的任務就是,給出初始位置和目標位置,要求找到一條到達目標的路徑。

深度優先就是,從初始點出發,不斷向前走,如果碰到死路了,就往回走一步,嘗試另一條路,直到發現了目標位置。這種不撞南牆不回頭的方法,即使成功也不一定找到一條好路,但好處是需要記住的位置比較少。
廣度優先就是,從初始點出發,把所有可能的路徑都走一遍,如果裏面沒有目標位置,則嘗試把所有兩步能夠到的位置都走一遍,看有沒有目標位置;如果還不行,則嘗試所有三步可以到的位置。這種方法,一定可以找到一條最短路徑,但需要記憶的內容實在很多,要量力而行。

1.4 最短路徑算法 (Shortest Path Algorithm)

  1. 無權圖:

  2. 有權圖:
    在有權圖中,常見的最短路徑算法有 Dijkstra 算法 Floyd 算法

  • 迪傑斯特拉 Dijkstra 算法:Dijkstra 算法適用於權值爲正的的圖
  • 佛洛伊德 Floyd 算法:可以求出任意兩點的最短距離

1.5 最小生成樹 (Minimum Spanning Trees MST)

例如:要在 n 個城市之間鋪設光纜,主要目標是要使這 n 個城市的任意兩個之間都可以通信,但鋪設光纜的費用很高,且各個城市之間鋪設光纜的費用不同,因此另一個目標是要使鋪設光纜的總費用最低。這就需要找到帶權的最小生成樹

    1. 普里姆算法 (Prim 算法):
    1. Kruskal 算法:需要一個集合用來升序存儲所有邊

參考:
https://www.cnblogs.com/skywang12345/p/3603935.html
https://zhuanlan.zhihu.com/p/25498681

國家分配好朋友

舉例來說:如下圖所示,如果在某一對男孩和女孩之間存在相連的邊,就意味着他們彼此喜歡。

  • 是否可能讓所有男孩和女孩兩兩配對,使得每對兒都互相喜歡呢?圖論中,這就是完美匹配問題
  • 如果換一個說法:最多有多少互相喜歡的男孩/女孩可以配對兒?這就是最大匹配問題

在這裏插入圖片描述

二分圖匹配

二分圖又稱作二部圖,是圖論中的一種特殊模型。

設G=(V, E)是一個無向圖。如果頂點集V可分割爲兩個互不相交的子集X和Y,並且圖中每條邊連接的兩個頂點一個在X中,另一個在Y中,則稱圖G爲二分圖。
可以得到線上的driver與order之間的匹配關係既是一個二分圖。

簡單來說,如果圖中點可以被分爲兩組,並且使得所有邊都跨越組的邊界,則這就是一個二分圖。
準確地說:把一個圖的頂點劃分爲兩個不相交集 U 和V ,使得每一條邊都分別連接U、V中的頂點。如果存在這樣的劃分,則此圖爲一個二分圖。

二分圖的一個等價定義是:不含有「含奇數條邊的環」的圖。

圖 1 是一個二分圖。爲了清晰,我們以後都把它畫成圖 2 的形式。

在這裏插入圖片描述
在這裏插入圖片描述
匹配:在圖論中,一個「匹配」(matching)是一個邊的集合,其中任意兩條邊都沒有公共頂點。例如,圖 3、圖 4 中紅色的邊就是圖 2 的匹配
在這裏插入圖片描述

在這裏插入圖片描述

我們定義匹配點、匹配邊、未匹配點、非匹配邊,它們的含義非常顯然。例如圖 3 中 1、4、5、7 爲匹配點,其他頂點爲未匹配點;1-5、4-7爲匹配邊,其他邊爲非匹配邊。

最大匹配:一個圖所有匹配中,所含匹配邊數最多的匹配,稱爲這個圖的最大匹配。圖 4 是一個最大匹配,它包含 4 條匹配邊。

完美匹配:如果一個圖的某個匹配中,所有的頂點都是匹配點,那麼它就是一個完美匹配。圖 4 是一個完美匹配。顯然,完美匹配一定是最大匹配(完美匹配的任何一個點都已經匹配,添加一條新的匹配邊一定會與已有的匹配邊衝突)。但並非每個圖都存在完美匹配。

https://www.cnblogs.com/shenben/p/5573788.html
最小點覆蓋:假如選了一個點就相當於覆蓋了以它爲端點的所有邊,你需要選擇最少的點來覆蓋所有的邊

最小割定理:是一個二分圖中很重要的定理:一個二分圖中的最大匹配數等於這個圖中的最小點覆蓋數。

最小點集覆蓋==最大匹配。在這裏解釋一下原因,首先,最小點集覆蓋一定>=最大匹配,因爲假設最大匹配爲n,那麼我們就得到了n條互不相鄰的邊,光覆蓋這些邊就要用到n個點。現在我們來思考爲什麼最小點擊覆蓋一定<=最大匹配。任何一種n個點的最小點擊覆蓋,一定可以轉化成一個n的最大匹配。因爲最小點集覆蓋中的每個點都能找到至少一條只有一個端點在點集中的邊(如果找不到則說明該點所有的邊的另外一個端點都被覆蓋,所以該點則沒必要被覆蓋,和它在最小點集覆蓋中相矛盾),只要每個端點都選擇一個這樣的邊,就必然能轉化爲一個匹配數與點集覆蓋的點數相等的匹配方案。所以最大匹配至少爲最小點集覆蓋數,即最小點擊覆蓋一定<=最大匹配。綜上,二者相等。

3. KM算法初步

KM算法全稱是Kuhn-Munkras,是這兩個人在1957年提出的,

3.1 交替路:

從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊…形成的路徑叫交替路。

3.2 增廣路徑作用與定義

作用:
增廣路主要應用於匈牙利算法中,用於求二分圖最大匹配。

定義:
若P是圖G中一條連通兩個未匹配頂點的路徑,並且屬於Matching(簡寫M)的邊和不屬於Matching(簡寫M)的邊(即已匹配和待匹配的邊)在P上交替出現,則稱P爲相對於M的一條增廣路徑【百度百科】

在這裏插入圖片描述

具體的:
從一個未匹配點出發,走交替路,如果途徑另一個未匹配點(出發的點不算),則這條交替路稱爲增廣路(agumenting path)。例如,圖 5 中的一條增廣路如圖 6 所示(圖中的匹配點均用紅色標出):

在這裏插入圖片描述

在這裏插入圖片描述

增廣路有一個重要特點:非匹配邊比匹配邊多一條。因此,研究增廣路的意義是改進匹配。只要把增廣路中的匹配邊和非匹配邊的身份交換即可。由於中間的匹配節點不存在其他相連的匹配邊,所以這樣做不會破壞匹配的性質。交換後,圖中的匹配邊數目比原來多了 1 條

我們可以通過不停地找增廣路來增加匹配中的匹配邊和匹配點。找不到增廣路時,達到最大匹配(這是增廣路定理)。

其他性質:【百度百科】
由增廣路的定義可以推出下述五個結論:
1-P的路徑長度必定爲奇數,第一條邊和最後一條邊都不屬於M。
2-不斷尋找增廣路可以得到一個更大的匹配M’,直到找不到更多的增廣路。
3-M爲G的最大匹配當且僅當不存在M的增廣路徑。
4-最大匹配數M+最大獨立數N=總的結點數
5 – 二分圖的最小路徑覆蓋數 = 原圖點數 - 最大匹配數

增廣路徑有如下特性:

  1. 有奇數條邊
  2. 起點在二分圖的X邊,終點在二分圖的Y邊
  3. 路徑上的點一定是一個在X邊,一個在Y邊,交錯出現。
  4. 整條路徑上沒有重複的點
  5. 起點和終點都是目前還沒有配對的點,其他的點都已經出現在匹配子圖中
  6. 路徑上的所有第奇數條邊都是目前還沒有進入目前的匹配子圖的邊,而所有第偶數條邊都已經進入目前的匹配子圖。奇數邊比偶數邊多一條邊
  7. 於是當我們把所有第奇數條邊都加到匹配子圖並把條偶數條邊都刪除,匹配數增加了1.

例如下圖,淡藍色路徑x0y0的是當前的匹配子圖,通過x1找到了增廣路徑粉紅色路徑:x1y0->y0x0->x0y2
在這裏插入圖片描述

增廣路徑有兩種尋徑方法,一個是深搜,一個是寬搜。

例如從x2出發尋找增廣路徑

  • 如果是深搜,x2找到y0匹配,但發現y0已經被x1匹配了,於是就深入到x1,去爲x1找新的匹配節點,結果發現x1沒有其他的匹配節點,於是匹配失敗,x2接着找y1,發現y1可以匹配,於是就找到了新的增廣路徑。
  • 如果是寬搜,x2找到y0節點的時候,由於不能馬上得到一個合法的匹配,於是將它做爲候選項放入隊列中,並接着找y1,由於y1已經匹配,於是匹配成功返回了。
    相對來說,深搜要容易理解些,其棧可以由遞歸過程來維護,而寬搜則需要自己維護一個隊列,並對一路過來的路線自己做標記,實現起來比較麻煩。

4.0 匈牙利樹

是在1965年提出的。
我們可以通過不停地找增廣路來增加匹配中的匹配邊和匹配點。找不到增廣路時,達到最大匹配(這是增廣路定理)。匈牙利算法正是這麼做的。在給出匈牙利算法 DFS 和 BFS 版本的代碼之前,先講一下匈牙利樹。

匈牙利樹一般由 BFS 構造(類似於 BFS 樹)。從一個未匹配點出發運行 BFS(唯一的限制是,必須走交替路),直到不能再擴展爲止。例如,由圖 7,可以得到如圖 8 的一棵 BFS 樹
在這裏插入圖片描述

在這裏插入圖片描述

這棵樹存在一個葉子節點爲非匹配點(7 號),但是匈牙利樹要求所有葉子節點均爲匹配點,因此這不是一棵匈牙利樹。

如果原圖中根本不含 7 號節點,那麼從 2 號節點出發就會得到一棵匈牙利樹。這種情況如圖 9 所示

  • (順便說一句,圖 8 中根節點 2 到非匹配葉子節點 7 顯然是一條增廣路,沿這條增廣路擴充後將得到一個完美匹配)。

在這裏插入圖片描述

5.0 匈牙利算法——最大匹配

用於求二分圖的最大匹配。何爲最大匹配?假設每條邊有權值,那麼一定會存在一個最大權值的匹配情況。

5.1 匈牙利算法步驟

算法根據一定的規則選擇二分圖的邊加入匹配子圖中,其基本模式爲:
1.初始化匹配子圖爲空
2.While 找得到增廣路徑
3.Do 把增廣路徑添加到匹配子圖中

5.2 最大匹配的講解 :匈牙利算法(二分圖)

由增廣路的性質,增廣路中的匹配邊總是比未匹配邊多一條,所以如果我們放棄一條增廣路中的匹配邊,選取未匹配邊作爲匹配邊,則匹配的數量就會增加。匈牙利算法就是在不斷尋找增廣路,如果找不到增廣路,就說明達到了最大匹配。

先給一個例子
1、起始沒有匹配
在這裏插入圖片描述

2、選中第一個x點找第一跟連線

在這裏插入圖片描述

3、選中第二個點找第二跟連線

在這裏插入圖片描述

4、發現x3的第一條邊x3y1已經被人佔了,找出x3出發的的交錯路徑x3-y1-x1-y4,把交錯路中已在匹配上的邊x1y1從匹配中去掉,剩餘的邊x3y1 x1y4加到匹配中去

在這裏插入圖片描述

5、同理加入x4,x5。

匈牙利算法可以深度有限或者廣度優先,剛纔的示例是深度優先,即x3找y1,y1已經有匹配,則找交錯路。若是廣度優先,應爲:x3找y1,y1有匹配,x3找y2。

5.4 深度優先搜索(又名交叉染色法)實現二分圖判定

簡要的瞭解一下圖的概念,以及表示、儲存的方法。

  • 主要就是鄰接矩陣和鄰接表兩種方式
    • 鄰接矩陣就不說了比較好實現
    • 鄰接表則主要用到不同的容器,比如vector。

使用鄰接表的主要思路是對每一個頂點都建立一個vector容器,當它和另一個頂點有邊的時候就將該頂點的編號插入vector中,注意無向表還要反過來插入一次

當使用類或者結構體來儲存圖的頂點和邊時就可以添加衆多的屬性來擴展

接下來就是圖的搜索,先以最基本的深度優先搜索爲例 其實很簡單,就是一個頂點走到頭再回來走下一支

現在有一個基本的問題,我們如何判斷一個圖是否是二分圖呢

最簡單的思路無非是窮舉,對每個頂點我們都可以有兩種顏色,採用搜索的思路,如果能走出一條滿足任意兩個頂點顏色的路徑就可以了。

具體的實現方法參考一下書本,大概可以這樣想:

  • 從一個頂點出發,把所有它相鄰的頂點染成另一個顏色。
  • 不停的繼續這一個過程,一層一層的染色。
  • 如果中間不出現矛盾情況(即相鄰的頂點同色)就判定爲二分圖,
  • 否則立即返回換一個沒有染色的頂點重新嘗試(這個的原理需要好好理解)

首先任意取出一個頂點進行染色,和該節點相鄰的點有三種情況:
1.如果節點沒有染過色,就染上與它相反的顏色,推入隊列,
2.如果節點染過色且相反,忽視掉,
3.如果節點染過色且與父節點相同,證明不是二分圖,return

6.0 KM算法——最佳匹配

KM算法,用於求二分圖匹配的最佳匹配。何爲最佳匹配?就是帶權二分圖的權值最大的完備匹配稱爲最佳匹配。 那麼何爲完備匹配?X部中的每一個頂點都與Y部中的一個頂點匹配,或者Y部中的每一個頂點也與X部中的一個頂點匹配,則該匹配爲完備匹配。

6.1 KM算法步驟

其算法步驟如下:
1.用鄰接矩陣(或其他方法也行啦)來儲存圖,注意:如果只是想求最大權值匹配而不要求是完全匹配的話,請把各個不相連的邊的權值設置爲0。
2.運用貪心算法初始化標杆。
3.運用匈牙利算法找到完備匹配。
4.如果找不到,則通過修改標杆,增加一些邊。
5.重複3,4的步驟,直到完全匹配時可結束。

6.2 KM算法標杆(又名頂標)的引入

二分圖最佳匹配還是二分圖匹配,所以跟和匈牙利算法思路差不多。
二分圖是特殊的網絡流,最佳匹配相當於求最大(小)費用最大流,所以FF算法(全名Ford-Fulkerson算法)也能實現。

  • 所以我們可以把這匈牙利算法和FF算法結合起來。這就是KM算法的思路了:儘量找最大的邊進行連邊,如果不能則換一條較大的。

FF算法裏面,我們每次是找最長(短)路進行通流,所以二分圖匹配裏面我們也按照FF算法找最大邊進行連邊!
但是遇到某個點被匹配了兩次怎麼辦?那就用匈牙利算法進行更改匹配!

所以,根據KM算法的思路,我們一開始要對邊權值最大的進行連線。

那問題就來了,我們如何讓計算機知道該點對應的權值最大的邊是哪一條?或許我們可以通過某種方式記錄邊的另一端點,但是呢,後面還要涉及改邊,又要記錄邊權值總和,而這個記錄端點方法似乎有點麻煩。

於是KM採用了一種十分巧妙的辦法(也是KM算法思想的精髓):添加標杆(頂標)

6.2.1 添加標杆(頂標)流程:

我們對左邊每個點Xi和右邊每個點Yi添加標杆Cx和Cy。其中我們要滿足Cx+Cy>=w[x][y](w[x][y]即爲點Xi、Yi之間的邊權值)
對於一開始的初始化,我們對於每個點分別進行如下操作:Cx=max(w[x][y]); Cy=0;

添加頂標之前的二分圖:
在這裏插入圖片描述

添加頂標之後的二分圖:

在這裏插入圖片描述

6.4 基本步驟

一般對KM算法的描述,基本上可以概括成以下幾個步驟:
(1) 初始化可行標杆
(2) 用匈牙利算法尋找完備匹配
(3) 若未找到完備匹配則修改可行標杆
(4) 重複(2)(3)直到找到相等子圖的完備匹配

7.0【KM算法及其具體過程】

(1)可行點標:每個點有一個標號,記lx[i]爲X方點i的標號,ly[j]爲Y方點j的標號。如果對於圖中的任意邊(i, j, W)都有lx[i]+ly[j]>=W,則這一組點標是可行的。特別地,對於lx[i]+ly[j]=W的邊(i, j, W),稱爲可行邊;
(2)KM算法的核心思想就是通過修改某些點的標號(但要滿足點標始終是可行的),不斷增加圖中的可行邊總數,直到圖中存在僅由可行邊組成的完全匹配爲止,此時這個匹配一定是最佳的(因爲由可行點標的的定義,圖中的任意一個完全匹配,其邊權總和均不大於所有點的標號之和,而僅由可行邊組成的完全匹配的邊權總和等於所有點的標號之和,故這個匹配是最佳的)。一開始,求出每個點的初始標號:lx[i]=max{e.W|e.x=i}(即每個X方點的初始標號爲與這個X方點相關聯的權值最大的邊的權值),ly[j]=0(即每個Y方點的初始標號爲0)。這個初始點標顯然是可行的,並且,與任意一個X方點關聯的邊中至少有一條可行邊;
(3)然後,從每個X方點開始DFS增廣。DFS增廣的過程與最大匹配的匈牙利Hungary算法基本相同,只是要注意兩點:一是隻找可行邊,二是要把搜索過程中遍歷到的X方點全部記下來(可以用vst搞一下),以進行後面的修改;
(4)增廣的結果有兩種:若成功(找到了增廣軌),則該點增廣完成,進入下一個點的增廣。若失敗(沒有找到增廣軌),則需要改變一些點的標號,使得圖中可行邊的數量增加。方法爲:將所有在增廣軌中(就是在增廣過程中遍歷到)的X方點的標號全部減去一個常數d,所有在增廣軌中的Y方點的標號全部加上一個常數d,則對於圖中的任意一條邊(i, j, W)(i爲X方點,j爲Y方點):

  • <1>i和j都在增廣軌中:此時邊(i, j)的(lx[i]+ly[j])值不變,也就是這條邊的可行性不變(原來是可行邊則現在仍是,原來不是則現在仍不是);
  • <2>i在增廣軌中而j不在:此時邊(i, j)的(lx[i]+ly[j])的值減少了d,也就是原來這條邊不是可行邊(否則j就會被遍歷到了),而現在可能是;
  • <3>j在增廣軌中而i不在:此時邊(i, j)的(lx[i]+ly[j])的值增加了d,也就是原來這條邊不是可行邊(若這條邊是可行邊,則在遍歷到j時會緊接着執行DFS(i),此時i就會被遍歷到),現在仍不是;
  • <4>i和j都不在增廣軌中:此時邊(i, j)的(lx[i]+ly[j])值不變,也就是這條邊的可行性不變。

這樣,在進行了這一步修改操作後,圖中原來的可行邊仍可行,而原來不可行的邊現在則可能變爲可行邊。那麼d的值應取多少?顯然,整個點標不能失去可行性,也就是對於上述的第<2>類邊,其lx[i]+ly[j]>=W這一性質不能被改變,故取所有第<2>類邊的(lx[i]+ly[j]-W)的最小值作爲d值即可。這樣一方面可以保證點標的可行性,另一方面,經過這一步後,圖中至少會增加一條可行邊。
(5)修改後,繼續對這個X方點DFS增廣,若還失敗則繼續修改,直到成功爲止;

7.1 分析整個算法的時間複雜度:

每次修改後,圖中至少會增加一條可行邊,故最多增廣M次、修改M次就可以找到僅由可行邊組成的完全匹配(除非圖中不存在完全匹配,這個可以通過預處理得到),故整個算法的時間複雜度爲O(M * (N + 一次修改點標的時間))。

而一次修改點標的時間取決於計算d值的時間,如果暴力枚舉計算,這一步的時間爲O(M),

優化:可以對每個Y方點設立一個slk值,表示在DFS增廣過程中,所有搜到的與該Y方點關聯的邊的(lx+ly-W)的最小值(這樣的邊的X方點必然在增廣軌中)。每次DFS增廣前,將所有Y方點的slk值設爲+∞,若增廣失敗,則取所有不在增廣軌中的Y方點的slk值的最小值爲d值。這樣一次修改點標的時間降爲O(N),總時間複雜度降爲O(NM)。

需要注意的一點是,在增廣過程中需要記下每個X、Y方點是否被遍歷到,即fx[i]、fy[j]。因此,在每次增廣前(不是對每個X方點增廣前)就要將所有fx和fy值清空。

O(n^3)的優化:
如果每次都花O(n^2)的時間時間去找 min(lx[i] + ly[j] - mp[i][j]);顯然,總的時間複雜度是O(n^3),這裏加一個slack[]數組,記錄每次dfs找完美匹配時lx[i] + ly[j] - mp[i][j]的最小值,在實現多次調整時只需要在slack[]裏找到調整值d就可以。

匈牙利算法和FF算法結合得到KM算法講的很詳細:二分圖匹配之最佳匹配——KM算法
https://www.cnblogs.com/Lanly/p/6291214.html
這個博客講的太清楚了!
http://www.cppblog.com/MatoNo1/archive/2012/04/26/151724.html

實踐: 利用匈牙利算法對目標框和檢測框進行關聯

在這裏我們對檢測框和跟蹤框進行匹配,整個流程是遍歷檢測框和跟蹤框,並進行匹配,匹配成功的將其保留,未成功的將其刪除。

https://blog.csdn.net/zimiao552147572/article/details/105985322

from scipy.optimize import linear_sum_assignment
import numpy as np
from numba import jit
 
@jit
def iou(bb_test, bb_gt):
    """
    在兩個box間計算IOU
    :param bb_test: box1 = [x1y1x2y2] 即 [左上角的x座標,左上角的y座標,右下角的x座標,右下角的y座標]
    :param bb_gt: box2 = [x1y1x2y2]
    :return: 交併比IOU
    """
    xx1 = np.maximum(bb_test[0], bb_gt[0]) #獲取交集面積四邊形的 左上角的x座標
    yy1 = np.maximum(bb_test[1], bb_gt[1]) #獲取交集面積四邊形的 左上角的y座標
    xx2 = np.minimum(bb_test[2], bb_gt[2]) #獲取交集面積四邊形的 右下角的x座標
    yy2 = np.minimum(bb_test[3], bb_gt[3]) #獲取交集面積四邊形的 右下角的y座標
    w = np.maximum(0., xx2 - xx1) #交集面積四邊形的 右下角的x座標 - 左上角的x座標 = 交集面積四邊形的寬
    h = np.maximum(0., yy2 - yy1) #交集面積四邊形的 右下角的y座標 - 左上角的y座標 = 交集面積四邊形的高
    wh = w * h #交集面積四邊形的寬 * 交集面積四邊形的高 = 交集面積
    """
    兩者的交集面積,作爲分子。
    兩者的並集面積作爲分母。
    一方box框的面積:(bb_test[2] - bb_test[0]) * (bb_test[3] - bb_test[1])
    另外一方box框的面積:(bb_gt[2] - bb_gt[0]) * (bb_gt[3] - bb_gt[1]) 
    """
    o = wh / ( (bb_test[2] - bb_test[0]) * (bb_test[3] - bb_test[1])
               + (bb_gt[2] - bb_gt[0]) * (bb_gt[3] - bb_gt[1])
               - wh)
    return o
 
"""
利用匈牙利算法對跟蹤目標框和yoloV3檢測結果框進行關聯匹配,整個流程是遍歷檢測結果框和跟蹤目標框,並進行兩兩的相似度最大的比對。
相似度最大的認爲是同一個目標則匹配成功的將其保留,相似度低的未成功匹配的將其刪除。
使用的是通過yoloV3得到的“並且和預測框相匹配的”檢測框來更新卡爾曼濾波器得到的預測框。
    detections:通過yoloV3得到的檢測結果框
    trackers:通過卡爾曼濾波器得到的預測結果跟蹤目標框
    iou_threshold=0.3:大於IOU閾值則認爲是同一個目標則匹配成功將其保留,小於IOU閾值則認爲不是同一個目標則未成功匹配將其刪除。
    return返回值:
        matches:跟蹤成功目標的矩陣。即前後幀都存在的目標,並且匹配成功同時大於iou閾值。
        np.array(unmatched_detections):新增目標指的就是存在於detections檢測結果框當中,但不存在於trackers預測結果跟蹤目標框當中。
        np.array(unmatched_trackers):離開畫面的目標指的就是存在於trackers預測結果跟蹤目標框當中,但不存在於detections檢測結果框當中。
"""
def associate_detections_to_trackers(detections, trackers, iou_threshold=0.3):
    """
    將檢測框bbox與卡爾曼濾波器的跟蹤框進行關聯匹配
    :param detections:通過yoloV3得到的檢測結果框
    :param trackers:通過卡爾曼濾波器得到的預測結果跟蹤目標框
    :param iou_threshold:大於IOU閾值則認爲是同一個目標則匹配成功將其保留,小於IOU閾值則認爲不是同一個目標則未成功匹配將其刪除。
    :return:跟蹤成功目標的矩陣:matchs。即前後幀都存在的目標,並且匹配成功同時大於iou閾值。
            新增目標的矩陣:unmatched_detections。
                            新增目標指的就是存在於detections檢測結果框當中,但不存在於trackers預測結果跟蹤目標框當中。
            跟蹤失敗即離開畫面的目標矩陣:unmatched_trackers。
                            離開畫面的目標指的就是存在於trackers預測結果跟蹤目標框當中,但不存在於detections檢測結果框當中。
    """
    """
    1.跟蹤器鏈(列表):
        實際就是多個的卡爾曼濾波KalmanBoxTracker自定義類的實例對象組成的列表。
        每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),
        KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統計參數,
        並且使用類屬性負責記錄卡爾曼濾波器的創建個數,增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。
        把每個卡爾曼濾波器(KalmanBoxTracker實例對象)都存儲到跟蹤器鏈(列表)中。
    2.unmatched_detections(列表):
        檢測框中出現新目標,但此時預測框(跟蹤框)中仍不不存在該目標,
        那麼就需要在創建新目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象),
        然後把新目標對應的KalmanBoxTracker類的實例對象放到跟蹤器鏈(列表)中。
    3.unmatched_trackers(列表):
        當跟蹤目標失敗或目標離開了畫面時,也即目標從檢測框中消失了,就應把目標對應的跟蹤框(預測框)從跟蹤器鏈中刪除。
        unmatched_trackers列表中保存的正是跟蹤失敗即離開畫面的目標,但該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)
        此時仍然存在於跟蹤器鏈(列表)中,因此就需要把該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)從跟蹤器鏈(列表)中刪除出去。
    """
    # 跟蹤目標數量爲0,直接構造結果
    if (len(trackers) == 0) or (len(detections) == 0):
        """
        如果卡爾曼濾波器得到的預測結果跟蹤目標框len(trackers)爲0 或者 yoloV3得到的檢測結果框len(detections)爲0 的話,
        跟蹤成功目標的矩陣:matchs 爲 np.empty((0, 2), dtype=int)
        新增目標的矩陣:unmatched_detections 爲 np.arange(len(detections))
        跟蹤失敗即離開畫面的目標矩陣:unmatched_trackers 爲 np.empty((0, 5), dtype=int)
        """
        return np.empty((0, 2), dtype=int), np.arange(len(detections)), np.empty((0, 5), dtype=int)
 
    """ 因爲要計算所有檢測結果框中每個框 和 所有跟蹤目標框中每個框 兩兩之間 的iou相似度計算,
        即所有檢測結果框中每個框 都要和 所有跟蹤目標框中每個框 進行兩兩之間 的iou相似度計算,
        所以iou_matrix需要初始化爲len(detections檢測結果框) * len(trackers跟蹤目標框) 形狀的0初始化的矩陣。 """
    # iou 不支持數組計算。逐個計算兩兩間的交併比,調用 linear_assignment 進行匹配
    iou_matrix = np.zeros((len(detections), len(trackers)), dtype=np.float32)
    # 遍歷目標檢測(yoloV3檢測)的bbox集合,每個檢測框的標識爲d,det爲檢測結果框
    for d, det in enumerate(detections):
        # 遍歷跟蹤框(卡爾曼濾波器預測)bbox集合,每個跟蹤框標識爲t,trackers爲跟蹤目標框
        for t, trk in enumerate(trackers):
            """ 
            遍歷每個檢測結果框 和 遍歷每個跟蹤目標框 進行兩兩之間 的iou相似度計算。
            行索引值對應的是目標檢測框。列索引值對應的是跟蹤目標框。
            """
            iou_matrix[d, t] = iou(det, trk)
 
    """ 
    row_ind, col_ind=linear_sum_assignment(-iou_matrix矩陣) 
        通過匈牙利算法得到最優匹配度的“跟蹤框和檢測框之間的”兩兩組合。
        通過相同下標位置的行索引和列索引即可從iou_matrix矩陣得到“跟蹤框和檢測框之間的”兩兩組合最優匹配度的IOU值。
        -iou_matrix矩陣:linear_assignment的輸入是cost成本矩陣,IOU越大對應的分配代價應越小,所以iou_matrix矩陣需要取負號。
        row_ind:行索引構建的一維數組。行索引值對應的是目標檢測框。
        col_ind:列索引構建的一維數組。列索引值對應的是跟蹤目標框。
        比如:
            row_ind:[0 1 2 3]。col_ind列索引:[3 2 1 0]。
            np.array(list(zip(*result))):[[0 3] [1 2] [2 1] [3 0]]
    """
    # 通過匈牙利算法將跟蹤框和檢測框以[[d,t]...]的二維矩陣的形式存儲在match_indices中
    result = linear_sum_assignment(-iou_matrix)
    matched_indices = np.array(list(zip(*result)))
 
    """ np.array(unmatched_detections):新增目標指的就是存在於detections檢測結果框當中,但不存在於trackers預測結果跟蹤目標框當中 """
    # 記錄未匹配的檢測框及跟蹤框
    # 未匹配的檢測框放入unmatched_detections中,表示有新的目標進入畫面,要新增跟蹤器跟蹤目標
    unmatched_detections = []
    for d, det in enumerate(detections):
        """ matched_indices[:, 0]:取出的是每行的第一列,代表的是目標檢測框。
           如果目標檢測框的索引d不存在於匹配成功的matched_indices中每行的第一列的話,代表目標檢測框中有新的目標出現在畫面中,
           則把未匹配的目標檢測框放入到unmatched_detections中表示需要新增跟蹤器進行跟蹤目標。
        """
        if d not in matched_indices[:, 0]:
            unmatched_detections.append(d)
 
    """ np.array(unmatched_trackers):離開畫面的目標指的就是存在於trackers預測結果跟蹤目標框當中,但不存在於detections檢測結果框當中 """
    # 未匹配的跟蹤框放入unmatched_trackers中,表示目標離開之前的畫面,應刪除對應的跟蹤器
    unmatched_trackers = []
    for t, trk in enumerate(trackers):
        """ matched_indices[:, 1]:取出的是每行的第二列,代表的是跟蹤目標框。
           如果跟蹤目標框的索引t不存在於匹配成功的matched_indices中每行的第二列的話,代表跟蹤目標框中有目標離開了畫面,
           則把未匹配的跟蹤目標框放入到unmatched_trackers中表示需要刪除對應的跟蹤器。
        """
        if t not in matched_indices[:, 1]:
            unmatched_trackers.append(t)
 
    """ matches:跟蹤成功目標的矩陣。即前後幀都存在的目標,並且匹配成功同時大於iou閾值。
        即把匹配成功的matched_indices中的並且小於iou閾值的[d,t]放到matches中。
    """
    # 將匹配成功的跟蹤框放入matches中
    matches = []
    for m in matched_indices:
        """
        m[0]:每行的第一列,代表的是目標檢測框。m[1]:每行的第二列,代表的是跟蹤目標框。
        iou_matrix[m[0], m[1]] < iou_threshold:
            根據目標檢測框的索引作爲行索引,跟蹤目標框的索引作爲列索引,
            即能找到“跟蹤框和檢測框之間的”兩兩組合最優匹配度的IOU值,如果該IOU值小於iou閾值的話,
            則把目標檢測框放到unmatched_detections中,把跟蹤目標框放到unmatched_trackers中。
        """
        # 過濾掉IOU低的匹配,將其放入到unmatched_detections和unmatched_trackers
        if iou_matrix[m[0], m[1]] < iou_threshold:
            unmatched_detections.append(m[0]) #m[0]:每行的第一列,代表的是目標檢測框。
            unmatched_trackers.append(m[1])   #m[1]:每行的第二列,代表的是跟蹤目標框。
        # 滿足條件的以[[d,t]...]的形式放入matches中
        else:
            """ 存儲到列表中的每個元素的形狀爲(1, 2) """
            matches.append(m.reshape(1, 2))
 
    """
    如果矩陣matches中不存在任何跟蹤成功的目標的話,則創建空數組返回。
    numpy.concatenate((a1,a2,...), axis=0):能夠一次完成多個數組a1,a2,...的拼接。
    >>> a=np.array([1,2,3])
    >>> b=np.array([11,22,33])
    >>> c=np.array([44,55,66])
    >>> np.concatenate((a,b,c),axis=0)  # 默認情況下,axis=0可以不寫
    array([ 1,  2,  3, 11, 22, 33, 44, 55, 66]) #對於一維數組拼接,axis的值不影響最後的結果
    """
    # 初始化matches,以np.array的形式返回
    if len(matches) == 0:
        matches = np.empty((0, 2), dtype=int)
    else:
        """ 
        np.concatenate(matches, axis=0):
            [array([[0, 0]], dtype=int64), array([[1, 1]], dtype=int64),  。。。] 轉換爲 [[0, 0] [1, 1] 。。。]
        """
        matches = np.concatenate(matches, axis=0) # 默認情況下,axis=0可以不寫
 
    return matches, np.array(unmatched_detections), np.array(unmatched_trackers)

多目標跟蹤,即MOT(Multi-Object Tracking),也就是在一段視頻中同時跟蹤多個目標

在這裏插入圖片描述

初始化問題

https://blog.csdn.net/zimiao552147572/article/details/105941324

多目標跟蹤問題中並不是所有目標都會在第一幀出現,也並不是所有目標都會出現在每一幀。那如何對出現的目標進行初始化,可以作爲跟蹤算法的分類錶針。

目標跟蹤的常見的分類方法

常見的初始化方法分爲兩大類,一個是Detection-Based-Tracking(DBT),一個是Detection-Free-Tracking(DFT)。下圖比較形象地說明了兩類算法的區別。
在這裏插入圖片描述
DBT

DBT的方式就是典型的tracking-by-detection模式,即先檢測目標,然後將目標關聯進入跟蹤軌跡中。那麼就存在兩個問題,第一,該跟蹤方式非常依賴目標檢測器的性能,第二,目標檢測的實質是分類和迴歸,即該跟蹤方式只能針對特定的目標類型,如:行人、車輛、動物。DBT則是目前業界研究的主流。

DFT

DFT是單目標跟蹤領域的常用初始化方法,即每當新目標出現時,人爲告訴算法新目標的位置,這樣做的好處是target free,壞處就是過程比較麻煩,存在過多的交互,所以DBT相對來說更受歡迎。

處理模式

MOT也存在着不同的處理模式,Online和Offline兩大類,其主要區別在於是否用到了後續幀的信息。下圖形象解釋了Online與Offline跟蹤的區別。
Online Tracking

Online Tracking是對視頻幀逐幀進行處理,當前幀的跟蹤僅利用過去的信息。

Offline Tracking

不同於Online Tracking,Offline Tracking會利用前後視頻幀的信息對當前幀進行目標跟蹤,這種方式只適用於視頻,如果應用於攝像頭,則會有滯後效應,通常採用時間窗方式進行處理,以節省內存和加速。

運動模型

爲了簡化多目標跟蹤的難度,我們引入運動模型類簡化求解過程,運動模型捕捉目標的動態行爲,它估計目標在未來幀中的潛在位置,從而減少搜索空間。

跟蹤方法

多目標跟蹤中基於神經網絡的算法,端到端的算法並不多,主要還在實驗室的刷榜階段,模型複雜,速度慢,追蹤結果也不好,我們就不再介紹,主要給大家介紹以下兩種主流的算法:

在這裏插入圖片描述

基於Kalman和KM算法的後端優化算法

該類算法能達到實時性,但依賴於檢測算法效果要好,特徵區分要好(輸出最終結果的好壞依賴於較強的檢測算法,而基於卡爾曼加匈牙利匹配的追蹤算法作用在於能夠輸出檢測目標的id,其次能保證追蹤算法的實時性),這樣追蹤效果會好,id切換少。代表性的算法是SORT/DeepSORT。
在這裏插入圖片描述
SORT 是一種實用的多目標跟蹤算法,引入了線性速度模型與卡爾曼濾波來進行位置預測,在無合適匹配檢測框的情況下,使用運動模型來預測物體的位置。匈牙利算法是一種尋找二分圖的最大匹配的算法,在多目標跟蹤問題中可以簡單理解爲尋找前後兩幀的若干目標的匹配最優解的一種算法。而卡爾曼濾波可以看作是一種運動模型,用來對目標的軌跡進行預測,並且使用確信度較高的跟蹤結果進行預測結果的修正。

多目標追蹤一般接在目標檢測後。在工業界目標檢測採用比較多的是yolo檢測網絡****,單階段式,速度快,精度不差,部署在NV的平臺幀率可以達到30fps以上。所以要實現目標檢測代碼和多目標追蹤代碼集成的任務,需要先將兩者的框架統一。先實現目標檢測網絡,檢測的輸出結果主要是將檢測框的位置信息輸入到多目標追蹤算法中。

基於多線程的單目標跟蹤的多目標跟蹤算法

這類算法特點是跟蹤效果會很好,因爲其爲每一類物體都單獨分配了一個跟蹤器。但該算法對目標尺度變化要求較大,參數調試需要合理,同時該算法極耗cpu資源,實時性不高,代表算法是利用KCF進行目標跟蹤。

多目標追蹤本質上是多個目標同時運動的問題,所以有提出將單目標跟蹤器引入到多目標追蹤的問題,爲每一個目標分配一個跟蹤器,然後間接地使用匹配算法來修正那些跟蹤失敗或新出現的目標。代表性的單目標跟蹤算法爲核相關濾波算法(KCF),在精度和速度上能夠同時達到很高的水平,是當時單目標跟蹤最優秀的算法之一,後來的很多單目標跟蹤算法都是基於此做的改進。

實際應用過程中會爲每個目標分配一個KCF跟蹤器並採用多線程的方式來組織這些跟蹤器。同時因爲實際硬件條件的限制,不可能提供強大的計算力資源,會採用檢測器與跟蹤器交替進行的跟蹤策略。由於檢測的幀率不高,使得跟蹤的維持效果出現滯後或框飄的現象較爲嚴重,實用性不大。

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