)
簡介基于YOLOv8與LPRNet的車牌識別系統(tǒng)是一套面向計算機相關專業(yè)畢業(yè)設計、課程實踐和小型智能監(jiān)控項目的完整工程。項目采用YOLOv8定位車牌區(qū)域LPRNet識別車牌字符并整合Flask后端與Vue前端實現(xiàn)了從圖像/視頻輸入到結果可視化展示的完整流程適合作為算法學習、系統(tǒng)集成練習或初期項目演示的起點。資源包共60個文件壓縮后約36.15MB。核心代碼為13個Python腳本覆蓋模型訓練、數(shù)據(jù)集生成、批量圖像預處理、標注創(chuàng)建等環(huán)節(jié)同時包含YOLOv8權重文件pt與LPRNet模型參數(shù)文件pth以及一批用于驗證的jpg圖片和Vue前端工程含vue、js、css、json等。整體目錄按功能模塊劃分便于按需查閱。目前已有1146人瀏覽學習代碼經測試可正常運行既能支持畢業(yè)設計中的實驗復現(xiàn)也可作為二次開發(fā)的基礎框架。1. 拿到「基于 YOLOv8 和 LPRNet 的車牌識別系統(tǒng) python源碼模型.zip」之后先別急著跑這標題背后是一個典型的兩段式車牌識別落地方案YOLOv8 負責在畫面里把車牌區(qū)域框出來LPRNet 負責把這塊小圖直接翻譯成“京A·12345”這樣的車牌字符串。很多讀者拿到這類壓縮包第一反應是趕緊裝環(huán)境跑推理實際真正決定項目能不能用的是數(shù)據(jù)集分布、字符解碼邏輯和部署轉換這幾層不是網絡結構本身。本篇按從推理反推訓練的順序拆解先講選型邊界再給訓練與推理的完整腳本最后把高頻踩坑點一次說透。適合正在做課設、工程預研或準備把識別能力集成到現(xiàn)有業(yè)務系統(tǒng)的開發(fā)者。2. 技術選型為什么是 YOLOv8 LPRNet而不是一個端到端模型2.1 兩段式架構的主流程與數(shù)據(jù)流整個識別系統(tǒng)的調用關系很直接攝像頭視頻幀或單張圖片進入 YOLOv8 檢測器輸出若干個車牌候選框及其置信度程序按坐標從原圖裁出車牌小圖做一次固定尺寸縮放后送給 LPRNetLPRNet 輸出一個字符序列概率分布經解碼變成最終車牌字符串。這個流程里最重要的一點是“裁剪”這個動作。檢測框的坐標精度直接影響識別輸入質量檢測框稍微偏一點LPRNet 見到的就是殘缺字符。因此實際工程里不會直接按檢測框硬裁而是在四個方向各向外擴 510 個像素把車牌邊框和鉚釘一起包進來讓識別網絡的輸入更接近訓練數(shù)據(jù)分布。兩段式架構另一個隱性好處是替換成本低。如果需要識別新能源綠牌、掛車黃牌或警車白牌只需要重新準備對應數(shù)據(jù)并微調 LPRNet如果攝像頭距離遠導致檢測框小只需要單獨換更大的 YOLOv8 模型。端到端方案雖然看起來簡潔但在這個場景下數(shù)據(jù)采集成本高、問題定位難落地時并不劃算。2.2 YOLOv8 做車牌定位生態(tài)與部署便利性車牌在畫面里屬于典型的小目標尤其停車場出入口抓拍時車牌可能只占整幀畫面的百分之一。YOLOv8 的 anchor-free 設計對小目標的回歸更直接而且模型家族從 n/s/m/l/x 到不同分辨率都有現(xiàn)成權重做工程原型很快。環(huán)境配置層面常見做法是pip install ultralytics然后命令行yolo就能直接跑起來。PyTorch 版本建議 2.0 以上Python 3.83.10 之間是比較穩(wěn)的區(qū)間opencv-python 必須單獨確認裝上了因為后面所有圖像預處理都依賴它。我踩過的坑是 conda 里 torch 裝的是 CPU 版檢測速度掉到一秒一幀所以配置完環(huán)境后一定要先跑一次 GPU 推理確認設備可用。YOLOv8 在這里只做一件事輸出(x1, y1, x2, y2, class_id, confidence)。車牌通常作為唯一類別訓練但實際監(jiān)控場景里也可以順便把車臉、車尾標成同一類提升召回率后由識別網絡去過濾非車牌區(qū)域。這種“檢測放寬、識別收緊”的策略在復雜場景下很實用。2.3 LPRNet 的定位不切字符直接輸出序列LPRNet 的核心價值是省掉字符切分這一步。傳統(tǒng)方案要先把車牌圖按字符寬度切開再逐個分類一旦字符粘連、模糊或傾斜切分就崩了。LPRNet 把整張車牌圖送進 CNN 提取特征輸出一個時序特征序列再用 CTC 損失對齊到最終字符序列字符個數(shù)由模型自己決定。具體實現(xiàn)上LPRNet 的輸入通常是(batch, 3, 24, 94)即高 24、寬 94 的橫向車牌圖。backbone 用幾層卷積把空間維度壓縮保留寬度方向上的序列信息再接一層 RNN/LSTM 增強時序建模最后每個時間步輸出一個字符類別概率分布。字符表一般包含 31 個省份漢字縮寫、24 個字母排除 I 和 O避免與數(shù)字 1、0 混淆、10 個數(shù)字外加一個 CTC blank合計 66 類。訓練時數(shù)據(jù)標簽是變長字符串比如“京A12345”拆成字符序列后與各時間步做對齊推理時常用的解碼策略是貪心解碼——每個時間步取概率最大的類別去掉連續(xù)重復和 blank。如果要求更高也可以用 beam search但車牌字符集小貪心解碼通常已經夠用。3. 訓練自己的數(shù)據(jù)集從標注到兩個模型的權重3.1 YOLOv8 訓練標注格式、數(shù)據(jù)劃分與關鍵參數(shù)車牌檢測模型的訓練數(shù)據(jù)組織方式跟通用目標檢測沒有區(qū)別。先用 LabelImg 按矩形框標注車牌位置導出為 YOLO TXT 格式每行class_id cx cy w h坐標全部歸一化到 01。目錄結構如下plate_dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/數(shù)據(jù)劃分建議訓練集與驗證集按 8:2 或 9:1 分配驗證集最好來自與訓練集不同的攝像頭點位避免場景重疊導致評估虛高。對應的數(shù)據(jù)集配置文件plate.yaml這樣寫path: ./plate_dataset train: images/train val: images/val names: 0: license_plate啟動訓練用一行命令即可yolo detect train dataplate.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0imgsz的選擇需要權衡。640 是通用默認值訓練和推理速度都快如果攝像頭畫面里車牌較小建議提到 960 或 1280小目標召回率會明顯上升。modelyolov8n.pt是最小的 nano 版本適合快速驗證流程正式訓練我一般換yolov8s.pt或yolov8m.pt精度更高部署到 RK3588 之類的板子前再蒸餾或量化回輕量版本。訓練日志里重點看val/box_loss和metrics/mAP50-95。前幾個 epoch 的 mAP 可能很低但 50 輪之后如果還在緩慢上升說明模型沒吃飽可以適當加大epochs或引入更多數(shù)據(jù)增強。3.2 LPRNet 訓練數(shù)據(jù)合成數(shù)據(jù)的生成思路車牌識別網絡最缺的往往不是背景多樣性而是漢字字符樣本。真實場景里“京、滬、粵、蘇”出現(xiàn)頻率遠高于“藏、青、寧”直接訓練漢字識別率會明顯偏向高頻字符。常見的做法是用合成數(shù)據(jù)打底選幾種常見的商用字體渲染白字藍底或黑字黃底的車牌圖再疊加透視變換、模糊、噪聲和亮度擾動。合成車牌時字符間距和排列要按真實規(guī)范來藍牌 7 位、綠牌 8 位第一位必須是漢字。生成腳本框架如下import random import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont char_set 京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領 plate_template 京A12345 # 也可隨機組合 # 在 240x60 畫布上繪制字符 img Image.new(RGB, (240, 60), (0, 0, 200)) # 藍底 draw ImageDraw.Draw(img) font ImageFont.truetype(./simhei.ttf, 40) draw.text((10, 5), plate_template, fontfont, fill(255, 255, 255)) # 轉為 OpenCV 格式加透視和噪聲 frame cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) frame cv2.resize(frame, (94, 24))合成數(shù)據(jù)的關鍵是讓 LPRNet 看到足夠的字符排列組合而不是只換背景。字體至少要準備三種否則模型會對特定字體的筆畫紋理過擬合真實照片上識別率掉得厲害。3.3 LPRNet 訓練腳本的結構與超參數(shù)LPRNet 網絡本身不大單卡訓練幾十個 epoch 就能收斂。核心網絡結構可以按下面的方式定義import torch import torch.nn as nn class SmallBasicBlock(nn.Module): def __init__(self, ch_in, ch_out): super().__init__() self.conv1 nn.Conv2d(ch_in, ch_out, kernel_size1) self.bn1 nn.BatchNorm2d(ch_out) self.conv2 nn.Conv2d(ch_out, ch_out, kernel_size3, padding1, groupsch_out) self.bn2 nn.BatchNorm2d(ch_out) self.conv3 nn.Conv2d(ch_out, ch_out, kernel_size1) self.bn3 nn.BatchNorm2d(ch_out) self.relu nn.ReLU(inplaceTrue) def forward(self, x): out self.relu(self.bn1(self.conv1(x))) out self.relu(self.bn2(self.conv2(out))) return self.relu(self.bn3(self.conv3(out))) class LPRNet(nn.Module): def __init__(self, class_num, dropout0.5): super().__init__() self.backbone nn.Sequential( nn.Conv2d(3, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), SmallBasicBlock(256, 256), nn.Conv2d(256, 512, kernel_size3, padding1), nn.BatchNorm2d(512), nn.ReLU(inplaceTrue), nn.Conv2d(512, 512, kernel_size3, padding1), nn.BatchNorm2d(512), nn.ReLU(inplaceTrue), ) self.rnn nn.LSTM(512, 256, num_layers2, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(512, class_num) def forward(self, x): x self.backbone(x) # (B, 512, 4, W) B, C, H, W x.size() x x.view(B, C * H, W) # 把高度方向壓縮成通道 x x.permute(0, 2, 1) # (B, W, C*H) x, _ self.rnn(x) # 雙向 LSTM x self.fc(x) # (B, W, class_num) return x網絡輸入是(batch, 3, 24, 94)經過三次 MaxPool 和一次卷積 stride 后寬度方向的時間步長會根據(jù)下采樣倍數(shù)動態(tài)變化forward里用W變量承接不需要硬編碼。訓練時損失函數(shù)直接調 PyTorch 內置的 CTC losscriterion nn.CTCLoss(blanklen(char_set) - 1, zero_infinityTrue) optimizer torch.optim.Adam(model.parameters(), lr1e-3)注意 CTC loss 的輸入格式是(time_steps, batch, num_classes)而模型輸出是(batch, time_steps, num_classes)喂給 loss 之前要做一次log_softmax再transpose(0, 1)。標簽則用字符在 char_set 里的索引序列表示長度可以不等這就是 CTC 能夠處理變長車牌藍牌 7 位、綠牌 8 位的原因。訓練過程中每幾個 epoch 保存一次權重同時打印驗證集整牌識別率。我習慣在組織訓練數(shù)據(jù)時把字符個數(shù)放在標簽張量的第一個維度并用torch.nn.utils.rnn處理批次內的變長標簽如果嫌麻煩也可以按固定長度 8 補齊并忽略 padding 位置效果差別不大。4. 推理流水線的 Python 實現(xiàn)從一幀畫面到車牌字符串4.1 檢測環(huán)節(jié)解析 YOLOv8 的輸出推理側我先寫一個獨立的檢測函數(shù)返回所有滿足置信度閾值的車牌框import cv2 import numpy as np from ultralytics import YOLO detector YOLO(./weights/yolov8_plate.pt) def detect_plates(frame, conf_threshold0.25, iou_threshold0.45): results detector.predict(frame, confconf_threshold, iouiou_threshold, verboseFalse)[0] boxes results.boxes.xyxy.cpu().numpy() # (N, 4)像素坐標 scores results.boxes.conf.cpu().numpy() # (N,) return boxes, scoresconf_threshold是置信度閾值調太低會把車身反光、路面文字等誤檢為車牌調太高又會漏檢遠處小目標。監(jiān)控場景我一般先用 0.25 跑一遍看誤檢率再決定是否提高到 0.4。iou_threshold控制 NMS 的抑制力度車牌與車牌之間不會重疊0.45 基本不需要動。4.2 識別環(huán)節(jié)裁剪、預處理與 CTC 解碼拿到檢測框后先按比例外擴再縮放成 LPRNet 期望的尺寸。這里的關鍵是盡量保持車牌寬高比不要原圖是 200x60 就直接拉成 94x24 導致字符、變形。實際代碼里先按檢測框寬高比裁剪再 resize。from lprnet_model import LPRNet recognizer LPRNet(num_classes66) recognizer.load_state_dict(torch.load(./weights/lprnet.pt, map_locationcpu)) recognizer.eval() CHARS 京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 def preprocess_crop(crop_bgr): crop cv2.resize(crop_bgr, (94, 24)) crop crop.astype(np.float32) / 255.0 crop (crop - 0.588) / 0.193 crop crop.transpose(2, 0, 1)[None, ...] return torch.from_numpy(crop) def decode_ctc(pred): pred pred.softmax(dim-1).argmax(dim-1) # (1, T) pred pred.squeeze(0).cpu().numpy() result [] prev None for idx in pred: if idx ! len(CHARS) and idx ! prev: result.append(CHARS[idx]) prev idx return .join(result)歸一化的均值和方差 0.588、0.193 是很多開源 LPRNet 復現(xiàn)里沿用的統(tǒng)計值如果自己重新訓練建議從訓練集重新統(tǒng)計。decode_ctc里的邏輯是貪心解碼每幀取最大概率字符空格重復的相鄰字符只保留一個遇到 blank 直接跳過。這套邏輯簡單直接適合車牌這種字符表小、序列短的任務。值得提醒的是許多工程實現(xiàn)會在解碼后做一次車牌格式校驗比如判斷第一位是否為省份漢字、第七位是否為數(shù)字或字母不滿足則標記為低置信度結果。這個校驗能擋掉不少誤識別別省。4.3 主流程檢測與識別串起來并繪制結果下面把檢測、外擴、識別、畫框合成一個可直接跑的主流程cap cv2.VideoCapture(./test_video.mp4) while True: ret, frame cap.read() if not ret: break boxes, scores detect_plates(frame) for (x1, y1, x2, y2), score in zip(boxes, scores): # 外擴 8 像素避免裁掉車牌邊緣字符 x1 max(0, int(x1) - 8) y1 max(0, int(y1) - 8) x2 min(frame.shape[1], int(x2) 8) y2 min(frame.shape[0], int(y2) 8) crop frame[y1:y2, x1:x2] if crop.size 0: continue input_tensor preprocess_crop(crop) with torch.no_grad(): pred recognizer(input_tensor) plate_text decode_ctc(pred) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, plate_text, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(plate_recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()外擴 8 像素是一個經驗值。檢測框通常緊貼車牌字符直接裁剪會把第一個漢字或最后一個數(shù)字壓掉但外擴太多又可能帶入車身紋理干擾識別。邊框、鉚釘這些是噪聲但也能為 LPRNet 提供位置上下文所以適度外擴反而提升精度。視頻流場景要注意檢測器的執(zhí)行頻率。如果攝像頭是 25 幀每秒GPU 足夠強可以每幀檢測邊緣設備上則每 23 幀做一次檢測中間幀沿用上一幀的檢測框識別網絡保持每幀運行。這樣可以顯著降低功耗和延遲。5. 避坑指南訓練和部署中最常遇到的五個翻車現(xiàn)場5.1 模型加載成功但識別結果是一串亂碼字符現(xiàn)象檢測框位置正常但輸出的字符串完全不是車牌格式例如“A1京B2C3D”這種順序混亂的內容。原因最常見的是裁剪后的圖像被直接拉伸到 94x24破壞了車牌寬高比或者是識別網絡輸入通道與訓練時不一致訓練用的灰度圖、推理時喂了 BGR 三通道特征分布全亂了。解決嚴格復用訓練時的預處理流程包括通道順序、歸一化均值和方差、縮放尺寸。換用不同來源的 LPRNet 權重時先確認它期望的輸入尺寸和字符表順序。字符表順序不一致是隱蔽坑同一個LPRNet.pt文件在不同項目里字符表排序可能完全不同但沒人會肉眼去對。5.2 漢字總是識別錯“京”變成“示”或“就”現(xiàn)象數(shù)字和字母識別率很高唯獨省份漢字頻繁出錯。原因漢字類別在字符表里只占 31 類真實數(shù)據(jù)中嚴重不均衡——粵、京、滬、蘇出現(xiàn)率高藏、青、寧樣本極少。模型學到的是高頻漢字特征遇到低頻漢字只會瞎猜。解決訓練時對低頻漢字做加權采樣或者通過合成數(shù)據(jù)把每個漢字類別的樣本數(shù)拉平。推理時利用格式先驗車牌第一位必然是省份漢字所以解碼時把這一步的候選類別限制為 31 個漢字再取最大概率。很多項目加了這一條規(guī)則后整牌識別率直接提升兩個點。5.3 新能源綠牌整牌識別率明顯低于藍牌現(xiàn)象藍牌識別 99%綠牌卻經常把第 8 位字符漏掉或把“D”讀成“0”。原因綠牌是 8 位字符LPRNet 訓練時藍牌樣本占多數(shù)模型隱含地傾向輸出 7 位序列另外綠牌字符是黑字配漸變綠底對比度低于藍牌白字預處理階段歸一化后字符信號更弱。解決訓練數(shù)據(jù)里綠牌比例至少占到 20%30%并且不要限制標簽固定為 7 位。輸入尺寸如果從 94x24 改成 110x24能容納更多字符間距但需要同比例調整網絡的時間步長并重新訓練。推理時對綠色像素占比高的檢測框可以走一套單獨的預處理參數(shù)。5.4 部署到 RK3588 或端側 NPU 后精度整體下降現(xiàn)象同一個權重在 PC 上跑得很好轉 RKNN 或 TensorRT 后整牌識別率掉了 5% 以上。原因端側量化把權重壓縮到 INT8對 LPRNet 這種小模型影響尤其明顯YOLOv8 導出 ONNX 時若未固定輸入尺寸NPU 上動態(tài)分辨率會觸發(fā)多余的 resize 操作特征圖發(fā)生偏移。解決部署前先用統(tǒng)一輸入尺寸導出 ONNX在 PC 上用 ONNX Runtime 驗證輸出與 PyTorch 原版一致后再轉 RKNN。量化時準備 200300 張真實車牌圖作為校準集不要用合成圖。如果量化后檢測框坐標明顯偏移優(yōu)先在導出配置里關閉某些算子的量化比較常見的耗時大戶Sigmoid和Gather算子可以保留為 FP16。5.5 訓練 loss 正常下降但驗證集整牌識別率幾乎為零現(xiàn)象CTC loss 從幾十降到 3 左右但解碼輸出和真實車牌完全對不上。原因標簽字符順序錯了。CTC 訓練要求標簽與輸入序列保持從左到右的對應關系如果數(shù)據(jù)管道里把車牌字符串反轉了或者按字符表索引映射時少了一個偏移量loss 照樣能降但解碼永遠錯位。解決在訓練腳本里加一個可視化 debug 開關隨機抽 10 個 batch把標簽索引還原成字符串打印出來人工確認與圖像內容一致。這個檢查只要做一次能省出后面一整周的排查時間。6. 從能跑到能用批量驗證、多幀投票與部署前的檢查清單6.1 批量驗證腳本用整牌正確率和字符正確率說話單張圖片能識別成功不代表系統(tǒng)可用。我建議準備一個 200500 張真實圖片的驗證集跑一個批量統(tǒng)計腳本def evaluate(predictor, dataset): total_plate 0 correct_plate 0 total_char 0 correct_char 0 for img_path, gt_text in dataset: plate_text predictor(img_path) total_plate 1 if plate_text gt_text: correct_plate 1 total_char len(gt_text) correct_char sum(a b for a, b in zip(plate_text, gt_text)) print(f整牌正確率: {correct_plate / total_plate:.2%}) print(f字符正確率: {correct_char / total_char:.2%})整牌正確率是你的業(yè)務指標字符正確率是排查指標。如果整牌率低但字符率高說明解碼層問題比如格式校驗規(guī)則太嚴如果兩者都低回頭看檢測或預處理。只要跑一次這個腳本模型到底是檢測的問題還是識別的問題基本一目了然。6.2 多幀投票穩(wěn)定輸出比單幀識別更重要監(jiān)控視頻里同一輛車會連續(xù)出現(xiàn)多幀單幀識別偶爾閃錯一個字符就會導致整個后續(xù)業(yè)務流程判斷失敗。常見做法是維護一個車牌結果隊列對同一檢測框 ID 在連續(xù) 510 幀內的識別結果做逐字符投票最終輸出出現(xiàn)頻率最高的組合。這個邏輯實現(xiàn)起來不復雜但效果非常明顯。字符投票能把偶發(fā)的1誤識成I這類噪聲消掉同時還可以統(tǒng)計每個字符的可信頻次低于 60% 的判定為低置信度結果。真實停車場場景下單幀精度做到 95% 已經不錯多幀投票后可以穩(wěn)定推到 99% 以上。6.3 部署前檢查清單與個人習慣把模型從 PyTorch 移到端側前建議對照下面的清單過一遍輸入尺寸是否固定、顏色通道順序是 BGR 還是 RGB、歸一化均值方差是否與訓練一致、檢測框坐標從模型輸出映射回原圖時是否處理了 letterbox、CTC 解碼是否保留在端側而不是交給服務端、LPRNet 導出的時間步長是否與訓練時一致。我現(xiàn)在的習慣是先把模型在驗證集上跑出基線指標再動手做任何部署優(yōu)化。如果 RK3588 上轉換后指標掉點先用 ONNX Runtime 逐層對比特征看是第一個卷積層輸入就不一致還是某個算子在 NPU 上行為不同。這個排查雖然費時間但比反復調量化參數(shù)更接近根因。車牌識別這種場景真正決定系統(tǒng)壽命的從來不是模型有多新而是數(shù)據(jù)、解碼和部署這三層有沒有被認真對待希望幫到你。本文還有配套的精品資源點擊獲取