毕业设计实战:基于OpenCV的车牌识别系统从原型到部署
1. 背景痛点:为什么“跑不通”的总是我
做车牌识别最容易踩的坑,90% 集中在以下三点:
- 光照敏感:手机随手拍一张,正午逆光、地库昏黄、夜间强闪光,灰度图一出来全是噪点,边缘检测直接“失踪”。
- 角度多样:毕业答辩现场老师随手举图,侧拍 30°、俯视 15°,车牌菱形变形,字符“挤”在一起,投影法直接裂开。
- 字符粘连:蓝牌白字在低分辨率下边界模糊,二值化后“8”和“B”粘成一块,Tesseract 只能认出一个“日”。
把以上问题全丢给深度学习当然能缓解,但 GPU 机房排队 3 天、显存 8 G 起步,对“两周交稿”的毕设节奏并不友好。于是我把目标拆成一句话:“用笔记本 CPU 也能跑,且 80% 场景一次通过。”
2. 技术选型:为什么不是 YOLOv8
在毕设场景里,“能跑”>“SOTA”。我对比过三条路线:
| 方案 | 训练成本 | 推理硬件 | 离线准确率 | 备注 |
|---|---|---|---|---|
| YOLOv8+CRNN | 6 h+ | ≥4 G 显存 | 95%+ | 标注 2 k 张起步,环境配置劝退 |
| PaddleOCR PP-OCRv3 | 0 | CPU 2 G | 90%+ | 中文模型 8 M,但 DLL 依赖多,打包发老师常缺库 |
| OpenCV + Tesseract | 0 | CPU 1 G | 82% | 零训练,单文件 200 行,pip 直接装 |
结论很现实:后两种在笔记本都能实时,但 OpenCV 版依赖最少,答辩电脑没网也能 pip install -r requirements.txt 一次过,于是敲定“传统图像 + 轻量 OCR”路线。
3. 核心实现:四步流水线
整个工程只保留 4 个模块,每个模块单独跑通,再串起来,方便老师提问时随时注释掉任一段演示。
3.1 车牌粗定位:边缘 + 形态学
思路:车牌矩形长宽比固定≈3:1,用“闭运算”粘成连通块后直接找外接矩形。
- 高斯模糊 5×5 去噪
- Sobel 横向梯度 → 二值化(Otsu)
- 水平方向闭运算 (30×1),把断续边缘粘成“一条”
- 找外轮廓,按长宽比、面积双重过滤
代码节选(带注释):
def locate_plate_roi(img): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (5, 5), 0) sobel = cv2.Sobel(blur, cv2.CV_16S, 1, 0) # 横向梯度 sobel = cv2.convertScaleAbs(sobel) _, binary = cv2.threshold(sobel, 0, 255, cv2.THRESH_OTSU) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 1)) morph = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) if 2.5 < w/h < 4 and w*h > 1000: # 长宽比+面积双重门限 return gray[y:y+h, x:x+w] return None3.2 倾斜校正:霍夫变换 + 透视变换
拍歪 15° 以内,Tesseract 还能忍;超过 20° 字符高度被压扁,识别率跳水。用“霍夫直线”求平均角度,再透视拉正:
- 二值化后 Canny 边缘
- 霍夫直线检测,累加器阈值 120
- 对所有直线角度做直方图,取峰值 ±5°
- 计算旋转矩阵,双线性插值旋转
- 如果透视变形严重,再找 4 个角点做透视变换(代码里留好开关,默认关闭,节省耗时)
3.3 字符分割:垂直投影 + 动态门限
旋转后的车牌上下留白多,先上下裁边(按行像素和 < 10% 均值切),再做垂直投影:
- 对每列统计白像素数,得到一维数组
- 找波谷,波谷宽度 > 2 px 且深度 < 30% 峰值视为切分点
- 对切分过窄的块,若相邻两块宽度均 < 20 px,则合并(解决“8”被劈成上下两个圈”)
经验:二值化前加 1 px 边框,可防止最左最右字符贴边导致投影谷值消失。
3.4 字符识别:Tesseract 白名单 + 单字裁剪
Tesseract 在 Linux/Win 下的版本差异大,统一做法:
- 训练数据只保留
eng+osd,省体积 - 设置白名单
0123456789ABCDEFGHJKLMNPQRSTUVXYZ(不含 I、O 易混淆) - 每个字符图缩放到 48×24,白边 4 px,再喂给 Tesseract,识别模式选
--psm 10(单字符)
单字符循环识别,最后拼成字符串,与车牌颜色规则(蓝牌首位汉字、新能源固定格式)做后校验,可再干掉 5% 误识。
4. 完整可读代码:Clean Code 示范
项目结构:
plate_recognition/ ├── main.py ├── plate_locate.py ├── perspective_correct.py ├── char_split.py ├── ocr_recognize.py └── requirements.txtmain.py 主循环 60 行,核心函数全部 <= 20 行,方便老师一眼看懂。所有路径用pathlib.Path,中文目录自动兼容;opencv-python headless 版减少打包体积。文末 GitHub 链接已放完整源码,这里只贴主循环:
from pathlib import Path import cv2 from plate_locate import locate_plate_roi from perspective_correct import correct_tilt from char_split import split_chars from ocr_recognize import recognize_str img_dir = Path("images") for img_path in img_dir.glob("*.*"): img = cv2.imread(str(img_path)) roi = locate_plate_roi(img) if roi is None: print(f"{img_path.name}: plate not found") continue roi = correct_tilt(roi) char_list = split_chars(roi) plate_str = recognize_str(char_list) print(f"{img_path.name} -> {plate_str}")5. 性能与鲁棒性:在 4 年老笔记本上跑分
测试集:自己拍 320 张,含晴天、地库、夜间、雨天 4 类场景,角度从 −30° 到 +30°。
| 场景 | 样本数 | 定位成功率 | 字符识别准确率 | 平均耗时 |
|---|---|---|---|---|
| 晴天正面 | 80 | 100% | 96% | 120 ms |
| 地库弱光 | 80 | 93% | 85% | 135 ms |
| 夜间闪光 | 80 | 89% | 78% | 140 ms |
| 雨天反光 | 80 | 91% | 80% | 130 ms |
内存占用:Python 单进程峰值 380 M;冷启动(含 Tesseract 加载)1.2 s,后续单图保持 120 ms 左右。对毕设“点按钮拍照识别”场景足够。
6. 生产环境避坑 10 条
- 路径硬编码:Windows 打包后
cv2.imread("images\test.jpg")会转义,全部改用Path()。 - 中文路径:Tesseract 在 C++ 层默认 ANSI,Python 端用
subprocess.run时加encoding="utf-8"。 - OpenCV 版本差异:
cv2.findContours3.4 返回 2 值,4.x 返回 3 值,统一拆包_占位。 - 形态学核大小:1366×768 摄像头与 4K 图,核要按宽度比例缩放,否则闭运算过度。
- 头文件缺失:pip 安装
opencv-python-headless可减 40 M,但服务器无 GUI,调试时本地换opencv-python。 - Tesseract 训练数据:GitHub 下载
eng.traineddata放tessdata,防止客户机缺失。 - 拍照分辨率:>1080p 时先缩 < 1200 px 宽,再处理,能降 30% 耗时。
- 日志中文:日志输出
print在 Win 控制台缺编码会卡,统一logging模块 + UTF-8。 - 打包 exe:PyInstaller 加
--add-data="tessdata;tessdata",否则运行时报找不到 traineddata。 - 评估脚本:写
evaluate.py批量跑图,输出 Excel,老师最爱看“准确率/召回率”曲线。
7. 后续可玩:替换 OCR 与视频流
整套代码把“字符识别”封装成recognize_str(char_imgs),只要返回同格式字符串,就能无缝替换。你可以:
- 把 Tesseract 换成 PaddleOCR 的
PaddlePPOCRv3,CPU 端提速 30%,中文车牌多一行汉字也能认。 - 加视频流:
cv2.VideoCapture(0)逐帧跑,配合threading把定位与识别放后台,前台实时画框。 - 用 PyQt 做图形界面,打包成单文件 EXE,老师插 U 盘就能演示。
毕业设计不是论文堆砌,而是“能跑、能讲、能改进”。希望这篇流水账能帮你把“车牌识别”从“跑不通”变成“跑得欢”。如果顺利过答辩,别忘了把代码再整理一遍开源,让下一届少掉两根头发。祝验收一次通过!