DeepSORT多目标跟踪算法
DeepSORT 多目標跟蹤算法
整體思路
SORT 算法的思路是將目標檢測算法得到的檢測框與預測的跟蹤框的 iou(交并比)輸入到匈牙利算法中進行線性分配來關(guān)聯(lián)幀間 Id。而 DeepSORT 算法則是將目標的外觀信息加入到幀間匹配的計算中,這樣在目標被遮擋但后續(xù)再次出現(xiàn)的情況下,還能正確匹配 Id,從而減少 Id Switch。
算法思路
狀態(tài)估計(state estimation)和軌跡處理(track handing)
狀態(tài)估計
用一個 8 維空間表示軌跡在某個時刻的狀態(tài)即(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表示高度,最后四個變量代表前四個變量的速度信息。使用一個基于勻速模型和線性觀測模型的標準卡爾曼濾波器進行目標狀態(tài)的預測,預測結(jié)果為(u,v,γ,h)(u,v,\gamma , h)(u,v,γ,h)。具體關(guān)于卡爾曼濾波的解釋,可以見我單獨說明的文章。
軌跡處理
針對跟蹤器:
設(shè)置計數(shù)器,在使用卡爾曼濾波進行預測時遞增,一旦預測的跟蹤結(jié)果和目標檢測算法檢測的結(jié)果成功匹配,則將該跟蹤器的計數(shù)器置零;如果一個跟蹤器在一段時間內(nèi)一直沒能匹配上檢測的結(jié)果,則認為跟蹤的目標消失,從跟蹤器列表中刪除該跟蹤器。
針對新檢測結(jié)果:
當某一幀出現(xiàn)了新的檢測結(jié)果時(即與當前跟蹤結(jié)果無法匹配的檢測結(jié)果),認為可能出現(xiàn)了新的目標,為其創(chuàng)建跟蹤器。不過,仍需要觀察,如果連續(xù)三幀中新跟蹤器對目標的預測結(jié)果都能和檢測結(jié)果匹配,那么確認出現(xiàn)了新的目標軌跡(代碼實現(xiàn)中軌跡state置為confirmed),否則刪除該跟蹤器。
匹配問題
SORT 算法是將檢測框和跟蹤框的 IOU 情況作為輸入,采用匈牙利算法(這是一種通過增廣路徑來求二分圖最大匹配的一種方法),輸出檢測框和跟蹤框的匹配結(jié)果。而 DeepSORT 為了避免大量的 Id Switch,同時考慮了運動信息的關(guān)聯(lián)和目標外觀信息的關(guān)聯(lián),使用融合度量的方式計算檢測結(jié)果和跟蹤結(jié)果的匹配程度。通過結(jié)合目標框的馬氏距離和特征余弦距離兩個度量來整合運動信息和外觀信息,一方面,馬氏距離基于運動信息,提供了有關(guān)目標的可能位置的信息,這對短期預測是有效的;另一方面,余弦距離考慮外觀信息,這對長期遮擋的目標找回ID比較有效,因為此時運動不具有辨別力。
運動信息的關(guān)聯(lián)
使用檢測框和跟蹤框之間的馬氏距離來描述運動關(guān)聯(lián)程度。
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?則表示檢測位置與平均跟蹤位置之間的協(xié)方差矩陣。馬氏距離通過計算檢測位置與平均預測位置之間的標準差將狀態(tài)測量的不確定性進行了考慮,并且通過逆χ2\chi^2χ2分布計算得來的95%95\%95%置信區(qū)間對馬氏距離進行閾值化處理,若某一次關(guān)聯(lián)的馬氏距離小于指定的閾值t(1)t^{(1)}t(1),則設(shè)置運動狀態(tài)關(guān)聯(lián)成功,實驗中設(shè)置閾值為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)]
目標外觀信息的關(guān)聯(lián)
當運動的不確定性很低的時候,上述的馬氏距離匹配是一個合適的關(guān)聯(lián)度量方法,但是在圖像空間中使用卡爾曼濾波進行運動狀態(tài)估計只是一個比較粗糙的預測。特別是相機存在運動時會在圖像平面中引入快速位移,會使得遮擋情況下馬氏距離度量非常不準確的使得關(guān)聯(lián)方法失效,造成 ID switch 的現(xiàn)象。
因此作者引入了第二種關(guān)聯(lián)方法,對每一個檢測框djd_jdj?求一個特征向量rjr_jrj? (通過 REID 的 CNN 網(wǎng)絡(luò)計算得到的對應(yīng)的 128 維特征向量),限制條件是∣∣rj∣∣=1||rj||=1∣∣rj∣∣=1。作者對每一個跟蹤目標構(gòu)建一個gallary,存儲每一個跟蹤目標成功關(guān)聯(lián)的最近100幀的特征向量。那么第二種度量方式就是計算第iii個跟蹤器的最近 100 個成功關(guān)聯(lián)的特征集與當前幀第jjj個檢測結(jié)果的特征向量間的最小余弦距離。計算公式如下:(注意:軌跡太長,導致外觀發(fā)生變化,發(fā)生變化后,再使用最小余弦距離作為度量會出問題,所以在計算距離時,軌跡中的檢測數(shù)量不能太多)
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?}
如果上面的距離小于指定的閾值,那么這個關(guān)聯(lián)就是成功的。閾值是從單獨的訓練集里得到的,具體如下。
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)]
關(guān)聯(lián)方式融合
使用兩種度量方式的線性加權(quán)作為最終的度量。
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)
注意:只有當兩個指標都滿足各自閾值條件的時候才進行融合。距離度量對短期的預測和匹配效果很好,但對于長時間的遮擋的情況,使用外觀特征的度量比較有效。作者指出,對于存在相機運動的情況,可以設(shè)置λ=0\lambda=0λ=0。但是,馬氏距離的閾值仍然生效,如果不滿足第一個度量的標準,就不能進入ci,jc_{i,j}ci,j?的融合階段。
此時,需要考慮的閾值就變?yōu)橄率健4藭r,僅當關(guān)聯(lián)在兩個度量的選通區(qū)域內(nèi),稱其為可接受。實際上在代碼實現(xiàn)的過程中,是以外觀距離為主,運動距離只是作為門限矩陣進行進一步過濾代價矩陣。
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)?
級聯(lián)匹配
一個目標長時間被遮擋之后,卡爾曼濾波預測的不確定性就會大大增加,狀態(tài)空間內(nèi)的可觀察性就會大大降低。假如此時兩個跟蹤器競爭同一個檢測結(jié)果的匹配權(quán),往往遮擋時間較長的那條軌跡因為長時間未更新位置信息,追蹤預測位置的不確定性更大,即協(xié)方差會更大,馬氏距離計算時使用了協(xié)方差的倒數(shù),因此馬氏距離會更小,因此使得檢測結(jié)果更可能和遮擋時間較長的那條軌跡相關(guān)聯(lián),這種不理想的效果往往會破壞追蹤的持續(xù)性。
簡單理解,假設(shè)本來協(xié)方差矩陣是一個正態(tài)分布,那么連續(xù)的預測不更新就會導致這個正態(tài)分布的方差越來越大,那么離均值歐氏距離遠的點可能和之前分布中離得較近的點獲得同樣的馬氏距離值。
所以,作者使用了級聯(lián)匹配來對更加頻繁出現(xiàn)的目標賦予優(yōu)先權(quán),具體算法如下圖(圖源自論文)。
T表示當前的跟蹤狀態(tài)集合,D表示當前的檢測狀態(tài)集合。
第一行根據(jù)公式5計算融合度量的代價矩陣;
第二行計算融合的閾值;
第三行初始化已匹配集合為空集;
第四行初始化未匹配集合U為檢測集合D;
第五行表示對跟蹤狀態(tài)集合從1到最大跟蹤時間Amax,由近到遠循環(huán);
第六行表示根據(jù)時間選擇跟蹤的軌跡;
第七行表示計算最小匹配的軌跡的ID即xi,jx_{i,j}xi,j?;
第八行表示將第七步中匹配的ID加入到M中;
第九行表示將上述ID從U中刪除;
第十行表示結(jié)束循環(huán);
第十一行表示返回最終匹配集合M和未匹配集合U。
級聯(lián)匹配的核心思想就是由小到大對消失時間相同的軌跡進行匹配,這樣首先保證了對最近出現(xiàn)的目標賦予最大的優(yōu)先權(quán),也解決了上面所述的問題。在匹配的最后階段還對 unconfirmed和age=1的未匹配軌跡進行基于IoU的匹配。這可以緩解因為表觀突變或者部分遮擋導致的較大變化。
深度特征提取
網(wǎng)絡(luò)結(jié)果如下圖。
要求輸入的圖像為128*64,輸出128維的特征向量。我在使用Pytorch實現(xiàn)時將上述結(jié)構(gòu)中的池化換成了stride為2的卷積,輸出隱層換位256維特征向量,以增大一定參數(shù)量的代價試圖獲得更好的結(jié)果。
由于主要用于行人識別,所以在行人重識別數(shù)據(jù)集(MARS)上離線訓練模型,學到的參數(shù)很適合提取行人特征,最后輸出256維的歸一化后的特征。
核心模型結(jié)構(gòu)代碼如下。
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: 分類器層輸出的類別數(shù)目: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數(shù)據(jù)集訓練50輪的結(jié)果如下。
流程描述
如下圖。
相比于SORT,DeepSORT主要更新就是加入了深度特征提取器和級聯(lián)匹配,其余并沒有太大變化,還是按照SORT那一套進行。
最后,DeepSORT算法封裝的跟蹤器類源碼如下,其中各功能已經(jīng)詳細備注。
import numpy as np from . import kalman_filter from . import linear_assignment from . import iou_matching from .track import Trackclass Tracker:"""多目標跟蹤器實現(xiàn)"""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):"""狀態(tài)預測"""for track in self.tracks:track.predict(self.kf)def update(self, detections):"""狀態(tài)更新"""# 級聯(lián)匹配matches, unmatched_tracks, unmatched_detections = self._match(detections)# Update track set.for track_idx, detection_idx in matches:# 成功匹配的要用檢測結(jié)果更新對于track的參數(shù)# 包括# 更新卡爾曼濾波一系列運動變量、命中次數(shù)以及重置time_since_update# 檢測的深度特征保存到track的特征集中# 連續(xù)命中三幀,將track狀態(tài)由tentative改為confirmedself.tracks[track_idx].update(self.kf, detections[detection_idx])for track_idx in unmatched_tracks:# 未成功匹配的track# 若未經(jīng)過confirm則刪除# 若已經(jīng)confirm但連續(xù)max_age幀未匹配到檢測結(jié)果也刪除self.tracks[track_idx].mark_missed()for detection_idx in unmatched_detections:# 未匹配的檢測,為其創(chuàng)建新的trackself._initiate_track(detections[detection_idx])self.tracks = [t for t in self.tracks if not t.is_deleted()]# Update distance metric.# 更新已經(jīng)確認的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):"""跟蹤結(jié)果和檢測結(jié)果的匹配: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和檢測結(jié)果進行級聯(lián)匹配(使用外觀特征)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于還沒有匹配結(jié)果的檢測結(jié)果進行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 # 組合獲得當前所有匹配結(jié)果unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))return matches, unmatched_tracks, unmatched_detectionsdef _initiate_track(self, detection):"""初始化新的跟蹤器,對應(yīng)新的檢測結(jié)果:param detection::return:"""# 初始化卡爾曼mean, covariance = self.kf.initiate(detection.to_xyah())# 創(chuàng)建新的跟蹤器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模塊。最終封裝為一個跟蹤器模塊,用于外部接口調(diào)用,該模塊接受一個視頻或者圖片序列。
其中,deepsort算法模塊包含深度外觀特征提取器的deep模塊(使用Pytorch實現(xiàn)及訓練)以及原始sort跟蹤算法模塊(該模塊部分內(nèi)容參考SORT論文源碼);yolo3檢測模塊調(diào)用封裝好的Pytorch實現(xiàn)的YOLO3算法,做了本部分API的兼容;web模塊則以Django為框架實現(xiàn)了模型的后端部署,用戶通過網(wǎng)頁提交視頻,后端解析生成跟蹤結(jié)果(由于機器限制,目前只返回部分幀的檢測結(jié)果,實時生成依賴GPU服務(wù)器,個人電腦FPS較低。)
具體演示如下(瀏覽器訪問)。
直接執(zhí)行腳本也可以生成跟蹤后的視頻文件,如下。
補充說明
具體代碼開源于我的Github,歡迎star或者fork。
總結(jié)
以上是生活随笔為你收集整理的DeepSORT多目标跟踪算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Zotero参考文献管理
- 下一篇: 基于TCP的Socket通讯