
摘要:本文詳細介紹如何利用深度學習中的YOLO及SORT算法實現車輛、行人等多目標的實時檢測和跟蹤,並利用PyQt5設計了清新簡約的系統UI界面,在界面中既可選擇自己的視頻、圖片文件進行檢測跟蹤,也可以通過電腦自帶的攝像頭進行實時處理,可選擇訓練好的YOLO v3/v4等模型參數。該系統界面優美、檢測精度高,功能強大,設計有多目標實時檢測、跟蹤、計數功能,可自由選擇感興趣的跟蹤目標。博文提供了完整的Python程序代碼和使用教程,適合新入門的朋友參考,完整代碼資源文件請轉至文末的下載鏈接。本博文目錄如下:
完整資源下載鏈接:https://mbd.pub/o/bread/mbd-YZiWlp5s
環境配置及演示視頻鏈接:https://www.bilibili.com/video/BV1S64y1U7VA/(正在更新中,歡迎關注博主B站視頻)
前言
前段時間博主寫了一篇基於深度學習的車輛檢測系統博文,里面是利用MATLAB實現的YOLO檢測器,效果還不錯,其完善的UI界面也受到不少粉絲的關注。最近有不少朋友發消息詢問是否打算出一期Python版的車輛檢測系統,其實我也早有寫一篇類似博文的想法,畢竟老不更新粉絲都要跑( ๑ŏ ﹏ ŏ๑ )了。但每次想寫博客又看到自己每天滿滿當當的日程,感覺無可奈何,沒辦法只得強行擠出一點點時間寫點東西。那這里就強行開一個新坑,更新一下最近推出了車輛行人檢測跟蹤系統,准備從算法到模型訓練、QT界面等實現細節跟大家做一個分享。由於博主實在太忙,這一系列可能不會更新很快,還請見諒,感興趣的朋友可以在下方評論督促一下或私信交流,如若大家反響熱烈,必定立即更新下一篇!
書歸正傳,車輛/行人等多目標的檢測跟蹤毫無疑問是當前視覺領域的網紅方向,因為智能交通、無人駕駛的時代正呼之欲出,視覺檢測自當是研究的基礎。那么拋開這些大概念,我們如果想自己實現多目標檢測跟蹤的功能,有沒有合適的算法去借鑒的呢?考慮到實時性,個人更加青睞YOLO算法,YOLO經過幾代的發展,性能上有了很大提升,這里便采用YOLO模型進行目標檢測。至於目標跟蹤,權衡幾種算法后我選擇 SORT (Simple Online and Realtime Tracking, SORT)算法。
除此之外,一個舒服的系統界面是非常必要的,網上檢測跟蹤的代碼很多,但幾乎沒有人將其開發成一個完整軟件,有的也是粗獷簡陋的界面。為此博主花了一番時間,精心設計了一款合適的界面,也是參考當前流行的客戶端樣式,不敢說精美,也算是保持了博主對界面清新、簡約的風格了。這里上一張清晰的初始界面截圖(點擊圖片可放大):

檢測圖片時的截圖(點擊圖片可放大)如下,若為視頻檢測則可選擇增加軌跡效果:

詳細的功能演示效果參見博主的B站視頻或下一節的動圖演示,覺得不錯的朋友敬請點贊、關注加收藏!系統UI界面的設計工作量較大,界面美化更需仔細雕琢,大家有任何建議或意見和可在下方評論交流。
1. 功能及效果演示
首先展示一下檢測跟蹤系統軟件的功能和效果,系統主要實現的功能是車輛、行人等多目標的實時檢測和跟蹤,在界面中既可選擇自己的視頻、圖片文件進行檢測跟蹤,也可以通過電腦自帶的攝像頭進行實時處理,可選擇訓練好的YOLO v3/v4等模型參數。
(1)選擇視頻文件進行檢測跟蹤:點擊左側視頻按鈕可彈出文件選擇窗口,選擇一個自己的MP4或AVI視頻文件即可顯示視頻畫面,目標標注在畫面框中,右側顯示用時、目標數、置信度、位置坐標,要跟蹤的目標可通過下拉框選擇。

(2)選擇畫面中要跟蹤的目標:在視頻或攝像檢測跟蹤的過程中,如若想指定某個目標進行跟蹤,可通過右側的目標下拉選框選擇,選擇時畫面暫停等待選擇完成,畫面中標注框定位到選中的目標。

(3)目標檢測、跟蹤、計數功能的切換:選擇左側選項,可切換檢測、跟蹤、計數功能,選擇“跟蹤計數”可在目標上標記運動軌跡並計數。

