輛檢測(cè)與軌跡分析桌面軟件實(shí)戰(zhàn))
1. 為什么我要做這個(gè)桌面軟件做視覺(jué)算法的人都有一個(gè)共同的痛點(diǎn)模型訓(xùn)練得再好最終要落地給非技術(shù)用戶(hù)使用的時(shí)候總不能讓對(duì)方去敲命令行。我前前后后做過(guò)好幾個(gè)檢測(cè)項(xiàng)目每次交付的時(shí)候最頭疼的就是怎么讓用戶(hù)能直觀(guān)地看到檢測(cè)結(jié)果、能自己切換視頻源、能導(dǎo)出數(shù)據(jù)。后來(lái)我干脆花了兩周時(shí)間基于YOLOv8做了一套交通目標(biāo)檢測(cè)與車(chē)輛軌跡分析的桌面軟件把檢測(cè)、跟蹤、軌跡繪制、數(shù)據(jù)導(dǎo)出全部打包進(jìn)一個(gè)圖形界面里。這個(gè)軟件解決的核心問(wèn)題很明確讓交通場(chǎng)景下的車(chē)輛檢測(cè)和軌跡分析變得開(kāi)箱即用。你不需要懂Python不需要配環(huán)境打開(kāi)軟件選一段視頻或者接一個(gè)攝像頭它就能實(shí)時(shí)框出車(chē)輛、標(biāo)出軌跡、統(tǒng)計(jì)流量。適合做智能交通方向的學(xué)生做課程設(shè)計(jì)也適合做安防監(jiān)控的工程人員快速驗(yàn)證方案更適合想學(xué)YOLOv8落地部署的開(kāi)發(fā)者拿來(lái)當(dāng)參考模板。整個(gè)項(xiàng)目我采用的是YOLOv8做目標(biāo)檢測(cè) backbone配合ByteTrack做多目標(biāo)跟蹤再用自定義的軌跡管理模塊來(lái)繪制車(chē)輛運(yùn)動(dòng)路徑。界面用的是PyQt5因?yàn)樗谧烂娑俗銐虺墒炜丶S富和OpenCV的兼容性也好。下面我會(huì)把整個(gè)項(xiàng)目的設(shè)計(jì)思路、核心代碼、踩過(guò)的坑全部拆開(kāi)講清楚你照著做基本能復(fù)現(xiàn)一個(gè)可用的版本。2. 整體架構(gòu)與技術(shù)選型拆解2.1 為什么選YOLOv8而不是其他檢測(cè)器目標(biāo)檢測(cè)這個(gè)環(huán)節(jié)我對(duì)比過(guò)Faster R-CNN、YOLOv5、YOLOv7和YOLOv8。Faster R-CNN精度確實(shí)不錯(cuò)但推理速度在普通顯卡上跑實(shí)時(shí)視頻流很吃力兩階段檢測(cè)器的天然劣勢(shì)擺在那里。YOLOv5雖然成熟但Ultralytics在v8版本里做了很多工程上的優(yōu)化比如新的C2f模塊、Anchor-Free檢測(cè)頭、Task-Aligned Assigner這些改進(jìn)在保持速度的同時(shí)把精度又往上推了一截。更關(guān)鍵的是YOLOv8的生態(tài)太方便了。ultralytics這個(gè)庫(kù)把訓(xùn)練、驗(yàn)證、推理、導(dǎo)出全部封裝成了幾行代碼的事而且支持ONNX、TensorRT、OpenVINO等多種部署格式。我實(shí)測(cè)下來(lái)在RTX 3060上用YOLOv8nnano版本跑640x640的輸入推理速度能到80FPS以上完全滿(mǎn)足實(shí)時(shí)視頻處理的需求。如果你對(duì)精度要求更高換成YOLOv8m或者YOLOv8l也可以速度會(huì)降一些但依然可用。選模型規(guī)模的時(shí)候有個(gè)經(jīng)驗(yàn)公式如果你處理的是1080p視頻車(chē)輛目標(biāo)比較大YOLOv8n就夠了如果是遠(yuǎn)處的小目標(biāo)或者密集車(chē)流建議上YOLOv8s或YOLOv8m。我一般先用nano版本跑通流程再根據(jù)實(shí)際效果決定要不要換大模型。2.2 跟蹤算法為什么用ByteTrack檢測(cè)只能告訴你每一幀里車(chē)在哪里但軌跡分析需要知道同一輛車(chē)在不同幀之間的對(duì)應(yīng)關(guān)系這就是多目標(biāo)跟蹤要解決的問(wèn)題。常見(jiàn)的跟蹤方案有SORT、DeepSORT、ByteTrack和OC-SORT。SORT速度快但I(xiàn)D切換太頻繁DeepSORT加了外觀(guān)特征匹配效果好一些但需要額外的ReID模型推理開(kāi)銷(xiāo)大。ByteTrack的核心思路很巧妙它把檢測(cè)框按置信度分成高分組和低分組先用高分組做匹配再用低分組去補(bǔ)救那些漏檢的目標(biāo)。這樣一來(lái)即使某輛車(chē)在某幾幀被遮擋導(dǎo)致置信度下降ByteTrack依然能通過(guò)低分檢測(cè)框把它找回來(lái)ID保持得相當(dāng)穩(wěn)定。我實(shí)際測(cè)試下來(lái)在交通場(chǎng)景里ByteTrack的ID Switch次數(shù)比SORT少了大概60%而且它不需要額外的外觀(guān)模型速度幾乎和SORT一樣快。對(duì)于車(chē)輛軌跡分析這種場(chǎng)景ID穩(wěn)定性直接決定了軌跡的連貫性所以ByteTrack是性?xún)r(jià)比最高的選擇。2.3 桌面端為什么選PyQt5界面框架的選擇其實(shí)糾結(jié)了一陣。Tkinter太簡(jiǎn)陋做出來(lái)的界面像上個(gè)世紀(jì)的軟件Electron太重為了一個(gè)檢測(cè)軟件裝個(gè)Chromium沒(méi)必要PyQt5剛好在中間控件豐富、文檔齊全、和OpenCV的QImage轉(zhuǎn)換也很順暢。PyQt5的信號(hào)槽機(jī)制特別適合這種多線(xiàn)程場(chǎng)景。視頻讀取和推理放在子線(xiàn)程里跑主線(xiàn)程只負(fù)責(zé)界面刷新通過(guò)信號(hào)槽傳遞檢測(cè)結(jié)果不會(huì)出現(xiàn)界面卡死的情況。而且PyQt5支持QSS樣式表稍微調(diào)一下就能做出比較現(xiàn)代的深色主題界面用戶(hù)體驗(yàn)比命令行好太多。2.4 整體數(shù)據(jù)流設(shè)計(jì)整個(gè)軟件的數(shù)據(jù)流是這樣的視頻源文件或攝像頭→ 幀讀取 → YOLOv8推理 → ByteTrack跟蹤更新 → 軌跡管理器更新軌跡 → 繪制檢測(cè)框和軌跡 → 界面顯示 數(shù)據(jù)記錄。這里有個(gè)關(guān)鍵的設(shè)計(jì)決策推理和繪制分離。推理線(xiàn)程只負(fù)責(zé)產(chǎn)出檢測(cè)結(jié)果和跟蹤ID繪制和界面更新放在主線(xiàn)程或者單獨(dú)的渲染線(xiàn)程里。這樣做的好處是即使推理偶爾卡頓界面也不會(huì)完全凍結(jié)。我用的是QThread加信號(hào)槽的方式推理線(xiàn)程每處理完一幀就發(fā)一個(gè)信號(hào)帶著結(jié)果數(shù)據(jù)主線(xiàn)程收到后更新畫(huà)面。軌跡管理模塊是我自己寫(xiě)的核心是一個(gè)字典結(jié)構(gòu)key是跟蹤IDvalue是該ID對(duì)應(yīng)的軌跡點(diǎn)列表。每幀更新時(shí)把當(dāng)前幀檢測(cè)到的ID和位置追加進(jìn)去同時(shí)清理掉超過(guò)一定幀數(shù)沒(méi)有更新的軌跡。軌跡繪制用OpenCV的cv2.polylines可以設(shè)置不同顏色區(qū)分不同車(chē)輛。3. 核心模塊的詳細(xì)實(shí)現(xiàn)3.1 YOLOv8推理模塊的封裝Ultralytics的API用起來(lái)很簡(jiǎn)單但直接在主線(xiàn)程里調(diào)用model.predict()會(huì)阻塞界面。我的做法是封裝一個(gè)DetectionWorker類(lèi)繼承自QThread在run方法里循環(huán)讀取幀并推理。from ultralytics import YOLO import cv2 import numpy as np class DetectionWorker(QThread): frame_ready pyqtSignal(np.ndarray, list, list) def __init__(self, source, model_pathyolov8n.pt): super().__init__() self.source source self.model YOLO(model_path) self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running and cap.isOpened(): ret, frame cap.read() if not ret: break results self.model.track( frame, persistTrue, trackerbytetrack.yaml, classes[2, 3, 5, 7], # car, motorcycle, bus, truck conf0.4, iou0.5, verboseFalse ) boxes results[0].boxes if boxes.id is not None: ids boxes.id.cpu().numpy().astype(int) xyxy boxes.xyxy.cpu().numpy().astype(int) clss boxes.cls.cpu().numpy().astype(int) self.frame_ready.emit(frame, xyxy, ids, clss) else: self.frame_ready.emit(frame, [], [], []) cap.release()這里有幾個(gè)細(xì)節(jié)值得說(shuō)。classes[2,3,5,7]是COCO數(shù)據(jù)集里car、motorcycle、bus、truck的類(lèi)別索引只檢測(cè)這幾類(lèi)可以過(guò)濾掉行人、紅綠燈等無(wú)關(guān)目標(biāo)減少后處理負(fù)擔(dān)。persistTrue讓跟蹤器在幀之間保持狀態(tài)這是ByteTrack能跨幀跟蹤的關(guān)鍵。conf0.4是我調(diào)出來(lái)的經(jīng)驗(yàn)值太低會(huì)引入大量誤檢太高會(huì)漏掉遠(yuǎn)處的小車(chē)。注意model.track()第一次調(diào)用時(shí)會(huì)自動(dòng)加載跟蹤器配置如果你用的是自定義數(shù)據(jù)集訓(xùn)練的模型類(lèi)別索引要改成你自己數(shù)據(jù)集的對(duì)應(yīng)值。3.2 ByteTrack跟蹤器的參數(shù)調(diào)優(yōu)ByteTrack的配置文件在ultralytics的cfg/trackers/bytetrack.yaml里默認(rèn)參數(shù)對(duì)大多數(shù)場(chǎng)景夠用但交通場(chǎng)景有幾個(gè)地方可以微調(diào)。track_high_thresh默認(rèn)0.5這是高分檢測(cè)框的閾值。交通場(chǎng)景里車(chē)輛通常比較清晰我調(diào)到0.6讓匹配更嚴(yán)格。track_low_thresh默認(rèn)0.1這是低分框的補(bǔ)救閾值保持默認(rèn)就行。new_track_thresh默認(rèn)0.6新軌跡的創(chuàng)建閾值我調(diào)到0.7避免誤檢產(chǎn)生假軌跡。track_buffer默認(rèn)30意思是軌跡丟失后保留30幀再刪除交通場(chǎng)景里車(chē)輛被遮擋的時(shí)間可能更長(zhǎng)我調(diào)到50。這些參數(shù)沒(méi)有絕對(duì)的最優(yōu)值需要根據(jù)你的視頻場(chǎng)景來(lái)調(diào)。我的建議是先用默認(rèn)參數(shù)跑一段觀(guān)察ID Switch的情況如果切換頻繁就提高track_high_thresh和new_track_thresh如果軌跡容易斷就增大track_buffer。3.3 軌跡管理器的設(shè)計(jì)與實(shí)現(xiàn)軌跡管理器是整個(gè)軟件的核心邏輯之一它決定了軌跡怎么存、怎么畫(huà)、怎么清理。我的設(shè)計(jì)是一個(gè)TrajectoryManager類(lèi)內(nèi)部維護(hù)一個(gè)字典from collections import defaultdict, deque class TrajectoryManager: def __init__(self, max_length60, max_age30): self.trajectories defaultdict(lambda: deque(maxlenmax_length)) self.ages defaultdict(int) self.max_age max_age self.colors {} def update(self, ids, boxes): current_ids set() for tid, box in zip(ids, boxes): current_ids.add(tid) cx int((box[0] box[2]) / 2) cy int((box[1] box[3]) / 2) self.trajectories[tid].append((cx, cy)) self.ages[tid] 0 if tid not in self.colors: self.colors[tid] self._random_color() for tid in list(self.trajectories.keys()): if tid not in current_ids: self.ages[tid] 1 if self.ages[tid] self.max_age: del self.trajectories[tid] del self.ages[tid] if tid in self.colors: del self.colors[tid] def get_trajectories(self): return self.trajectories, self.colors def _random_color(self): import random return (random.randint(100, 255), random.randint(100, 255), random.randint(100, 255))max_length60表示每條軌跡最多保留60個(gè)點(diǎn)按30FPS算就是2秒的軌跡長(zhǎng)度畫(huà)出來(lái)不會(huì)太亂。max_age30表示軌跡丟失30幀后刪除避免已經(jīng)離開(kāi)畫(huà)面的車(chē)輛還留著軌跡線(xiàn)。顏色用隨機(jī)生成的方式每個(gè)ID固定一個(gè)顏色這樣看起來(lái)比較清晰。這里有個(gè)坑要注意deque的maxlen參數(shù)會(huì)讓舊數(shù)據(jù)自動(dòng)彈出但如果你需要做軌跡回放或者速度計(jì)算就不能用deque得用普通list然后手動(dòng)管理長(zhǎng)度。我一開(kāi)始用deque做速度估算發(fā)現(xiàn)歷史點(diǎn)被彈掉了算不準(zhǔn)后來(lái)改成list加手動(dòng)裁剪。3.4 界面布局與交互設(shè)計(jì)PyQt5的界面我用的是左右分欄布局左邊是視頻顯示區(qū)域占大概70%的寬度右邊是控制面板放視頻源選擇、模型選擇、參數(shù)調(diào)節(jié)、數(shù)據(jù)導(dǎo)出這些功能。視頻顯示區(qū)域用一個(gè)QLabel來(lái)承載每幀檢測(cè)結(jié)果通過(guò)cv2.cvtColor轉(zhuǎn)成RGB再用QImage和QPixmap顯示出來(lái)。這里有個(gè)性能優(yōu)化的點(diǎn)不要每幀都創(chuàng)建新的QImage對(duì)象可以復(fù)用同一個(gè)緩沖區(qū)。我實(shí)測(cè)下來(lái)復(fù)用緩沖區(qū)能減少大概15%的CPU占用。控制面板里比較重要的控件包括視頻文件選擇按鈕、攝像頭開(kāi)關(guān)、置信度滑塊、IOU滑塊、軌跡長(zhǎng)度滑塊、開(kāi)始/暫停按鈕、導(dǎo)出CSV按鈕。置信度和IOU滑塊實(shí)時(shí)綁定到檢測(cè)參數(shù)上用戶(hù)拖動(dòng)就能看到效果變化這個(gè)交互體驗(yàn)比改配置文件好太多。提示PyQt5的滑塊控件QSlider默認(rèn)是整數(shù)如果你需要0.1精度的參數(shù)可以把范圍設(shè)成0到100用的時(shí)候除以100。3.5 數(shù)據(jù)記錄與導(dǎo)出模塊交通分析軟件的一個(gè)核心價(jià)值是數(shù)據(jù)導(dǎo)出。我實(shí)現(xiàn)了兩個(gè)導(dǎo)出功能檢測(cè)結(jié)果CSV和軌跡數(shù)據(jù)CSV。檢測(cè)結(jié)果CSV每幀記錄一行幀號(hào)、時(shí)間戳、目標(biāo)ID、類(lèi)別、置信度、左上角坐標(biāo)、右下角坐標(biāo)。軌跡數(shù)據(jù)CSV記錄每個(gè)ID的完整軌跡點(diǎn)序列。導(dǎo)出用Python內(nèi)置的csv模塊就行不需要額外依賴(lài)。import csv from datetime import datetime class DataExporter: def __init__(self): self.detection_records [] self.trajectory_records {} def add_detection(self, frame_id, timestamp, tid, cls_name, conf, box): self.detection_records.append({ frame: frame_id, timestamp: timestamp, track_id: tid, class: cls_name, confidence: round(conf, 3), x1: box[0], y1: box[1], x2: box[2], y2: box[3] }) def export_detections(self, filepath): if not self.detection_records: return False keys self.detection_records[0].keys() with open(filepath, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameskeys) writer.writeheader() writer.writerows(self.detection_records) return Trueencodingutf-8-sig這個(gè)細(xì)節(jié)很重要不加的話(huà)用Excel打開(kāi)中文會(huì)亂碼。newline是為了避免Windows下出現(xiàn)空行。4. 實(shí)操過(guò)程中的關(guān)鍵環(huán)節(jié)4.1 環(huán)境搭建與依賴(lài)安裝環(huán)境這塊我踩過(guò)不少坑這里直接給出一套驗(yàn)證過(guò)的方案。Python版本建議3.9到3.11太新的版本有些庫(kù)還沒(méi)適配。CUDA版本根據(jù)你的顯卡來(lái)30系顯卡用CUDA 11.840系用CUDA 12.1。conda create -n traffic_yolo python3.10 conda activate traffic_yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics pip install PyQt5 opencv-python pandas安裝順序有講究先裝PyTorch再裝ultralytics因?yàn)閡ltralytics會(huì)檢查torch版本如果先裝ultralytics它會(huì)自動(dòng)拉一個(gè)CPU版本的torch后面再換GPU版本很麻煩。opencv-python不要裝opencv-python-headless因?yàn)槲覀冃枰猚v2.imshow之外的GUI功能headless版本缺少一些窗口相關(guān)的函數(shù)。驗(yàn)證安裝是否成功import torch print(torch.cuda.is_available()) # 應(yīng)該輸出True from ultralytics import YOLO model YOLO(yolov8n.pt) print(model.names) # 應(yīng)該輸出COCO的80個(gè)類(lèi)別如果torch.cuda.is_available()返回False檢查CUDA版本和顯卡驅(qū)動(dòng)是否匹配。我遇到過(guò)驅(qū)動(dòng)太舊導(dǎo)致CUDA不可用的情況更新驅(qū)動(dòng)就好了。4.2 模型下載與自定義訓(xùn)練YOLOv8的預(yù)訓(xùn)練模型在第一次調(diào)用YOLO(yolov8n.pt)時(shí)會(huì)自動(dòng)下載但國(guó)內(nèi)網(wǎng)絡(luò)有時(shí)候會(huì)卡住。你可以手動(dòng)從Ultralytics的GitHub Release頁(yè)面下載放到當(dāng)前目錄下代碼里直接寫(xiě)文件名就行。如果你要做特定場(chǎng)景的檢測(cè)比如只檢測(cè)某種特殊車(chē)輛就需要自定義訓(xùn)練。數(shù)據(jù)標(biāo)注用LabelImg或者Roboflow導(dǎo)出YOLO格式的txt標(biāo)注文件。訓(xùn)練命令很簡(jiǎn)單yolo train modelyolov8n.pt datatraffic.yaml epochs100 imgsz640 batch16traffic.yaml里配置訓(xùn)練集和驗(yàn)證集的路徑、類(lèi)別數(shù)量和類(lèi)別名稱(chēng)。我建議至少準(zhǔn)備2000張以上的標(biāo)注圖片每個(gè)類(lèi)別至少500個(gè)實(shí)例否則模型容易過(guò)擬合。訓(xùn)練100輪大概需要2到4小時(shí)取決于顯卡性能。注意自定義訓(xùn)練時(shí)推理代碼里的classes參數(shù)要改成你自己數(shù)據(jù)集的類(lèi)別索引不要照搬COCO的2、3、5、7。4.3 視頻源接入與幀率控制視頻源支持三種本地視頻文件、USB攝像頭、RTSP網(wǎng)絡(luò)流。本地文件用cv2.VideoCapture(path)攝像頭用cv2.VideoCapture(0)RTSP流用cv2.VideoCapture(rtsp://...)。幀率控制是個(gè)容易被忽略的點(diǎn)。如果推理速度比視頻幀率高不加控制的話(huà)視頻會(huì)飛快播完。我的做法是記錄每幀的處理時(shí)間如果處理太快就sleep補(bǔ)足到目標(biāo)幀率import time target_fps 30 frame_time 1.0 / target_fps while running: start time.time() ret, frame cap.read() # ... 推理和繪制 ... elapsed time.time() - start if elapsed frame_time: time.sleep(frame_time - elapsed)如果推理速度比目標(biāo)幀率慢那就沒(méi)辦法了只能跳幀或者降低分辨率。我一般會(huì)把輸入分辨率從1080p降到720p推理速度能提升差不多一倍精度損失在可接受范圍內(nèi)。4.4 軌跡繪制的美化處理軌跡繪制不是簡(jiǎn)單地把點(diǎn)連起來(lái)就行有幾個(gè)美化技巧能讓效果專(zhuān)業(yè)很多。第一軌跡線(xiàn)用漸變色或者透明度漸變。越新的點(diǎn)越亮越舊的點(diǎn)越暗這樣能直觀(guān)看出車(chē)輛的運(yùn)動(dòng)方向。實(shí)現(xiàn)方式是給每個(gè)點(diǎn)設(shè)置不同的alpha值但OpenCV的polylines不支持透明度需要先畫(huà)到一個(gè)透明圖層上再疊加。第二軌跡線(xiàn)寬度隨速度變化。速度快的時(shí)候線(xiàn)粗一點(diǎn)速度慢的時(shí)候線(xiàn)細(xì)一點(diǎn)視覺(jué)上更有動(dòng)感。速度可以通過(guò)相鄰兩幀的中心點(diǎn)距離除以時(shí)間間隔來(lái)估算。第三檢測(cè)框的樣式。我用的是圓角矩形加半透明填充比單純的矩形框好看很多。圓角用cv2.ellipse畫(huà)四個(gè)角直線(xiàn)用cv2.line連接。半透明填充需要cv2.addWeighted疊加。def draw_rounded_rect(img, pt1, pt2, color, thickness2, r8): x1, y1 pt1 x2, y2 pt2 cv2.line(img, (x1r, y1), (x2-r, y1), color, thickness) cv2.line(img, (x1r, y2), (x2-r, y2), color, thickness) cv2.line(img, (x1, y1r), (x1, y2-r), color, thickness) cv2.line(img, (x2, y1r), (x2, y2-r), color, thickness) cv2.ellipse(img, (x1r, y1r), (r, r), 180, 0, 90, color, thickness) cv2.ellipse(img, (x2-r, y1r), (r, r), 270, 0, 90, color, thickness) cv2.ellipse(img, (x1r, y2-r), (r, r), 90, 0, 90, color, thickness) cv2.ellipse(img, (x2-r, y2-r), (r, r), 0, 0, 90, color, thickness)4.5 多線(xiàn)程與界面響應(yīng)優(yōu)化PyQt5的多線(xiàn)程有個(gè)鐵律所有界面更新必須在主線(xiàn)程做。子線(xiàn)程里直接操作控件會(huì)崩潰或者出現(xiàn)奇怪的問(wèn)題。我的做法是子線(xiàn)程只發(fā)信號(hào)主線(xiàn)程收到信號(hào)后更新界面。信號(hào)傳遞的數(shù)據(jù)類(lèi)型要注意PyQt5的信號(hào)支持pyqtSignal(np.ndarray, list, list)這種多參數(shù)形式但numpy數(shù)組需要確保是連續(xù)的。如果遇到信號(hào)傳遞失敗的情況檢查一下數(shù)組是不是被切片成了非連續(xù)內(nèi)存。還有一個(gè)性能優(yōu)化的點(diǎn)界面刷新不需要每幀都做。如果推理是30FPS界面可以降到15FPS刷新人眼看起來(lái)依然流暢但CPU占用能降不少。實(shí)現(xiàn)方式是設(shè)置一個(gè)計(jì)數(shù)器每?jī)蓭鸥乱淮谓缑妗?. 常見(jiàn)問(wèn)題與排查實(shí)錄5.1 檢測(cè)框閃爍和ID跳變這是最常見(jiàn)的問(wèn)題表現(xiàn)為同一輛車(chē)在相鄰幀里ID突然變了或者檢測(cè)框忽大忽小。原因通常有三個(gè)檢測(cè)置信度閾值設(shè)得太低導(dǎo)致誤檢、ByteTrack參數(shù)不匹配、視頻幀間差異太大。排查步驟先把conf閾值提高到0.6以上看ID跳變是否減少。如果還不行檢查track_high_thresh和new_track_thresh適當(dāng)提高。如果視頻本身抖動(dòng)嚴(yán)重可以考慮加一個(gè)簡(jiǎn)單的幀間平滑或者用光流做預(yù)處理。我遇到過(guò)一次特別詭異的情況同一輛車(chē)在畫(huà)面中間ID很穩(wěn)定一到畫(huà)面邊緣就跳變。后來(lái)發(fā)現(xiàn)是邊緣的車(chē)輛被截?cái)鄼z測(cè)框不完整導(dǎo)致匹配失敗。解決辦法是在軌跡管理里對(duì)邊緣區(qū)域的檢測(cè)框做特殊處理或者干脆忽略畫(huà)面邊緣10%區(qū)域內(nèi)的目標(biāo)。5.2 推理速度慢導(dǎo)致界面卡頓如果推理速度跟不上視頻幀率界面會(huì)明顯卡頓。先確認(rèn)是不是在用GPU推理torch.cuda.is_available()返回False的話(huà)就是CPU在跑速度會(huì)慢10倍以上。如果GPU確認(rèn)沒(méi)問(wèn)題但速度還是慢檢查輸入分辨率。YOLOv8默認(rèn)640x640如果你傳的是1080p原圖模型內(nèi)部會(huì)先縮放這個(gè)縮放過(guò)程有開(kāi)銷(xiāo)??梢允謩?dòng)把幀縮放到640再傳入減少不必要的計(jì)算。還有一個(gè)容易被忽略的點(diǎn)model.track()每次調(diào)用都會(huì)做NMS后處理如果檢測(cè)框很多NMS會(huì)很耗時(shí)??梢酝ㄟ^(guò)設(shè)置max_det參數(shù)限制每幀最大檢測(cè)數(shù)交通場(chǎng)景一般50個(gè)就夠了。5.3 軌跡線(xiàn)拖尾和殘留軌跡線(xiàn)拖尾是指車(chē)輛已經(jīng)離開(kāi)畫(huà)面了軌跡線(xiàn)還留在那里。這是max_age設(shè)得太大導(dǎo)致的。max_age30意味著軌跡丟失后還要保留30幀按30FPS算就是1秒。如果你覺(jué)得拖尾太長(zhǎng)把max_age降到15或20。殘留問(wèn)題是指軌跡線(xiàn)畫(huà)在了錯(cuò)誤的位置。這通常是坐標(biāo)系轉(zhuǎn)換的問(wèn)題。OpenCV的坐標(biāo)原點(diǎn)在左上角PyQt的坐標(biāo)原點(diǎn)也在左上角但如果你做了縮放顯示坐標(biāo)就要同步縮放。我的做法是在繪制之前把所有坐標(biāo)統(tǒng)一轉(zhuǎn)換到顯示分辨率下避免混用。5.4 內(nèi)存泄漏與長(zhǎng)時(shí)間運(yùn)行崩潰長(zhǎng)時(shí)間運(yùn)行后內(nèi)存持續(xù)增長(zhǎng)最后崩潰這是桌面軟件常見(jiàn)的問(wèn)題。原因通常是信號(hào)槽連接沒(méi)有斷開(kāi)、QImage對(duì)象沒(méi)有釋放、或者軌跡字典無(wú)限增長(zhǎng)。排查方法用tracemalloc或者memory_profiler監(jiān)控內(nèi)存變化找到增長(zhǎng)最快的對(duì)象。我遇到過(guò)一次是QImage對(duì)象在每次刷新時(shí)都新建舊對(duì)象沒(méi)有被GC回收。解決辦法是復(fù)用QImage緩沖區(qū)或者手動(dòng)調(diào)用del釋放。軌跡字典的增長(zhǎng)也要控制。即使有max_age清理如果視頻里車(chē)輛特別多字典還是會(huì)很大。我加了一個(gè)上限比如最多保留500條軌跡超過(guò)就清理最舊的。5.5 常見(jiàn)問(wèn)題速查表問(wèn)題現(xiàn)象可能原因排查方法解決方案檢測(cè)框閃爍置信度閾值過(guò)低提高conf到0.6調(diào)整conf和iou參數(shù)ID頻繁跳變ByteTrack參數(shù)不匹配觀(guān)察ID Switch次數(shù)提高track_high_thresh界面卡頓推理在CPU上跑檢查cuda.is_available()安裝GPU版本PyTorch軌跡拖尾max_age過(guò)大觀(guān)察軌跡消失延遲降低max_age到15-20內(nèi)存持續(xù)增長(zhǎng)對(duì)象未釋放用memory_profiler監(jiān)控復(fù)用緩沖區(qū)限制字典大小視頻播放過(guò)快未做幀率控制檢查是否有sleep加幀率同步邏輯導(dǎo)出CSV亂碼編碼問(wèn)題用記事本打開(kāi)檢查用utf-8-sig編碼攝像頭打不開(kāi)被其他程序占用檢查設(shè)備管理器關(guān)閉其他占用程序提示這張表是我在實(shí)際調(diào)試中總結(jié)出來(lái)的大部分問(wèn)題都能在這里找到對(duì)應(yīng)方案。如果遇到表里沒(méi)有的問(wèn)題優(yōu)先檢查版本兼容性u(píng)ltralytics和PyQt5的版本更新比較頻繁有時(shí)候降級(jí)一個(gè)版本就好了。6. 一些實(shí)操心得和擴(kuò)展思路這個(gè)軟件做完之后我用了大概兩個(gè)月跑過(guò)各種場(chǎng)景的視頻包括高速公路、城市路口、停車(chē)場(chǎng)、夜間監(jiān)控。有幾個(gè)心得值得分享。夜間場(chǎng)景的檢測(cè)效果明顯下降這是YOLOv8的固有局限訓(xùn)練數(shù)據(jù)里夜間樣本少。如果你的應(yīng)用場(chǎng)景主要是夜間建議收集夜間數(shù)據(jù)做微調(diào)訓(xùn)練或者加一個(gè)圖像增強(qiáng)預(yù)處理比如直方圖均衡化或者CLAHE能提升一些效果。密集車(chē)流場(chǎng)景下ByteTrack的ID Switch會(huì)增多因?yàn)檐?chē)輛之間遮擋嚴(yán)重。這種情況可以考慮換成OC-SORT它對(duì)遮擋的處理更好但速度會(huì)慢一些?;蛘哂肂oT-SORT它結(jié)合了外觀(guān)特征和運(yùn)動(dòng)信息效果最好但需要額外的ReID模型。軌跡分析這塊還可以擴(kuò)展很多功能。比如車(chē)輛速度估算通過(guò)軌跡點(diǎn)的像素距離和幀率換算成實(shí)際速度需要標(biāo)定像素和實(shí)際距離的比例。再比如車(chē)輛變道檢測(cè)通過(guò)軌跡的橫向偏移量來(lái)判斷。還有流量統(tǒng)計(jì)在畫(huà)面里畫(huà)一條虛擬線(xiàn)統(tǒng)計(jì)穿越線(xiàn)的車(chē)輛數(shù)量。導(dǎo)出數(shù)據(jù)這塊也可以做得更豐富。除了CSV還可以導(dǎo)出JSON格式方便程序讀取或者直接生成HTML報(bào)告里面嵌入軌跡圖和統(tǒng)計(jì)圖表。我用matplotlib生成過(guò)軌跡熱力圖能直觀(guān)看出車(chē)輛的主要行駛路徑對(duì)交通分析很有價(jià)值。最后說(shuō)一個(gè)部署上的經(jīng)驗(yàn)。如果你要把這個(gè)軟件交給完全不懂技術(shù)的用戶(hù)最好用PyInstaller打包成exe。打包的時(shí)候注意把模型文件、配置文件、圖標(biāo)資源都包含進(jìn)去用--add-data參數(shù)指定。打包后的exe體積會(huì)比較大因?yàn)镻yTorch和OpenCV的庫(kù)很占空間但用戶(hù)不需要裝任何環(huán)境雙擊就能用體驗(yàn)好很多。