實戰(zhàn):從數(shù)據(jù)標(biāo)注到部署避坑)
簡介這份資源是面向高校學(xué)生與深度學(xué)習(xí)初學(xué)者的車牌識別系統(tǒng)完整項目包適用于畢業(yè)設(shè)計、課程設(shè)計及期末大作業(yè)等場景幫助讀者將卷積神經(jīng)網(wǎng)絡(luò)與OCR技術(shù)落地到真實圖像識別任務(wù)中。包內(nèi)共1177個文件以Python腳本、Markdown文檔、JPG示例圖、TXT說明、YAML配置為主另含少量C、Rust、Jupyter筆記本及模型權(quán)重文件壓縮包約39.43MB覆蓋數(shù)據(jù)準(zhǔn)備、模型訓(xùn)練、驗證評估與預(yù)測推理全流程。系統(tǒng)以yolov11n完成車牌區(qū)域定位再借助paddleocr實現(xiàn)文字識別訓(xùn)練、驗證與預(yù)測腳本分工明確示例圖片可輔助理解運行流程。目前已有85人學(xué)習(xí)下載讀者可據(jù)此掌握從數(shù)據(jù)預(yù)處理到模型部署的完整開發(fā)鏈路積累目標(biāo)檢測與OCR結(jié)合的工程經(jīng)驗并作為進(jìn)一步優(yōu)化識別精度與響應(yīng)速度的實驗平臺。1. 車牌識別系統(tǒng)選型為什么是 yolov11n 加 paddleocr 這套組合車牌識別這件事單靠一個模型很難端到端搞定。真實場景里一張卡口圖里車牌可能只占幾十個像素還伴隨逆光、雨霧、傾斜、遮擋直接上 OCR 大模型去讀整圖準(zhǔn)確率和速度都撐不住。所以工程上更穩(wěn)的做法是拆成兩段先用目標(biāo)檢測把車牌框出來再對裁剪出來的小圖做文字識別。這套「檢測 識別」的流水線就是 yolov11n_paddleocr 車牌識別系統(tǒng)的核心思路。yolov11n 是 YOLO 系列里偏輕量的 n 版本參數(shù)量小、推理快適合部署在邊緣設(shè)備或者單卡服務(wù)器上跑實時視頻流paddleocr 負(fù)責(zé)把檢測框里的車牌字符讀出來它對中文車牌、雙層黃牌、新能源綠牌的字符集支持比較完整而且自帶方向分類器能處理 180 度倒置的車牌。兩者拼起來一個管「在哪」一個管「是什么」職責(zé)清晰替換和調(diào)優(yōu)都方便。這套方案適合誰做智慧停車、園區(qū)卡口、交通違章抓拍、車輛出入管理的團(tuán)隊尤其是預(yù)算有限、要在 Jetson 或普通 GPU 上跑實時推理的場景。如果你手上只有幾百到幾千張標(biāo)注圖又不想從零訓(xùn)一個端到端大模型這條路線落地周期短、可控性強(qiáng)。下面按數(shù)據(jù)準(zhǔn)備、檢測訓(xùn)練、識別對接、避坑、進(jìn)階的順序把每一步的參數(shù)和坑講清楚。2. 數(shù)據(jù)準(zhǔn)備與標(biāo)注讓 yolov11n 學(xué)得動的車牌數(shù)據(jù)集怎么造2.1 車牌檢測數(shù)據(jù)集的組織格式y(tǒng)olov11n 用的是 YOLO 格式標(biāo)注每張圖對應(yīng)一個同名 txt每行是class x_center y_center width height坐標(biāo)都?xì)w一化到 0 到 1。車牌檢測通常只有一個類別class 寫 0 就行。目錄結(jié)構(gòu)按 ultralytics 的約定來dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml里寫清楚路徑和類別名path: /home/user/dataset train: images/train val: images/val nc: 1 names: [plate]這里nc是類別數(shù)車牌檢測只檢「車牌」這一類所以是 1。names的順序必須和標(biāo)注里的 class 編號一致寫反了模型會把車牌當(dāng)背景學(xué)。路徑建議用絕對路徑相對路徑在不同工作目錄下跑訓(xùn)練容易翻車。2.2 標(biāo)注的四個實操要點標(biāo)注工具用 labelImg 或 X-AnyLabeling 都行導(dǎo)出 YOLO 格式。標(biāo)注時注意幾點框要貼緊車牌四角不要留太多背景否則 OCR 階段裁出來的圖會帶干擾雙層車牌黃牌大車要把上下兩層整體框住不要只框一層傾斜車牌按最小外接矩形框別硬拉成水平框YOLO 的框是軸對齊的傾斜太厲害時框會包含大量背景這種情況要么增加傾斜樣本要么在預(yù)處理階段做矯正。數(shù)據(jù)量上單類別車牌檢測train 集 2000 到 5000 張基本能出一個可用的模型val 集留 10% 到 15%。如果場景固定比如就一個停車場出口500 張左右也能收斂但泛化會差。樣本要覆蓋白天、夜間、逆光、雨霧、不同角度尤其是夜間和逆光這兩類不專門補樣本模型上線后大概率在這些場景集體翻車。2.3 用腳本檢查標(biāo)注是否合法標(biāo)注完別急著訓(xùn)先跑個校驗?zāi)_本把越界坐標(biāo)、空標(biāo)注、圖片和標(biāo)簽不匹配的問題揪出來import os from pathlib import Path img_dir Path(dataset/images/train) lbl_dir Path(dataset/labels/train) for lbl in lbl_dir.glob(*.txt): img img_dir / (lbl.stem .jpg) if not img.exists(): print(f缺圖: {lbl.name}) continue with open(lbl) as f: for i, line in enumerate(f): parts line.strip().split() if len(parts) ! 5: print(f{lbl.name} 第{i}行字段數(shù)不對: {line}) continue cls, x, y, w, h parts vals list(map(float, [x, y, w, h])) if any(v 0 or v 1 for v in vals): print(f{lbl.name} 第{i}行坐標(biāo)越界: {line}) if float(w) 0 or float(h) 0: print(f{lbl.name} 第{i}行寬高非正: {line})這段腳本做三件事檢查每張標(biāo)簽有沒有對應(yīng)圖片、每行是不是 5 個字段、歸一化坐標(biāo)是否在 0 到 1 之間且寬高為正。越界坐標(biāo)在訓(xùn)練時會被 ultralytics 靜默裁剪導(dǎo)致框變形模型學(xué)到的位置就偏了所以必須在訓(xùn)練前清掉。寬高為 0 的框會讓 loss 計算出 NaN訓(xùn)練直接崩。3. 訓(xùn)練 yolov11n 檢測模型參數(shù)怎么設(shè)、指標(biāo)怎么看3.1 從預(yù)訓(xùn)練權(quán)重起步的最小訓(xùn)練命令不要從隨機(jī)初始化訓(xùn)用 COCO 預(yù)訓(xùn)練的 yolov11n 權(quán)重做遷移學(xué)習(xí)收斂快很多。ultralytics 的命令行方式最省事yolo detect train \ modelyolo11n.pt \ datadataset/data.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ device0 \ projectruns/plate \ nameyolo11n_plate逐項說下參數(shù)。modelyolo11n.pt是加載預(yù)訓(xùn)練權(quán)重第一次跑會自動下載。imgsz640是輸入分辨率車牌在整圖里偏小的話可以提到 960 或 1280但顯存和速度會漲。batch16按顯存調(diào)8G 顯存跑 640 大概能到 16跑 1280 就得降到 4 或 8。lr00.01是初始學(xué)習(xí)率遷移學(xué)習(xí)常用 0.01如果 loss 震蕩厲害降到 0.001。patience20是早停20 輪指標(biāo)不漲就停省時間。device0指定第一塊 GPUCPU 訓(xùn)練把 device 設(shè)成 cpu但速度會慢到?jīng)]法接受。3.2 訓(xùn)練過程要盯的三個指標(biāo)訓(xùn)練日志里重點看box_loss、cls_loss和驗證集的mAP50。box_loss 管框的位置回歸cls_loss 管分類車牌單類別時 cls_loss 會降得很快。mAP50 是 IoU 閾值 0.5 下的平均精度車牌檢測一般能到 0.95 以上算不錯。如果 mAP50 卡在 0.7 左右上不去先查標(biāo)注質(zhì)量再看是不是小目標(biāo)太多——車牌在 640 分辨率下小于 16 像素的話模型很難學(xué)好這時候要么提高 imgsz要么在數(shù)據(jù)增強(qiáng)里加 mosaic 和 copy_paste。驗證時用命令行跑一批圖看效果yolo detect predict \ modelruns/plate/yolo11n_plate/weights/best.pt \ sourcetest_images/ \ conf0.25 \ saveTrueconf0.25是置信度閾值低于這個值的框不輸出。車牌檢測可以適當(dāng)調(diào)低到 0.2寧可多檢幾個框讓 OCR 去過濾也別漏檢。saveTrue把畫了框的結(jié)果存下來肉眼過一遍重點看夜間和逆光樣本有沒有漏。3.3 導(dǎo)出推理格式訓(xùn)練完的 best.pt 是 PyTorch 權(quán)重部署時按目標(biāo)平臺導(dǎo)出。GPU 服務(wù)器上跑 TensorRT 最快邊緣設(shè)備用 ONNX 或 OpenVINOyolo export modelbest.pt formatonnx opset12 simplifyTrue yolo export modelbest.pt formatengine halfTrue device0opset12是 ONNX 算子集版本兼容性比較好。simplifyTrue會做圖優(yōu)化去掉冗余算子。TensorRT 導(dǎo)出加halfTrue用 FP16速度能再提一截精度掉得很少。注意導(dǎo)出 engine 時的 GPU 型號要和部署機(jī)器一致?lián)Q卡要重新導(dǎo)出這個坑很多人踩過。4. 對接 paddleocr從檢測框到車牌字符串的完整鏈路4.1 裁剪、矯正、識別的三步流程yolov11n 輸出的是車牌框坐標(biāo)paddleocr 吃的是裁剪后的小圖。中間要做裁剪和可選的透視矯正。先裝依賴pip install paddlepaddle-gpu paddleocr opencv-python如果只用 CPU 推理裝paddlepaddle就行。然后寫識別主流程import cv2 from paddleocr import PaddleOCR from ultralytics import YOLO det_model YOLO(best.pt) ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def recognize_plate(img_path): img cv2.imread(img_path) results det_model(img, conf0.25, verboseFalse) plates [] for box in results[0].boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 map(int, box) crop img[y1:y2, x1:x2] if crop.size 0: continue ocr_res ocr.ocr(crop, clsTrue) if not ocr_res or not ocr_res[0]: continue text .join([line[1][0] for line in ocr_res[0]]) score min([line[1][1] for line in ocr_res[0]]) plates.append({text: text, score: score, box: [x1, y1, x2, y2]}) return plates邏輯上分三步Y(jié)OLO 出框、按框裁圖、裁圖送 OCR。use_angle_clsTrue開啟方向分類處理倒置車牌。langch用中文模型能識別漢字省份簡稱。ocr.ocr返回的結(jié)構(gòu)是「行」的列表每行含文本和置信度車牌一般只有一行多行的情況雙層牌用 join 拼起來。score取所有行里最低的置信度作為整塊車牌的可信度低于閾值就丟棄。4.2 透視矯正傾斜車牌的后悔藥YOLO 的框是軸對齊的車牌傾斜時框里會混入大量背景OCR 容易讀錯。如果檢測階段能拿到四個角點可以做透視變換把車牌拉正。ultralytics 默認(rèn)不輸出角點需要自己加一個關(guān)鍵點分支或者用傳統(tǒng)方法找輪廓。簡單場景下用最小外接矩形旋轉(zhuǎn)矯正也能救一部分import numpy as np def deskew(crop): gray cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) coords np.column_stack(np.where(thresh 0)) if len(coords) 10: return crop angle cv2.minAreaRect(coords)[-1] if angle -45: angle 90 angle h, w crop.shape[:2] M cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) return cv2.warpAffine(crop, M, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE)這段用 Otsu 二值化后找前景點算最小外接矩形的角度再旋轉(zhuǎn)回正。borderModecv2.BORDER_REPLICATE用邊緣像素填充旋轉(zhuǎn)后的空白避免出現(xiàn)黑邊干擾 OCR。注意這個方法對背景復(fù)雜的圖會失效前景點里混入背景時角度算不準(zhǔn)所以只適合車牌區(qū)域比較干凈的裁剪圖。4.3 識別結(jié)果的清洗規(guī)則OCR 出來的字符串不能直接用車牌有固定格式得做規(guī)則校驗。普通藍(lán)牌是「省份簡稱 字母 5 位字母數(shù)字」新能源綠牌是 8 位。寫個簡單的校驗import re PROVINCES 京津冀晉蒙遼吉黑滬蘇浙皖閩贛魯豫鄂湘粵桂瓊渝川貴云藏陜甘青寧新 def clean_plate(text): text re.sub(r[^A-Z0-9\u4e00-\u9fa5], , text.upper()) if len(text) 7: return None if text[0] not in PROVINCES: return None if not re.match(r^[A-Z][A-Z0-9]{5,6}$, text[1:]): return None return text先去掉非字母數(shù)字漢字的字符再檢查首位是不是合法省份簡稱后面是不是字母開頭加 5 到 6 位。\u4e00-\u9fa5是漢字 Unicode 范圍。這個規(guī)則能過濾掉大部分 OCR 誤讀比如把「京」讀成「涼」的情況。規(guī)則不是萬能的新能源牌和使館牌格式特殊按實際業(yè)務(wù)調(diào)整。5. 避坑與排查車牌識別上線后最常見的五個問題5.1 夜間車牌漏檢嚴(yán)重現(xiàn)象是白天檢測正常夜間 mAP 掉到 0.5 以下大量車牌框不出來。原因是訓(xùn)練集里夜間樣本太少模型沒見過低照度下的車牌紋理。解決分兩步一是補夜間樣本至少占訓(xùn)練集 20%二是在預(yù)處理階段做自適應(yīng)直方圖均衡提升暗部對比度。用 OpenCV 的 CLAHEclahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) lab[:, :, 0] clahe.apply(lab[:, :, 0]) img cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)在 LAB 空間的 L 通道做均衡不影響顏色。clipLimit2.0控制對比度增強(qiáng)幅度太大噪聲會被放大車牌邊緣反而糊。5.2 OCR 把相似字符讀混現(xiàn)象是「0」和「O」、「1」和「I」、「8」和「B」經(jīng)常讀錯。原因是車牌字體和 OCR 訓(xùn)練用的通用字體有差異加上裁剪圖分辨率低。解決辦法一是把裁剪圖放大到 OCR 推薦的輸入尺寸一般高度 32 或 48 像素二是用規(guī)則后處理強(qiáng)制替換。車牌里字母 I 和 O 基本不出現(xiàn)避免和 1、0 混淆可以在清洗時把 I 換成 1、O 換成 0text text.replace(I, 1).replace(O, 0)這個替換要在省份簡稱校驗之后做避免把漢字誤傷。5.3 檢測框偏大導(dǎo)致 OCR 讀入背景現(xiàn)象是車牌能檢出但 OCR 結(jié)果里混入廣告字、車身貼紙。原因是標(biāo)注時框留了太多背景或者模型回歸不準(zhǔn)。解決訓(xùn)練時把框貼緊車牌推理后對框做內(nèi)縮按框?qū)捀叩?5% 到 10% 往里收pad_x int((x2 - x1) * 0.05) pad_y int((y2 - y1) * 0.05) crop img[y1 pad_y:y2 - pad_y, x1 pad_x:x2 - pad_x]內(nèi)縮能去掉框邊緣的背景但縮太多會切掉車牌字符5% 到 10% 是經(jīng)驗值按實際效果調(diào)。5.4 推理速度達(dá)不到實時現(xiàn)象是單幀處理超過 100ms視頻流卡頓。排查順序先看 YOLO 推理耗時再看 OCR 耗時。YOLO 用 TensorRT FP16 一般能到 5ms 以內(nèi)OCR 是瓶頸paddleocr 的檢測加識別在 GPU 上大概 20 到 40ms。優(yōu)化手段OCR 只跑識別不跑檢測因為車牌已經(jīng)裁好了關(guān)掉不需要的模塊把 OCR 的det設(shè)為 False直接用識別模型ocr PaddleOCR(use_angle_clsTrue, langch, detFalse, recTrue)detFalse跳過 OCR 自帶的文本檢測省一大半時間。另外批處理也能提吞吐把多塊車牌拼成一個 batch 送 OCR。5.5 換部署機(jī)器后精度暴跌現(xiàn)象是開發(fā)機(jī)上 mAP 0.95換到另一臺機(jī)器掉到 0.6。原因是導(dǎo)出的 TensorRT engine 綁定了特定 GPU 架構(gòu)和 TensorRT 版本換卡后不兼容或者 FP16 在舊卡上精度損失大。解決部署機(jī)器上重新導(dǎo)出 engine別跨機(jī)器拷貝舊卡算力低于 7.0用 FP32 導(dǎo)出別開 half。ONNX 格式跨平臺兼容性好實在搞不定就用 ONNX Runtime 跑。6. 進(jìn)階技巧用關(guān)鍵點回歸把車牌矯正做到端到端前面用最小外接矩形矯正傾斜車牌背景復(fù)雜時會失效。更穩(wěn)的做法是讓 yolov11n 在檢測的同時回歸車牌的四個角點拿到角點后直接做透視變換一步到位。ultralytics 支持關(guān)鍵點任務(wù)把數(shù)據(jù)集標(biāo)注從框擴(kuò)展成框加四個角點格式是class x y w h px1 py1 px2 py2 px3 py3 px4 py4角點也歸一化。訓(xùn)練時把任務(wù)設(shè)成 poseyolo pose train modelyolo11n-pose.pt dataplate_pose.yaml epochs100 imgsz640推理時拿到角點用cv2.getPerspectiveTransform把車牌拉成標(biāo)準(zhǔn)矩形def perspective_crop(img, pts): pts np.array(pts, dtypenp.float32).reshape(4, 2) rect np.zeros((4, 2), dtypenp.float32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] d np.diff(pts, axis1) rect[1] pts[np.argmin(d)] rect[3] pts[np.argmax(d)] w int(max(np.linalg.norm(rect[0] - rect[1]), np.linalg.norm(rect[2] - rect[3]))) h int(max(np.linalg.norm(rect[0] - rect[3]), np.linalg.norm(rect[1] - rect[2]))) dst np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]], dtypenp.float32) M cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(img, M, (w, h))角點排序的邏輯是左上角坐標(biāo)和最小右下角和最大右上角坐標(biāo)差最小左下角差最大。透視變換后車牌變成水平矩形OCR 準(zhǔn)確率能明顯提升尤其是大角度傾斜的場景。代價是標(biāo)注成本高每張圖要標(biāo)四個角點數(shù)據(jù)量需求也更大幾千張起步。如果業(yè)務(wù)場景里車牌基本都是正對攝像頭用前面的旋轉(zhuǎn)矯正就夠了不必上關(guān)鍵點。驗證整套系統(tǒng)時我習(xí)慣準(zhǔn)備一個包含 200 張圖的回歸測試集覆蓋白天、夜間、逆光、傾斜、雙層牌每次改完模型或參數(shù)都跑一遍記錄端到端準(zhǔn)確率檢測框 IoU 大于 0.5 且 OCR 字符串完全正確。這個數(shù)字比單看 mAP 更能反映上線效果。踩過的坑里最虧的一次是沒做回歸測試就換了 OCR 版本結(jié)果新能源牌識別率掉了 15%上線后第二天才發(fā)現(xiàn)?,F(xiàn)在我改任何一環(huán)都先跑回歸集寧可多花十分鐘也不賭線上不出事。希望幫到你。本文還有配套的精品資源點擊獲取