測(cè):從訓(xùn)練到邊緣部署全流程)
簡(jiǎn)介這份資源面向深度學(xué)習(xí)入門者、圖像識(shí)別方向的畢業(yè)設(shè)計(jì)或課程設(shè)計(jì)學(xué)生提供一套基于YOLOv8的交通人群監(jiān)測(cè)完整實(shí)現(xiàn)方案可用于行人流量統(tǒng)計(jì)、公共安全監(jiān)控與智能交通管理等場(chǎng)景。壓縮包共24個(gè)文件約25.56MB包含3個(gè)Python腳本、1個(gè)訓(xùn)練好的pt權(quán)重、1個(gè)ipynb開發(fā)筆記以及png、jpg、gif等圖像素材和txt、md、log等說明與日志文件覆蓋從模型訓(xùn)練到檢測(cè)運(yùn)行的完整鏈路。其中攝像頭實(shí)時(shí)檢測(cè)與靜態(tài)圖片檢測(cè)腳本可直接運(yùn)行權(quán)重文件省去重新訓(xùn)練成本筆記與文檔幫助理解項(xiàng)目結(jié)構(gòu)與使用方法。目前已有40人學(xué)習(xí)適合希望快速上手目標(biāo)檢測(cè)實(shí)戰(zhàn)、完成期末大作業(yè)或積累項(xiàng)目經(jīng)驗(yàn)的讀者參考借鑒。1. 交通人群監(jiān)測(cè)為什么值得用 YOLOv8 重做一遍路口攝像頭每天都在產(chǎn)生海量視頻但真正能拿來用的結(jié)構(gòu)化數(shù)據(jù)少得可憐。傳統(tǒng)做法要么靠人工盯屏要么用背景建模加 HOG 加 SVM 那套老流程一到早晚高峰、行人互相遮擋、電動(dòng)車混行就集體翻車。基于 YOLOv8 的交通人群監(jiān)測(cè)設(shè)計(jì)核心就是把「人」和「車」這兩類目標(biāo)從視頻流里穩(wěn)定摳出來再疊加計(jì)數(shù)、密度估計(jì)和越界告警讓一段監(jiān)控視頻變成可查詢、可統(tǒng)計(jì)的表格。它適合兩類人一類是正在做基于 YOLOv8 的畢業(yè)設(shè)計(jì)、需要一套能跑通全流程方案的學(xué)生另一類是想在邊緣盒子上落地交通人群監(jiān)測(cè)的工程師。這篇筆記不講空泛概念從環(huán)境搭建、數(shù)據(jù)集處理、訓(xùn)練參數(shù)、模型導(dǎo)出到板端部署把每一步的命令和踩坑點(diǎn)都攤開講新手能照著復(fù)現(xiàn)熟手能直接對(duì)參數(shù)和邊界。2. 環(huán)境搭建與數(shù)據(jù)準(zhǔn)備從零把 YOLOv8 跑起來2.1 選 CPU 版還是 GPU 版先看手頭硬件很多人一上來就問 yolov8 環(huán)境配置怎么做其實(shí)答案取決于你手上有什么卡。如果只有一臺(tái)辦公筆記本沒有獨(dú)顯那就老老實(shí)實(shí)裝 CPU 版本用 ubuntu20.04 搭建 yolov8 環(huán)境 cpu 版本完全可行只是訓(xùn)練慢適合先跑通推理和小規(guī)模驗(yàn)證。如果手上有 gtx1660ti 這類 6GB 顯存的卡可以跑訓(xùn)練但 batch 要壓到 8 甚至 4否則顯存直接爆。如果是 rk3588 或 orin 這類邊緣板訓(xùn)練不在板上做板上只負(fù)責(zé)推理訓(xùn)練在帶獨(dú)顯的機(jī)器上完成后再導(dǎo)出模型。我一般會(huì)先用 conda 建一個(gè)干凈環(huán)境避免和系統(tǒng)里的 python 包打架。ultralytics 這個(gè)包把 YOLOv8 的訓(xùn)練、驗(yàn)證、導(dǎo)出都封裝好了不需要自己去 clone 一堆倉庫。# 創(chuàng)建并激活虛擬環(huán)境python 版本建議 3.8 到 3.10 conda create -n yolov8 python3.9 -y conda activate yolov8 # 安裝 pytorchCPU 版本用下面這條 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 如果有 NVIDIA 顯卡換成對(duì)應(yīng) CUDA 版本的命令例如 CUDA 11.8 # pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安裝 ultralytics pip install ultralytics # 驗(yàn)證安裝能打印出版本號(hào)就說明環(huán)境通了 yolo version這段命令的邏輯是先隔離環(huán)境再裝深度學(xué)習(xí)框架最后裝 YOLOv8 的官方封裝。參數(shù)上唯一要注意的是 python 版本3.11 以上有些依賴輪子還沒跟上容易在安裝階段就報(bào)錯(cuò)。裝完之后用yolo version做一次自檢比直接跑訓(xùn)練再排錯(cuò)省時(shí)間。提示如果 pip 安裝 torch 時(shí)卡在下載先換國內(nèi)鏡像源或者手動(dòng)下載對(duì)應(yīng)版本的 whl 文件本地安裝不要反復(fù)重試同一個(gè)命令。2.2 交通人群數(shù)據(jù)集怎么標(biāo)、怎么轉(zhuǎn)yolov8訓(xùn)練自己的數(shù)據(jù)集第一步不是寫訓(xùn)練腳本而是把數(shù)據(jù)整理成 YOLO 能吃的格式。交通人群監(jiān)測(cè)場(chǎng)景里我一般只保留兩個(gè)大類person 和 vehicle如果要做細(xì)一點(diǎn)把 car、bus、truck、motorcycle 拆開但類別越多小目標(biāo)越難學(xué)。標(biāo)注工具用 labelme 標(biāo)注用于 yolov8 是可以的但 labelme 默認(rèn)輸出的是 JSON需要轉(zhuǎn)成 YOLO 的 txt 格式。轉(zhuǎn)換腳本的核心是把 labelme 的矩形框坐標(biāo)歸一化到 0 到 1 之間并生成對(duì)應(yīng)的類別索引。下面這個(gè)腳本我用了很多次直接改路徑就能跑。import json import os from pathlib import Path # 類別映射順序要和訓(xùn)練時(shí) data.yaml 里的 names 一致 class_map {person: 0, vehicle: 1} def convert_labelme_json(json_dir, output_dir): json_dir Path(json_dir) output_dir Path(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) for json_file in json_dir.glob(*.json): with open(json_file, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue # labelme 給的是兩個(gè)對(duì)角點(diǎn)需要算出左上和右下 points shape[points] x1 min(points[0][0], points[1][0]) y1 min(points[0][1], points[1][1]) x2 max(points[0][0], points[1][0]) y2 max(points[0][1], points[1][1]) # YOLO 格式類別 中心x 中心y 寬 高全部歸一化 cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{class_map[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) # 輸出同名 txt txt_path output_dir / (json_file.stem .txt) with open(txt_path, w) as f: f.write(\n.join(lines)) if __name__ __main__: convert_labelme_json(./labels_json, ./labels_txt)邏輯說明遍歷每個(gè) JSON讀取圖像寬高把每個(gè)框的坐標(biāo)歸一化。參數(shù)上class_map必須和后面 data.yaml 里的 names 順序嚴(yán)格一致否則訓(xùn)練出來的模型會(huì)把人和車認(rèn)反。歸一化用六位小數(shù)足夠YOLO 讀取時(shí)不會(huì)因?yàn)榫葋G框。數(shù)據(jù)整理完之后目錄結(jié)構(gòu)要長這樣images 下放 train 和 val 兩個(gè)子目錄labels 下也放 train 和 val文件名一一對(duì)應(yīng)。然后寫一個(gè) data.yamlpath: ./traffic_dataset train: images/train val: images/val nc: 2 names: [person, vehicle]nc是類別數(shù)names的順序就是類別索引。這個(gè)文件路徑寫錯(cuò)是新手最常見的翻車點(diǎn)訓(xùn)練一開始報(bào)找不到圖片九成是這里的問題。2.3 預(yù)訓(xùn)練權(quán)重從哪來要不要用yolov8預(yù)訓(xùn)練權(quán)重下載這件事官方在 ultralytics 的發(fā)布頁提供了 yolov8n.pt、yolov8s.pt 等不同尺度的權(quán)重。交通人群監(jiān)測(cè)我一般從 yolov8s 起步n 太小精度不夠m 以上在邊緣板上跑不動(dòng)。下載之后放在項(xiàng)目根目錄訓(xùn)練時(shí)用modelyolov8s.pt指定即可框架會(huì)自動(dòng)加載。如果網(wǎng)絡(luò)不通可以先在有網(wǎng)的機(jī)器上下好再拷貝過去不要用來源不明的權(quán)重文件避免結(jié)構(gòu)不匹配導(dǎo)致加載失敗。3. 訓(xùn)練參數(shù)怎么設(shè)把損失曲線壓下去3.1 一份能直接抄的訓(xùn)練命令yolov8訓(xùn)練參數(shù)含義看起來很多但交通人群監(jiān)測(cè)真正需要調(diào)的就那么幾個(gè)。下面這條命令是我在 6GB 顯存卡上跑通的配置yolo detect train \ data./traffic_dataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch8 \ lr00.01 \ lrf0.01 \ patience20 \ device0 \ project./runs/traffic \ nameexp1逐項(xiàng)說imgsz640是輸入分辨率交通場(chǎng)景里遠(yuǎn)處行人很小降到 416 會(huì)丟目標(biāo)升到 1280 顯存吃不消640 是精度和速度的平衡點(diǎn)。batch8是 6GB 顯存的安全值顯存更大的卡可以往上加。lr0是初始學(xué)習(xí)率0.01 是官方默認(rèn)數(shù)據(jù)集小的時(shí)候可以降到 0.001 防止震蕩。patience20表示 20 輪驗(yàn)證指標(biāo)不提升就早停省時(shí)間。device0指定第一塊 GPUCPU 訓(xùn)練改成devicecpu。訓(xùn)練開始后終端會(huì)打印每一輪的 box_loss、cls_loss 和 mAP。如果 box_loss 一直不降先檢查標(biāo)注框有沒有越界或者寬高為負(fù)如果 cls_loss 降但 mAP 不漲多半是類別不平衡交通場(chǎng)景里車多行人少可以適當(dāng)增加行人樣本。3.2 用損失曲線判斷該不該繼續(xù)訓(xùn)yolov8畫損失函數(shù)曲線圖訓(xùn)練結(jié)束后在runs/traffic/exp1目錄下會(huì)有 results.csv里面記錄了每輪的損失和指標(biāo)。用 pandas 加 matplotlib 幾行就能畫出來import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/traffic/exp1/results.csv) df.columns df.columns.str.strip() # 列名可能帶空格先清理 plt.figure(figsize(10, 5)) plt.plot(df[epoch], df[train/box_loss], labelbox_loss) plt.plot(df[epoch], df[train/cls_loss], labelcls_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.savefig(loss_curve.png, dpi150)看曲線有個(gè)經(jīng)驗(yàn)正常情況是前 10 輪快速下降之后緩慢收斂。如果驗(yàn)證損失在某個(gè)點(diǎn)開始往上翹說明過擬合了要么加數(shù)據(jù)增強(qiáng)要么提前停。交通人群監(jiān)測(cè)的數(shù)據(jù)集如果只來自一個(gè)路口模型換到另一個(gè)路口就會(huì)掉點(diǎn)這是數(shù)據(jù)分布問題不是調(diào)參能救的必須補(bǔ)不同場(chǎng)景的樣本。3.3 數(shù)據(jù)增強(qiáng)里哪幾個(gè)參數(shù)對(duì)人群監(jiān)測(cè)最有用YOLOv8 默認(rèn)開了 mosaic、HSV 抖動(dòng)和隨機(jī)翻轉(zhuǎn)。交通人群監(jiān)測(cè)里mosaic 能提升小目標(biāo)召回但會(huì)把四張圖拼在一起如果標(biāo)注框在拼接邊緣被截?cái)喾炊朐肼暋N业淖龇ㄊ怯?xùn)練前期開 mosaic最后 10 輪關(guān)掉讓模型在真實(shí)分布上收尾。HSV 抖動(dòng)對(duì)光照變化大的路口有用hsv_v 可以調(diào)到 0.5。隨機(jī)翻轉(zhuǎn)要慎用水平翻轉(zhuǎn)對(duì)行人沒問題但交通標(biāo)志和車牌方向敏感如果類別里包含這些翻轉(zhuǎn)會(huì)制造錯(cuò)誤標(biāo)簽。4. 模型導(dǎo)出與邊緣部署從權(quán)重到板端推理4.1 導(dǎo)出 ONNX 和 RKNN 的差別訓(xùn)練完的 .pt 權(quán)重不能直接扔到 rk3588 或 hi3516cv610 上跑需要先轉(zhuǎn)成中間格式。rk3588部署yolov8 的常見路徑是 pt 轉(zhuǎn) onnx再用 rknn-toolkit2 轉(zhuǎn) rknn。hi3516cv610 yolov8模型轉(zhuǎn)換與部署實(shí)戰(zhàn)里通常還要經(jīng)過量化把浮點(diǎn)權(quán)重壓成 int8否則算力跟不上。導(dǎo)出 ONNX 的命令很簡(jiǎn)單yolo export modelruns/traffic/exp1/weights/best.pt formatonnx opset12 simplifyTrueopset12是兼容性比較好的版本simplifyTrue會(huì)做一次圖優(yōu)化去掉冗余算子。導(dǎo)出后可以用 onnxruntime 先驗(yàn)證一遍推理結(jié)果和 pt 是否一致確認(rèn)無誤再往板端轉(zhuǎn)。注意導(dǎo)出時(shí) imgsz 要和訓(xùn)練時(shí)一致訓(xùn)練用 640 導(dǎo)出也用 640否則板端預(yù)處理和后處理對(duì)不上框會(huì)整體偏移。4.2 板端推理的預(yù)處理對(duì)齊邊緣板上跑 YOLOv8最容易翻車的不是模型本身而是預(yù)處理。訓(xùn)練時(shí)框架做了 letterbox 填充把圖像等比縮放到 640 并補(bǔ)灰邊。板端如果直接 resize 到 640x640長寬比變了檢測(cè)框會(huì)變形。所以板端代碼里必須復(fù)現(xiàn) letterbox記錄縮放比例和填充偏移后處理時(shí)再映射回原圖坐標(biāo)。import cv2 import numpy as np def letterbox(img, new_shape640, color(114, 114, 114)): h, w img.shape[:2] scale min(new_shape / h, new_shape / w) nh, nw int(round(h * scale)), int(round(w * scale)) img_resized cv2.resize(img, (nw, nh), interpolationcv2.INTER_LINEAR) canvas np.full((new_shape, new_shape, 3), color, dtypenp.uint8) top (new_shape - nh) // 2 left (new_shape - nw) // 2 canvas[top:top nh, left:left nw] img_resized return canvas, scale, left, top邏輯是等比縮放后居中填充返回的 scale、left、top 用于后處理還原坐標(biāo)。參數(shù)上 color 用 114 是因?yàn)橛?xùn)練時(shí)默認(rèn)填充值就是 114板端必須一致否則邊緣區(qū)域的激活值會(huì)有偏差。4.3 量化掉點(diǎn)的排查順序int8 量化后 mAP 掉 3 到 5 個(gè)點(diǎn)是正常的掉 10 個(gè)點(diǎn)以上就要查。排查順序是先看校準(zhǔn)集是否覆蓋了真實(shí)場(chǎng)景校準(zhǔn)集不能用訓(xùn)練集隨便抽幾張要包含不同光照和密度再看量化配置里哪些層被跳過了檢測(cè)頭部分通常對(duì)量化敏感可以保留浮點(diǎn)最后對(duì)比量化前后同一張圖的輸出看是分類錯(cuò)還是框回歸錯(cuò)。交通人群監(jiān)測(cè)里行人密集時(shí)框重疊嚴(yán)重量化后 NMS 閾值要適當(dāng)調(diào)低否則會(huì)吞掉正確框。5. 避坑與排查那些讓我返工的細(xì)節(jié)5.1 訓(xùn)練 loss 為 nan現(xiàn)象訓(xùn)練一開始 box_loss 就是 nan幾輪后進(jìn)程退出。原因?qū)W習(xí)率過大或者標(biāo)注文件里有寬高為 0 的框。解決先把 lr0 降到 0.001 試一輪如果還是 nan寫腳本掃一遍 labels 目錄把寬或高小于 1e-6 的行刪掉。交通人群數(shù)據(jù)集里標(biāo)注時(shí)手抖點(diǎn)出重復(fù)點(diǎn)就會(huì)產(chǎn)生這種廢框。5.2 驗(yàn)證集 mAP 很高但實(shí)際推理全錯(cuò)現(xiàn)象訓(xùn)練日志里 mAP50 到 0.9但拿視頻一跑框全在背景上。原因data.yaml 里 train 和 val 路徑寫反了或者驗(yàn)證集圖片和標(biāo)簽沒對(duì)齊框架拿訓(xùn)練集當(dāng)驗(yàn)證集評(píng)估。解決打開 data.yaml 逐行核對(duì)路徑再隨機(jī)抽一張 val 圖片用 labelimg 打開對(duì)應(yīng) txt 看框是否貼合。這個(gè)坑我踩過兩次血淚經(jīng)驗(yàn)是訓(xùn)練前先可視化十張帶框圖。5.3 邊緣板上推理速度遠(yuǎn)低于預(yù)期現(xiàn)象rk3588 上單幀推理要 200ms達(dá)不到實(shí)時(shí)。原因模型用了 yolov8m 甚至 l或者沒有開 NPU 加速跑在 CPU 上。解決換 yolov8n 或 yolov8s確認(rèn) rknn 模型確實(shí)加載到了 NPU用板子自帶的性能監(jiān)控看 NPU 占用率。如果 NPU 占用為 0說明推理走的是 CPU檢查 rknn 運(yùn)行時(shí)版本和模型是否匹配。5.4 類別索引錯(cuò)位導(dǎo)致人和車互換現(xiàn)象推理結(jié)果里行人被標(biāo)成 vehicle車被標(biāo)成 person。原因data.yaml 里 names 的順序和標(biāo)注轉(zhuǎn)換時(shí)的 class_map 不一致。解決統(tǒng)一以 data.yaml 的 names 為準(zhǔn)回頭改轉(zhuǎn)換腳本的 class_map重新生成全部標(biāo)簽。這個(gè)錯(cuò)誤在訓(xùn)練指標(biāo)上看不出來因?yàn)?mAP 照樣高只有可視化才能發(fā)現(xiàn)。5.5 視頻推理結(jié)果抖動(dòng)嚴(yán)重現(xiàn)象同一輛車在連續(xù)幀里框忽大忽小類別偶爾跳變。原因單幀檢測(cè)沒有時(shí)序約束加上置信度閾值設(shè)得太低。解決把 conf 閾值從 0.25 提到 0.4再在跟蹤層加一個(gè)簡(jiǎn)單的 IOU 匹配用上一幀的框約束當(dāng)前幀。交通人群監(jiān)測(cè)如果只做計(jì)數(shù)可以每 5 幀檢測(cè)一次中間幀用跟蹤補(bǔ)速度還能提上去。6. 把監(jiān)測(cè)做成能用的系統(tǒng)計(jì)數(shù)邏輯與閾值調(diào)優(yōu)單幀檢測(cè)只是半成品交通人群監(jiān)測(cè)真正要的是統(tǒng)計(jì)數(shù)字。我一般會(huì)在檢測(cè)之上加一個(gè)簡(jiǎn)單的越線計(jì)數(shù)在畫面里畫一條虛擬線當(dāng)目標(biāo)框中心從線的一側(cè)移動(dòng)到另一側(cè)時(shí)計(jì)數(shù)加一。這里有個(gè)細(xì)節(jié)目標(biāo)在線上來回抖動(dòng)會(huì)導(dǎo)致重復(fù)計(jì)數(shù)解決辦法是給每個(gè)目標(biāo)分配一個(gè)跟蹤 ID用 ID 記錄是否已經(jīng)計(jì)過同一個(gè) ID 只計(jì)一次。# 簡(jiǎn)化版越線計(jì)數(shù)邏輯track_id 由跟蹤器提供 counted_ids set() line_y 360 # 虛擬線在畫面中的 y 坐標(biāo) def update_count(track_id, center_y): if track_id in counted_ids: return 0 # 中心點(diǎn)越過線且方向向下 if center_y line_y: counted_ids.add(track_id) return 1 return 0這段邏輯的關(guān)鍵是counted_ids集合它保證每個(gè)跟蹤 ID 只貢獻(xiàn)一次計(jì)數(shù)。參數(shù)line_y要根據(jù)實(shí)際攝像頭安裝高度和視角來定一般放在畫面下半部分避免遠(yuǎn)處小目標(biāo)剛進(jìn)畫面就觸發(fā)。如果要做雙向計(jì)數(shù)就維護(hù)兩個(gè)集合分別記錄從上往下和從下往上的 ID。閾值調(diào)優(yōu)上conf 和 iou 這兩個(gè)參數(shù)決定了系統(tǒng)的性格。conf 調(diào)高漏檢多但誤報(bào)少conf 調(diào)低誤報(bào)多但漏檢少。交通人群監(jiān)測(cè)里如果用于告警寧可誤報(bào)也別漏報(bào)conf 可以設(shè) 0.3如果用于統(tǒng)計(jì)報(bào)表要求數(shù)字準(zhǔn)conf 設(shè) 0.5 再配合跟蹤去重。iou 是 NMS 的閾值人群密集時(shí)調(diào)低到 0.5 能保留更多重疊框稀疏場(chǎng)景調(diào)到 0.7 減少重復(fù)框。最后說一個(gè)我自己的習(xí)慣每次換攝像頭或者換場(chǎng)景不要直接信之前的參數(shù)先抽 100 幀跑一遍把結(jié)果導(dǎo)成 CSV人工核對(duì) 20 幀算一下漏檢和誤檢的比例再?zèng)Q定要不要微調(diào)。這個(gè)動(dòng)作花不了半小時(shí)但能省掉后面反復(fù)返工的后悔藥。模型不是一次訓(xùn)練就完事的交通場(chǎng)景會(huì)變數(shù)據(jù)要持續(xù)補(bǔ)參數(shù)要跟著調(diào)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取