(4)利用攝像頭進行檢測跟蹤:點擊左側攝像頭按鈕,則自動打開電腦上的攝像頭設備,檢測跟蹤的標記信息同樣顯示在界面中。

(5)選擇圖片進行目標檢測:點擊圖片選擇按鈕,彈出圖片選擇框選中一張圖片進行檢測,可自由瀏覽選中某個或多個對象。

2. 視頻中的目標檢測
由於整個軟件的實現代碼復雜,為了使得介紹循序漸進,首先介紹如何利用YOLO進行視頻中目標對的檢測。對於圖像中的目標檢測算法,其中比較流行的有YOLO、SSD等算法。這里我們使用YOLO v4/v3,這篇博文更多介紹的是如何通過代碼使用YOLO,對於算法的原理細節和訓練過程會在接下來的博文介紹。首先導入需要的依賴包,其代碼如下:
from collections import deque
import numpy as np
import imutils
import time
import cv2
import os
from tqdm import tqdm
接下來進行參數設置,首先設置要檢測的視頻路徑,這里需要修改為自己的視頻路徑;然后我們需要加載訓練好的配置、模型權重參數,以及訓練數據集的標簽名稱(類別)文件,它們的路徑分別由變量labelPath、weightsPath、configPath表示。還有一些預定義的參數:filter_confidence(置信度閾值)和threshold_prob(非極大值抑制閾值),它們分別用於篩除置信度過低的識別結果和利用NMS去除重復的錨框:
# 參數設置
video_path = "./video/pedestrian.mp4" # 要檢測的視頻路徑
filter_confidence = 0.5 # 用於篩除置信度過低的識別結果
threshold_prob = 0.3 # 用於NMS去除重復的錨框
model_path = "./yolo-obj" # 模型文件的目錄
# 載入數據集標簽
labelsPath = os.path.sep.join([model_path, "coco.names"])
LABELS = open(labelsPath).read().strip().split("\n")
# 載入模型參數文件及配置文件
weightsPath = os.path.sep.join([model_path, "yolov4.weights"])
configPath = os.path.sep.join([model_path, "yolov4.cfg"])
以下代碼通過上面給出的路徑從配置和參數文件中載入模型,載入模型使用OpenCV的readNetFromDarknet方法載入,我們可以利用它載入自行訓練的模型權重以進行檢測操作:
# 從配置和參數文件中載入模型
print("[INFO] 正在載入模型...")
net = cv2.dnn.readNetFromDarknet(configPath, weightsPath)
ln = net.getLayerNames()
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]
為了后面標記檢測的目標標記框以及目標移動路徑,這里創建兩個變量存儲標記框的顏色及移動路徑的點坐標,其代碼如下:
# 初始化用於標記框的顏色
np.random.seed(42)
COLORS = np.random.randint(0, 255, size=(200, 3), dtype="uint8")
# 用於展示目標移動路徑
pts = [deque(maxlen=30) for _ in range(9999)]
准備就緒,開始從視頻文件路徑初始化視頻對象,其代碼如下:
# 初始化視頻流
vs = cv2.VideoCapture(video_path)
(W, H) = (None, None)
frameIndex = 0 # 視頻幀數
嘗試讀取視頻幀並獲取視頻總幀數total、每幀畫面的尺寸(vw, vh),同時創建一個視頻寫入對象output_video用於后面保存檢測標記的視頻,該部分代碼如下:
# 試運行,獲取總的畫面幀數
try:
prop = cv2.cv.CV_CAP_PROP_FRAME_COUNT if imutils.is_cv2() \
else cv2.CAP_PROP_FRAME_COUNT
total = int(vs.get(prop))
print("[INFO] 視頻總幀數:{}".format(total))
# 若讀取失敗,報錯退出
except:
print("[INFO] could not determine # of frames in video")
print("[INFO] no approx. completion time can be provided")
total = -1
fourcc = cv2.VideoWriter_fourcc(*'XVID')
ret, frame = vs.read()
vw = frame.shape[1]
vh = frame.shape[0]
print("[INFO] 視頻尺寸:{} * {}".format(vw, vh))
output_video = cv2.VideoWriter(video_path.replace(".mp4", "-det.avi"), fourcc, 20.0, (vw, vh)) # 處理后的視頻對象
接下來開始遍歷視頻幀進行檢測,為了清楚地顯示檢測進度,我這里使用了tqdm,它可以在運行的命令行中顯示當前的進度條。讀取當前視頻幀可以使用OpenCV中VideoCapture的read(),該方法返回當前畫面和讀取標記,可通過標記判斷是否到達視頻最后一幀:
# 遍歷視頻幀進行檢測
for fr in tqdm(range(total)):
# 從視頻文件中逐幀讀取畫面
(grabbed, frame) = vs.read()
# 若grabbed為空,表示視頻到達最后一幀,退出
if not grabbed:
break
# 獲取畫面長寬
if W is None or H is None:
(H, W) = frame.shape[:2]
接下來的代碼在以上for循環中進行。首先將當前讀取到的畫面幀讀入YOLO網絡中,在利用網絡預測前需要對輸入畫面(圖片)進行處理,利用cv2.dnn.blobFromImage對圖像進行歸一化並將其尺寸設置為(416,416),這也是YOLO網絡訓練時圖片的尺寸。處理后可利用net.forward進行預測,得到檢測結果,其代碼實現如下:
# 將一幀畫面讀入網絡
blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (416, 416), swapRB=True, crop=False)
net.setInput(blob)
start = time.time()
layerOutputs = net.forward(ln)
end = time.time()
以上代碼中layerOutputs即為檢測結果,這里寫一個循環從中將檢測框坐標、置信度值、識別到的類別序號分別存放在boxes、confidences、classIDs變量中。layerOutputs的結果是按照檢測對象存放的,在循環中我們還需根據置信度值的閾值過濾掉一些置信度值不高的結果:
boxes = [] # 用於檢測框坐標
confidences = [] # 用於存放置信度值
classIDs = [] # 用於識別的類別序號
# 逐層遍歷網絡獲取輸出
for output in layerOutputs:
# loop over each of the detections
for detection in output:
# extract the class ID and confidence (i.e., probability)
# of the current object detection
scores = detection[5:]
classID = np.argmax(scores)
confidence = scores[classID]
# 過濾低置信度值的檢測結果
if confidence > filter_confidence:
box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int")
# 轉換標記框
x = int(centerX - (width / 2))
y = int(centerY - (height / 2))
# 更新標記框、置信度值、類別列表
boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
classIDs.append(classID)
對於以上整理出的結果,可能存在重復或者接近的標記框位置,我們可以使用NMS(非極大值抑制)技術去除:
# 使用NMS去除重復的標記框
idxs = cv2.dnn.NMSBoxes(boxes, confidences, filter_confidence, threshold_prob)
最終我們將得到去除后的索引,利用它可以得到NMS操作后的標記框坐標、置信度值、類別序號列表,可通過以下的for循環實現,最終結果存放在dets變量中:
dets = []
if len(idxs) > 0:
# 遍歷索引得到檢測結果
for i in idxs.flatten():
(x, y) = (boxes[i][0], boxes[i][1])
(w, h) = (boxes[i][2], boxes[i][3])
dets.append([x, y, x + w, y + h, confidences[i], classIDs[i]])
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
dets = np.asarray(dets)
目標檢測的效果如下圖所示:

