:從數(shù)據(jù)預(yù)處理到模型訓(xùn)練與部署)
簡介基于Python與卷積神經(jīng)網(wǎng)絡(luò)的車牌識別項目面向計算機視覺初學(xué)者及智能交通開發(fā)者目標(biāo)是幫助用戶掌握從數(shù)據(jù)預(yù)處理、模型構(gòu)建到實際部署的完整流程。壓縮包共25個文件包含jpg/png圖像樣本、py訓(xùn)練腳本、md說明文檔、dat數(shù)據(jù)文件以及7z/zip等歸檔格式整體約29.2MB文件類型覆蓋代碼、圖像、文檔與模型備份目錄結(jié)構(gòu)清晰適合直接解壓后按模塊學(xué)習(xí)。資源目前已有206人學(xué)習(xí)/下載。內(nèi)容體系完整系統(tǒng)講解了Python環(huán)境下使用Keras/TensorFlow搭建CNN模型的核心要點包括卷積層、池化層與全連接層設(shè)計圖像灰度化、歸一化與增強等預(yù)處理手段以及模型訓(xùn)練中的優(yōu)化器選擇、損失計算、早停策略和評估指標(biāo)準(zhǔn)確率、召回率等。同時附帶可運行的代碼與標(biāo)注數(shù)據(jù)樣本既可用于復(fù)現(xiàn)實驗也能為智能交通中的車牌檢測與識別提供直接的工程參考是理解深度學(xué)習(xí)視覺應(yīng)用的良好實例。1. 車牌識別不是玄學(xué)PythonCNN 這套資源到底能解決什么先給結(jié)論這份基于 Python 和 CNN 的車牌識別資源包不是那種只能跑個 demo 的玩具代碼里面是完整的 License-Plate-Recognition 工程包含數(shù)據(jù)整理、模型訓(xùn)練、字符識別和推理腳本。你把它下載下來按步驟把環(huán)境配好拿自己的車牌圖片走一遍訓(xùn)練是能出實際識別結(jié)果的。很多人以為車牌識別很難搞深度學(xué)習(xí)的人覺得是常規(guī)圖像分類搞傳統(tǒng)圖像處理的人覺得要寫一堆形態(tài)學(xué)操作。實際上用 CNN 做車牌識別核心就三件事把車牌區(qū)域從圖片里摳出來把車牌上的字符切成一個個單獨字符再用 CNN 逐個字符分類。這套資源正好把這三件事都覆蓋了適合剛接觸深度學(xué)習(xí)、想拿真實任務(wù)練手的開發(fā)者也適合要做智能交通道閘、停車場管理的技術(shù)人員當(dāng)作基線工程。2. 先搞懂 CNN 怎么認(rèn)車牌卷積、池化與全連接的分工2.1 為什么車牌識別選 CNN 而不是傳統(tǒng)模板匹配傳統(tǒng)車牌識別走的是圖像處理老路先灰度化、二值化再用邊緣檢測和輪廓查找定位車牌然后用投影法切出字符最后用模板匹配或特征加 SVM 分類。這套方案在固定場景、固定光照下能用但一遇到傾斜、模糊、反光、雨天泥點特征就亂了模板匹配直接翻車。CNN 的好處在于特征是自己學(xué)的。卷積層自動提取邊緣、紋理、筆畫等低層特征池化層做降維保留主要信息全連接層把特征映射到類別概率。你不需要手工設(shè)計“車牌字符長什么樣”只要喂足夠多樣性的數(shù)據(jù)網(wǎng)絡(luò)自己會學(xué)出抗干擾的特征。常見做法是用兩階段先檢測車牌位置YOLO、SSD 或傳統(tǒng)輪廓法再對車牌區(qū)域做字符分割最后用 CNN 分類。這套資源里的主干就是 CNN 分類部分適合先跑通字符識別環(huán)節(jié)。核心區(qū)別一句話傳統(tǒng)方法靠人定義規(guī)則CNN 靠數(shù)據(jù)定義規(guī)則。數(shù)據(jù)足夠多CNN 的泛化能力明顯更好。2.2 最小可用 CNN 結(jié)構(gòu)層數(shù)與過濾器怎么定資源包里的模型結(jié)構(gòu)不復(fù)雜典型的“卷積池化全連接”堆疊。我自己復(fù)現(xiàn)時通常這樣搭from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout model Sequential([ # 第一層卷積32 個 3x3 過濾器輸入 64x64 的灰度圖 Conv2D(32, (3, 3), activationrelu, input_shape(64, 64, 1)), MaxPooling2D(pool_size(2, 2)), # 第二層卷積64 個過濾器進一步提取筆畫特征 Conv2D(64, (3, 3), activationrelu), MaxPooling2D(pool_size(2, 2)), # 第三層卷積128 個過濾器提取高階組合特征 Conv2D(128, (3, 3), activationrelu), MaxPooling2D(pool_size(2, 2)), Flatten(), Dropout(0.5), Dense(128, activationrelu), Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy])這段代碼的邏輯是三層卷積逐步從低級邊緣特征學(xué)到高級筆畫組合每層后面接 MaxPooling 把特征圖尺寸減半防止過擬合同時減少計算量。Dropout 在全連接前隨機丟棄一半神經(jīng)元這是防止過擬合的關(guān)鍵。最后一層 Dense 的 num_classes 是字符類別總數(shù)比如“省份簡稱字母數(shù)字”一共幾十類。參數(shù)說明輸入尺寸 64x64 是車牌字符裁剪后的常見大小不能用太大否則顯存吃不住過濾器數(shù)量從 32 到 64 到 128 翻倍是經(jīng)驗做法太深容易過擬合太淺學(xué)不到特征。dropout 0.5 是通用值如果你的訓(xùn)練集很小可以調(diào)到 0.6。2.3 數(shù)據(jù)預(yù)處理順序灰度、歸一化、增強的坑車牌識別數(shù)據(jù)預(yù)處理有個固定順序先灰度化再歸一化最后做數(shù)據(jù)增強?;叶然且驗檐嚺祁伾畔ψ址R別不是必需的反而會引入光照偏色的干擾歸一化把像素值縮到 0~1 之間加速收斂數(shù)據(jù)增強在訓(xùn)練時在線做不改變原始數(shù)據(jù)。# 常見做法用 OpenCV 讀圖直接轉(zhuǎn)灰度再歸一化 import cv2 import numpy as np img cv2.imread(plate_region.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度化 gray cv2.resize(gray, (64, 64)) # 統(tǒng)一尺寸 gray gray.astype(float32) / 255.0 # 歸一化到 [0,1] gray gray.reshape((64, 64, 1)) # 加通道維度這四步的順序不能亂。如果先歸一化再灰度化等于在彩色空間做了歸一化灰度后還得重新算一遍純屬浪費如果先 resize 再灰度化某些 resize 插值算法對彩色圖和灰度圖的結(jié)果不一樣可能導(dǎo)致字符邊緣變形。數(shù)據(jù)增強要在訓(xùn)練時放在 ImageDataGenerator 里做而不是提前把圖片改掉。比如旋轉(zhuǎn) 10 度、寬度平移 0.1、縮放 0.1這些操作模擬了車牌拍攝角度和距離的變化。注意旋轉(zhuǎn)角度別超過 15 度車牌字符識別對角度很敏感過大的旋轉(zhuǎn)會把“0”和“O”、“1”和“I”搞混。2.4 數(shù)據(jù)集目錄結(jié)構(gòu)標(biāo)簽怎么對應(yīng)資源包里的數(shù)據(jù)通常按字符類別分文件夾或者用一個 CSV 記錄圖片路徑和標(biāo)簽。常見做法是“一個類別一個文件夾”比如 train/京/、train/A/、train/0/這樣用 Keras 的 ImageDataGenerator.flow_from_directory 直接讀取不需要手寫標(biāo)簽文件。from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator( rescale1./255, rotation_range10, width_shift_range0.1, height_shift_range0.1, zoom_range0.1, validation_split0.2 ) train_gen datagen.flow_from_directory( train, target_size(64, 64), color_modegrayscale, batch_size32, subsettraining, shuffleTrue ) val_gen datagen.flow_from_directory( train, target_size(64, 64), color_modegrayscale, batch_size32, subsetvalidation, shuffleFalse )這里 flow_from_directory 會自動根據(jù)子文件夾名生成類別索引比如 0、1、2、A、B、京、滬 這些文件夾名就成了分類標(biāo)簽。注意類別索引是按文件夾名排序的如果你后面要手動推理必須用 generator.class_indices 把這個映射存下來否則模型輸出的概率對應(yīng)到哪個字符你就不知道了。我一般會把 class_indices 存成 JSON推理時讀回來做反查。這一步看著不起眼但很多人訓(xùn)練完模型推理時發(fā)現(xiàn)結(jié)果亂套十有八九是類別映射沒對上。3. 把資源包跑起來環(huán)境搭建、訓(xùn)練參數(shù)與推理3.1 Python 環(huán)境版本、Keras/TensorFlow 搭配與安裝先說版本搭配。這個資源包是基于 TensorFlow/Keras 的建議 Python 3.7~3.10 之間。TensorFlow 2.x 自帶 Keras不需要單獨裝 Keras。我用的組合是 Python 3.8 TensorFlow 2.6這個搭配最穩(wěn)太高版本的 TensorFlow 在 Windows 上經(jīng)常出現(xiàn) GPU 庫不匹配的問題。# 建議用虛擬環(huán)境別污染系統(tǒng) Python python -m venv plate_env source plate_env/bin/activate # Windows 用 plate_env\Scripts\activate pip install tensorflow2.6.0 pip install opencv-python pip install numpy matplotlib scikit-learn裝完驗證一下import tensorflow as tf print(tf.__version__) print(tf.test.is_gpu_available()) # 如果只有 CPU會顯示 False如果 GPU 不可用也不用慌車牌字符識別這種小圖分類任務(wù)CPU 也能訓(xùn)練就是慢一些。一個 64x64 灰度圖的 CNN在 CPU 上跑幾十個 epoch 也就幾分鐘到十幾分鐘。注意OpenCV 的安裝包名是 opencv-python不是 opencv。很多人 pip install opencv 直接報錯這是最常見的新手坑。3.2 訓(xùn)練腳本關(guān)鍵參數(shù)batch_size、epoch、學(xué)習(xí)率怎么調(diào)資源包里的訓(xùn)練腳本一般有默認(rèn)參數(shù)但直接跑默認(rèn)參數(shù)不一定適合你的數(shù)據(jù)量。我最常調(diào)的三個參數(shù)是 batch_size、epoch、學(xué)習(xí)率。# 常見訓(xùn)練循環(huán)配置 batch_size 32 # 每批喂 32 張圖 epochs 50 # 最多訓(xùn)練 50 輪 learning_rate 0.001 # Adam 默認(rèn)學(xué)習(xí)率 model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] ) history model.fit( train_gen, validation_dataval_gen, epochsepochs, callbacks[ tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue) ] )batch_size 決定每次更新權(quán)重用多少張圖。顯存夠的情況下32 是個穩(wěn)妥起點太小比如 4梯度噪聲大訓(xùn)練不穩(wěn)太大比如 128可能收斂到平坦的極小值泛化反而差。epoch 不是越大越好。我習(xí)慣設(shè)置一個較大的上限然后用 EarlyStopping 監(jiān)控驗證集損失連續(xù) 5 個 epoch 沒下降就停restore_best_weights 會把權(quán)重回滾到最好的那一步。這是防過擬合的后悔藥比手動瞪眼睛看曲線靠譜。學(xué)習(xí)率對 Adam 來說 0.001 是默認(rèn)值但如果你的數(shù)據(jù)量很小幾千張0.0005 會更穩(wěn)。學(xué)習(xí)率太大loss 會震蕩太小收斂慢還容易停在局部最優(yōu)。3.3 模型保存與推理從 h5 文件到車牌字符輸出訓(xùn)練完別急著關(guān)電腦模型保存和推理鏈路一定要跑通。Keras 的 save 方法會把結(jié)構(gòu)、權(quán)重、優(yōu)化器狀態(tài)一起存下來。model.save(plate_model.h5)推理時的關(guān)鍵點是用和訓(xùn)練時完全相同的預(yù)處理。很多人在這一步翻車訓(xùn)練時圖像是 64x64 灰度推理時直接讀一張彩色原圖扔給模型shape 對不上結(jié)果全是亂碼。from tensorflow.keras.models import load_model import cv2 import numpy as np model load_model(plate_model.h5) def predict_char(plate_img): # 預(yù)處理必須與訓(xùn)練完全一致 gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (64, 64)) gray gray.astype(float32) / 255.0 gray gray.reshape((1, 64, 64, 1)) # 增加 batch 維度 probs model.predict(gray) idx np.argmax(probs, axis1)[0] return class_indices_reverse[idx] # 從索引反查字符這里的 shape 是 (1, 64, 64, 1)第一個 1 表示 batch 大小為 1最后一個 1 是灰度通道。如果你推理時忘了加 batch 維度會直接報錯說維度不匹配。class_indices_reverse 是訓(xùn)練時存下來的反查字典沒有它你只能得到一堆概率數(shù)字不知道對應(yīng)什么字符。3.4 訓(xùn)練日志怎么看loss 不降和 acc 突變的信號訓(xùn)練過程中如果 loss 一直不降先別急著調(diào)網(wǎng)絡(luò)結(jié)構(gòu)先看學(xué)習(xí)率。最常見的是學(xué)習(xí)率太大loss 在初期就卡在高位震蕩其次是數(shù)據(jù)沒歸一化輸入值范圍太大導(dǎo)致梯度爆炸。把訓(xùn)練歷史畫出來一目了然import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.show()如果 train_loss 下降但 val_loss 上升說明過擬合了增加 dropout 或數(shù)據(jù)增強。如果兩者都居高不下檢查是不是標(biāo)簽和圖片對不上。我遇到過一個問題數(shù)據(jù)增強時開了水平翻轉(zhuǎn)結(jié)果把“鄂”翻成了鏡像模型怎么訓(xùn)都學(xué)不好。所以車牌字符不能做隨機水平翻轉(zhuǎn)只能小角度旋轉(zhuǎn)和平移。4. 避坑手冊車牌識別最常見的 5 個翻車點這一章全是血淚經(jīng)驗每一條我都當(dāng)面踩過能幫你省至少兩個通宵。4.1 現(xiàn)象藍牌訓(xùn)練集遇到黃牌就崩用一套藍牌數(shù)據(jù)訓(xùn)練完模型在測試集上準(zhǔn)確率 98%一接到黃牌、綠牌新能源車牌就全錯。原因訓(xùn)練集里只有藍底白字網(wǎng)絡(luò)把“藍色背景”當(dāng)成了隱式特征字符本身反而沒學(xué)透。解決收集多種底色車牌或者做顏色通道的隨機擾動。常見做法是在預(yù)處理時把圖像的飽和度、對比度做隨機抖動模擬不同材質(zhì)和光照下的車牌。更直接的辦法是數(shù)據(jù)層面加入黃牌、綠牌樣本哪怕每種只有幾百張也能把模型的注意力拉回到字符上。4.2 現(xiàn)象字符分割把“京”切成兩半做完車牌定位后用輪廓查找或投影法切字符結(jié)果“京”字中間被斷開或者偏旁和整體被分成兩個輪廓。原因二值化閾值選得太激進把漢字內(nèi)部的筆畫斷裂了。解決先做形態(tài)學(xué)閉運算把斷開的筆畫連接起來再找輪廓。內(nèi)核大小用 3x3 或 5x5不要太大否則會把相鄰字符粘連。如果投影法切割建議先做垂直投影找到波谷的連續(xù)區(qū)域作為切分點同時對過窄的區(qū)域做合并判斷。4.3 現(xiàn)象驗證集 99%測試集只有 70%訓(xùn)練時驗證集準(zhǔn)確率一路飆升到 99%換成測試集直接掉到 70%。原因大概率是數(shù)據(jù)泄露驗證集和訓(xùn)練集來自同一個視頻序列的連續(xù)幀兩張圖幾乎一樣或者你用了 flow_from_directory 時 shuffleFalse驗證集順序固定模型記住了順序。解決按視頻或者按拍攝時間劃分?jǐn)?shù)據(jù)集保證訓(xùn)練和驗證的圖片沒有重疊訓(xùn)練前先檢查驗證集里有沒有和訓(xùn)練集 hash 值完全相同的圖。另一個可能是過擬合但過擬合通常不會差到 30 個點先查數(shù)據(jù)泄露。4.4 現(xiàn)象GPU 顯存 OOM訓(xùn)練直接崩報錯 ResourceExhaustedError顯存不夠。原因batch_size 太大或者輸入圖片尺寸設(shè)置得太大也可能是 TensorFlow 默認(rèn)給 GPU 預(yù)留了全部顯存。解決先把 batch_size 從 64 降到 32再降到 16 試如果還崩把輸入圖片從 64x64 降到 48x48。還可以限制 TensorFlow 的顯存使用physical_devices tf.config.list_physical_devices(GPU) tf.config.experimental.set_memory_growth(physical_devices[0], True)這樣顯存會按需增長而不是一次性占滿。我自己的習(xí)慣是 set_memory_growth(True) 之后顯存不夠時會報錯而不是殺進程方便我定位到具體是哪個 batch 超限。4.5 現(xiàn)象中文車牌字符識別率低漢字總是認(rèn)錯英文字母和數(shù)字都準(zhǔn)但漢字字符“滬”經(jīng)常被認(rèn)成“護”、“湘”認(rèn)成“油”。原因漢字類別多、結(jié)構(gòu)相似且訓(xùn)練數(shù)據(jù)里漢字樣本數(shù)量通常遠少于字母數(shù)字。解決對漢字類別做數(shù)據(jù)增強加權(quán)比如對漢字樣本做更復(fù)雜的仿射變換或者把漢字分類單獨訓(xùn)練一個模型專門處理省份簡稱這樣網(wǎng)絡(luò)不用跟字母數(shù)字搶容量。另一個技巧是檢查標(biāo)簽我遇到過中文路徑導(dǎo)致 OpenCV 讀取失敗圖片是壞的標(biāo)簽卻是對的模型學(xué)了一堆噪聲。5. 別只盯著準(zhǔn)確率評估指標(biāo)、端到端與部署選擇5.1 準(zhǔn)確率、精確率、召回率、F1 在車牌識別里怎么用車牌識別是個多分類任務(wù)很多人只報一個整體準(zhǔn)確率這不夠。字符類別有幾十個有的類別樣本多有的類別樣本少整體準(zhǔn)確率會被多數(shù)類掩蓋。精確率關(guān)心的是“模型識別成滬的結(jié)果里有多少真是滬”召回率關(guān)心的是“所有滬的樣本里模型找回了多少”。對車牌識別來說召回率更重要——漏識別一個字符會導(dǎo)致整個車牌號錯寧可多給幾個候選字符也不要漏掉正確的那個。from sklearn.metrics import classification_report y_true [...] # 真實標(biāo)簽 y_pred [...] # 模型預(yù)測標(biāo)簽 print(classification_report(y_true, y_pred, target_namesclass_names))看 report 時重點關(guān)注每個漢字類別的 recall。如果某個漢字 recall 低于 0.8就把這個類別的數(shù)據(jù)加倍或者單獨優(yōu)化。整體準(zhǔn)確率 98% 不代表漢字也 98%經(jīng)常是字母數(shù)字 99.5%漢字 90%。5.2 端到端模型與兩階段模型的取舍資源包里的方案是兩階段切字符 CNN 分類?,F(xiàn)在也有端到端模型直接用 YOLOCTC 或者 Transformer 識別整串車牌。但端到端模型需要大量的整牌標(biāo)注數(shù)據(jù)訓(xùn)練成本高調(diào)試?yán)щy。兩階段的好處是每個環(huán)節(jié)都可解釋、可單獨優(yōu)化車牌定位不準(zhǔn)就調(diào)定位字符切歪了就調(diào)分割分類不準(zhǔn)就調(diào) CNN。缺點是多了一步分割誤差會累積。如果是快速落地我更建議先用兩階段方案跑通把字符分類準(zhǔn)確率做到 95% 以上再考慮是否換端到端。如果你的車牌圖片都是標(biāo)準(zhǔn)角度、標(biāo)準(zhǔn)比例兩階段完全夠用。5.3 視頻流識別幀采樣、閾值與后處理部署到視頻流時不能每一幀都跑完整識別計算開銷大且結(jié)果抖動。常見做法是每 3~5 幀采樣一次并且用時間維度去重。# 模擬視頻流識別 prev_plate stable_count 0 for frame_index, frame in enumerate(video_frames): if frame_index % 3 ! 0: # 每 3 幀處理一次 continue plate recognize_plate(frame) if plate prev_plate: stable_count 1 else: stable_count 0 prev_plate plate if stable_count 5: # 連續(xù) 5 次識別到同一車牌才確認(rèn) print(f確認(rèn)車牌: {plate}) break這個邏輯解決的是單幀識別偶發(fā)錯誤的問題。一幀識別錯是常事但連續(xù) 5 幀在同一位置識別出同一串字符的概率就低很多。注意閾值要按攝像頭幀率調(diào)25fps 的攝像頭每 3 幀采樣一次5 次確認(rèn)大約需要 0.6 秒基本滿足道閘場景。6. 把模型用到自己的場景微調(diào)、字符集擴展與最終驗證6.1 用自己的數(shù)據(jù)微調(diào)凍結(jié)層數(shù)與學(xué)習(xí)率拿到的預(yù)訓(xùn)練模型通常是在通用車牌字符上訓(xùn)練的你的場景如果是新能源綠牌或者個性化車牌直接拿來用可能不兼容。微調(diào)時我一般凍結(jié)前兩層卷積只訓(xùn)練第三層卷積和全連接層學(xué)習(xí)率降到 0.0001。6.2 字符集擴展從 31 個省到新能源車牌新能源車牌多一個 D/F 字母還有 8 位字符長度需要把模型的輸出類別數(shù)從原來的幾十類擴展。做法是加載原模型丟掉最后一層 Dense接一個新的 Dense(num_classes)。注意新類別的樣本要收集足夠否則新類別的權(quán)重更新會破壞舊特征。6.3 最終驗證拿一張沒見過的圖走完整流程微調(diào)完不要只看測試集指標(biāo)。我從那以后每次都會強制走一遍從網(wǎng)上下載一張真實場景的車牌照片不做任何手工裁剪讓定位、分割、識別全流程跑一遍。如果這張圖能一次識別對我才敢往上線走。這個習(xí)慣救了我很多次上次就是一張傾斜 15 度的綠牌讓分割腳本直接崩了被迫加了透視矯正。希望幫到你。本文還有配套的精品資源點擊獲取