DeepSORT多目标跟踪算法
DeepSORT 多目標跟蹤算法
整體思路
SORT 算法的思路是將目標檢測算法得到的檢測框與預測的跟蹤框的 iou(交并比)輸入到匈牙利算法中進行線性分配來關聯幀間 Id。而 DeepSORT 算法則是將目標的外觀信息加入到幀間匹配的計算中,這樣在目標被遮擋但后續再次出現的情況下,還能正確匹配 Id,從而減少 Id Switch。
算法思路
狀態估計(state estimation)和軌跡處理(track handing)
狀態估計
用一個 8 維空間表示軌跡在某個時刻的狀態即(u,v,γ,h,x˙,y˙,γ˙,h˙)(u, v, \gamma, h, \dot{x}, \dot{y}, \dot{\gamma}, \dot{h})(u,v,γ,h,x˙,y˙?,γ˙?,h˙),(u,v)(u,v)(u,v)表示 bbox 的中心坐標,γ\gammaγ表示寬高比,hhh表示高度,最后四個變量代表前四個變量的速度信息。使用一個基于勻速模型和線性觀測模型的標準卡爾曼濾波器進行目標狀態的預測,預測結果為(u,v,γ,h)(u,v,\gamma , h)(u,v,γ,h)。具體關于卡爾曼濾波的解釋,可以見我單獨說明的文章。
軌跡處理
針對跟蹤器:
設置計數器,在使用卡爾曼濾波進行預測時遞增,一旦預測的跟蹤結果和目標檢測算法檢測的結果成功匹配,則將該跟蹤器的計數器置零;如果一個跟蹤器在一段時間內一直沒能匹配上檢測的結果,則認為跟蹤的目標消失,從跟蹤器列表中刪除該跟蹤器。
針對新檢測結果:
當某一幀出現了新的檢測結果時(即與當前跟蹤結果無法匹配的檢測結果),認為可能出現了新的目標,為其創建跟蹤器。不過,仍需要觀察,如果連續三幀中新跟蹤器對目標的預測結果都能和檢測結果匹配,那么確認出現了新的目標軌跡(代碼實現中軌跡state置為confirmed),否則刪除該跟蹤器。
匹配問題
SORT 算法是將檢測框和跟蹤框的 IOU 情況作為輸入,采用匈牙利算法(這是一種通過增廣路徑來求二分圖最大匹配的一種方法),輸出檢測框和跟蹤框的匹配結果。而 DeepSORT 為了避免大量的 Id Switch,同時考慮了運動信息的關聯和目標外觀信息的關聯,使用融合度量的方式計算檢測結果和跟蹤結果的匹配程度。通過結合目標框的馬氏距離和特征余弦距離兩個度量來整合運動信息和外觀信息,一方面,馬氏距離基于運動信息,提供了有關目標的可能位置的信息,這對短期預測是有效的;另一方面,余弦距離考慮外觀信息,這對長期遮擋的目標找回ID比較有效,因為此時運動不具有辨別力。
運動信息的關聯
使用檢測框和跟蹤框之間的馬氏距離來描述運動關聯程度。
d(1)(i,j)=(dj?yi)TSi?1(dj?yi)d^{(1)}(i, j)=\left(\boldsymbolozvdkddzhkzd_{j}-\boldsymbol{y}_{i}\right)^{\mathrm{T}} \boldsymbol{S}_{i}^{-1}\left(\boldsymbolozvdkddzhkzd_{j}-\boldsymbol{y}_{i}\right)d(1)(i,j)=(dj??yi?)TSi?1?(dj??yi?)
其中,djd_jdj?表示第jjj個檢測框的位置,yiy_iyi?表示第iii個跟蹤器的預測框位置,SiS_iSi?則表示檢測位置與平均跟蹤位置之間的協方差矩陣。馬氏距離通過計算檢測位置與平均預測位置之間的標準差將狀態測量的不確定性進行了考慮,并且通過逆χ2\chi^2χ2分布計算得來的95%95\%95%置信區間對馬氏距離進行閾值化處理,若某一次關聯的馬氏距離小于指定的閾值t(1)t^{(1)}t(1),則設置運動狀態關聯成功,實驗中設置閾值為9.4877。
bi,j(1)=1[d(1)(i,j)≤t(1)]b_{i, j}^{(1)}=\mathbb{1}\left[d^{(1)}(i, j) \leq t^{(1)}\right]bi,j(1)?=1[d(1)(i,j)≤t(1)]
目標外觀信息的關聯
當運動的不確定性很低的時候,上述的馬氏距離匹配是一個合適的關聯度量方法,但是在圖像空間中使用卡爾曼濾波進行運動狀態估計只是一個比較粗糙的預測。特別是相機存在運動時會在圖像平面中引入快速位移,會使得遮擋情況下馬氏距離度量非常不準確的使得關聯方法失效,造成 ID switch 的現象。
因此作者引入了第二種關聯方法,對每一個檢測框djd_jdj?求一個特征向量rjr_jrj? (通過 REID 的 CNN 網絡計算得到的對應的 128 維特征向量),限制條件是∣∣rj∣∣=1||rj||=1∣∣rj∣∣=1。作者對每一個跟蹤目標構建一個gallary,存儲每一個跟蹤目標成功關聯的最近100幀的特征向量。那么第二種度量方式就是計算第iii個跟蹤器的最近 100 個成功關聯的特征集與當前幀第jjj個檢測結果的特征向量間的最小余弦距離。計算公式如下:(注意:軌跡太長,導致外觀發生變化,發生變化后,再使用最小余弦距離作為度量會出問題,所以在計算距離時,軌跡中的檢測數量不能太多)
d(2)(i,j)=min?{1?rjTrk(i)∣rk(i)∈Ri}d^{(2)}(i, j)=\min \left\{1-r_{j}^{\mathrm{T}} \boldsymbol{r}_{k}^{(i)} | \boldsymbol{r}_{k}^{(i)} \in \mathcal{R}_{i}\right\}d(2)(i,j)=min{1?rjT?rk(i)?∣rk(i)?∈Ri?}
如果上面的距離小于指定的閾值,那么這個關聯就是成功的。閾值是從單獨的訓練集里得到的,具體如下。
bi,j(2)=1[d(2)(i,j)≤t(2)]b_{i, j}^{(2)}=\mathbb{1}\left[d^{(2)}(i, j) \leq t^{(2)}\right]bi,j(2)?=1[d(2)(i,j)≤t(2)]
關聯方式融合
使用兩種度量方式的線性加權作為最終的度量。
ci,j=λd(1)(i,j)+(1?λ)d(2)(i,j)c_{i, j}=\lambda d^{(1)}(i, j)+(1-\lambda) d^{(2)}(i, j)ci,j?=λd(1)(i,j)+(1?λ)d(2)(i,j)
注意:只有當兩個指標都滿足各自閾值條件的時候才進行融合。距離度量對短期的預測和匹配效果很好,但對于長時間的遮擋的情況,使用外觀特征的度量比較有效。作者指出,對于存在相機運動的情況,可以設置λ=0\lambda=0λ=0。但是,馬氏距離的閾值仍然生效,如果不滿足第一個度量的標準,就不能進入ci,jc_{i,j}ci,j?的融合階段。
此時,需要考慮的閾值就變為下式。此時,僅當關聯在兩個度量的選通區域內,稱其為可接受。實際上在代碼實現的過程中,是以外觀距離為主,運動距離只是作為門限矩陣進行進一步過濾代價矩陣。
bi,j=∏m=12bi,j(m)b_{i, j}=\prod_{m=1}^{2} b_{i, j}^{(m)}bi,j?=m=1∏2?bi,j(m)?
級聯匹配
一個目標長時間被遮擋之后,卡爾曼濾波預測的不確定性就會大大增加,狀態空間內的可觀察性就會大大降低。假如此時兩個跟蹤器競爭同一個檢測結果的匹配權,往往遮擋時間較長的那條軌跡因為長時間未更新位置信息,追蹤預測位置的不確定性更大,即協方差會更大,馬氏距離計算時使用了協方差的倒數,因此馬氏距離會更小,因此使得檢測結果更可能和遮擋時間較長的那條軌跡相關聯,這種不理想的效果往往會破壞追蹤的持續性。
簡單理解,假設本來協方差矩陣是一個正態分布,那么連續的預測不更新就會導致這個正態分布的方差越來越大,那么離均值歐氏距離遠的點可能和之前分布中離得較近的點獲得同樣的馬氏距離值。
所以,作者使用了級聯匹配來對更加頻繁出現的目標賦予優先權,具體算法如下圖(圖源自論文)。
T表示當前的跟蹤狀態集合,D表示當前的檢測狀態集合。
第一行根據公式5計算融合度量的代價矩陣;
第二行計算融合的閾值;
第三行初始化已匹配集合為空集;
第四行初始化未匹配集合U為檢測集合D;
第五行表示對跟蹤狀態集合從1到最大跟蹤時間Amax,由近到遠循環;
第六行表示根據時間選擇跟蹤的軌跡;
第七行表示計算最小匹配的軌跡的ID即xi,jx_{i,j}xi,j?;
第八行表示將第七步中匹配的ID加入到M中;
第九行表示將上述ID從U中刪除;
第十行表示結束循環;
第十一行表示返回最終匹配集合M和未匹配集合U。
級聯匹配的核心思想就是由小到大對消失時間相同的軌跡進行匹配,這樣首先保證了對最近出現的目標賦予最大的優先權,也解決了上面所述的問題。在匹配的最后階段還對 unconfirmed和age=1的未匹配軌跡進行基于IoU的匹配。這可以緩解因為表觀突變或者部分遮擋導致的較大變化。
深度特征提取
網絡結果如下圖。
要求輸入的圖像為128*64,輸出128維的特征向量。我在使用Pytorch實現時將上述結構中的池化換成了stride為2的卷積,輸出隱層換位256維特征向量,以增大一定參數量的代價試圖獲得更好的結果。
由于主要用于行人識別,所以在行人重識別數據集(MARS)上離線訓練模型,學到的參數很適合提取行人特征,最后輸出256維的歸一化后的特征。
核心模型結構代碼如下。
class BasicBlock(nn.Module):def __init__(self, c_in, c_out, is_downsample=False):super(BasicBlock, self).__init__()self.is_downsample = is_downsampleif is_downsample:self.conv1 = nn.Conv2d(c_in, c_out, 3, stride=2, padding=1, bias=False)else:self.conv1 = nn.Conv2d(c_in, c_out, 3, stride=1, padding=1, bias=False)self.bn1 = nn.BatchNorm2d(c_out)self.relu = nn.ReLU(True)self.conv2 = nn.Conv2d(c_out, c_out, 3, stride=1, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(c_out)if is_downsample:self.downsample = nn.Sequential(nn.Conv2d(c_in, c_out, 1, stride=2, bias=False),nn.BatchNorm2d(c_out))elif c_in != c_out:self.downsample = nn.Sequential(nn.Conv2d(c_in, c_out, 1, stride=1, bias=False),nn.BatchNorm2d(c_out))self.is_downsample = Truedef forward(self, x):y = self.conv1(x)y = self.bn1(y)y = self.relu(y)y = self.conv2(y)y = self.bn2(y)if self.is_downsample:x = self.downsample(x)return F.relu(x.add(y), True) # 殘差連接def make_layers(c_in, c_out, repeat_times, is_downsample=False):blocks = []for i in range(repeat_times):if i == 0:blocks += [BasicBlock(c_in, c_out, is_downsample=is_downsample), ]else:blocks += [BasicBlock(c_out, c_out), ]return nn.Sequential(*blocks)class Net(nn.Module):def __init__(self, num_classes=1261, reid=False):""":param num_classes: 分類器層輸出的類別數目:param reid: 是否為reid模式,若為True,直接返回特征向量而不做分類"""super(Net, self).__init__()# 3 128 64self.conv = nn.Sequential(nn.Conv2d(3, 64, 3, stride=1, padding=1),nn.BatchNorm2d(64),nn.ReLU(inplace=True),nn.MaxPool2d(3, 2, padding=1),)# 32 64 32self.layer1 = make_layers(64, 64, 2, False)# 32 64 32self.layer2 = make_layers(64, 128, 2, True)# 64 32 16self.layer3 = make_layers(128, 256, 2, True)# 128 16 8self.layer4 = make_layers(256, 512, 2, True)# 256 8 4self.avgpool = nn.AvgPool2d((8, 4), 1)# 256 1 1 self.reid = reidself.classifier = nn.Sequential(nn.Linear(512, 256),nn.BatchNorm1d(256),nn.ReLU(inplace=True),nn.Dropout(),nn.Linear(256, num_classes),)def forward(self, x):x = self.conv(x)x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)x = self.avgpool(x)x = x.view(x.size(0), -1)# B x 256if self.reid:x = x / x.norm(p=2, dim=1, keepdim=True) # 張量單位化return x# 分類器x = self.classifier(x)return x使用GPU在MARS數據集訓練50輪的結果如下。
流程描述
如下圖。
相比于SORT,DeepSORT主要更新就是加入了深度特征提取器和級聯匹配,其余并沒有太大變化,還是按照SORT那一套進行。
最后,DeepSORT算法封裝的跟蹤器類源碼如下,其中各功能已經詳細備注。
import numpy as np from . import kalman_filter from . import linear_assignment from . import iou_matching from .track import Trackclass Tracker:"""多目標跟蹤器實現"""def __init__(self, metric, max_iou_distance=0.7, max_age=70, n_init=3):self.metric = metricself.max_iou_distance = max_iou_distanceself.max_age = max_ageself.n_init = n_initself.kf = kalman_filter.KalmanFilter()self.tracks = []self._next_id = 1def predict(self):"""狀態預測"""for track in self.tracks:track.predict(self.kf)def update(self, detections):"""狀態更新"""# 級聯匹配matches, unmatched_tracks, unmatched_detections = self._match(detections)# Update track set.for track_idx, detection_idx in matches:# 成功匹配的要用檢測結果更新對于track的參數# 包括# 更新卡爾曼濾波一系列運動變量、命中次數以及重置time_since_update# 檢測的深度特征保存到track的特征集中# 連續命中三幀,將track狀態由tentative改為confirmedself.tracks[track_idx].update(self.kf, detections[detection_idx])for track_idx in unmatched_tracks:# 未成功匹配的track# 若未經過confirm則刪除# 若已經confirm但連續max_age幀未匹配到檢測結果也刪除self.tracks[track_idx].mark_missed()for detection_idx in unmatched_detections:# 未匹配的檢測,為其創建新的trackself._initiate_track(detections[detection_idx])self.tracks = [t for t in self.tracks if not t.is_deleted()]# Update distance metric.# 更新已經確認的track的特征集active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]features, targets = [], []for track in self.tracks:if not track.is_confirmed():continuefeatures += track.featurestargets += [track.track_id for _ in track.features]track.features = []self.metric.partial_fit(np.asarray(features), np.asarray(targets), active_targets)def _match(self, detections):"""跟蹤結果和檢測結果的匹配:param detections::return:"""def gated_metric(tracks, dets, track_indices, detection_indices):features = np.array([dets[i].feature for i in detection_indices])targets = np.array([tracks[i].track_id for i in track_indices])cost_matrix = self.metric.distance(features, targets)cost_matrix = linear_assignment.gate_cost_matrix(self.kf, cost_matrix, tracks, dets, track_indices,detection_indices)return cost_matrix# 將track分為確認track和未確認trackconfirmed_tracks = [i for i, t in enumerate(self.tracks) if t.is_confirmed()]unconfirmed_tracks = [i for i, t in enumerate(self.tracks) if not t.is_confirmed()]# 將確認的track和檢測結果進行級聯匹配(使用外觀特征)matches_a, unmatched_tracks_a, unmatched_detections = linear_assignment.matching_cascade(gated_metric, self.metric.matching_threshold, self.max_age,self.tracks, detections, confirmed_tracks)# 將上一步未成功匹配的track和未確認的track組合到一起形成iou_track_candidates于還沒有匹配結果的檢測結果進行IOU匹配iou_track_candidates = unconfirmed_tracks + [k for k in unmatched_tracks_a ifself.tracks[k].time_since_update == 1]unmatched_tracks_a = [k for k in unmatched_tracks_a ifself.tracks[k].time_since_update != 1]# 計算兩兩之間的iou,再通過1-iou得到cost matrixmatches_b, unmatched_tracks_b, unmatched_detections = linear_assignment.min_cost_matching(iou_matching.iou_cost, self.max_iou_distance, self.tracks,detections, iou_track_candidates, unmatched_detections)matches = matches_a + matches_b # 組合獲得當前所有匹配結果unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))return matches, unmatched_tracks, unmatched_detectionsdef _initiate_track(self, detection):"""初始化新的跟蹤器,對應新的檢測結果:param detection::return:"""# 初始化卡爾曼mean, covariance = self.kf.initiate(detection.to_xyah())# 創建新的跟蹤器self.tracks.append(Track(mean, covariance, self._next_id, self.n_init, self.max_age,detection.feature))# id自增self._next_id += 1項目說明
本項目主要分為三大模塊,deepsort算法模塊(其中又分deep模塊和sort模塊),yolo3檢測模塊,以及web模塊。最終封裝為一個跟蹤器模塊,用于外部接口調用,該模塊接受一個視頻或者圖片序列。
其中,deepsort算法模塊包含深度外觀特征提取器的deep模塊(使用Pytorch實現及訓練)以及原始sort跟蹤算法模塊(該模塊部分內容參考SORT論文源碼);yolo3檢測模塊調用封裝好的Pytorch實現的YOLO3算法,做了本部分API的兼容;web模塊則以Django為框架實現了模型的后端部署,用戶通過網頁提交視頻,后端解析生成跟蹤結果(由于機器限制,目前只返回部分幀的檢測結果,實時生成依賴GPU服務器,個人電腦FPS較低。)
具體演示如下(瀏覽器訪問)。
直接執行腳本也可以生成跟蹤后的視頻文件,如下。
補充說明
具體代碼開源于我的Github,歡迎star或者fork。
總結
以上是生活随笔為你收集整理的DeepSORT多目标跟踪算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Zotero参考文献管理
- 下一篇: Ubuntu循环登录