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