牌識(shí)別停車(chē)場(chǎng)收費(fèi)系統(tǒng):從圖像到賬單的完整實(shí)現(xiàn))
簡(jiǎn)介這份資源是面向計(jì)算機(jī)相關(guān)專(zhuān)業(yè)畢業(yè)設(shè)計(jì)學(xué)生與項(xiàng)目實(shí)戰(zhàn)學(xué)習(xí)者的Python停車(chē)場(chǎng)收費(fèi)系統(tǒng)源碼核心采用OpenCV實(shí)現(xiàn)車(chē)牌識(shí)別將圖像處理、車(chē)牌定位與計(jì)費(fèi)管理整合為完整可運(yùn)行項(xiàng)目。項(xiàng)目經(jīng)導(dǎo)師指導(dǎo)并通過(guò)評(píng)審獲98分源碼均經(jīng)本地編譯與嚴(yán)格調(diào)試難度適中適合作為高分畢設(shè)參考或課程設(shè)計(jì)練手。壓縮包共908個(gè)文件約18.07MB以584個(gè)js、86個(gè)jpg、76個(gè)gif等前端與素材文件為主同時(shí)包含43個(gè)java、24個(gè)xml、18個(gè)class及4個(gè)py等后端與識(shí)別邏輯代碼另有sql建表腳本、html頁(yè)面和配置文件結(jié)構(gòu)覆蓋前后端與數(shù)據(jù)庫(kù)。已有248人學(xué)習(xí)下載。讀者可獲取完整項(xiàng)目源碼、車(chē)牌識(shí)別與收費(fèi)管理模塊實(shí)現(xiàn)思路、可運(yùn)行工程結(jié)構(gòu)及調(diào)試經(jīng)驗(yàn)便于快速理解業(yè)務(wù)閉環(huán)并完成二次開(kāi)發(fā)。1. 從一張模糊的入場(chǎng)照說(shuō)起這套 OpenCV 車(chē)牌識(shí)別停車(chē)場(chǎng)系統(tǒng)到底能跑出什么很多同學(xué)做畢設(shè)時(shí)都遇到過(guò)這種場(chǎng)景攝像頭架在道閘旁邊車(chē)一進(jìn)來(lái)畫(huà)面里車(chē)牌歪著、反光、還帶運(yùn)動(dòng)模糊后臺(tái)卻要立刻給出「京A·12345入場(chǎng)時(shí)間 14:03計(jì)費(fèi)開(kāi)始」。這套基于 Python OpenCV 的車(chē)牌識(shí)別停車(chē)場(chǎng)收費(fèi)系統(tǒng)解決的正是這條從「圖像」到「賬單」的完整鏈路。它不是單純調(diào)個(gè) OCR 接口的玩具而是把車(chē)牌定位、字符分割、識(shí)別、入場(chǎng)出場(chǎng)記錄、計(jì)費(fèi)規(guī)則、用戶(hù)與現(xiàn)金管理都串了起來(lái)源碼里能看到carPlateRecognition、Parking、Record、ManageCashController這些類(lèi)說(shuō)明它是一套有業(yè)務(wù)閉環(huán)的工程而不是只跑一張圖的 demo。適合兩類(lèi)人一是計(jì)算機(jī)相關(guān)專(zhuān)業(yè)正在找畢設(shè)項(xiàng)目的同學(xué)二是想拿一個(gè)「圖像處理 后臺(tái)管理」綜合案例練手的開(kāi)發(fā)者。難度適中但前提是你得把環(huán)境依賴(lài)和識(shí)別參數(shù)調(diào)明白否則很容易卡在「圖能讀、牌認(rèn)不出」這一步。2. 拆開(kāi)源碼看結(jié)構(gòu)OpenCV 車(chē)牌識(shí)別與停車(chē)場(chǎng)收費(fèi)系統(tǒng)是怎么拼起來(lái)的2.1 從類(lèi)名反推模塊劃分識(shí)別、計(jì)費(fèi)、管理三條線(xiàn)拿到一份源碼我習(xí)慣先看類(lèi)名和目錄而不是急著跑。這套項(xiàng)目里出現(xiàn)的類(lèi)大致能分成三組carPlateRecognition負(fù)責(zé)圖像處理與車(chē)牌識(shí)別Parking、Record負(fù)責(zé)車(chē)輛進(jìn)出場(chǎng)記錄和停車(chē)時(shí)長(zhǎng)UserController、ManageController、ManageCashController以及對(duì)應(yīng)的ServiceImpl、Mapper負(fù)責(zé)用戶(hù)、后臺(tái)管理和現(xiàn)金收費(fèi)。這種分層說(shuō)明它大概率是「Web 后臺(tái) 圖像識(shí)別模塊」的組合識(shí)別部分可能被包裝成一個(gè)服務(wù)由 Controller 調(diào)用。從技術(shù)選型上看用 OpenCV 做車(chē)牌識(shí)別而不是直接上深度學(xué)習(xí)模型原因很現(xiàn)實(shí)畢設(shè)環(huán)境通常沒(méi)有 GPU裝 PyTorch 或 TensorFlow 動(dòng)輒幾個(gè) G而 OpenCV 輕量、跨平臺(tái)配合 HSV 顏色空間和形態(tài)學(xué)操作就能把藍(lán)色車(chē)牌的區(qū)域框出來(lái)。常見(jiàn)做法是先轉(zhuǎn)灰度、高斯模糊、邊緣檢測(cè)再按顏色閾值篩出藍(lán)色區(qū)域最后用輪廓篩選和長(zhǎng)寬比過(guò)濾出候選車(chē)牌。字符分割則靠垂直投影識(shí)別階段可以用模板匹配也可以接 Tesseract。這套源碼里carPlateRecognition這個(gè)類(lèi)名很直白說(shuō)明識(shí)別邏輯集中在這里改參數(shù)時(shí)優(yōu)先看它。提示如果你拿到的源碼里識(shí)別準(zhǔn)確率不穩(wěn)定先別懷疑算法八成是輸入圖像的分辨率、光照或者車(chē)牌顏色閾值沒(méi)適配你手頭的測(cè)試圖。2.2 環(huán)境搭起來(lái)Python、OpenCV、依賴(lài)庫(kù)的版本對(duì)齊這類(lèi)項(xiàng)目最怕的就是「代碼沒(méi)問(wèn)題環(huán)境跑不起來(lái)」。我一般會(huì)先確認(rèn) Python 版本3.8 是比較穩(wěn)的選擇很多老教程和庫(kù)都兼容。OpenCV 用opencv-python就夠不需要編譯源碼除非你要用某些 contrib 模塊。安裝命令如下# 建議先建虛擬環(huán)境避免污染系統(tǒng) Python python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 安裝核心依賴(lài)版本不要盲目追新 pip install opencv-python4.5.5.64 pip install numpy1.21.6 pip install pytesseract0.3.10 pip install flask2.0.3 pip install pymysql1.0.2這里每個(gè)包都有原因opencv-python提供圖像處理函數(shù)numpy是 OpenCV 的底層數(shù)組依賴(lài)版本太新有時(shí)會(huì)報(bào)np.bool之類(lèi)的兼容錯(cuò)誤pytesseract是 Tesseract 的 Python 封裝用來(lái)做字符識(shí)別flask可能是后臺(tái) Web 框架pymysql用于連接 MySQL 數(shù)據(jù)庫(kù)。裝完后用下面這段代碼驗(yàn)證 OpenCV 和 Tesseract 是否就緒import cv2 import pytesseract # 打印 OpenCV 版本確認(rèn)不是 headless 版本導(dǎo)致 imshow 報(bào)錯(cuò) print(OpenCV:, cv2.__version__) # 指定 Tesseract 路徑Windows 下通常需要手動(dòng)指定 # pytesseract.pytesseract.tesseract_cmd rC:\Program Files\Tesseract-OCR\tesseract.exe # 簡(jiǎn)單測(cè)試讀一張圖并轉(zhuǎn)灰度 img cv2.imread(test_plate.jpg) if img is None: print(圖片沒(méi)讀到檢查路徑和文件名) else: gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) print(圖像尺寸:, gray.shape)邏輯說(shuō)明先確認(rèn) OpenCV 能正常導(dǎo)入且版本符合預(yù)期再檢查 Tesseract 是否在系統(tǒng)路徑里Windows 下經(jīng)常需要手動(dòng)指定tesseract_cmd最后用一張測(cè)試圖驗(yàn)證讀圖和灰度轉(zhuǎn)換。參數(shù)上cv2.imread的路徑不要帶中文否則在某些 Windows 環(huán)境下會(huì)返回None這是血淚經(jīng)驗(yàn)。2.3 車(chē)牌定位與字符分割參數(shù)怎么調(diào)才不翻車(chē)識(shí)別流程里車(chē)牌定位是第一個(gè)坎。常見(jiàn)做法是把圖像轉(zhuǎn)到 HSV 空間用藍(lán)色范圍做掩膜。下面是一段可抄的定位代碼import cv2 import numpy as np def locate_plate(image_path): img cv2.imread(image_path) if img is None: return None # 轉(zhuǎn) HSV藍(lán)色車(chē)牌的大致范圍 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) lower_blue np.array([100, 80, 80]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 形態(tài)學(xué)操作把車(chē)牌區(qū)域連成一片 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 找輪廓按面積和長(zhǎng)寬比篩選 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / float(h) area w * h # 車(chē)牌長(zhǎng)寬比一般在 2.5 到 5.5 之間面積別太小 if 2.5 ratio 5.5 and area 2000: plate img[y:yh, x:xw] return plate return None邏輯說(shuō)明lower_blue和upper_blue是 HSV 閾值不同攝像頭和光照下要微調(diào)如果識(shí)別不到就把范圍放寬比如下限降到[90, 60, 60]。kernel的尺寸決定形態(tài)學(xué)閉運(yùn)算的強(qiáng)度太大容易把相鄰區(qū)域粘在一起太小則車(chē)牌內(nèi)部斷裂。長(zhǎng)寬比和面積是過(guò)濾非車(chē)牌區(qū)域的關(guān)鍵參數(shù)要根據(jù)你的圖像分辨率調(diào)整1080P 下面積閾值可以設(shè)到 5000 以上。字符分割一般用垂直投影把車(chē)牌灰度圖二值化后統(tǒng)計(jì)每列白色像素?cái)?shù)波谷處就是字符間隙。這一步容易受車(chē)牌邊框和鉚釘干擾常見(jiàn)做法是先裁掉上下各 10% 再投影。3. 把識(shí)別接進(jìn)收費(fèi)業(yè)務(wù)入場(chǎng)、出場(chǎng)、計(jì)費(fèi)與后臺(tái)管理的落地步驟3.1 數(shù)據(jù)庫(kù)表與實(shí)體類(lèi)映射Record、Parking、User 怎么對(duì)應(yīng)這套系統(tǒng)既然有Record、Parking、UserMapper說(shuō)明數(shù)據(jù)持久層用的是 MyBatis 風(fēng)格的映射只不過(guò)換成了 Python 側(cè)的 ORM 或手寫(xiě) SQL。我一般會(huì)先找建表語(yǔ)句或?qū)嶓w類(lèi)定義確認(rèn)字段。典型表結(jié)構(gòu)如下表名關(guān)鍵字段用途userid, username, password, role后臺(tái)管理員和普通用戶(hù)parkingid, plate_number, entry_time, exit_time, status車(chē)輛進(jìn)出場(chǎng)記錄recordid, plate_number, fee, pay_time, cashier收費(fèi)流水cash_manageid, operator, amount, type現(xiàn)金收支管理如果源碼里沒(méi)有.sql文件就去Mapper對(duì)應(yīng)的 XML 或 Python 文件里找 SQL 語(yǔ)句反推表結(jié)構(gòu)。Parking類(lèi)通常對(duì)應(yīng)parking表status字段用來(lái)標(biāo)記車(chē)輛是「在場(chǎng)」還是「已出場(chǎng)」。入場(chǎng)時(shí)插入一條記錄出場(chǎng)時(shí)更新exit_time并計(jì)算費(fèi)用。計(jì)費(fèi)規(guī)則常見(jiàn)的是「首小時(shí) X 元之后每小時(shí) Y 元不足一小時(shí)按一小時(shí)算」這段邏輯一般在Parking或Record的服務(wù)類(lèi)里。注意如果數(shù)據(jù)庫(kù)連接報(bào)Access denied先檢查pymysql.connect里的用戶(hù)名、密碼、庫(kù)名再確認(rèn) MySQL 是否允許本地連接。別一上來(lái)就改代碼。3.2 入場(chǎng)識(shí)別到計(jì)費(fèi)寫(xiě)庫(kù)一條完整調(diào)用鏈把識(shí)別模塊接進(jìn)業(yè)務(wù)核心是「識(shí)別出車(chē)牌后拿車(chē)牌號(hào)去查是否已有在場(chǎng)記錄」。如果沒(méi)有就新建入場(chǎng)記錄如果有就說(shuō)明是出場(chǎng)計(jì)算費(fèi)用并寫(xiě)收費(fèi)流水。下面是一段模擬調(diào)用鏈的代碼import datetime from carPlateRecognition import locate_plate, recognize_text def handle_vehicle(image_path, db): plate_img locate_plate(image_path) if plate_img is None: return {code: 400, msg: 未定位到車(chē)牌} plate_number recognize_text(plate_img) if not plate_number: return {code: 400, msg: 車(chē)牌識(shí)別失敗} cursor db.cursor() # 查詢(xún)?cè)撥?chē)牌是否有未出場(chǎng)的記錄 cursor.execute( SELECT id, entry_time FROM parking WHERE plate_number%s AND statusin, (plate_number,) ) row cursor.fetchone() if row is None: # 入場(chǎng)插入新記錄 now datetime.datetime.now() cursor.execute( INSERT INTO parking (plate_number, entry_time, status) VALUES (%s, %s, in), (plate_number, now) ) db.commit() return {code: 200, msg: 入場(chǎng)成功, plate: plate_number} else: # 出場(chǎng)計(jì)算費(fèi)用 entry_time row[1] now datetime.datetime.now() duration (now - entry_time).total_seconds() / 3600.0 fee calculate_fee(duration) cursor.execute( UPDATE parking SET exit_time%s, statusout WHERE id%s, (now, row[0]) ) cursor.execute( INSERT INTO record (plate_number, fee, pay_time) VALUES (%s, %s, %s), (plate_number, fee, now) ) db.commit() return {code: 200, msg: 出場(chǎng)成功, fee: fee} def calculate_fee(hours): # 首小時(shí) 5 元之后每小時(shí) 3 元不足一小時(shí)按一小時(shí)算 if hours 1: return 5.0 return 5.0 (int(hours - 1) (1 if hours % 1 0 else 0)) * 3.0邏輯說(shuō)明handle_vehicle是入口函數(shù)先定位再識(shí)別識(shí)別失敗直接返回錯(cuò)誤碼。數(shù)據(jù)庫(kù)查詢(xún)用參數(shù)化寫(xiě)法避免 SQL 注入。calculate_fee里的計(jì)費(fèi)規(guī)則可以根據(jù)實(shí)際需求改比如改成「前 30 分鐘免費(fèi)」。參數(shù)上status字段的取值要統(tǒng)一別一會(huì)兒in一會(huì)兒entry否則查詢(xún)會(huì)漏。時(shí)間計(jì)算用total_seconds() / 3600.0得到小時(shí)數(shù)注意時(shí)區(qū)問(wèn)題本地測(cè)試一般沒(méi)問(wèn)題。3.3 后臺(tái)管理接口UserController 與 ManageCashController 的職責(zé)UserController和ManageCashController這類(lèi)命名說(shuō)明系統(tǒng)有后臺(tái)管理頁(yè)面可能用 Flask 或 Django 提供 REST 接口。常見(jiàn)接口包括用戶(hù)登錄、車(chē)輛記錄查詢(xún)、收費(fèi)統(tǒng)計(jì)、現(xiàn)金收支錄入。如果你要二次開(kāi)發(fā)先跑通登錄接口拿到 token 或 session再調(diào)其他接口。下面是一個(gè)用 Flask 暴露識(shí)別接口的示例from flask import Flask, request, jsonify import pymysql app Flask(__name__) def get_db(): return pymysql.connect( hostlocalhost, userroot, passwordyour_password, databaseparking_db, charsetutf8mb4 ) app.route(/api/vehicle, methods[POST]) def vehicle(): file request.files.get(image) if not file: return jsonify({code: 400, msg: 沒(méi)有上傳圖片}) file.save(temp.jpg) db get_db() result handle_vehicle(temp.jpg, db) db.close() return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)邏輯說(shuō)明/api/vehicle接收 multipart 表單上傳的圖片保存到本地臨時(shí)文件后調(diào)用識(shí)別和計(jì)費(fèi)邏輯。debugTrue方便開(kāi)發(fā)時(shí)看報(bào)錯(cuò)但上線(xiàn)要關(guān)掉。數(shù)據(jù)庫(kù)連接每次請(qǐng)求新建簡(jiǎn)單但低效練手夠用。參數(shù)上host0.0.0.0允許局域網(wǎng)訪(fǎng)問(wèn)如果只想本機(jī)測(cè)試改成127.0.0.1。4. 避坑與排查車(chē)牌識(shí)別停車(chē)場(chǎng)系統(tǒng)最容易翻車(chē)的五個(gè)地方4.1 現(xiàn)象ModuleNotFoundError: No module named cv2→ 原因OpenCV 沒(méi)裝或裝到了別的 Python 環(huán)境 → 解決確認(rèn)虛擬環(huán)境激活用pip list看有沒(méi)有opencv-python沒(méi)有就重裝別用conda和pip混裝。4.2 現(xiàn)象圖片讀進(jìn)來(lái)是None→ 原因路徑含中文、文件不存在或格式不支持 → 解決把圖片放到純英文路徑下用cv2.imread前先os.path.exists判斷或者改用cv2.imdecode(np.fromfile(path, dtypenp.uint8), -1)讀中文路徑。4.3 現(xiàn)象車(chē)牌定位框到了車(chē)身或背景 → 原因HSV 藍(lán)色閾值太寬或者形態(tài)學(xué)核太大 → 解決把lower_blue和upper_blue的范圍收窄先用cv2.imshow看掩膜效果再調(diào)kernel尺寸別一次改太多參數(shù)。4.4 現(xiàn)象字符識(shí)別結(jié)果亂碼或只識(shí)別出一半 → 原因字符分割不干凈或者 Tesseract 語(yǔ)言包沒(méi)裝中文 → 解決檢查分割后的字符圖是否完整Tesseract 需要chi_sim語(yǔ)言包安裝時(shí)勾選或者用--psm 7單行模式識(shí)別。4.5 現(xiàn)象出場(chǎng)計(jì)費(fèi)金額不對(duì) → 原因入場(chǎng)時(shí)間存的是字符串計(jì)算時(shí)沒(méi)轉(zhuǎn)datetime或者時(shí)區(qū)差 → 解決數(shù)據(jù)庫(kù)用DATETIME類(lèi)型Python 里統(tǒng)一用datetime.datetime.now()別混用時(shí)間戳和字符串。5. 進(jìn)階技巧把識(shí)別準(zhǔn)確率再往上提一檔的驗(yàn)證方法如果你已經(jīng)把系統(tǒng)跑通但覺(jué)得識(shí)別率不夠穩(wěn)我一般會(huì)做兩件事一是建立一個(gè)小測(cè)試集二是給識(shí)別結(jié)果加置信度過(guò)濾。測(cè)試集不用大20 到 50 張不同光照、不同角度的車(chē)牌圖就夠每張手動(dòng)標(biāo)好真實(shí)車(chē)牌號(hào)然后寫(xiě)個(gè)腳本批量跑統(tǒng)計(jì)準(zhǔn)確率。下面這個(gè)腳本可以抄import os import cv2 from carPlateRecognition import locate_plate, recognize_text def evaluate(test_dir): total 0 correct 0 for fname in os.listdir(test_dir): if not fname.lower().endswith((.jpg, .png)): continue # 文件名格式真實(shí)車(chē)牌_序號(hào).jpg例如 京A12345_01.jpg true_plate fname.split(_)[0] img_path os.path.join(test_dir, fname) plate_img locate_plate(img_path) if plate_img is None: total 1 continue pred recognize_text(plate_img) total 1 if pred and pred.replace( , ) true_plate: correct 1 else: print(f識(shí)別錯(cuò)誤: {fname} - 預(yù)測(cè) {pred}) print(f總數(shù) {total}正確 {correct}準(zhǔn)確率 {correct / total:.2%}) if __name__ __main__: evaluate(test_plates)邏輯說(shuō)明文件名里帶真實(shí)車(chē)牌方便自動(dòng)比對(duì)。locate_plate返回None說(shuō)明定位失敗也計(jì)入總數(shù)但不計(jì)正確。跑完后看錯(cuò)誤樣本如果是定位失敗就回去調(diào) HSV 閾值如果是字符識(shí)別錯(cuò)就換 Tesseract 的--psm參數(shù)或者自己訓(xùn)練模板。參數(shù)上測(cè)試集要覆蓋白天、夜晚、順光、逆光別只用一張圖反復(fù)調(diào)那樣調(diào)出來(lái)的參數(shù)換張圖就廢了。另一個(gè)技巧是給識(shí)別結(jié)果加長(zhǎng)度和字符集校驗(yàn)。中國(guó)車(chē)牌一般是 7 位第一位是漢字后面是字母數(shù)字。如果識(shí)別結(jié)果長(zhǎng)度不對(duì)或者出現(xiàn)非法字符直接判為失敗讓系統(tǒng)走人工輸入兜底。這個(gè)邏輯加在recognize_text后面就行幾行代碼的事但能擋掉不少玄學(xué)錯(cuò)誤。從那以后我每次拿到這類(lèi)圖像識(shí)別項(xiàng)目都先跑一遍小測(cè)試集再談優(yōu)化不然改了半天參數(shù)可能只是過(guò)擬合了一張圖。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取