3. 多目標跟蹤
通過上一節的介紹我們了解了如何使用YOLO進行目標檢測,當在對視頻中的多個對象進行檢測時,可以看到標記框隨着目標的移動而不斷移動,那么如何才能確定當前幀中的對象與之前一幀中的對象是否是同一個呢?這其實涉及到目標跟蹤的概念,可以理解為隨着時間的推移,多次進行檢測以識別某些特定的目標,並得到目標運動的軌跡。
對於目標跟蹤部分,在權衡幾種算法后,我決定選擇 SORT (Simple Online and Realtime Tracking, SORT)算法,它易於實現、運行速度快。該算法其實來源於Alex Bewley等人在2017年發表的一篇論文:Bewley A, Ge Z, Ott L, et al. Simple online and realtime tracking[C]//2016 IEEE international conference on image processing (ICIP). IEEE, 2016: 3464-3468.,該論文提出使用卡爾曼濾波器來預測先前識別出的物體的軌跡,並將它們與新的檢測結果相匹配。論文作者給出了SORT算法的Python實現,網址為:https://github.com/abewley/sort,博主目標跟蹤的這部分代碼引用自該實現,在其基礎上我作了改寫以適合使用。
首先初始化一個SORT對象tracker,使用tracker.update方法進行跟蹤,得到跟蹤到的標記結果boxes(標記框坐標)、indexIDs(當前目標計數序號,即第幾個出現的目標)、cls_IDs(類別序號),該部分代碼如下:
# 使用sort算法,開始進行追蹤
tracker = Sort() # 實例化追蹤器對象
tracks = tracker.update(dets)
boxes = [] # 存放追蹤到的標記框
indexIDs = []
cls_IDs = []
c = []
for track in tracks:
boxes.append([track[0], track[1], track[2], track[3]])
indexIDs.append(int(track[4]))
cls_IDs.append(int(track[5]))
得到跟蹤結果后,就剩在圖像幀中進行標記了。我們遍歷所有的標記框,按照標記框的坐標以及對應的類別、置信度值、目標個數可以達到可視化的效果。為了更加形象了解目標運動的情況,通過遍歷pts變量,利用OpenCV的cv2.line方法可以繪制出目標的運動軌跡:
if len(boxes) > 0:
i = int(0)
for box in boxes: # 遍歷所有標記框
(x, y) = (int(box[0]), int(box[1]))
(w, h) = (int(box[2]), int(box[3]))
# 在圖像上標記目標框
color = [int(c) for c in COLORS[indexIDs[i] % len(COLORS)]]
cv2.rectangle(frame, (x, y), (w, h), color, 4)
center = (int(((box[0]) + (box[2])) / 2), int(((box[1]) + (box[3])) / 2))
pts[indexIDs[i]].append(center)
thickness = 5
# 顯示某個對象標記框的中心
cv2.circle(frame, center, 1, color, thickness)
# 顯示目標運動軌跡
for j in range(1, len(pts[indexIDs[i]])):
if pts[indexIDs[i]][j - 1] is None or pts[indexIDs[i]][j] is None:
continue
thickness = int(np.sqrt(64 / float(j + 1)) * 2)
cv2.line(frame, (pts[indexIDs[i]][j - 1]), (pts[indexIDs[i]][j]), color, thickness)
# 標記跟蹤到的目標和數目
text = "{}-{}".format(LABELS[int(cls_IDs[i])], indexIDs[i])
cv2.putText(frame, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 3)
i += 1
以上操作成功將標記信息寫入了frame畫面中,要想實時顯示在屏幕上只需調用OpenCV中的imshow方法開啟一個窗口顯示:
# 實時顯示檢測畫面
cv2.imshow('Stream', frame)
output_video.write(frame) # 保存標記后的視頻
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# print("FPS:{}".format(int(0.6/(end-start))))
frameIndex += 1
if frameIndex >= total: # 可設置檢測的最大幀數提前退出
print("[INFO] 運行結束...")
output_video.release()
vs.release()
exit()
運行以上代碼可以得到下圖中的效果:

