:本地推理與實時告警實現(xiàn))
簡介成套的畢業(yè)設(shè)計行人識別檢測系統(tǒng)源碼基于OpenCV與PyQt開發(fā)核心場景是車輛行駛中自動探測車前行人一旦有人進(jìn)入行進(jìn)路線立即觸發(fā)警告適用于計算機相關(guān)專業(yè)的學(xué)生作為畢業(yè)設(shè)計、課程設(shè)計或期末大作業(yè)也適合希望上手深度學(xué)習(xí)視覺實戰(zhàn)的開發(fā)者參考。壓縮包共21個文件大小約7.79MB主要包含9個Python源文件、2個UI界面文件、1個中文字體文件以及使用說明、依賴清單和README等文檔其中Python代碼負(fù)責(zé)行人檢測與邏輯控制UI文件對應(yīng)可視化操作界面文檔輔助理解結(jié)構(gòu)和啟動方式。項目已通過調(diào)試、解壓即可運行目前已有214人學(xué)習(xí)下載。壓縮包內(nèi)除了入口腳本和核心檢測模塊還配有說明文檔、圖標(biāo)和背景圖片目錄劃分清晰從環(huán)境依賴、界面交互到檢測預(yù)警的完整鏈路都有對應(yīng)文件支撐便于讀者按模塊研讀、替換模型或擴展功能快速完成自己的畢業(yè)設(shè)計或課程項目。1. 行人檢測必須本地推理整車控制鏈路耗不起一次云端往返車載前置攝像頭場景下的行人識別檢測系統(tǒng)最怕的不是模型精度不夠而是響應(yīng)鏈路太長。一個行人從進(jìn)入車前區(qū)域到出現(xiàn)在擋風(fēng)玻璃前留給系統(tǒng)的判斷時間往往只有幾百毫秒。把畫面?zhèn)髟贫?、等推理結(jié)果、再回傳告警一次往返的網(wǎng)絡(luò)抖動就足夠讓告警失去意義。所以這套基于深度學(xué)習(xí) OpencvPyQt 的行人識別檢測系統(tǒng)源碼把檢測推理全部放在本地完成OpenCV 讀取攝像頭幀PyQt 構(gòu)建實時界面threads 模塊管理采集與檢測的多線程調(diào)度。它適合兩類人消化一類是正在做計算機相關(guān)畢業(yè)設(shè)計、需要一套能跑通全鏈路的完整項目源碼另一類是剛接觸目標(biāo)檢測落地、想搞清楚檢測框到告警之間那層工程邏輯的開發(fā)者。run.py 是唯一入口GUI、檢測、線程、資源目錄劃分得很干凈順著調(diào)用鏈往下拆每一步都能對上號。2. OpenCV 深度學(xué)習(xí)檢測鏈路detect 模塊的模型加載與前向傳播2.1 模型選型為什么走 OpenCV DNN 而不是 PyTorch 直接推理打開項目的 requirements.txt 看一眼依賴檢測權(quán)重通常是 .weights 或 .onnx 格式放在 detect 目錄下。這套系統(tǒng)的核心推理走的是 OpenCV DNN 路線用 cv2.dnn.readNetFromDarknet 或 readNetFromONNX 加載模型通過 blobFromImage 做預(yù)處理再 net.forward 拿到輸出張量。源碼里 detect 模塊的 Detector 類就是這個邏輯的載體。選這條路線而不是 PyTorch 直接推理主要是部署成本。畢設(shè)機器上很可能沒有 GPUPyTorch 的 CPU 推理在 640 分辨率下跑一次 YOLOv5s 大約需要 100 到 200 毫秒而同樣的模型導(dǎo)出成 onnx 后用 OpenCV DNN 在 CPU 上能做到 60 到 120 毫秒還不用裝 torch 全家桶。對畢設(shè)來說少一個裝不上的依賴就少一個答辯現(xiàn)場翻車的可能。另一個原因是 OpenCV DNN 天生為部署設(shè)計setPreferableBackend 可以切 OpenCV 自帶算子也可以切 CUDA/OpenCL權(quán)重文件和推理代碼解耦換模型不用動業(yè)務(wù)邏輯。這個項目把它作為默認(rèn)推理后端是典型的工程化取舍。檢測器的初始化部分值得逐行看class Detector: def __init__(self, cfg_path, weights_path, conf_thresh0.4, iou_thresh0.45): self.net cv2.dnn.readNetFromDarknet(cfg_path, weights_path) # 沒有 GPU 就鎖 CPU避免部分機器自動選后端失敗 self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) self.conf_thresh conf_thresh self.iou_thresh iou_thresh self.output_layers [ self.net.getLayerNames()[i[0] - 1] for i in self.net.getUnconnectedOutLayers() ]conf_thresh 是置信度閾值低于它的檢測框直接丟棄。0.4 意味著模型對行人遮擋、小目標(biāo)的召回會打折扣場景里行人大多是中近景時可以放松到 0.35。iou_thresh 是 NMS 的 IoU 閾值0.45 是 YOLO 系列的慣例行人密集時可以降到 0.4 減少重疊框。getUnconnectedOutLayers 拿到的是 YOLO 三個輸出層的名字forward 只跑這幾層不會把整張?zhí)卣鲌D全部算一遍。注意一個版本差異OpenCV 4.5.2 之前 getUnconnectedOutLayers 返回的是二維數(shù)組索引要寫[i[0] - 1]4.5.2 之后返回一維數(shù)組直接[i - 1]。如果運行時報IndexError: invalid index to scalar variable八成是這里的問題改成新寫法即可。2.2 blobFromImage 預(yù)處理與輸出張量解析推理前的預(yù)處理核心是 cv2.dnn.blobFromImage。這一行決定了模型看到的輸入長什么樣blob cv2.dnn.blobFromImage( frame, 1/255.0, (416, 416), swapRBTrue, cropFalse ) self.net.setInput(blob) outs self.net.forward(self.output_layers)blobFromImage 的參數(shù)順序容易記混。第一個參數(shù)是 BGR 幀第二個 scale 是像素縮放1/255 把 0 到 255 歸一化到 0 到 1第三個是模型要求的輸入尺寸源碼里是 416x416。想提速可以降到 320x320想提升小目標(biāo)精度可以提到 608x608但推理尺寸盡量貼近訓(xùn)練時的輸入尺寸跨太多模型泛化會受影響。swapRBTrue 是因為 OpenCV 讀進(jìn)來是 BGR訓(xùn)練時一般用 RGBcropFalse 表示等比縮放不裁剪避免目標(biāo)形變。拿到 outs 之后需要把 YOLO 輸出解成坐標(biāo)。每個輸出張量的最后維度是 85等于 4 個框坐標(biāo)加 1 個 objectness 加 80 個類別分?jǐn)?shù)。COCO 數(shù)據(jù)集里行人 ID 是 0所以過濾條件寫成 class_id 0for out in outs: for detection in out[0]: scores detection[5:] class_id np.argmax(scores) if class_id ! 0: continue confidence scores[class_id] if confidence self.conf_thresh: continue cx, cy, w, h detection[:4] * np.array( [frame_w, frame_h, frame_w, frame_h] ) x1, y1 int(cx - w / 2), int(cy - h / 2) boxes.append([x1, y1, int(w), int(h)]) confidences.append(float(confidence)) idxs cv2.dnn.NMSBoxes(boxes, confidences, self.conf_thresh, self.iou_thresh)這里做了兩件事把歸一化坐標(biāo)乘回原圖尺寸再用 NMSBoxes 做非極大值抑制。detection[:4] 拿到的 cx、cy、w、h 是 0 到 1 的歸一化值必須乘上原圖寬高。很多復(fù)現(xiàn)出錯就是跳過這一步直接用 416 尺寸畫框?qū)е驴虻奈恢谜w偏移。NMSBoxes 返回的是保留框的索引列表后續(xù)畫框和告警判定都以它為輸入。檢測耗時對整個采集循環(huán)影響最大可以在 forward 前后各打一次 time.time()如果單幀推理超過 200 毫秒界面刷新會明顯卡頓。2.3 檢測結(jié)果如何交給上層Detector 類對外只暴露一個 detect 方法輸入原始 BGR 幀輸出過濾后的框列表和置信度列表不關(guān)心界面和線程。上層不管是從視頻文件讀、從攝像頭讀還是從 QThread 里調(diào)用都只跟這個方法打交道。這個設(shè)計保證了 GUI 與檢測邏輯解耦是這套畢設(shè)源碼里值得直接抄的部分。后續(xù)換模型比如把 Darknet 權(quán)重?fù)Q成 YOLOv5 導(dǎo)出的 onnx只需要改 Detector 內(nèi)部的加載函數(shù)和輸出解析調(diào)用方代碼一概不動。3. PyQt QThread攝像頭采集、檢測與界面刷新怎么協(xié)同3.1 GUI 的文件結(jié)構(gòu)與主窗口邏輯gui 目錄下是 PyQt 的主窗口和界面文件入口由最外層的 run.py 統(tǒng)一拉起。主窗口是一個 QMainWindow中間用 QLabel 作為視頻畫布底部或側(cè)邊放開始、停止、選擇視頻源幾個 QPushButton狀態(tài)欄顯示幀率、檢測耗時和告警信息。啟動時先實例化 Detector再創(chuàng)建采集線程和檢測線程界面本身不參與任何 cv2 調(diào)用只負(fù)責(zé)顯示和交互。界面刷新的核心是 QLabel.setPixmap把當(dāng)前幀轉(zhuǎn)成 QImage 再轉(zhuǎn) QPixmap 顯示。這一步比較重每幀都做高頻刷新會把 Qt 事件循環(huán)拖慢。這套源碼里線程之間用 signal/slot 傳幀界面收到幀后判斷距上次刷新的時間差超過 50 毫秒約 20FPS才真正更新畫布沒到間隔的幀直接丟棄。3.2 threads 模塊采集線程與檢測線程的職責(zé)邊界線程劃分的常見做法是兩條線程一條負(fù)責(zé) VideoCapture.read()另一條負(fù)責(zé)模型推理。為什么要拆開因為攝像頭 read 的阻塞時間不穩(wěn)定USB 攝像頭在弱光下偶爾會卡幾十毫秒模型推理又是純 CPU 密集。兩條疊在一起界面信號會被拖垮。分開之后采集線程只管把新幀塞進(jìn)隊列檢測線程取一幀跑一次推理互不阻塞。采集線程的簡化實現(xiàn)class CaptureThread(QThread): frame_captured pyqtSignal(object) def __init__(self, video_source0): super().__init__() self.cap cv2.VideoCapture(video_source) self.running True def run(self): while self.running: ret, frame self.cap.read() if ret and frame is not None: self.frame_captured.emit(frame.copy()) time.sleep(0.03) # 約 30FPS 上限防止隊列被塞爆emit 出去的是 frame.copy()這一步值得解釋。VideoCapture.read() 內(nèi)部可能復(fù)用緩沖如果直接把原 frame emit 給檢測線程檢測線程處理時緩沖區(qū)被下一幀覆蓋畫面會出現(xiàn)撕裂或花屏。copy 是花錢買穩(wěn)定畢設(shè)場景下寧可多一次拷貝也不要偶發(fā)詭異 bug。檢測線程接收 frame_captured 信號后調(diào)用 Detector.detect再把標(biāo)注好框的幀通過信號發(fā)回界面線程。Qt 的信號跨線程默認(rèn)是隊列連接天然不會同時寫一塊內(nèi)存。關(guān)鍵點在于不要在檢測線程里直接調(diào)用 QLabel 的更新方法必須通過信號回到 GUI 線程否則會報 Cannot create children for a parent that is in a different thread。提示檢測線程里任何對 QWidget 的直接操作都會觸發(fā)跨線程訪問異常統(tǒng)一通過 pyqtSignal 回到主線程處理這是 PyQt 多線程界面的鐵律。3.3 信號槽傳幀的裁剪時機與界面防抖檢測完成的幀通常先畫框再發(fā)信號。給 GUI 的幀可以縮小比如畫布只有 960 寬就不需要把 1920 的原圖整個發(fā)過去。在檢測線程里先frame cv2.resize(frame, (960, 540))再 emit信號傳輸?shù)拈_銷能省將近四分之三。numpy 數(shù)組傳給信號需要包一層輔助函數(shù)def to_qimage(frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape bytes_per_line ch * w return QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)這里注意 rgb.data 在 PyQt 下返回的是 memoryview轉(zhuǎn)換成 QImage 后必須保證原數(shù)組生命周期在 QPixmap 轉(zhuǎn)換完成之前不被釋放。常見坑是 QImage 和 QPixmap 每次轉(zhuǎn)換都新建對象導(dǎo)致內(nèi)存碎片運行半小時后變卡。解決方法是成員變量保存 QPixmap尺寸沒變就復(fù)用。界面收到幀后加時間閘def on_frame_ready(self, frame): now time.time() if now - self._last_render 0.05: return self._last_render now qimg to_qimage(frame) self.video_label.setPixmap(QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ))50 毫秒的時間閘把刷新率壓在 20FPS 以下配合檢測線程滿速跑界面既不卡檢測結(jié)果也不至于因為渲染阻塞而丟幀。這個參數(shù)是經(jīng)驗值如果機器性能好可以壓到 0.03人眼對流暢度的感知極限大概在 30FPS。4. 告警邏輯與行進(jìn)路線判定檢測框到“是否走入路線”的幾何判斷4.1 如何定義“行人的行進(jìn)路線”摘要里說得清楚探測車前行人如果有人走入汽車的行進(jìn)路線就發(fā)出警告。要落地成代碼先把“行進(jìn)路線”翻譯成幾何區(qū)域。車輛直行時路線是一個以車為原點向前延伸的梯形區(qū)域近處寬、遠(yuǎn)處窄。放在圖像坐標(biāo)系里就是梯形四個頂點組成的多邊形 ROI。檢測框的底邊中點可以近似為行人的落腳點這個點落在 ROI 內(nèi)就認(rèn)為行人處于行車路線中。這個簡化為什么成立車輛直線行駛時地面平面與像平面的映射是單應(yīng)關(guān)系梯形比矩形更貼近真實的可通行空間。矩形會把路邊行人誤判進(jìn)路線梯形把兩側(cè)干擾排除了。源碼里 ROI_POINTS 通常是init里的常量不同攝像頭安裝角度需要重新標(biāo)定。我調(diào)試時的做法是對著平整路面把畫面里“左邊路沿、右邊路沿、近處車頭下沿、遠(yuǎn)處視野消失點”四個位置用鼠標(biāo)點出來記到配置里。4.2 點與 ROI 的包含判定及距離估算OpenCV 自帶包含判定函數(shù) cv2.pointPolygonTest不用自己寫射線法。配合檢測框底邊中點判定邏輯可以獨立成一個函數(shù)def on_route(box, roi_points): x1, y1, w, h box bottom_x int(x1 w / 2) bottom_y int(y1 h) # 底邊近似為腳點 # measureDistFalse 只返回正負(fù)不計算距離 return cv2.pointPolygonTest( roi_points, (bottom_x, bottom_y), False ) 0box 是 NMS 之后保留的檢測框roi_points 是 numpy 的 int32 坐標(biāo)數(shù)組形狀 (N, 2)。pointPolygonTest 第三個參數(shù)是 measureDistTrue 返回帶符號距離表達(dá)“在邊界內(nèi)多深”False 只返回 1內(nèi)、0邊、-1外。二值判定用 False 速度更快。多邊形頂點要首尾相接cv2.pointPolygonTest 不要求順時針或逆時針但要求是連續(xù)點集。進(jìn)一步估算距離是為了分級告警。只有“在不在 ROI 內(nèi)”只能給布爾值加上距離才能區(qū)分“前方 3 米有行人”和“前方 15 米有行人”。常見做法是在地面平坦假設(shè)下把檢測框底邊 y 坐標(biāo)線性映射到距離def estimate_distance(bottom_y, near_y600, far_y250, near_dist1.0, far_dist20.0): # 畫面下方的行人近畫面上方的行人遠(yuǎn) t (bottom_y - far_y) / (near_y - far_y) t np.clip(t, 0.0, 1.0) return near_dist (far_dist - near_dist) * t這個線性映射不完全準(zhǔn)確因為透視關(guān)系本質(zhì)非線性但對畢設(shè)場景夠用。想更精確可以標(biāo)定地面平面的單應(yīng)矩陣 H用np.dot(H, [x, y, 1])做透視逆變換再算距離。單應(yīng)矩陣標(biāo)定需要四個以上地面點工程量大如果論文沒要求線性映射完全撐得住答辯。距離值被后續(xù)告警策略用來分級。4.3 告警策略連續(xù)幀確認(rèn)與分級觸發(fā)單幀命中不代表要立刻告警。檢測模型偶爾跳變一幀檢出、下一幀丟掉如果每次都觸發(fā)界面會閃爍不停。源碼里的常見做法是連續(xù) N 幀命中才發(fā)一次告警同時用遞減計數(shù)做遺忘class AlertManager: def __init__(self, confirm_frames3): self.confirm_frames confirm_frames self.hit_streak 0 def update(self, is_hit): if is_hit: self.hit_streak 1 else: self.hit_streak max(0, self.hit_streak - 2) if self.hit_streak self.confirm_frames: self.hit_streak 0 return True return False連續(xù) 3 幀確認(rèn)漏檢一幀衰減 2偶發(fā)漏檢不會立刻清零單幀誤檢也不會立刻觸發(fā)。confirm_frames 按幀率調(diào)整30FPS 時 3 幀約 100 毫秒車輛 30km/h 行駛約走 0.8 米還在反應(yīng)時間范圍內(nèi)拉流場景幀率只有 10可以降到 2 幀。分級告警對應(yīng)的動作告警級別估算距離范圍y 坐標(biāo)參考界面動作不處理大于 15mbottom_y 250只畫框預(yù)警8~15m250 ~ 420狀態(tài)欄黃色提示告警3~8m420 ~ 550標(biāo)簽變紅 蜂鳴緊急小于 3mbottom_y 550暫停檢測強制彈窗QMessageBox 在車機場景不能直接彈模態(tài)框會阻塞整個 GUI司機反而看不到畫面。改成標(biāo)簽變紅加系統(tǒng)蜂鳴更合理。這一點在答辯時可以講成“從交互設(shè)計角度考慮實時告警的可用性”比較加分。4.4 告警的聯(lián)動與線程回收觸發(fā)告警后檢測線程不能停行人走出路線后告警要能撤銷。AlertManager 的狀態(tài)變化通過 alert_state 信號發(fā)給界面界面根據(jù)狀態(tài)切換樣式。測試時用項目提供的 images/1.jpg、2.jpg、3.jpg 三張靜態(tài)圖做單元驗證分別對應(yīng)無行人、行人在 ROI 外、行人在 ROI 內(nèi)三種情況比對 on_route 輸出是否符合預(yù)期。從 run.py 啟動后先跑一段視頻再切攝像頭可以避免答辯現(xiàn)場攝像頭初始化的意外。5. 調(diào)參與排錯技巧幀率、置信度、字體與模型加載邊界5.1 幀率上不去先看這三個地方單幀耗時超標(biāo)的排查順序是先量化再優(yōu)化。在檢測線程的 run 里對 read、detect、畫框、emit 四段分別打時間戳輸出到日志找到瓶頸再動手不要憑感覺調(diào)。第一是模型輸入尺寸416x416 改 320x320耗時大約降 30% 到 40%代價是小目標(biāo)漏檢率上升第二是置信度閾值從 0.4 提到 0.5誤檢下降但遠(yuǎn)處半身行人容易丟適合行人密度不高的場景第三是判定順序pointPolygonTest 非常快但如果先做距離估算就浪費了。把 on_route 放最前面不在 ROI 內(nèi)的框直接跳過距離計算if not on_route(box, self.roi_points): continue distance estimate_distance(box[1] box[3])按這個順序大部分檢測框在第一步就被過濾距離估算只對少數(shù)目標(biāo)執(zhí)行整體耗時能再壓一截。5.2 中文字體與依賴的兩個老坑源碼帶了一個 SimHei.ttf這是為了避免 Linux 服務(wù)器上漢字亂碼。PyQt 默認(rèn)字體在部分 Linux 發(fā)行版上不包含中文字體QLabel 顯示“行人告警”會出現(xiàn)方框。正確做法是啟動時加載字體文件而不是依賴系統(tǒng)字體font_dir SimHei.ttf font_id QFontDatabase.addApplicationFont(font_dir) families QFontDatabase.applicationFontFamilies(font_id) if families: QApplication.setFont(QFont(families[0], 10))依賴方面requirements.txt 里如果是 opencv-python 而不是 opencv-contrib-python部分舊教程的 xfeatures2d 不可用本項目只用到 DNN 和圖像基礎(chǔ)操作opencv-python 完全夠用。遇到 ModuleNotFoundError 時優(yōu)先檢查安裝的是不是 CPU 版ARM 設(shè)備上需要 pip install opencv-python-headless 去 GUI 依賴但 headless 沒有 imshow 和 HighGUI調(diào)試畫框預(yù)覽會失效需要注意區(qū)分環(huán)境。5.3 驗證告警正確性的閉環(huán)方法在項目根目錄放一段測試腳本依次加載 images/1.jpg、2.jpg、3.jpg調(diào)用 Detector.detect打印每張圖的框數(shù)量和 on_route 結(jié)果。再把 AlertManager 接到模擬信號源上連續(xù)發(fā) True、False、True、True、True確認(rèn)第 5 次才輸出告警驗證 confirm_frames 邏輯沒寫錯。最后用視頻文件而不是攝像頭完整跑一遍 run.py對比告警出現(xiàn)時機和行人實際位置的時間差。整套系統(tǒng)的檢測、線程、告警三層鏈路在源碼里是解耦的排查時按 run.py → CaptureThread → Detector → AlertManager 的順序依次打點問題一定落在其中一個環(huán)節(jié)的邊界上。本文還有配套的精品資源點擊獲取