车流量检测实现
日萌社
人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度學習實戰(zhàn)(不定時更新)
3.車流量檢測實現(xiàn)
學習目標
- 了解多目標跟蹤的實現(xiàn)方法
- 知道車流量統(tǒng)計的方法
隨著城市交通量的迅猛增加,車流量統(tǒng)計已成為智能交通系統(tǒng)中一項關鍵技術和熱門研究方向。高效而精確的車流量檢測可以交通管理者和決策者,以及駕駛員提供數(shù)據(jù)支撐,從而為交通調度,降低擁堵情況的發(fā)生,提高道路利用率有非常重要的意義。
車流量統(tǒng)計主要有以下幾種方式:
-
人工統(tǒng)計,需要消耗大量的人力且當工作人員在長時間計數(shù)后會因疲憊造成漏檢或重復計數(shù),統(tǒng)計結果具有不可驗證性。
-
通過安裝可接觸式或不可接觸式的傳感器于路面進行車輛計數(shù),可接觸式傳感器一般鋪設于道路下方,當車輛經(jīng)過時,傳感器內部的電壓,磁場或壓力會發(fā)生變換彎成車輛計數(shù)。但這類傳感器的安裝和維護費用很高,現(xiàn)在已不再大量鋪設。不可接觸式的包括超聲,紅外,雷達傳感器等,這類容易受到惡劣天氣的影響使檢測精度降低。
-
基于視頻的車流量統(tǒng)計,也就是本項目中實現(xiàn)的方法。
該項目對輸入的視頻進行處理,主要包括以下幾個步驟:
-
使用yoloV3模型進行目標檢測,
-
然后使用SORT算法進行目標追蹤,使用卡爾曼濾波器進行目標位置預測,并利用匈牙利算法對比目標的相似度,完成車輛目標追蹤,
-
利用虛擬線圈的思想實現(xiàn)車輛目標的計數(shù),完成車流量的統(tǒng)計。
項目流程如下圖所示:
總結
- 目標跟蹤方法:使用的是sort算法,其中使用卡爾曼濾波器對目標位置進行估計,利用匈牙利算法進行目標關聯(lián)
- 車流量計數(shù):使用虛擬線圈算法對車輛進行計數(shù)
1.SORT核心是卡爾曼濾波和匈牙利算法。流程圖如下所示,可以看到整體可以拆分為兩個部分,分別是匈牙利匹配過程和卡爾曼預測加更新過程,都用灰色框標出來了。關鍵步驟:--> 卡爾曼濾波預測出預測框--> 使用匈牙利算法將卡爾曼濾波的預測框和yolo的檢測框進行IOU匹配來計算相似度 --> 卡爾曼濾波使用yolo的檢測框更新卡爾曼濾波的預測框2.卡爾曼濾波分為兩個過程:預測過程和更新過程。SORT引入了線性速度模型與卡爾曼濾波來進行位置預測,先進行位置預測然后再進行匹配。運動模型的結果可以用來預測物體的位置。匈牙利算法解決的是一個分配問題,用IOU距離作為權重(也即cost代價矩陣),并且當IOU小于一定數(shù)值(IOU閾值)時,不認為是同一個目標,理論基礎是視頻中兩幀之間物體移動不會過多。在代碼中選取的IOU閾值是0.3。scipy庫的linear_sum_assignment實現(xiàn)了匈牙利算法,只需要輸入cost_matrix代價矩陣(全部預測框和全部檢測框兩兩IOU計算結果)到linear_sum_assignment中就能得到預測框和檢測框兩兩最優(yōu)匹配的組合。 1.跟蹤器鏈(列表):實際就是多個的卡爾曼濾波KalmanBoxTracker自定義類的實例對象組成的列表。每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。把每個卡爾曼濾波器(KalmanBoxTracker實例對象)都存儲到跟蹤器鏈(列表)中。2.unmatched_detections(列表):檢測框中出現(xiàn)新目標,但此時預測框(跟蹤框)中仍不不存在該目標,那么就需要在創(chuàng)建新目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象),然后把新目標對應的KalmanBoxTracker類的實例對象放到跟蹤器鏈(列表)中。3.unmatched_trackers(列表):當跟蹤目標失敗或目標離開了畫面時,也即目標從檢測框中消失了,就應把目標對應的跟蹤框(預測框)從跟蹤器鏈中刪除。unmatched_trackers列表中保存的正是跟蹤失敗即離開畫面的目標,但該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)此時仍然存在于跟蹤器鏈(列表)中,因此就需要把該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)從跟蹤器鏈(列表)中刪除出去。 DeepSORT是SORT的續(xù)作,整體框架沒有大改,還是延續(xù)了卡爾曼濾波加匈牙利算法的思路,并且在這個基礎上增加了鑒別網(wǎng)絡Deep Association Metric。 下圖是deepSORT流程圖,和SORT基本一樣,就多了級聯(lián)匹配(Matching Cascade)和新軌跡的確認(confirmed)。 關鍵步驟:--> 卡爾曼濾波預測出預測框--> 使用匈牙利算法將卡爾曼濾波的預測框和yolo的檢測框進行級聯(lián)匹配加IOU匹配兩者分別來計算相似度 --> 卡爾曼濾波使用yolo的檢測框更新卡爾曼濾波的預測框級聯(lián)匹配計算相似度的流程圖如下所示:上半部分為相似度估計,也就是計算這個分配問題的代價矩陣。下半部分依舊是使用匈牙利算法進行檢測框和預測框的匹配。
1.原始圖片resize到448x448,經(jīng)過前面卷積網(wǎng)絡之后,將圖片輸出成了一個7 x 7 x 30的特征圖。根據(jù)7x7的特征圖大小在輸入原圖上分成7x7=49個網(wǎng)格,如果目標的中心落入到某個網(wǎng)格單元cell中,那么該網(wǎng)格單元cell就負責檢測該目標。7x7的特征圖大小:7x7=49個像素值,理解成49個單元格,每個單元格可以代表原圖的一個方塊。 2.每個網(wǎng)格單元cell都會預測N個邊界框bounding boxes和每個bbox框的1個置信度分數(shù)confidence scores,這些置信度分數(shù)反映了該模型對那個框內是否包含目標的信心,以及它對自己的預測的準確度的估量。 3.yolo V1、yolo V2、yolo V3 的bbox(邊界框bounding boxes)數(shù)目變化1.yolo V1:1.每個網(wǎng)格單元cell預測2個(默認)bbox(邊界框bounding boxes):輸入圖像一共有 7x7x2=98個bbox(邊界框bounding boxes)2.每個bbox(邊界框bounding boxes)包含4個預測位置(x、y、w、h),1個bbox置信度分數(shù)confidence scores3.一個網(wǎng)格單元預測cell的2個(默認)bbox(邊界框bounding boxes)一共的預測數(shù)據(jù)量:2x(4+1)+20=304個預測位置(x、y、w、h)和1個bbox置信度分數(shù)confidence scores代表一個bbox的預測數(shù)據(jù)量。20代表1個bbox的20個類別的預測概率值,每個單元格選出20個類別的預測概率中的最大概率值的一個類別,那么一個單元格就只能代表一個類別,這也是yolo V1的缺點,yolo V1就只會1個單元格cell預測1個目標物體,對于檢測效果并不好,因此并不建議使用yolo V1。4.輸入圖像一共預測的數(shù)據(jù)量:7x7x(2x(4+1)+20)=14707x7的特征圖大小即輸入圖像中的網(wǎng)格數(shù)。2x(4+1)即每個網(wǎng)格都預測2個邊界框bounding boxes的4個預測位置(x、y、w、h),1個bbox置信度分數(shù)confidence scores。加20即是因為在yolo V1中一個網(wǎng)格中的2個bbox中最終只會有1個bbox用于預測目標物體,因此如果多個目標物體都在一個單元格cell中出現(xiàn)的話,如果使用了yolo V1就只會1個單元格cell預測1個目標物體,對于檢測效果并不好,因此并不建議使用yolo V1。2.yolo V2:1.每個網(wǎng)格單元cell都使用5種(默認)不同尺寸的錨框Anchor boxes來預測bbox(邊界框bounding boxes),一個網(wǎng)格單元cell中每種不同尺寸的錨框Anchor boxes各預測一個bbox(邊界框bounding boxes),一共預測5個(默認)bbox(邊界框bounding boxes)。輸入圖像一共預測有 13x13x5=845個bbox(邊界框bounding boxes)。注意:5個(默認)的錨框Anchor boxes的尺寸大小都是不一樣的。2.每種不同尺寸的錨框Anchor boxes所預測的bbox(邊界框bounding boxes)包含:4個預測位置(x、y、w、h),1個bbox置信度分數(shù)confidence scores,N個分類類別的預測概率值。3.一個網(wǎng)格單元cell中5種(默認)不同尺寸的錨框Anchor boxes所預測的5個(默認)bbox(邊界框bounding boxes)一共預測的數(shù)據(jù)量(假如預測20個類別):5x(4+1+20)=1255代表5個(默認)bbox(邊界框bounding boxes)。每個bbox(邊界框bounding boxes)都分別有4個預測位置(x、y、w、h),1個bbox置信度分數(shù)confidence scores,20個類別的預測概率值。4.輸入圖像一共預測的數(shù)據(jù)量(假如預測20個類別和在13x13特征圖上做預測):13x13x(5x(4+1+20))=169*125=211255.YOLO V2基于卷積的Anchor機制(Convolutional With Anchor Boxes):從YOLOV1中移除全連接層,并使用5個(默認)不同尺寸的錨框Anchor boxes來預測bbox(邊界框bounding boxes)。YOLO V2通過縮減網(wǎng)絡,使用416x416的輸入,模型下采樣的總步長為32,最后得到13x13的特征圖,13x13的特征圖對應在輸入原圖分割13x13個單元格cell。每個單元格cell預測5個不同尺寸錨框anchor boxes對應的bbox(邊界框bounding boxes),每個錨框anchor box所預測的bbox(邊界框bounding boxes)包含4個位置信息、1個置信度、N個分類類別的概率值。YOLO V2采用的5種不同尺寸錨框Anchor boxes可以預測13x13x5=845個bbox(邊界框bounding boxes)。YOLO V2引?faster rcnn中anchor機制,anchor尺度就是用來預測網(wǎng)絡預測值和目標GT做尺度變換的。6.維度聚類Faster-RCNN中anchor boxes的個數(shù)和寬高維度往往是手動精選的先驗框(hand-picked priors),設想能否一開始就選擇了更好的、更有代表性的先驗boxes維度,那么網(wǎng)絡就應該更容易學到準確的預測位置,YOLOv2使用k-means聚類算法對訓練集中的邊界框做了聚類分析,嘗試通過維度聚類找到合適尺寸的錨框Anchor boxes。YOLOV2沒有使用FasterRCNN預測偏移和尺度變換。而是遵循YOLOV1的方法,預測相對于網(wǎng)格單元位置的位置坐標。這使得真實值的界限在0到1之間。3.yolo V3:1.特征金字塔(FPN網(wǎng)絡)yolo V3使用了特征金字塔(FPN網(wǎng)絡),yolov3在3個不同尺度的特征圖上做預測。比如:13x13,26x26,52x52三個不同尺度特征圖上的每個單元格cell分別使用3種(默認)不同尺寸的錨框Anchor boxes來預測bbox(邊界框bounding boxes)。每種不同尺寸的錨框Anchor boxes所預測的bbox(邊界框bounding boxes)包含:4個預測位置(x、y、w、h),1個bbox置信度分數(shù)confidence scores,N個分類類別的概率值。那么一個NxN的特征圖大小就有NxN個網(wǎng)格單元cell,那么每個網(wǎng)格單元cell預測數(shù)據(jù)量為3x(4+1+N個分類類別的概率值),一整個特征圖一共預測數(shù)據(jù)量為NxNx(3x(4+1+N個分類類別的概率值))。yolo V3在13x13,26x26,52x52大小的三個特征圖做預測計算,特征圖比例大小分別是13x13為NxN,26x26為2x(NxN),52x52為4x(NxN),那么3個不同尺度特征圖一共預測數(shù)據(jù)量為(NxN + 2x(NxN) + 4x(NxN)) x (3x(4+1+N個分類類別的概率值))2.每種不同尺度特征圖上所設置的先驗框(bbox邊界框bounding boxes)大小,會從下面的array數(shù)組yolo_anchors中選出對應合適的組合作為先驗框(bbox邊界框bounding boxes)的大小。yolo_anchors = np.array([(10, 13), (16, 30), (33, 23), (30, 61), (62, 45), (59, 119), (116, 90), (156, 198), (373, 326)], np.float32) / 416 1.yolo V2、yolo V3中的每個bbox(邊界框bounding boxes)的預測值:4個預測位置(x、y、w、h)、1個bbox置信度分數(shù)confidence scores、N個類別的預測概率值。 2.(x, y) 表示bbox的中心點相對于單元格(grid cell)原點的偏移值,單元格(grid cell)的原點即為當前單元格的左上角的頂點(top-left),yolo將左上角的頂點(原點)設置為(0, 0),右下角的頂點設置為(1, 1),所以x和y的取值范圍都分別是在0到1之間。x和y將始終介于0到1之間,因為中心點始終位于單元格(grid cell)內。之所以把(x, y)預測為相對于網(wǎng)格單元cell的位置坐標,使得真實值的界限在0到1之間,因此參數(shù)化更容易學習,從而使網(wǎng)絡更加穩(wěn)定。 3.(w, h) 表示為相對于整張圖片的寬和高, 即使用圖片的寬和高標準化自己。如果邊界框bbox的尺寸小于單元格(grid cell)的尺寸的話,w和h的取值范圍都分別是在0到1之間。如果邊界框bbox的尺寸大于單元格(grid cell)的尺寸的話,w和h的取值范圍都可以大于1。 4.yolo V2、yolo V3都基于卷積的Anchor機制(Convolutional With Anchor Boxes)yolo V2使用5種不同尺寸的錨框Anchor boxes預測邊界框的4個位置信息、1個置信度、N個分類類別的概率值。yolo V3使用3種不同尺寸的錨框Anchor boxes預測邊界框的4個位置信息、1個置信度、N個分類類別的概率值。 5.比如在yolo V2中,13*13特征圖上的每個單元格(grid cell)中預測5個不同尺寸的錨框Anchor boxes。anchor尺寸就是用來預測網(wǎng)絡預測值和目標GT之間做尺度變換的。比如下面的藍色框是預測的bbox(邊界框bounding boxes),黑色點的矩形框是錨框Anchor boxes。每一個bbox(邊界框bounding boxes)都預測:tx、ty、tw、th、to(置信度)。如果這個單元格(grid cell)距離輸入原圖的左上角原點的邊距離為(cx,cy),該單元格(grid cell)對應的邊界框bbox維度(邊界框優(yōu)先bounding box prior)的長和寬分別為(pw,ph),那么對應的邊界框bbox計算結果實際為:1.yolo V2中不同尺寸的錨框Anchor boxes所預測的bbox(邊界框bounding boxes)的4個位置信息為(tx, ty, tw, th),那么tx和ty分別為相對于單元格(grid cell)原點的0到1之間取值的值,tw和th則根據(jù)所預測的bbox(邊界框bounding boxes)是大于還是小于單元格(grid cell)的尺寸來決定tw和th的取值范圍是在0到1之間還是在大于1。2.pw和ph分別為手動設定的錨框Anchor boxes寬和高,而網(wǎng)絡最終計算的預測結果為(bx, by, bw, bh),因此需要把(tx, ty, tw, th)轉換為(bx, by, bw, bh)。3.把(tx, ty, tw, th)轉換為(bx, by, bw, bh)作為yolo輸出層的最終輸出:σ讀作sigma。Cx和Cy分別為當前單元格(grid cell)距離輸入原圖的左上角原點的邊距離。W和H為輸入原圖像的寬和高。分別除以W和H,目的是歸一化。tx->bx:bx = (σ(tx) + Cx) / Wty->by:by = (σ(ty) + Cy) / Htw->bw:bw = (pw * e^tw) / Wth->bh:bh = (ph * e^th) / H4.σ(tx) + Cx:邊界框的中心點在輸入原圖像中的x坐標,也即邊界框的中心點離輸入原圖像原點的x方向長度σ(ty) + Cy:邊界框的中心點在輸入原圖像中的y坐標,也即邊界框的中心點離輸入原圖像原點的y方向長度pw * e^tw:邊界框在輸入原圖像中的寬度ph * e^th:邊界框在輸入原圖像中的高度
from __future__ import print_function # 對for循環(huán)有較好的效果 from numba import jit import numpy as np # 用于線性分配,匈牙利匹配的實現(xiàn) # from sklearn.utils.linear_assignment_ import linear_assignment from scipy.optimize import linear_sum_assignment # 使用卡爾曼濾波器 from filterpy.kalman import KalmanFilterdef convert_bbox_to_z(bbox):"""將[x1,y1,x2,y2]形式的檢測框轉為濾波器的狀態(tài)表示形式[x,y,s,r]。其中x、y是框的中心坐標點,s 是面積尺度,r 是寬高比w/h:param bbox: [x1,y1,x2,y2] 分別是左上角坐標和右下角坐標 即 [左上角的x坐標,左上角的y坐標,右下角的x坐標,右下角的y坐標]:return: [ x, y, s, r ] 4行1列,其中x、y是box中心位置的坐標,s是面積,r是縱橫比w/h"""w = bbox[2] - bbox[0] # 右下角的x坐標 - 左上角的x坐標 = 檢測框的寬h = bbox[3] - bbox[1] # 右下角的y坐標 - 左上角的y坐標 = 檢測框的高x = bbox[0] + w / 2. # 左上角的x坐標 + 寬/2 = 檢測框中心位置的x坐標y = bbox[1] + h / 2. # 左上角的y坐標 + 高/2 = 檢測框中心位置的y坐標s = w * h # 檢測框的寬 * 高 = 檢測框面積r = w / float(h) # 檢測框的寬w / 高h = 寬高比# 因為卡爾曼濾波器的輸入格式要求為4行1列,因此該[x, y, s, r]的形狀要轉換為4行1列再輸入到卡爾曼濾波器return np.array([x, y, s, r]).reshape((4, 1))""" 將候選框從中心面積的形式[x,y,s,r] 轉換為 坐標的形式[x1,y1,x2,y2] """ def convert_x_to_bbox(x, score=None):"""將[cx,cy,s,r]的目標框表示轉為[x_min,y_min,x_max,y_max]的形式:param x:[ x, y, s, r ],其中x,y是box中心位置的坐標,s是面積,r是縱橫比w/h:param score: 置信度:return:[x1,y1,x2,y2],左上角坐標和右下角坐標""""""x[2]:s是面積,原公式s的來源為s = w * h,即檢測框的寬 * 高 = 檢測框面積。x[3]:r是縱橫比w/h,原公式r的來源為r = w / float(h),即檢測框的寬w / 高h = 寬高比。x[2] * x[3]:s*r 即(w * h) * (w / float(h)) = w^2sqrt(x[2] * x[3]):sqrt(w^2) = w"""w = np.sqrt(x[2] * x[3]) # sqrt(w^2) = wh = x[2] / w # w * h / w = hif score is None:return np.array([x[0] - w / 2., # 檢測框中心位置的x坐標 - 寬 / 2 = 左上角的x坐標x[1] - h / 2., # 檢測框中心位置的y坐標 - 高 / 2 = 左上角的y坐標x[0] + w / 2., # 檢測框中心位置的x坐標 + 寬 / 2 = 右下角的x坐標x[1] + h / 2.] # 檢測框中心位置的y坐標 + 高 / 2 = 右下角的y坐標).reshape((1, 4))else:return np.array([x[0] - w / 2.,x[1] - h / 2.,x[0] + w / 2.,x[1] + h / 2.,score]).reshape((1, 5))""" 卡爾曼濾波器進行跟蹤的相關內容的實現(xiàn)目標估計模型:1.根據(jù)上一幀的目標框結果來預測當前幀的目標框狀態(tài),預測邊界框(目標框)的模型定義為一個等速運動/勻速運動模型。2.每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。 3.yoloV3、卡爾曼濾波器預測/更新流程步驟1.第一步:yoloV3目標檢測階段:--> 1.檢測到目標則創(chuàng)建檢測目標鏈/跟蹤目標鏈,反之檢測不到目標則重新循環(huán)目標檢測。--> 2.檢測目標鏈/跟蹤目標鏈不為空則進入卡爾曼濾波器predict預測階段,反之為空則重新循環(huán)目標檢測。2.第二步:卡爾曼濾波器predict預測階段:連續(xù)多次預測而不進行一次更新操作,那么代表了每次預測之后所進行的“預測目標和檢測目標之間的”相似度匹配都不成功,所以才會出現(xiàn)連續(xù)多次的“預測然后相似度匹配失敗的”情況,導致不會進入一次更新階段。如果一次預測然后相似度匹配成功的話,那么然后就會進入更新階段。--> 1.目標位置預測1.kf.predict():目標位置預測2.目標框預測總次數(shù):age+=1。3.if time_since_update > 0:hit_streak = 0time_since_update += 11.連續(xù)預測的次數(shù),每執(zhí)行predict一次即進行time_since_update+=1。2.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,一旦執(zhí)行update的話,time_since_update就會被重置為0。3.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,只要連續(xù)預測的次數(shù)time_since_update大于0的話,就會把hit_streak(連續(xù)更新的次數(shù))重置為0,表示連續(xù)預測的過程中沒有出現(xiàn)過一次更新狀態(tài)更新向量x(狀態(tài)變量x)的操作,即連續(xù)預測的過程中沒有執(zhí)行過一次update。4.在連續(xù)更新(連續(xù)執(zhí)行update)的過程中,一旦開始連續(xù)執(zhí)行predict兩次或以上的情況下,當連續(xù)第一次執(zhí)行predict時,因為time_since_update仍然為0,并不會把hit_streak重置為0,然后才會進行time_since_update+=1;當連續(xù)第二次執(zhí)行predict時,因為time_since_update已經(jīng)為1,那么便會把hit_streak重置為0,然后繼續(xù)進行time_since_update+=1。--> 2.預測的目標和檢測的目標之間的相似度匹配成功則進入update更新階段,反之匹配失敗則刪除跟蹤目標。3.第三步:卡爾曼濾波器update更新階段:如果一次預測然后“預測目標和檢測目標之間的”相似度匹配成功的話,那么然后就會進入更新階段。kf.update([x,y,s,r]):使用的是通過yoloV3得到的“并且和預測框相匹配的”檢測框來更新預測框。--> 1.目標位置信息更新到檢測目標鏈/跟蹤目標鏈 1.目標框更新總次數(shù):hits+=1。2.history = []time_since_update = 0hit_streak += 11.history列表用于在預測階段保存單個目標框連續(xù)預測的多個結果,一旦執(zhí)行update就會清空history列表。2.連續(xù)更新的次數(shù),每執(zhí)行update一次即進行hit_streak+=1。3.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,一旦執(zhí)行update的話,time_since_update就會被重置為0。4.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,只要連續(xù)預測的次數(shù)time_since_update大于0的話,就會把hit_streak(連續(xù)更新的次數(shù))重置為0,表示連續(xù)預測的過程中沒有出現(xiàn)過一次更新狀態(tài)更新向量x(狀態(tài)變量x)的操作,即連續(xù)預測的過程中沒有執(zhí)行過一次update。5.在連續(xù)更新(連續(xù)執(zhí)行update)的過程中,一旦開始連續(xù)執(zhí)行predict兩次或以上的情況下,當連續(xù)第一次執(zhí)行predict時,因為time_since_update仍然為0,并不會把hit_streak重置為0,然后才會進行time_since_update+=1;當連續(xù)第二次執(zhí)行predict時,因為time_since_update已經(jīng)為1,那么便會把hit_streak重置為0,然后繼續(xù)進行time_since_update+=1。--> 2.目標位置修正。1.kf.update([x,y,s,r]):使用觀測到的目標框bbox更新狀態(tài)變量x(狀態(tài)更新向量x)。使用的是通過yoloV3得到的“并且和預測框相匹配的”檢測框來更新卡爾曼濾波器得到的預測框。1.初始化、預測、更新1.__init__(bbox):初始化卡爾曼濾波器的狀態(tài)更新向量x(狀態(tài)變量x)、觀測輸入[x,y,s,r](通過[x1,y1,x2,y2]轉化而來)、狀態(tài)轉移矩陣F、量測矩陣H(觀測矩陣H)、測量噪聲的協(xié)方差矩陣R、先驗估計的協(xié)方差矩陣P、過程激勵噪聲的協(xié)方差矩陣Q。2.update(bbox):根據(jù)觀測輸入來對狀態(tài)更新向量x(狀態(tài)變量x)進行更新3.predict():根據(jù)狀態(tài)更新向量x(狀態(tài)變量x)更新的結果來預測目標的邊界框2.狀態(tài)變量、狀態(tài)轉移矩陣F、量測矩陣H(觀測矩陣H)、測量噪聲的協(xié)方差矩陣R、先驗估計的協(xié)方差矩陣P、過程激勵噪聲的協(xié)方差矩陣Q1.狀態(tài)更新向量x(狀態(tài)變量x)狀態(tài)更新向量x(狀態(tài)變量x)的設定是一個7維向量:x=[u,v,s,r,u^,v^,s^]T。u、v分別表示目標框的中心點位置的x、y坐標,s表示目標框的面積,r表示目標框的縱橫比/寬高比。u^、v^、s^分別表示橫向u(x方向)、縱向v(y方向)、面積s的運動變化速率。u、v、s、r初始化:根據(jù)第一幀的觀測結果進行初始化。u^、v^、s^初始化:當?shù)谝粠_始的時候初始化為0,到后面幀時會根據(jù)預測的結果來進行變化。2.狀態(tài)轉移矩陣F定義的是一個7*7的方陣(其對角線上的值都是1)。。運動形式和轉換矩陣的確定都是基于勻速運動模型,狀態(tài)轉移矩陣F根據(jù)運動學公式確定,跟蹤的目標假設為一個勻速運動的目標。通過7*7的狀態(tài)轉移矩陣F 乘以 7*1的狀態(tài)更新向量x(狀態(tài)變量x)即可得到一個更新后的7*1的狀態(tài)更新向量x,其中更新后的u、v、s即為當前幀結果。3.量測矩陣H(觀測矩陣H)量測矩陣H(觀測矩陣H),定義的是一個4*7的矩陣。通過4*7的量測矩陣H(觀測矩陣H) 乘以 7*1的狀態(tài)更新向量x(狀態(tài)變量x) 即可得到一個 4*1的[u,v,s,r]的估計值。4.測量噪聲的協(xié)方差矩陣R、先驗估計的協(xié)方差矩陣P、過程激勵噪聲的協(xié)方差矩陣Q1.測量噪聲的協(xié)方差矩陣R:diag([1,1,10,10]T)2.先驗估計的協(xié)方差矩陣P:diag([10,10,10,10,1e4,1e4,1e4]T)。1e4:1x10的4次方。3.過程激勵噪聲的協(xié)方差矩陣Q:diag([1,1,1,1,0.01,0.01,1e-4]T)。1e-4:1x10的-4次方。4.1e數(shù)字的含義1e4:1x10的4次方1e-4:1x10的-4次方5.diag表示對角矩陣,寫作為diag(a1,a2,...,an)的對角矩陣實際表示為主對角線上的值依次為a1,a2,...,an,而主對角線之外的元素皆為0的矩陣。對角矩陣(diagonal matrix)是一個主對角線之外的元素皆為0的矩陣,常寫為diag(a1,a2,...,an) 。對角矩陣可以認為是矩陣中最簡單的一種,值得一提的是:對角線上的元素可以為 0 或其他值,對角線上元素相等的對角矩陣稱為數(shù)量矩陣;對角線上元素全為1的對角矩陣稱為單位矩陣。對角矩陣的運算包括和、差運算、數(shù)乘運算、同階對角陣的乘積運算,且結果仍為對角陣。 """ """ 1.跟蹤器鏈(列表):實際就是多個的卡爾曼濾波KalmanBoxTracker自定義類的實例對象組成的列表。每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。把每個卡爾曼濾波器(KalmanBoxTracker實例對象)都存儲到跟蹤器鏈(列表)中。 2.unmatched_detections(列表):1.檢測框中出現(xiàn)新目標,但此時預測框(跟蹤框)中仍不不存在該目標,那么就需要在創(chuàng)建新目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象),然后把新目標對應的KalmanBoxTracker類的實例對象放到跟蹤器鏈(列表)中。2.同時如果因為“跟蹤框和檢測框之間的”兩兩組合的匹配度IOU值小于iou閾值,則也要把目標檢測框放到unmatched_detections中。 3.unmatched_trackers(列表):1.當跟蹤目標失敗或目標離開了畫面時,也即目標從檢測框中消失了,就應把目標對應的跟蹤框(預測框)從跟蹤器鏈中刪除。unmatched_trackers列表中保存的正是跟蹤失敗即離開畫面的目標,但該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)此時仍然存在于跟蹤器鏈(列表)中,因此就需要把該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)從跟蹤器鏈(列表)中刪除出去。2.同時如果因為“跟蹤框和檢測框之間的”兩兩組合的匹配度IOU值小于iou閾值,則也要把跟蹤目標框放到unmatched_trackers中。 """# 目標估計模型-卡爾曼濾波 class KalmanBoxTracker(object):"""每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。"""count = 0 # 類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象"""__init__(bbox)使用目標框bbox為卡爾曼濾波的狀態(tài)進行初始化。初始化時傳入bbox,即根據(jù)觀測到的檢測框的結果來進行初始化。每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。1.kf = KalmanFilter(dim_x=7, dim_z=4)定義一個卡爾曼濾波器,利用這個卡爾曼濾波器對目標的狀態(tài)進行估計。dim_x=7定義是一個7維的狀態(tài)更新向量x(狀態(tài)變量x):x=[u,v,s,r,u^,v^,s^]T。dim_z=4定義是一個4維的觀測輸入,即中心面積的形式[x,y,s,r],即[檢測框中心位置的x坐標,y坐標,面積,寬高比]。2.kf.F = np.array(7*7的方陣)狀態(tài)轉移矩陣F,定義的是一個7*7的方陣其(對角線上的值都是1)。通過7*7的狀態(tài)轉移矩陣F 乘以 7*1的狀態(tài)更新向量x(狀態(tài)變量x)即可得到一個更新后的7*1的狀態(tài)更新向量x,其中更新后的u、v、s即為當前幀結果。通過狀態(tài)轉移矩陣對當前的觀測結果進行估計獲得預測的結果,然后用當前的預測的結果來作為下一次估計預測的基礎。3.kf.H = np.array(4*7的矩陣)量測矩陣H(觀測矩陣H),定義的是一個4*7的矩陣。通過4*7的量測矩陣H(觀測矩陣H) 乘以 7*1的狀態(tài)更新向量x(狀態(tài)變量x) 即可得到一個 4*1的[u,v,s,r]的估計值。4.相應的協(xié)方差參數(shù)的設定,根據(jù)經(jīng)驗值進行設定。1.R是測量噪聲的協(xié)方差矩陣,即真實值與測量值差的協(xié)方差。R=diag([1,1,10,10]T)kf.R[2:, 2:] *= 10.2.P是先驗估計的協(xié)方差矩陣diag([10,10,10,10,1e4,1e4,1e4]T)。1e4:1x10的4次方。kf.P[4:, 4:] *= 1000. # 設置了一個較大的值,給無法觀測的初始速度帶來很大的不確定性kf.P *= 10.3.Q是過程激勵噪聲的協(xié)方差矩陣diag([1,1,1,1,0.01,0.01,1e-4]T)。1e-4:1x10的-4次方。kf.Q[-1, -1] *= 0.01kf.Q[4:, 4:] *= 0.015.kf.x[:4] = convert_bbox_to_z(bbox)convert_bbox_to_z負責將[x1,y1,x2,y2]形式的檢測框bbox轉為中心面積的形式[x,y,s,r]。狀態(tài)更新向量x(狀態(tài)變量x)設定是一個七維向量:x=[u,v,s,r,u^,v^,s^]T。x[:4]即表示 u、v、s、r初始化為第一幀bbox觀測到的結果[x,y,s,r]。6.單個目標框對應的單個卡爾曼濾波器中的統(tǒng)計參數(shù)的更新每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。1.卡爾曼濾波器的個數(shù)有多少個目標框就有多少個卡爾曼濾波器,每個目標框都會有一個卡爾曼濾波器,即每個目標框都會有一個KalmanBoxTracker實例對象。count = 0:類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象。id = KalmanBoxTracker.count:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個.KalmanBoxTracker.count += 1:每增加一個目標框,即增加一個KalmanBoxTracker實例對象(卡爾曼濾波器),那么類屬性count+=1。2.統(tǒng)計一個目標框對應的卡爾曼濾波器中各參數(shù)統(tǒng)計的次數(shù)1.age = 0:該目標框進行預測的總次數(shù)。每執(zhí)行predict一次,便age+=1。2.hits = 0:該目標框進行更新的總次數(shù)。每執(zhí)行update一次,便hits+=1。3.time_since_update = 01.連續(xù)預測的次數(shù),每執(zhí)行predict一次即進行time_since_update+=1。2.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,一旦執(zhí)行update的話,time_since_update就會被重置為0。3.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,只要連續(xù)預測的次數(shù)time_since_update大于0的話,就會把hit_streak(連續(xù)更新的次數(shù))重置為0,表示連續(xù)預測的過程中沒有出現(xiàn)過一次更新狀態(tài)更新向量x(狀態(tài)變量x)的操作,即連續(xù)預測的過程中沒有執(zhí)行過一次update。4.hit_streak = 01.連續(xù)更新的次數(shù),每執(zhí)行update一次即進行hit_streak+=1。2.在連續(xù)更新(連續(xù)執(zhí)行update)的過程中,一旦開始連續(xù)執(zhí)行predict兩次或以上的情況下,當連續(xù)第一次執(zhí)行predict時,因為time_since_update仍然為0,并不會把hit_streak重置為0,然后才會進行time_since_update+=1;當連續(xù)第二次執(zhí)行predict時,因為time_since_update已經(jīng)為1,那么便會把hit_streak重置為0,然后繼續(xù)進行time_since_update+=1。7.history = []:保存單個目標框連續(xù)預測的多個結果到history列表中,一旦執(zhí)行update就會清空history列表。將預測的候選框從中心面積的形式[x,y,s,r]轉換為坐標的形式[x1,y1,x2,y2] 的bbox 再保存到 history列表中。"""def __init__(self, bbox):# 定義等速模型# 內部使用KalmanFilter,7個狀態(tài)變量和4個觀測輸入self.kf = KalmanFilter(dim_x=7, dim_z=4)# F是狀態(tài)變換模型,為7*7的方陣self.kf.F = np.array([[1, 0, 0, 0, 1, 0, 0],[0, 1, 0, 0, 0, 1, 0],[0, 0, 1, 0, 0, 0, 1],[0, 0, 0, 1, 0, 0, 0],[0, 0, 0, 0, 1, 0, 0],[0, 0, 0, 0, 0, 1, 0],[0, 0, 0, 0, 0, 0, 1]])# H是量測矩陣,是4*7的矩陣self.kf.H = np.array([[1, 0, 0, 0, 0, 0, 0],[0, 1, 0, 0, 0, 0, 0],[0, 0, 1, 0, 0, 0, 0],[0, 0, 0, 1, 0, 0, 0]])# R是測量噪聲的協(xié)方差,即真實值與測量值差的協(xié)方差self.kf.R[2:, 2:] *= 10.# P是先驗估計的協(xié)方差self.kf.P[4:, 4:] *= 1000. # 給無法觀測的初始速度帶來很大的不確定性self.kf.P *= 10.# Q是過程激勵噪聲的協(xié)方差self.kf.Q[-1, -1] *= 0.01self.kf.Q[4:, 4:] *= 0.01# 狀態(tài)估計self.kf.x[:4] = convert_bbox_to_z(bbox)# 參數(shù)的更新self.time_since_update = 0self.id = KalmanBoxTracker.countKalmanBoxTracker.count += 1self.history = []self.hits = 0self.hit_streak = 0self.age = 0"""update(bbox):使用觀測到的目標框bbox更新狀態(tài)更新向量x(狀態(tài)變量x)1.time_since_update = 01.連續(xù)預測的次數(shù),每執(zhí)行predict一次即進行time_since_update+=1。2.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,一旦執(zhí)行update的話,time_since_update就會被重置為0。2.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,只要連續(xù)預測的次數(shù)time_since_update大于0的話,就會把hit_streak(連續(xù)更新的次數(shù))重置為0,表示連續(xù)預測的過程中沒有出現(xiàn)過一次更新狀態(tài)更新向量x(狀態(tài)變量x)的操作,即連續(xù)預測的過程中沒有執(zhí)行過一次update。2.history = [] 清空history列表。history列表保存的是單個目標框連續(xù)預測的多個結果([x,y,s,r]轉換后的[x1,y1,x2,y2]),一旦執(zhí)行update就會清空history列表。3.hits += 1:該目標框進行更新的總次數(shù)。每執(zhí)行update一次,便hits+=1。4.hit_streak += 11.連續(xù)更新的次數(shù),每執(zhí)行update一次即進行hit_streak+=1。2.在連續(xù)更新(連續(xù)執(zhí)行update)的過程中,一旦開始連續(xù)執(zhí)行predict兩次或以上的情況下,當連續(xù)第一次執(zhí)行predict時,因為time_since_update仍然為0,并不會把hit_streak重置為0,然后才會進行time_since_update+=1;當連續(xù)第二次執(zhí)行predict時,因為time_since_update已經(jīng)為1,那么便會把hit_streak重置為0,然后繼續(xù)進行time_since_update+=1。5.kf.update(convert_bbox_to_z(bbox))convert_bbox_to_z負責將[x1,y1,x2,y2]形式的檢測框轉為濾波器的狀態(tài)表示形式[x,y,s,r],那么傳入的為kf.update([x,y,s,r])。然后根據(jù)觀測結果修改內部狀態(tài)x(狀態(tài)更新向量x)。使用的是通過yoloV3得到的“并且和預測框相匹配的”檢測框來更新卡爾曼濾波器得到的預測框。"""# 更新狀態(tài)變量,使用觀測到的目標框bbox更新狀態(tài)變量def update(self, bbox):"""使用觀察到的目標框更新狀態(tài)向量。filterpy.kalman.KalmanFilter.update 會根據(jù)觀測修改內部狀態(tài)估計self.kf.x。重置self.time_since_update,清空self.history。:param bbox:目標框:return:"""# 重置self.time_since_update = 0# 清空historyself.history = []# hits計數(shù)加1self.hits += 1self.hit_streak += 1# 根據(jù)觀測結果修改內部狀態(tài)x。# convert_bbox_to_z負責將[x1,y1,x2,y2]形式的檢測框轉為濾波器的狀態(tài)表示形式[x,y,s,r],那么update傳入的為(x,y,s,r)self.kf.update(convert_bbox_to_z(bbox))"""predict:進行目標框的預測并返回預測的邊界框結果1.if(kf.x[6] + kf.x[2]) <= 0:self.kf.x[6] *= 0.0狀態(tài)更新向量x(狀態(tài)變量x)為[u,v,s,r,u^,v^,s^]T,那么x[6]為s^,x[2]為s。如果x[6]+x[2]<= 0,那么x[6] *= 0.0,即把s^置為0.0。2.kf.predict()進行目標框的預測。3.age += 1該目標框進行預測的總次數(shù)。每執(zhí)行predict一次,便age+=1。4.if time_since_update > 0:hit_streak = 0time_since_update += 11.連續(xù)預測的次數(shù),每執(zhí)行predict一次即進行time_since_update+=1。2.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,一旦執(zhí)行update的話,time_since_update就會被重置為0。3.在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,只要連續(xù)預測的次數(shù)time_since_update大于0的話,就會把hit_streak(連續(xù)更新的次數(shù))重置為0,表示連續(xù)預測的過程中沒有出現(xiàn)過一次更新狀態(tài)更新向量x(狀態(tài)變量x)的操作,即連續(xù)預測的過程中沒有執(zhí)行過一次update。4.在連續(xù)更新(連續(xù)執(zhí)行update)的過程中,一旦開始連續(xù)執(zhí)行predict兩次或以上的情況下,當連續(xù)第一次執(zhí)行predict時,因為time_since_update仍然為0,并不會把hit_streak重置為0,然后才會進行time_since_update+=1;當連續(xù)第二次執(zhí)行predict時,因為time_since_update已經(jīng)為1,那么便會把hit_streak重置為0,然后繼續(xù)進行time_since_update+=1。5.history.append(convert_x_to_bbox(kf.x))convert_x_to_bbox(kf.x):將目標框所預測的結果從中心面積的形式[x,y,s,r] 轉換為 坐標的形式[x1,y1,x2,y2] 的bbox。history列表保存的是單個目標框連續(xù)預測的多個結果([x,y,s,r]轉換后的[x1,y1,x2,y2]),一旦執(zhí)行update就會清空history列表。6.predict 返回值:history[-1]把目標框當前該次的預測的結果([x,y,s,r]轉換后的[x1,y1,x2,y2])進行返回輸出。"""# 進行目標框的預測,推進狀態(tài)變量并返回預測的邊界框結果def predict(self):"""推進狀態(tài)向量并返回預測的邊界框估計。將預測結果追加到self.history。由于 get_state 直接訪問 self.kf.x,所以self.history沒有用到:return:"""# 推進狀態(tài)變量if (self.kf.x[6] + self.kf.x[2]) <= 0:self.kf.x[6] *= 0.0# 進行預測self.kf.predict()# 卡爾曼濾波的次數(shù)self.age += 1# 若過程中未更新過,將hit_streak置為0if self.time_since_update > 0:self.hit_streak = 0self.time_since_update += 1# 將預測結果追加到history中self.history.append(convert_x_to_bbox(self.kf.x))return self.history[-1]"""get_state():獲取當前目標框預測的結果([x,y,s,r]轉換后的[x1,y1,x2,y2])。return convert_x_to_bbox(kf.x):將候選框從中心面積的形式[x,y,s,r] 轉換為 坐標的形式[x1,y1,x2,y2] 的bbox并進行返回輸出。直接訪問 kf.x并進行返回,所以history沒有用到。"""def get_state(self):"""返回當前邊界框估計值。由于 get_state 直接訪問 self.kf.x,所以self.history沒有用到。:return:"""# 將候選框從中心面積的形式[x,y,s,r] 轉換為 坐標的形式[x1,y1,x2,y2] 的bboxreturn convert_x_to_bbox(self.kf.x)@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檢測結果框進行關聯(lián)匹配,整個流程是遍歷檢測結果框和跟蹤目標框,并進行兩兩的相似度最大的比對。 相似度最大的認為是同一個目標則匹配成功的將其保留,相似度低的未成功匹配的將其刪除。 使用的是通過yoloV3得到的“并且和預測框相匹配的”檢測框來更新卡爾曼濾波器得到的預測框。detections:此處傳入的檢測框的位置預測值為“已經(jīng)把yoloV3得到的檢測框的位置預測值”轉換成了的[x1,y1,x2,y2,score]。x1、y1 代表檢測框的左上角坐標;x2、y2代表檢測框的右上角坐標;score代表檢測框對應預測類別的概率值。trackers:通過卡爾曼濾波器得到的預測結果跟蹤目標框iou_threshold=0.3:大于IOU閾值則認為是同一個目標則匹配成功將其保留,小于IOU閾值則認為不是同一個目標則未成功匹配將其刪除。return返回值:matches:跟蹤成功目標的矩陣。即前后幀都存在的目標,并且匹配成功同時大于iou閾值。np.array(unmatched_detections):新增目標指的就是存在于detections檢測結果框當中,但不存在于trackers預測結果跟蹤目標框當中。np.array(unmatched_trackers):離開畫面的目標指的就是存在于trackers預測結果跟蹤目標框當中,但不存在于detections檢測結果框當中。matches:[[檢測框的索引值, 跟蹤框的索引值] [檢測框的索引值, 跟蹤框的索引值] 。。。]跟蹤成功并且兩兩匹配組合的IOU值大于iou閾值的檢測框和跟蹤框組成的矩陣unmatched_detections:[檢測框的索引值,。。。]1.新增目標的檢測框在detections檢測框列表中的索引位置2.兩兩匹配組合的IOU值小于iou閾值的檢測框在detections檢測框列表中的索引位置unmatched_trackers:[跟蹤框的索引值,。。。]1.跟蹤失敗的跟蹤框/預測框在trackers跟蹤框列表中的索引位置2.兩兩匹配組合的IOU值小于iou閾值的跟蹤框/預測框在trackers跟蹤框列表中的索引位置 """ def associate_detections_to_trackers(detections, trackers, iou_threshold=0.3):"""將檢測框bbox與卡爾曼濾波器的跟蹤框進行關聯(lián)匹配: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類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。把每個卡爾曼濾波器(KalmanBoxTracker實例對象)都存儲到跟蹤器鏈(列表)中。2.unmatched_detections(列表):1.檢測框中出現(xiàn)新目標,但此時預測框(跟蹤框)中仍不不存在該目標,那么就需要在創(chuàng)建新目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象),然后把新目標對應的KalmanBoxTracker類的實例對象放到跟蹤器鏈(列表)中。2.同時如果因為“跟蹤框和檢測框之間的”兩兩組合的匹配度IOU值小于iou閾值,則也要把目標檢測框放到unmatched_detections中。3.unmatched_trackers(列表):1.當跟蹤目標失敗或目標離開了畫面時,也即目標從檢測框中消失了,就應把目標對應的跟蹤框(預測框)從跟蹤器鏈中刪除。unmatched_trackers列表中保存的正是跟蹤失敗即離開畫面的目標,但該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)此時仍然存在于跟蹤器鏈(列表)中,因此就需要把該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)從跟蹤器鏈(列表)中刪除出去。2.同時如果因為“跟蹤框和檢測框之間的”兩兩組合的匹配度IOU值小于iou閾值,則也要把跟蹤目標框放到unmatched_trackers中。"""# 跟蹤目標數(shù)量為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 不支持數(shù)組計算。逐個計算兩兩間的交并比,調用 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矩陣) 通過匈牙利算法得到最優(yōu)匹配度的“跟蹤框和檢測框之間的”兩兩組合。通過相同下標位置的行索引和列索引即可從iou_matrix矩陣得到“跟蹤框和檢測框之間的”兩兩組合最優(yōu)匹配度的IOU值。-iou_matrix矩陣:linear_assignment的輸入是cost成本矩陣,IOU越大對應的分配代價應越小,所以iou_matrix矩陣需要取負號。row_ind:行索引構建的一維數(shù)組。行索引值對應的是目標檢測框。col_ind:列索引構建的一維數(shù)組。列索引值對應的是跟蹤目標框。比如: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中每行的第一列的話,代表目標檢測框中有新的目標出現(xiàn)在畫面中,則把未匹配的目標檢測框放入到unmatched_detections中表示需要新增跟蹤器進行跟蹤目標。"""if d not in matched_indices[:, 0]:""" 新增目標的檢測框在detections檢測框列表中的索引位置 """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]:""" 跟蹤失敗的跟蹤框/預測框在trackers跟蹤框列表中的索引位置 """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:根據(jù)目標檢測框的索引作為行索引,跟蹤目標框的索引作為列索引,即能找到“跟蹤框和檢測框之間的”兩兩組合最優(yōu)匹配度的IOU值,如果該IOU值小于iou閾值的話,則把目標檢測框放到unmatched_detections中,把跟蹤目標框放到unmatched_trackers中。"""# 過濾掉IOU低的匹配,將其放入到unmatched_detections和unmatched_trackersif iou_matrix[m[0], m[1]] < iou_threshold:""" 兩兩匹配組合的IOU值小于iou閾值的檢測框在detections檢測框列表中的索引位置 """unmatched_detections.append(m[0]) #m[0]:每行的第一列,代表的是目標檢測框。""" 兩兩匹配組合的IOU值小于iou閾值的跟蹤框/預測框在trackers跟蹤框列表中的索引位置 """unmatched_trackers.append(m[1]) #m[1]:每行的第二列,代表的是跟蹤目標框。# 滿足條件的以[[d,t]...]的形式放入matches中else:""" 存儲到列表中的每個元素的形狀為(1, 2) """matches.append(m.reshape(1, 2))"""如果矩陣matches中不存在任何跟蹤成功的目標的話,則創(chuàng)建空數(shù)組返回。numpy.concatenate((a1,a2,...), axis=0):能夠一次完成多個數(shù)組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]) #對于一維數(shù)組拼接,axis的值不影響最后的結果"""# 初始化matches,以np.array的形式返回if len(matches) == 0:""" np.empty((0, 2), dtype=int)輸出值:array([], shape=(0, 2), dtype=int32)輸出值類型:<class 'numpy.ndarray'>"""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可以不寫"""matches:[[檢測框的索引值, 跟蹤框的索引值] [檢測框的索引值, 跟蹤框的索引值] 。。。]跟蹤成功并且兩兩匹配組合的IOU值大于iou閾值的檢測框和跟蹤框組成的矩陣unmatched_detections:[檢測框的索引值,。。。]1.新增目標的檢測框在detections檢測框列表中的索引位置2.兩兩匹配組合的IOU值小于iou閾值的檢測框在detections檢測框列表中的索引位置unmatched_trackers:[跟蹤框的索引值,。。。]1.跟蹤失敗的跟蹤框/預測框在trackers跟蹤框列表中的索引位置2.兩兩匹配組合的IOU值小于iou閾值的跟蹤框/預測框在trackers跟蹤框列表中的索引位置"""return matches, np.array(unmatched_detections), np.array(unmatched_trackers)""" 利用sort算法完成多目標追蹤在這里我們主要實現(xiàn)了一個多目標跟蹤器,管理多個卡爾曼濾波器對象,主要包括以下內容:1.初始化:最大檢測數(shù),目標未被檢測的最大幀數(shù)2.目標跟蹤結果的更新,即跟蹤成功和失敗的目標的更新該方法實現(xiàn)了SORT算法,輸入是當前幀中所有物體的檢測框的集合,包括目標的score,輸出是當前幀的跟蹤框集合,包括目標的跟蹤的id要求是即使檢測框為空,也必須對每一幀調用此方法,返回一個類似的輸出數(shù)組,最后一列是目標對像的id。需要注意的是,返回的目標對象數(shù)量可能與檢測框的數(shù)量不同。 """# 1.SORT目標跟蹤: # 1.第一幀剛開始時:對第一幀所有的檢測框生成對應的新跟蹤框。 # 2.第二幀開始到以后所有幀: # 上一幀成功跟蹤并且保留下來的的跟蹤框 在當前幀中 進行新一輪的預測新的跟蹤框, # 并且針對所預測的新跟蹤框和當前幀中的檢測框進行iou計算和使用匈牙利算法對該兩者進行關聯(lián)匹配, # 通過上述操作后成功返回跟蹤目標成功的跟蹤框(即和當前幀中的目標檢測框相匹配的跟蹤框), # 并且另外發(fā)現(xiàn)了新出現(xiàn)目標的檢測框、跟蹤目標失敗的跟蹤框(即目標離開了畫面/兩者匹配度IOU值小于iou閾值), # 那么首先使用當前幀中的檢測框對“成功關聯(lián)匹配的跟蹤框中的”狀態(tài)向量進行更新, # 然后對新增目標的檢測框生成對應新的跟蹤框,最后把跟蹤目標失敗的跟蹤框從跟蹤器鏈列表中移除出去。 # 2.傳入的檢測框dets:[檢測框的左上角的x/y坐標, 檢測框的右下角的x/y坐標, 檢測框的預測類別的概率值] # 3.返回值tracks: # 當前幀中跟蹤目標成功的跟蹤框/預測框的集合,包含目標的跟蹤的id(也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個) # 第一種返回值方案:[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度, trk.id] ...] # 第二種返回值方案(當前使用的為該種):[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, trk.id] ...] # d:[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標] # trk.id:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個。 class Sort(object):"""Sort 是一個多目標跟蹤器的管理類,管理多個 跟蹤器鏈中的多個 KalmanBoxTracker 卡爾曼濾波對象"""def __init__(self, max_age=1, min_hits=3):"""初始化:設置SORT算法的關鍵參數(shù):param max_age: 最大檢測數(shù):目標未被檢測到的幀數(shù),超過之后會被刪除:param min_hits: 目標命中的最小次數(shù),小于該次數(shù)update函數(shù)不返回該目標的KalmanBoxTracker卡爾曼濾波對象""""""max_age:跟蹤框的最大連續(xù)跟丟幀數(shù)。如果當前跟蹤框連續(xù)N幀大于最大連續(xù)跟丟幀數(shù)的話,則從跟蹤器鏈中刪除該卡爾曼濾波對象的預測框(跟蹤框)。min_hits:跟蹤框連續(xù)成功跟蹤到目標的最小次數(shù)(目標連續(xù)命中的最小次數(shù)),也即跟蹤框至少需要連續(xù)min_hits次成功跟蹤到目標。trackers:卡爾曼濾波跟蹤器鏈,存儲多個 KalmanBoxTracker 卡爾曼濾波對象frame_count:當前視頻經(jīng)過了多少幀的計數(shù)"""self.max_age = max_age # 最大檢測數(shù):目標未被檢測到的幀數(shù),超過之后會被刪self.min_hits = min_hits # 目標連續(xù)命中的最小次數(shù),小于該次數(shù)update函數(shù)不返回該目標的KalmanBoxTracker卡爾曼濾波對象self.trackers = [] # 卡爾曼濾波跟蹤器鏈,存儲多個 KalmanBoxTracker 卡爾曼濾波對象self.frame_count = 0 # 幀計數(shù)"""update(dets):輸入dets:當前幀中yolo所檢測出的所有目標的檢測框的集合,包含每個目標的score以[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]形式輸入的numpy.arrayx1、y1 代表檢測框的左上角坐標;x2、y2代表檢測框的右上角坐標;score代表檢測框對應預測類別的概率值。輸出ret:當前幀中跟蹤目標成功的跟蹤框/預測框的集合,包含目標的跟蹤的id(也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個)第一種返回值方案:[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度, trk.id] ...]第二種返回值方案(當前使用的為該種):[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, trk.id] ...]d:[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]trk.id:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個。注意:即使檢測框為空,也必須對每一幀調用此方法,返回一個類似的輸出數(shù)組,最后一列是目標對像的id。返回的目標對象數(shù)量可能與檢測框的數(shù)量不同。"""def update(self, dets):"""該方法實現(xiàn)了SORT算法,輸入是當前幀中所有物體的檢測框的集合,包括目標的score,輸出是當前幀目標的跟蹤框集合,包括目標的跟蹤的id要求是即使檢測框為空,也必須對每一幀調用此方法,返回一個類似的輸出數(shù)組,最后一列是目標對像的id注意:返回的目標對象數(shù)量可能與檢測框的數(shù)量不同:param dets:以[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]形式輸入的numpy.array:return:"""""" 每經(jīng)過一幀,frame_count+=1"""self.frame_count += 1"""1.trackers:上一幀中的跟蹤器鏈(列表),保存的是上一幀中成功跟蹤目標的跟蹤框,也即上一幀中成功跟蹤目標的KalmanBoxTracker卡爾曼濾波對象。2.trks = np.zeros((len(trackers), 5))上一幀中的跟蹤器鏈(列表)中的所有跟蹤框(卡爾曼濾波對象)在當前幀中成功進行predict預測新跟蹤框后返回的值。所有新跟蹤框的左上角的x坐標和y坐標、右下角的x坐標和y坐標、置信度 的一共5個值。1.因為一開始第一幀時,trackers跟蹤器鏈(列表)仍然為空,所以此時的trks初始化如下:np.zeros((0, 5)) 輸出值:array([], shape=(0, 5), dtype=float64)輸出值類型:<class 'numpy.ndarray'>2.np.zeros((len(trackers), 5)) 創(chuàng)建目的:1.用于存儲上一幀中的跟蹤器鏈中所有跟蹤框(KalmanBoxTracker卡爾曼濾波對象)在當前幀中進行predict預測新跟蹤框后返回的值,之所以創(chuàng)建的numpy數(shù)組的列數(shù)為5,是因為一個跟蹤框在當前幀中進行predict預測新跟蹤框后返回的值為1行5列的矩陣,返回值分別為新跟蹤框的左上角的x坐標和y坐標、右下角的x坐標和y坐標、置信度 的一共5個值。2.如果是在視頻的第一幀中,那么因為跟蹤器鏈不存在任何跟蹤框(KalmanBoxTracker卡爾曼濾波對象),因此np.zeros((len(trackers), 5))創(chuàng)建的是空列表:array([], shape=(0, 5), dtype=float64)。3.trackers:跟蹤器鏈(列表)1.跟蹤器鏈中存儲了上一幀中成功跟蹤目標并且在當前幀中的預測框(跟蹤框),同時也存儲了“為了當前幀中的檢測框中的新增目標所創(chuàng)建的”新預測框(新跟蹤框),但是同時不存儲當前幀中預測跟蹤失敗的預測框(跟蹤框),同時也不存儲2.跟蹤器鏈實際就是多個的卡爾曼濾波KalmanBoxTracker自定義類的實例對象組成的列表。每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),KalmanBoxTracker類中的實例屬性專門負責記錄其對應的一個目標框中各種統(tǒng)計參數(shù),并且使用類屬性負責記錄卡爾曼濾波器的創(chuàng)建個數(shù),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。把每個卡爾曼濾波器(KalmanBoxTracker實例對象)都存儲到跟蹤器鏈(列表)中。"""# 在當前幀逐個預測軌跡位置,記錄狀態(tài)異常的跟蹤器索引# 根據(jù)當前所有的卡爾曼跟蹤器個數(shù)(即上一幀中跟蹤的目標個數(shù))創(chuàng)建二維數(shù)組:行號為卡爾曼濾波器的標識索引,列向量為跟蹤框的位置和IDtrks = np.zeros((len(self.trackers), 5)) # 存儲跟蹤器的預測""" to_del:存儲“跟蹤器鏈中某個要刪除的”KalmanBoxTracker卡爾曼濾波對象的索引 """to_del = [] # 存儲要刪除的目標框ret = [] # 存儲要返回的追蹤目標框"""for t, trk in enumerate(ndarray類型的trks)t:為從0到列表長度-1的索引值trk:ndarray類型的trks中每個(1, 5)形狀的一維數(shù)組"""""" 遍歷trks 用于存儲上一幀中的跟蹤器鏈中所有跟蹤框(KalmanBoxTracker卡爾曼濾波對象)在當前幀中進行predict預測新跟蹤框后返回的值 """# 循環(huán)遍歷卡爾曼跟蹤器列表for t, trk in enumerate(trks):""" 上一幀中的跟蹤器鏈中所有跟蹤框(KalmanBoxTracker卡爾曼濾波對象)在當前幀中進行predict預測新跟蹤框 """# 使用卡爾曼跟蹤器t產(chǎn)生對應目標的跟蹤框pos = self.trackers[t].predict()[0]""" 新跟蹤框的左上角的x坐標和y坐標、右下角的x坐標和y坐標、置信度 的一共5個值。trk中存儲了上一幀中目標的跟蹤框在當前幀中新的跟蹤框的信息值。"""# 遍歷完成后,trk中存儲了上一幀中跟蹤的目標的預測跟蹤框trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]""" 如果預測的新的跟蹤框的信息(1行5列一共5個值)中包含空值的話,則將該跟蹤框在跟蹤器鏈(列表)中的索引值t放到to_del列表中。使用np.any(np.isnan(pos))即能判斷這1行5列一共5個值是否包含空值。后面下一步將會根據(jù)to_del列表中保存的跟蹤框的索引值到跟蹤器鏈(列表)中將該跟蹤框從其中移除出去。"""# 如果跟蹤框中包含空值則將該跟蹤框添加到要刪除的列表中if np.any(np.isnan(pos)):to_del.append(t)""" np.ma.masked_invalid(跟蹤器鏈trks矩陣):將會對跟蹤器鏈trks矩陣中出現(xiàn)了NaN或inf的某行進行生成掩碼,用于屏蔽出現(xiàn)無效值該整行的跟蹤器框。np.ma.compress_rows(包含掩碼值的跟蹤器鏈trks矩陣):將包含掩碼值的整行從中進行移除出去。最終跟蹤器鏈trks矩陣:只包含“上一幀中的跟蹤器鏈中所有跟蹤框在當前幀中成功進行predict預測”的新跟蹤框。"""# numpy.ma.masked_invalid 屏蔽出現(xiàn)無效值的數(shù)組(NaN 或 inf)# numpy.ma.compress_rows 壓縮包含掩碼值的2-D 數(shù)組的整行,將包含掩碼值的整行去除# trks中存儲了上一幀中成功跟蹤目標并且在當前幀中的預測框(跟蹤框)trks = np.ma.compress_rows(np.ma.masked_invalid(trks))"""1.for t in reversed(列表):1.t:列表中的元素值2.要想從List列表中刪除任意索引位置的元素的話,必須不能從列表頭開始遍歷刪除元素,必須從列表尾向列表頭的方向進行遍歷刪除元素,因為如果從列表頭開始遍歷刪除元素的話,便會導致后面的元素會移動補充到被刪除元素的索引位置上,那么再向后進行遍歷時便會出現(xiàn)漏遍歷的元素,也即防止破壞索引,因此刪除列表中元素時需要從列表尾向列表頭的方向進行遍歷。2.for t in reversed(to_del)1.t:列表中的元素值2.此處to_del列表中的元素值保存的是trackers跟蹤器鏈(列表)中要刪除元素的索引值,因此從to_del列表的列表尾向列表頭的方向進行遍歷出“trackers跟蹤器鏈(列表)中要刪除元素的”索引值。然后使用trackers.pop(t)根據(jù)trackers跟蹤器鏈(列表)中元素的索引值t自動從列表中移除該元素。3.List pop()方法1.pop()方法語法:list.pop([index=-1])2.pop()函數(shù)用于移除列表中的一個元素(默認最后一個元素),并且返回該元素的值。3.pop(可選參數(shù))中參數(shù):可選參數(shù),要移除列表元素的索引值,不能超過列表總長度,默認為 index=-1,刪除最后一個列表值。4.pop()返回值:該方法返回從列表中被移除的元素對象。5.pop(要移除的列表中元素的索引值):根據(jù)列表中元素的索引值自動從列表中移除"""# 逆向刪除異常的跟蹤器,防止破壞索引for t in reversed(to_del):"""根據(jù)to_del列表中保存的跟蹤框的索引值到跟蹤器鏈(列表)中將該跟蹤框從其中移除出去。trackers:上一幀中的跟蹤器鏈(列表),保存的是上一幀中成功跟蹤目標的跟蹤框,也即成功跟蹤目標的KalmanBoxTracker卡爾曼濾波對象。trackers.pop(要移除的某個跟蹤框的索引值):即能根據(jù)該索引值從跟蹤器鏈(列表)中把該跟蹤框移除出去"""#pop(要移除的列表中元素的索引值):根據(jù)列表中元素的索引值自動從列表中移除self.trackers.pop(t)"""matches:[[檢測框的索引值, 跟蹤框的索引值] [檢測框的索引值, 跟蹤框的索引值] 。。。]跟蹤成功并且兩兩匹配組合的IOU值大于iou閾值的檢測框和跟蹤框組成的矩陣unmatched_detections:[檢測框的索引值,。。。]1.新增目標的檢測框在detections檢測框列表中的索引位置2.兩兩匹配組合的IOU值小于iou閾值的檢測框在detections檢測框列表中的索引位置unmatched_trackers:[跟蹤框的索引值,。。。]1.跟蹤失敗的跟蹤框/預測框在trackers跟蹤框列表中的索引位置2.兩兩匹配組合的IOU值小于iou閾值的跟蹤框/預測框在trackers跟蹤框列表中的索引位置1.matched:跟蹤成功目標的矩陣。即前后幀都存在的目標,并且匹配成功同時大于iou閾值。2.unmatched_detections(列表):1.檢測框中出現(xiàn)新目標,但此時預測框(跟蹤框)中仍不不存在該目標,那么就需要在創(chuàng)建新目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象),然后把新目標對應的KalmanBoxTracker類的實例對象放到跟蹤器鏈(列表)中。2.同時如果因為“跟蹤框和檢測框之間的”兩兩組合的匹配度IOU值小于iou閾值,則也要把目標檢測框放到unmatched_detections中。3.unmatched_trackers(列表):1.當跟蹤目標失敗或目標離開了畫面時,也即目標從檢測框中消失了,就應把目標對應的跟蹤框(預測框)從跟蹤器鏈中刪除。unmatched_trackers列表中保存的正是跟蹤失敗即離開畫面的目標,但該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)此時仍然存在于跟蹤器鏈(列表)中,因此就需要把該目標對應的預測框/跟蹤框(KalmanBoxTracker類的實例對象)從跟蹤器鏈(列表)中刪除出去。2.同時如果因為“跟蹤框和檢測框之間的”兩兩組合的匹配度IOU值小于iou閾值,則也要把跟蹤目標框放到unmatched_trackers中。"""# 將目標檢測框與卡爾曼濾波器預測的跟蹤框關聯(lián)獲取跟蹤成功的目標,新增的目標,離開畫面的目標matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets, trks)"""for t, trk in enumerate(trackers列表)t:為從0到列表長度-1的索引值trk:trackers列表中每個KalmanBoxTracker卡爾曼濾波對象"""# 將跟蹤成功的目標框更新到對應的卡爾曼濾波器for t, trk in enumerate(self.trackers):""" 1.trackers:上一幀中的跟蹤器鏈(列表),保存的是上一幀中成功跟蹤目標的跟蹤框,也即成功跟蹤目標的KalmanBoxTracker卡爾曼濾波對象。2.for t, trk in enumerate(trackers):遍歷上一幀中的跟蹤器鏈(列表)中從0到列表長度-1的索引值t 和 每個KalmanBoxTracker卡爾曼濾波對象trk。3.if t not in unmatched_trks:如果上一幀中的跟蹤框(KalmanBoxTracker卡爾曼濾波對)的索引值不在當前幀中的unmatched_trackers(列表)中的話,即代表上一幀中的跟蹤框在當前幀中成功跟蹤到目標,并且代表了“上一幀中的跟蹤框在當前幀中的”預測框和當前幀中的檢測框的匹配度IOU值大于iou閾值。4.matched[:, 1]:獲取的是跟蹤框的索引值,即[[檢測框的索引值, 跟蹤框的索引值] 。。。]中的跟蹤框的索引值。5.np.where(matched[:, 1] == t)[0]:where返回的為符合條件的“[檢測框的索引值, 跟蹤框的索引值]”數(shù)組在matched矩陣中的索引值,即行值。因此最后使用[0]就是從array([索引值/行值])中把索引值/行值取出來。6.matched[索引值/行值, 0]:根據(jù)索引值/行值獲取出matched矩陣中的[檢測框的索引值, 跟蹤框的索引值],然后獲取出第一列的“檢測框的索引值”。7.dets[d, :]:根據(jù)檢測框的索引值/行值從當前幀中的dets檢測框列表獲取出該檢測框的所有列值,最終返回的是一個二維矩陣如下所示:第一種方案:[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度]]第二種方案(當前使用的為該種):[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]]8.dets[d, :][0]:獲取出[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]9.trk.update(檢測框的5個值的列表):使用檢測框進行更新狀態(tài)更新向量x(狀態(tài)變量x),也即使用檢測框更新跟蹤框。"""if t not in unmatched_trks:d = matched[np.where(matched[:, 1] == t)[0], 0]# 使用觀測的邊界框更新狀態(tài)向量trk.update(dets[d, :][0])"""unmatched_detections(列表)保存了出現(xiàn)新目標的檢測框的索引值,還保存了“因為跟蹤框和檢測框之間的兩兩組合的匹配度IOU值小于iou閾值的”目標檢測框的索引值。dets[i, :]:根據(jù)索引值從當前幀中的檢測框列表dets中獲取對應的檢測框,即該行的所有列值。該檢測框的值為:第一種方案:[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度]]第二種方案(當前使用的為該種):[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]]KalmanBoxTracker(dets[i, :]):傳入檢測框進行創(chuàng)建該新目標對應的跟蹤框KalmanBoxTracker卡爾曼濾波對象trk。每個目標框都有對應的一個卡爾曼濾波器(KalmanBoxTracker實例對象),增加一個目標框就增加一個卡爾曼濾波器(KalmanBoxTracker實例對象)。trackers.append(trk):把新增的卡爾曼濾波器(KalmanBoxTracker實例對象trk)存儲到跟蹤器鏈(列表)trackers中"""# 為新增的目標創(chuàng)建新的卡爾曼濾波器對象進行跟蹤for i in unmatched_dets:trk = KalmanBoxTracker(dets[i, :])self.trackers.append(trk)# 自后向前遍歷,僅返回在當前幀出現(xiàn)且命中周期大于self.min_hits(除非跟蹤剛開始)的跟蹤結果;如果未命中時間大于self.max_age則刪除跟蹤器。# hit_streak忽略目標初始的若干幀""" i為trackers跟蹤器鏈(列表)長度,從列表尾向列表頭的方向 每遍歷trackers跟蹤器鏈(列表)一次 即進行 i-=1 """i = len(self.trackers)""" reversed逆向遍歷trackers跟蹤器鏈(列表),目的為刪除列表中的元素的同時不會造成漏遍歷元素的問題 """for trk in reversed(self.trackers):""" (跟蹤框)KalmanBoxTracker卡爾曼濾波對象trk.get_state():獲取跟蹤框所預測的在當前幀中的預測結果(已經(jīng)從[x,y,s,r]轉換為[x1,y1,x2,y2]) [x1,y1,x2,y2]即為[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]。get_state()[0] 中使用[0] 是因為返回的為二維矩陣如下: 第一種方案:[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度]]第二種方案(當前使用的為該種):[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]]"""# 返回當前邊界框的估計值d = trk.get_state()[0]"""1.trk.time_since_update < 1:1.time_since_update:記錄了該目標對應的卡爾曼濾波器中的預測框(跟蹤框)進行連續(xù)預測的次數(shù),每執(zhí)行predict一次即進行time_since_update+=1。在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,一旦執(zhí)行update的話,time_since_update就會被重置為0。2. time_since_update < 1:該目標對應的卡爾曼濾波器一旦update更新的話該變量值便重置為0,因此要求該目標對應的卡爾曼濾波器必須執(zhí)行update更新步驟。update更新代表了使用檢測框來更新狀態(tài)更新向量x(狀態(tài)變量x)的操作,實際即代表了使用“通過yoloV3得到的并且和預測框(跟蹤框)相匹配的”檢測框來更新該目標對應的卡爾曼濾波器中的預測框(跟蹤框)。2.trk.hit_streak >= min_hits:1.hit_streak1.連續(xù)更新的次數(shù),每執(zhí)行update一次即進行hit_streak+=1。2.在連續(xù)更新(連續(xù)執(zhí)行update)的過程中,一旦開始連續(xù)執(zhí)行predict兩次或以上的情況下,當連續(xù)第一次執(zhí)行predict時,因為time_since_update仍然為0,并不會把hit_streak重置為0,然后才會進行time_since_update+=1;當連續(xù)第二次執(zhí)行predict時,因為time_since_update已經(jīng)為1,那么便會把hit_streak重置為0,然后繼續(xù)進行time_since_update+=1。 2.min_hits跟蹤框連續(xù)成功跟蹤到目標的最小次數(shù),也即跟蹤框至少需要連續(xù)min_hits次成功跟蹤到目標。3.hit_streak >= min_hits跟蹤框連續(xù)更新的次數(shù)hit_streak必須大于等于min_hits。而小于該min_hits次數(shù)的話update函數(shù)不返回該目標的KalmanBoxTracker卡爾曼濾波對象。3.frame_count <= min_hits:因為視頻的一開始frame_count為0,而需要每經(jīng)過一幀frame_count才會+=1。因此在視頻的一開始前N幀中,即使frame_count 小于等于min_hits 也可以。"""# 跟蹤成功目標的box與id放入ret列表中if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):""" 1.ret:當前幀中跟蹤目標成功的跟蹤框/預測框的集合,包含目標的跟蹤的id(也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個)第一種返回值方案:[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度, trk.id] ...]第二種返回值方案(當前使用的為該種):[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, trk.id] ...]d:[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]trk.id:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個。2.np.concatenate((d, [trk.id + 1])).reshape(1, -1)[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, 該跟蹤框是創(chuàng)建出來的第幾個]]"""ret.append(np.concatenate((d, [trk.id + 1])).reshape(1, -1)) # +1 as MOT benchmark requires positive""" i為trackers跟蹤器鏈(列表)長度,從列表尾向列表頭的方向 每遍歷trackers跟蹤器鏈(列表)一次 即進行 i-=1 """i -= 1"""trk.time_since_update > max_age1.time_since_update:記錄了該目標對應的卡爾曼濾波器中的預測框(跟蹤框)進行連續(xù)預測的次數(shù),每執(zhí)行predict一次即進行time_since_update+=1。在連續(xù)預測(連續(xù)執(zhí)行predict)的過程中,一旦執(zhí)行update的話,time_since_update就會被重置為0。2.max_age:最大跟丟幀數(shù)。如果當前連續(xù)N幀大于最大跟丟幀數(shù)的話,則從跟蹤器鏈中刪除該卡爾曼濾波對象的預測框(跟蹤框)。3.time_since_update > max_age:每預測一幀time_since_update就會+=1,只有預測的跟蹤框跟蹤到目標(即預測的跟蹤框和檢測框相似度匹配)才會執(zhí)行update更新,那么time_since_update才會被重置為0。那么當連續(xù)time_since_update幀都沒有跟蹤到目標的話,即當連續(xù)time_since_update幀大于最大跟丟幀數(shù)時,那么就需要根據(jù)該跟蹤失敗的跟蹤器框的索引把該跟蹤器框從跟蹤器鏈(列表)trackers中進行移除出去。"""# 跟蹤失敗或離開畫面的目標從卡爾曼跟蹤器中刪除if trk.time_since_update > self.max_age:"""trackers:上一幀中的跟蹤器鏈(列表),保存的是上一幀中成功跟蹤目標的跟蹤框,也即成功跟蹤目標的KalmanBoxTracker卡爾曼濾波對象。trackers.pop(要移除的某個跟蹤框的索引值):即能根據(jù)該索引值從跟蹤器鏈(列表)中把該跟蹤框移除出去"""# pop(要移除的列表中元素的索引值):根據(jù)列表中元素的索引值自動從列表中移除self.trackers.pop(i)# 返回當前畫面中所有目標的box與id,以二維矩陣形式返回if len(ret) > 0:""" ret:當前幀中跟蹤目標成功的跟蹤框/預測框的集合,包含目標的跟蹤的id(也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個)第一種返回值方案:[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度, trk.id] ...]第二種返回值方案(當前使用的為該種):[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, trk.id] ...]d:[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]trk.id:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個。[[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度, 該跟蹤框是創(chuàng)建出來的第幾個] [...][...]]"""return np.concatenate(ret)return np.empty((0, 5)) from 項目一.day05.kalman import * import imutils import time import cv2 import numpy as np import matplotlib.pyplot as plt""" 基于虛擬線圈法的車輛統(tǒng)計1.基于虛擬線圈的車流量統(tǒng)計算法原理與交通道路上的常見的傳統(tǒng)的物理線圈類似,由于物理線圈需要埋設在路面之下,因此會有安裝、維護費用高,造成路面破壞等問題,而采用基于視頻的虛擬線圈的車輛計數(shù)方法完全避免了以上問題,且可以針對多個感興趣區(qū)域進行檢測。2.虛擬線圈車輛計數(shù)法的原理是在采集到的交通流視頻中,在需要進行車輛計數(shù)的道路或路段上設置一條或一條以上的檢測線對通過車輛進行檢測,從而完成計數(shù)工作。檢測線的設置原則一般是在檢測車道上設置一條垂直于車道線,居中的虛擬線段,通過判斷其與通過車輛的相對位置的變化,完成車流量統(tǒng)計的工作。如下圖所示,綠色的線就是虛擬檢測線: """""" 1.虛擬線圈法檢測的方法是,計算前后兩幀圖像的車輛檢測框的中心點連線,若該連線與檢測線相交,則計數(shù)加一,否則計數(shù)不變。 2.那怎么判斷兩條線段是否相交呢?假設有兩條線段AB,CD,若AB,CD相交,我們可以確定:1.線段AB與CD相交,即點A和點B分別在線段CD的兩邊;2.線段CD與AB相交,即點C和點D分別在線段AB的兩邊;上面兩個條件同時滿足是兩線段相交的充要條件,所以我們只需要證明點A和點B分別在線段CD的兩邊,點C和點D分別在線段AB的兩邊,這樣便可以證明線段AB與CD相交了。3.在上圖中,線段AB與線段CD相交,于是我們可以得到兩個向量AC、AD,其中C和D分別在AB的兩邊。1.向量AC在向量AB的逆時針方向,得AB×AC > 0。AB×AC實際是以A點為時鐘圓盤的中心點,AB和AC分別是時鐘的兩個時針。向量AC在向量AB的逆時針方向的意思即為時針AB向時針AC進行逆時針移動,也即為B點向C點進行逆時針移動,最終得出AB×AC > 0;2.向量AD在向量AB的順時針方向,得AB×AD < 0。AB×AD實際是以A點為時鐘圓盤的中心點,AB和AD分別是時鐘的兩個時針。向量AD在向量AB的順時針方向的意思即為時針AB向時針AD進行順時針移動,也即為B點向D點進行順時針移動,最終得出AB×AD < 0;最終得出 AB×AC > 0 和 AB×AD < 0 兩個向量叉乘的結果為異號。3.這樣,方法就出來了:如果線段CD的兩個端點C和D,與另一條線段AB中的一個端點(A或B,只能是其中一個)連成的向量(比如AC/AD),然后AC/AD與向量AB做叉乘。若結果異號,表示C和D分別在線段AB的兩邊;若結果同號,則表示CD兩點都在AB的其中一邊,則肯定不相交。所以我們利用叉乘的方法來判斷車輛是否經(jīng)過檢測線。4.此處的叉乘使用的是兩個向量進行叉乘計算1.向量AB(線段AB):可以是圖像畫面中的檢測線,檢測線的設置原則一般是在檢測車道上設置一條垂直于車道的虛擬線段。2.向量CD(線段CD):可以是前后兩幀的目標框圖像中的中心點所連成的一條線段。3.那么當線段CD中的C、D兩個點(前后兩幀的兩個中心點)分別位于線段AB(檢測線)的兩邊時,那么此時可以通過兩個向量的叉乘計算,得出是否線段CD和線段AB是否相交。 """# 線與線的碰撞檢測:叉乘的方法判斷兩條線是否相交 # 計算叉乘符號 def ccw(A, B, C):return (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0]) """ (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0])1.[0]:線段的其中一點的x坐標[1]:線段的其中一點的y坐標2.A點坐標(x1, y1)、B點坐標(x2, y2)、C點坐標(x3, y3)。A點坐標(x1, y1) 和 B點坐標(x2, y2) 那么得 A×B = x1*y2 - x2*y1。3.BA為(x2-x1, y2-y1),CA為(x3-x1, y3-y1)。CA(x3-x1, y3-y1)中將x3-x1看作是w1,將y3-y1看做是h1;BA(x2-x1, y2-y1)中將x2-x1看作是w2,將y2-y1看做是h2;得出CA為(w1, h1),BA為(w2, h2),那么CA*BA = w1*h2 - w2*h1。4.可以把 (C[1] - A[1]) * (B[0] - A[0]) - (B[1] - A[1]) * (C[0] - A[0]) 轉換為 (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0]) 來使用,兩者等同。5.BA為(x2-x1, y2-y1),BA還可以看作為(w2, h2)。CA為(x3-x1, y3-y1),CA還可以看作為(w1, h1)。(C[1] - A[1]):y3-y1,也即h1(B[0] - A[0]):x2-x1,也即w2(B[1] - A[1]):y2-y1,也即h2(C[0] - A[0]):x3-x1,也即w16.(C[1] - A[1]) * (B[0] - A[0]) - (B[1] - A[1]) * (C[0] - A[0]) 即可以看做 BA*CA = h1*w2 - h2*w1 7.如果線段CD的兩個端點C和D,與另一條線段AB中的一個端點(A或B,只能是其中一個)連成的向量(比如AC/AD),然后AC/AD與向量AB做叉乘。此處便使用BA*CA做叉乘,根據(jù)BA*CA = h1*w2 - h2*w1 得出 (C[1] - A[1]) * (B[0] - A[0]) - (B[1] - A[1]) * (C[0] - A[0])。 """ # 檢測AB和CD兩條直線是否相交 def intersect(A, B, C, D):return ccw(A, C, D) != ccw(B, C, D) and ccw(A, B, C) != ccw(A, B, D)""" CA(x3-x1, y3-y1)中將x3-x1看作是w1,將y3-y1看做是h1 BA(x2-x1, y2-y1)中將x2-x1看作是w2,將y2-y1看做是h2 DA(x4-x1, y4-y1)中將x4-x1看作是w3,將y4-y1看做是h3 CB(x3-x2, y3-y2)中將x3-x2看作是w4,將y3-y2看做是h4 DB(x4-x2, y4-y2)中將x4-x2看作是w5,將y4-y2看做是h5ccw(A, C, D) != ccw(B, C, D) and ccw(A, B, C) != ccw(A, B, D) 1.ccw(A, C, D):(D[1] - A[1]) * (C[0] - A[0]) > (C[1] - A[1]) * (D[0] - A[0]) 即可以看做 CA*DA = h3*w1 - h1*w3 2.ccw(B, C, D):(D[1] - B[1]) * (C[0] - B[0]) > (C[1] - B[1]) * (D[0] - B[0]) 即可以看做 CB*DB = h5*w4 - h4*w5 3.ccw(A, B, C):(C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0]) 即可以看做 BA*CA = h1*w2 - h2*w1 4.ccw(A, B, D):(D[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (D[0] - A[0]) 即可以看做 BA*DA = h3*w2 - h2*w3 5.ccw(A, C, D) != ccw(B, C, D):AC×AD < 0 和 BC×BD > 0 兩個向量叉乘的結果為異號。向量AD在向量AC的順時針方向。向量BD在向量BC的逆時針方向。 6.ccw(A, B, C) != ccw(A, B, D):AB×AC > 0 和 AB×AD < 0 兩個向量叉乘的結果為異號。向量AC在向量AB的逆時針方向。向量AD在向量AB的順時針方向。 7.ccw(A, C, D) != ccw(B, C, D) and ccw(A, B, C) != ccw(A, B, D):同時符合上述兩者關系則得到AB和CD相交。 """# 虛擬線圈的檢測線line,從圖左邊的(0, 150)點 畫一直線連接到 圖右邊的(2560, 150)。 # 一旦有同一目標的前后兩幀的檢測框的中心點所連成的線段 相交于 虛擬線圈的檢測線line 時,則認為兩條線段相交。 line = [(0, 150), (2560, 150)] # 車輛總數(shù) counter = 0 # 正向車道的車輛數(shù)據(jù) counter_up = 0 # 逆向車道的車輛數(shù)據(jù) counter_down = 0# 創(chuàng)建跟蹤器對象 tracker = Sort() #當前幀跟蹤成功的跟蹤框(KalmanBoxTracker卡爾曼濾波對象) #key:跟蹤框是創(chuàng)建出來的第幾個的序號。value:跟蹤框[左上角的x坐標, 左上角的x坐標y坐標, 右下角的x坐標, 右下角的y坐標]。 memory = {}# 利用yoloV3模型進行目標檢測 # 加載模型相關信息 # 加載可以檢測的目標的類型 # 1.加載可以識別物體的名稱,將其存放在LABELS中,一共有80種,在這我們只使用car labelPath = "./yolo-coco/coco.names" LABELS = open(labelPath).read().strip().split("\n") # 設置隨機數(shù)種子,生成多種不同的顏色,當一個畫面中有多個目標時,使用不同顏色的框將其框起來 np.random.seed(42) COLORS = np.random.randint(0, 255, size=(200, 3), dtype='uint8') # 加載已訓練好的yolov3網(wǎng)絡的權重和相應的配置數(shù)據(jù) # 加載好數(shù)據(jù)之后,開始利用上述數(shù)據(jù)恢復yolo神經(jīng)網(wǎng)絡 weightsPath = "./yolo-coco/yoloV3.weights" configPath = "./yolo-coco/yoloV3.cfg" #創(chuàng)建出yoloV3網(wǎng)絡 net = cv2.dnn.readNetFromDarknet(configPath, weightsPath) # 獲取yolo中每一層的名稱 ln = net.getLayerNames() # print("yolo中每一層的名稱",ln) #['conv_0', 'bn_0', 'relu_1', 'conv_1', 'bn_1', 'relu_2', 'conv_2', 'bn_2', 'relu_3', 'conv_3', 'bn_3', 'relu_4', # 'shortcut_4', 'conv_5', 'bn_5', 'relu_6', 'conv_6', 'bn_6', 'relu_7', 'conv_7', 'bn_7', 'relu_8', 'shortcut_8', # 'conv_9', 'bn_9', 'relu_10', 'conv_10', 'bn_10', 'relu_11', 'shortcut_11', 'conv_12', 'bn_12', 'relu_13', # 'conv_13', 'bn_13', 'relu_14', 'conv_14', 'bn_14', 'relu_15', 'shortcut_15', 'conv_16', 'bn_16', 'relu_17', # 'conv_17', 'bn_17', 'relu_18', 'shortcut_18', 'conv_19', 'bn_19', 'relu_20', 'conv_20', 'bn_20', 'relu_21', # 'shortcut_21', 'conv_22', 'bn_22', 'relu_23', 'conv_23', 'bn_23', 'relu_24', 'shortcut_24', 'conv_25', 'bn_25', # 'relu_26', 'conv_26', 'bn_26', 'relu_27', 'shortcut_27', 'conv_28', 'bn_28', 'relu_29', 'conv_29', 'bn_29', # 'relu_30', 'shortcut_30', 'conv_31', 'bn_31', 'relu_32', 'conv_32', 'bn_32', 'relu_33', 'shortcut_33', # 'conv_34', 'bn_34', 'relu_35', 'conv_35', 'bn_35', 'relu_36', 'shortcut_36', 'conv_37', 'bn_37', 'relu_38', # 'conv_38', 'bn_38', 'relu_39', 'conv_39', 'bn_39', 'relu_40', 'shortcut_40', 'conv_41', 'bn_41', 'relu_42', # 'conv_42', 'bn_42', 'relu_43', 'shortcut_43', 'conv_44', 'bn_44', 'relu_45', 'conv_45', 'bn_45', 'relu_46', # 'shortcut_46', 'conv_47', 'bn_47', 'relu_48', 'conv_48', 'bn_48', 'relu_49', 'shortcut_49', 'conv_50', # 'bn_50', 'relu_51', 'conv_51', 'bn_51', 'relu_52', 'shortcut_52', 'conv_53', 'bn_53', 'relu_54', 'conv_54', # 'bn_54', 'relu_55', 'shortcut_55', 'conv_56', 'bn_56', 'relu_57', 'conv_57', 'bn_57', 'relu_58', 'shortcut_58', # 'conv_59', 'bn_59', 'relu_60', 'conv_60', 'bn_60', 'relu_61', 'shortcut_61', 'conv_62', 'bn_62', 'relu_63', # 'conv_63', 'bn_63', 'relu_64', 'conv_64', 'bn_64', 'relu_65', 'shortcut_65', 'conv_66', 'bn_66', 'relu_67', # 'conv_67', 'bn_67', 'relu_68', 'shortcut_68', 'conv_69', 'bn_69', 'relu_70', 'conv_70', 'bn_70', 'relu_71', # 'shortcut_71', 'conv_72', 'bn_72', 'relu_73', 'conv_73', 'bn_73', 'relu_74', 'shortcut_74', 'conv_75', # 'bn_75', 'relu_76', 'conv_76', 'bn_76', 'relu_77', 'conv_77', 'bn_77', 'relu_78', 'conv_78', 'bn_78', # 'relu_79', 'conv_79', 'bn_79', 'relu_80', 'conv_80', 'bn_80', 'relu_81', 'conv_81', 'permute_82', 'yolo_82', # 'identity_83', 'conv_84', 'bn_84', 'relu_85', 'upsample_85', 'concat_86', 'conv_87', 'bn_87', 'relu_88', # 'conv_88', 'bn_88', 'relu_89', 'conv_89', 'bn_89', 'relu_90', 'conv_90', 'bn_90', 'relu_91', 'conv_91', # 'bn_91', 'relu_92', 'conv_92', 'bn_92', 'relu_93', 'conv_93', 'permute_94', 'yolo_94', 'identity_95', # 'conv_96', 'bn_96', 'relu_97', 'upsample_97', 'concat_98', 'conv_99', 'bn_99', 'relu_100', 'conv_100', # 'bn_100', 'relu_101', 'conv_101', 'bn_101', 'relu_102', 'conv_102', 'bn_102', 'relu_103', 'conv_103', # 'bn_103', 'relu_104', 'conv_104', 'bn_104', 'relu_105', 'conv_105', 'permute_106', 'yolo_106']""" un connected Out Layers(未連接的輸出層): [[200] [227] [254]]""" print("net.getUnconnectedOutLayers()",net.getUnconnectedOutLayers()) # 獲取輸出層在網(wǎng)絡中的索引位置,并以列表的形式:['yolo_82', 'yolo_94', 'yolo_106'] ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]# 讀取圖像 # frame = cv2.imread('./images/car2.jpg') # (W,H) = (None,None) # (H,W) = frame.shape[:2] # 初始化vediocapture類,參數(shù)指定打開的視頻文件,也可以是攝像頭 vs = cv2.VideoCapture('./input/test_1.mp4') # 視頻的寬度和高度,即幀尺寸 (W, H) = (None, None) # 視頻文件寫對象 writer = None try:# 確定獲取視頻幀數(shù)的方式prop = cv2.cv.CV_CAP_PROP_Frame_COUNT if imutils.is_cv2() else cv2.CAP_PROP_FRAME_COUNT# 獲取視頻的總幀數(shù)total = int(vs.get(prop))# 打印視頻的幀數(shù)print("INFO:{} total Frame in video".format(total)) except:print("[INFO] could not determine in video")# 遍歷每一幀圖像 while True:# 讀取幀:grabbed是bool,表示是否成功捕獲幀,frame是捕獲的幀(grabed, frame) = vs.read()#讀取完整個視頻之后,grabed為False# 若未捕獲幀,則退出循環(huán)if not grabed:break# 若W或H為空,則將第一幀畫面的寬度和高度 即幀尺寸賦值給他if W is None or H is None:#獲取圖像的寬高(H, W) = frame.shape[:2]# 根據(jù)輸入圖像構造blob,利用OPenCV進行深度網(wǎng)路的計算時,一般將圖像轉換為blob形式,對圖片進行預處理,包括縮放,減均值,通道交換等# 還可以設置尺寸,一般設置為在進行網(wǎng)絡訓練時的圖像的大小# 將圖像轉換為blob,下一步可用于前向傳播的輸入數(shù)據(jù)blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (416, 416), swapRB=True, crop=False)# 將blob送入yoloV3前向網(wǎng)絡中net.setInput(blob)start = time.time()# yoloV3網(wǎng)絡 前向傳播,進行預測,返回目標框邊界和相應的概率layerOutputs = net.forward(ln)end = time.time()# 用于存放識別物體目標的檢測框信息,包括框的左上角橫坐標x和縱坐標y以及框的高h和寬wboxes = []# 置信度:此處存儲的為最大概率的類別的預測概率值confidences = [] #表示識別目標是某種物體的可信度# 目標類別:此處存儲的為 最大概率值的類別索引值classIDs = [] # 表示識別的目標歸屬于哪一類,['person', 'bicycle', 'car', 'motorbike'....]"""輸出layerOutsputs介紹:是YOLO算法在圖片中檢測到的bbx的信息由于YOLO v3有三個輸出,也就是上面提到的['yolo_82', 'yolo_94', 'yolo_106']因此layerOutsputs是一個長度為3的列表其中,列表中每一個元素的維度是(num_detection, 85)num_detections表示該層輸出檢測到bbx的個數(shù)85:因為該模型在COCO數(shù)據(jù)集上訓練,[5:]表示類別概率;[0:4]表示bbx的位置信息;[5]表示置信度下面對網(wǎng)絡輸出的bbx進行檢查:判定每一個bbx的置信度是否足夠的高,以及執(zhí)行NMS算法去除冗余的bbx"""# 遍歷每個輸出層[yolo-82, yolo-94, yolo-106]for output in layerOutputs:# 遍歷某個輸出層的檢測框結果for detection in output:# detction檢測框:1*85維度的向量。其中[5:]表示類別,[0:4]bbox的位置信息 [4]置信度scores = detection[5:] #80個類別的概率值。scores的大小應該是1*80,因為在訓練yolo模型時是80類目標classID = np.argmax(scores) #獲取最大概率值的類別索引值confidence = scores[classID] #根據(jù)最大概率值的類別索引值 獲取出對應的類別#如果該最大概率的類別的預測概率值 大于 0.3if confidence > 0.3:"""1.pw和ph分別為手動設定的錨框Anchor boxes寬和高,而網(wǎng)絡最終計算的預測結果為(bx, by, bw, bh),因此需要把(tx, ty, tw, th)轉換為(bx, by, bw, bh)。2.把(tx, ty, tw, th)轉換為(bx, by, bw, bh)作為yolo輸出層的最終輸出:σ讀作sigma。Cx和Cy分別為當前單元格(grid cell)距離輸入原圖的左上角原點的邊距離。W和H為輸入原圖像的寬和高。分別除以W和H,目的是歸一化。tx->bx:bx = (σ(tx) + Cx) / Wty->by:by = (σ(ty) + Cy) / Htw->bw:bw = (pw * e^tw) / Wth->bh:bh = (ph * e^th) / Hσ(tx) + Cx:邊界框的中心點在輸入原圖像中的x坐標,也即邊界框的中心點離輸入原圖像原點的x方向長度σ(ty) + Cy:邊界框的中心點在輸入原圖像中的y坐標,也即邊界框的中心點離輸入原圖像原點的y方向長度pw * e^tw:邊界框在輸入原圖像中的寬度ph * e^th:邊界框在輸入原圖像中的高度"""# 將檢測結果邊界框的坐標還原至與原圖片適配,YOLO返回的是邊界框的中心坐標以及邊界框的寬度和高度box = detection[0:4] * np.array([W, H, W, H])# 使用 astype("int") 對上述 array 進行強制類型轉換# centerX:檢測框的中心點橫坐標, centerY:檢測框的中心點縱坐標,width:檢測框的寬度,height:檢測框的高度(centerX, centerY, width, height) = box.astype("int")# 計算邊界框的左上角的橫坐標:檢測框的中心點橫坐標 - 檢測框的寬度/2x = int(centerX - width / 2)# 計算邊界框的左上角的縱坐標:檢測框的中心點縱坐標 - 檢測框的高度/2y = int(centerY - height / 2)# 更新檢測到的目標框,置信度和類別ID# boxes:[邊界框的左上角的橫坐標, 邊界框的左上角的縱坐標, 檢測框的寬度, 檢測框的高度]boxes.append([x, y, int(width), int(height)]) # 將邊框的信息添加到列表boxesconfidences.append(float(confidence)) # 將識別出是某種物體的置信度添加到列表confidencesclassIDs.append(classID) # 將識別物體歸屬于哪一類的信息添加到列表classIDs"""上一步中已經(jīng)得到y(tǒng)olo的檢測框,但其中會存在冗余的bbox,即一個目標對應多個檢測框,所以使用NMS去除重復的檢測框。利用OpenCV內置的NMS DNN模塊實現(xiàn)即可實現(xiàn)非最大值抑制 ,所需要的參數(shù)是邊界框、置信度、以及置信度閾值和NMS閾值。第一個參數(shù)是存放邊界框的列表,第二個參數(shù)是存放置信度的列表,第三個參數(shù)是自己設置的置信度,第四個參數(shù)是NMS閾值。返回的idxs是一個一維數(shù)組,數(shù)組中的元素是保留下來的檢測框boxes的索引位置。dnn.NMSBoxes作用:根據(jù)給定的檢測boxes和對應的scores進行NMS(非極大值抑制)處理原型:NMSBoxes(bboxes, scores, score_threshold, nms_threshold, eta=None, top_k=None)參數(shù):boxes: 待處理的邊界框 bounding boxesscores: 對于于待處理邊界框的 scoresscore_threshold: 用于過濾 boxes 的 score 閾值nms_threshold: NMS 用到的閾值indices: NMS 處理后所保留的邊界框的索引值eta: 自適應閾值公式中的相關系數(shù):nms_threshold_i+1 = eta * nms_threshold_itop_k: 如果 top_k>0,則保留最多 top_k 個邊界框索引值."""# 非極大值抑制# 此處的confidences使用的是 最大概率的類別的預測概率值。返回值idxs:保留下來的檢測框boxes的索引位置的一維數(shù)組idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.3)# 存放檢測框的信息,包括左上角橫坐標/縱坐標,右下角橫坐標/縱坐標,以及檢測到的物體的置信度(檢測框的預測類別的概率值),用于目標跟蹤dets = []# 存在檢測框的話(即檢測框個數(shù)大于0)。idxs也即 保留下來的檢測框boxes的索引位置的一維數(shù)組if len(idxs) > 0:# 循環(huán)檢測出的每一個檢測框boxes的索引位置for i in idxs.flatten():# yolo模型可以識別很多目標,因為我們在這里只是識別車,所以只有目標是車的我們進行檢測,其他的忽略# classIDs[檢測框boxes的索引位置]:根據(jù)檢測框boxes的索引位置從classIDs列表中取出該檢測框boxes對應的類別if LABELS[classIDs[i]] == "car":(x, y) = (boxes[i][0], boxes[i][1]) # 得到檢測框的左上角的x/y坐標(w, h) = (boxes[i][2], boxes[i][3]) # 得到檢測框的寬和高# cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)#檢測框dets:[檢測框的左上角的x/y坐標, 檢測框的右下角的x/y坐標, 檢測框的預測類別的概率值]dets.append([x, y, x + w, y + h, confidences[i]]) # 將檢測框的信息的放入dets中# 類型設置# 設置數(shù)據(jù)類型,將整型數(shù)據(jù)轉換為浮點數(shù)類型,且保留小數(shù)點后三位np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})# 將檢測框數(shù)據(jù)轉換為ndarray,其數(shù)據(jù)類型為浮點型dets = np.asarray(dets)# # 顯示# plt.imshow(frame[:,:,::-1])# plt.show()#檢測框為0if np.size(dets) == 0:continueelse:#1.SORT目標跟蹤:# 1.第一幀剛開始時:對第一幀所有的檢測框生成對應的新跟蹤框。# 2.第二幀開始到以后所有幀:# 上一幀成功跟蹤并且保留下來的的跟蹤框 在當前幀中 進行新一輪的預測新的跟蹤框,# 并且針對所預測的新跟蹤框和當前幀中的檢測框進行iou計算和使用匈牙利算法對該兩者進行關聯(lián)匹配,# 通過上述操作后成功返回跟蹤目標成功的跟蹤框(即和當前幀中的目標檢測框相匹配的跟蹤框),# 并且另外發(fā)現(xiàn)了新出現(xiàn)目標的檢測框、跟蹤目標失敗的跟蹤框(即目標離開了畫面/兩者匹配度IOU值小于iou閾值),# 那么首先使用當前幀中的檢測框對“成功關聯(lián)匹配的跟蹤框中的”狀態(tài)向量進行更新,# 然后對新增目標的檢測框生成對應新的跟蹤框,最后把跟蹤目標失敗的跟蹤框從跟蹤器鏈列表中移除出去。#2.傳入的檢測框dets:[檢測框的左上角的x/y坐標, 檢測框的右下角的x/y坐標, 檢測框的預測類別的概率值]#3.返回值tracks:# 當前幀中跟蹤目標成功的跟蹤框/預測框的集合,包含目標的跟蹤的id(也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個)# 第一種返回值方案:[[左上角的x坐標, 左上角的y坐標, 右下角的x坐標, 右下角的y坐標, yolo識別目標是某種物體的可信度, trk.id] ...]# 第二種返回值方案(當前使用的為該種):[[左上角的x坐標, 左上角的y坐標, 右下角的x坐標, 右下角的y坐標, trk.id] ...]# d:[左上角的x坐標, 左上角的y坐標, 右下角的x坐標, 右下角的y坐標]# trk.id:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個。tracks = tracker.update(dets)# 跟蹤框boxes = []# indexIDs 也即 trk.id:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個indexIDs = []# 上一幀跟蹤成功的跟蹤框:把上一幀保留下來的跟蹤成功的跟蹤框 深拷貝 一份previous = memory.copy()# 創(chuàng)建新的集合 用于保存 當前幀跟蹤成功的跟蹤框(KalmanBoxTracker卡爾曼濾波對象)# key:跟蹤框是創(chuàng)建出來的第幾個的序號。value:跟蹤框[左上角的x坐標, 左上角的y坐標, 右下角的x坐標, 右下角的y坐標]。memory = {}# 遍歷 當前幀中跟蹤目標成功的跟蹤框for track in tracks:#當前幀中跟蹤目標成功的跟蹤框(即當前幀中檢測框相匹配的跟蹤框):[左上角的x坐標, 左上角的y坐標, 右下角的x坐標, 右下角的y坐標]boxes.append([track[0], track[1], track[2], track[3]])# 即 trk.id:卡爾曼濾波器的個數(shù)/目標框的個數(shù),也即該跟蹤框(卡爾曼濾波實例對象)是創(chuàng)建出來的第幾個indexIDs.append(int(track[4]))#key:跟蹤框是創(chuàng)建出來的第幾個的序號。value:跟蹤框[左上角的x坐標, 左上角的y坐標, 右下角的x坐標, 右下角的y坐標]。memory[indexIDs[-1]] = boxes[-1]# 碰撞檢測:虛擬線圈法檢測的方法是 計算前后兩幀圖像的車輛檢測框的中心點連線,若該連線與檢測線相交,則計數(shù)加一,否則計數(shù)不變。if len(boxes) > 0:i = int(0)# 遍歷跟蹤框for box in boxes:(x, y) = (int(box[0]), int(box[1])) # 左上角的x坐標, 左上角的x坐標(w, h) = (int(box[2]), int(box[3])) # 右下角的x坐標, 右下角的y坐標color = [int(c) for c in COLORS[indexIDs[i] % len(COLORS)]]cv2.rectangle(frame, (x, y), (w, h), color, 2)# 根據(jù)在上一幀和當前幀的檢測結果,利用虛擬線圈完成車輛計數(shù)#indexIDs[i]:跟蹤框是創(chuàng)建出來的第幾個的序號。previous:key:跟蹤框是創(chuàng)建出來的第幾個的序號。#判斷的是 跟蹤框是創(chuàng)建出來的第幾個的序號 是否和 previous中有相同的 keyif indexIDs[i] in previous:#previous[key:跟蹤框是創(chuàng)建出來的第幾個的序號] 獲取出value 跟蹤框[左上角的x坐標, 左上角的y坐標, 右下角的x坐標, 右下角的y坐標]previous_box = previous[indexIDs[i]](x2, y2) = (int(previous_box[0]), int(previous_box[1])) # 左上角的x坐標, 左上角的y坐標(w2, h2) = (int(previous_box[2]), int(previous_box[3])) # 右下角的x坐標, 右下角的y坐標#上一幀中跟蹤框的 中心點的x坐標:左上角的x坐標 + (右下角的x坐標 - 左上角的x坐標) / 2#上一幀中跟蹤框的 中心點的y坐標:左上角的y坐標 + (右下角的y坐標 - 左上角的y坐標) / 2p1 = (int(x2 + (w2 - x2) / 2), int(y2 + (h2 - y2) / 2))#當前幀中跟蹤框的 中心點的x坐標:左上角的x坐標 + (右下角的x坐標 - 左上角的x坐標) / 2#當前幀中跟蹤框的 中心點的y坐標:左上角的y坐標 + (右下角的y坐標 - 左上角的y坐標) / 2p0 = (int(x + (w - x) / 2), int(y + (h - y) / 2))# 利用p0,p1與line進行碰撞檢測# 檢測AB和CD兩條直線是否相交:p0和p1即為AB,line[0]和line[1]即為CD# 同一個目標的前后兩幀的跟蹤框的中心點構建為一條線段,即AB。檢測線即CD。if intersect(p0, p1, line[0], line[1]):counter += 1# 判斷行進方向#上一幀中跟蹤框的 左上角的y坐標 大于 當前幀中跟蹤框的 左上角的y坐標if y2 > y:# 逆向車道的車輛數(shù)據(jù)counter_down += 1else:# 正向車道的車輛數(shù)據(jù)counter_up += 1i += 1# 將車輛計數(shù)的相關結果放在視頻上cv2.line(frame, line[0], line[1], (0, 255, 0), 3)cv2.putText(frame, str(counter), (30, 80), cv2.FONT_HERSHEY_DUPLEX, 3.0, (255, 0, 0), 3)cv2.putText(frame, str(counter_up), (130, 80), cv2.FONT_HERSHEY_DUPLEX, 3.0, (0, 255, 0), 3)cv2.putText(frame, str(counter_down), (230, 80), cv2.FONT_HERSHEY_DUPLEX, 3.0, (0, 0, 255), 3)# 將檢測結果保存在視頻if writer is None:fourcc = cv2.VideoWriter_fourcc(*"mp4v")writer = cv2.VideoWriter("./output/output.mp4", fourcc, 30, (frame.shape[1], frame.shape[0]), True)writer.write(frame)cv2.imshow("", frame)if cv2.waitKey(1) & 0xFF == ord('q'):break"釋放資源" writer.release() vs.release() cv2.destroyAllWindows()
總結
- 上一篇: 前端学习(1713):前端系列javas
- 下一篇: gulp4.0构建任务(一次执行多个任务