至此視頻中多目標檢測跟蹤的代碼實現部分介紹完畢,后面的博文中將給出訓練程序以及UI界面的詳細介紹,至於程序如何使用、依賴包安裝、pycharm及anaconda軟件的安裝過程將通過博主的B站視頻進行演示介紹,敬請關注!
下載鏈接
若您想獲得博文中涉及的實現完整全部程序文件(包括模型權重,py, UI文件等,如下圖),這里已打包上傳至博主的面包多平台和CSDN下載資源。本資源已上傳至面包多網站和CSDN下載資源頻道,可以點擊以下鏈接獲取,已將所有涉及的文件同時打包到里面,點擊即可運行,完整文件截圖如下:

注意:本資源已經過調試通過,下載后可通過PyCharm運行;運行界面的主程序為runMain.py,在配置好Python環境后可完美運行;camera_detection_tracking.py及video_detection_tracking.py這兩個分別為使用攝像頭、視頻檢測跟蹤的腳本文件,亦可直接運行;為確保程序順利運行,建議配置的Python依賴包版本如下:➷➷➷
(Python版本:3.8)
opencv-contrib-python 4.5.1.48
PyQt5 5.15.2
scikit-learn 0.22
numba 0.53.0
imutils 0.5.4
filterpy 1.4.5
tqdm 4.56.0

完整資源下載鏈接:https://mbd.pub/o/bread/mbd-YZiWlp5s
代碼使用介紹及演示視頻鏈接:https://www.bilibili.com/video/BV1S64y1U7VA/(尚在更新中,歡迎關注博主B站視頻)
結束語
由於博主能力有限,博文中提及的方法即使經過試驗,也難免會有疏漏之處。希望您能熱心指出其中的錯誤,以便下次修改時能以一個更完美更嚴謹的樣子,呈現在大家面前。同時如果有更好的實現方法也請您不吝賜教。如果本博文反響較好,其界面部分也將在下篇博文中介紹,所有涉及的GUI界面程序也會作細致講解,敬請期待!