简介:这是一份基于Python与卷积神经网络的车牌识别仿真软件毕业设计完整项目,面向计算机、人工智能相关专业学生,以及需要快速搭建车牌识别原型的开发者。项目不止包含可直接运行的源码,还配有数据库、演示视频和系统详细设计文档,覆盖登录页面、后台首页、车牌照片上传识别、系统测试等完整环节;测试部分专门介绍了黑盒与白盒方法,便于理解软件质量保障流程,可直接用于毕业设计答辩或作为二次开发基础。压缩包共700个文件,以Python源码(.py)、图片素材(.png/.jpg/.gif)、前端样式和交互脚本(.css/.js)为主,同时包含数据库脚本(.db/.sql)、CNN模型权重(.pth)以及多种格式的说明文档,其中图片素材多达近450张,可支撑界面展示与测试样例搭建,整体约275.72MB,按功能模块组织,查阅便捷。目前已有154人学习下载,适合希望系统参考真实车牌识别项目实现全过程、并快速完成毕设方案部署的学习者。
1. 这个仿真软件到底在做什么:CNN 车牌识别不是一句“能识别”就完事
车牌识别这几年在毕设里出现频率极高,但大多数同学拿到手的“源码”跑起来之后才发现,它离“能演示、能答辩”还差一大截。你手里这个项目标题写得很清楚:基于 Python + 卷积神经网络做车牌识别仿真软件,附带源码、数据库和演示视频。拆开看,它其实是一个完整的闭环——用 CNN 做字符识别只是中间一环,前面有车牌定位和字符分割,后面有识别结果的数据库存储,外面还包了一个能点按钮、能看结果的 GUI 仿真界面。
这个组合恰好踩中了毕业设计的典型需求:算法要有深度学习含量(CNN),系统要有交互界面(仿真软件),数据要有落库逻辑(数据库),交付要有演示素材(演示视频)。但正因为环节多,任何一个环节断了,整个项目就卡住。适合的人群也明确:已经上过机器学习或 Python 课程、想在两周内跑通一个人工智能方向软工系统的本科生,而不是要在学术上做出新模型的科研型读者。
我见过太多人栽在同一个地方:模型在测试集上准确率 99%,一打开软件传一张真实车牌照片就识别错误。问题从来不在卷积神经网络本身,而在从“测试集”到“真实图片”之间的数据口径差异。这篇文章就沿着这个标题的技术链路拆开讲,从 CNN 选型、数据集构造、字符分割到数据库和 GUI 串起来,每一步给可执行的代码和参数,并把你大概率会踩的坑提前标出来。
2. 技术选型与原理:为什么用 CNN 而不是传统图像识别
2.1 车牌识别完整链路:定位、分割、字符识别三层各管什么
一个完整的车牌识别系统,在软件层面通常拆成三步:车牌定位、字符分割、字符识别。很多人误以为“车牌识别”只是一个 CNN 模型的事,实际上 CNN 在绝大多数落地项目里只负责最后一步——把分割好的单个字符图像识别成对应的汉字、字母或数字。定位和分割这两步,反而更依赖传统图像处理手段。
先说车牌定位。常见做法是用 OpenCV 的颜色过滤加形态学操作:先把 BGR 图像转到 HSV 色彩空间,筛选出蓝色或绿色的像素区域(对应蓝牌和新能源绿牌),再用开闭运算把零散像素连成块,最后用轮廓检测得到候选矩形框,按宽高比过滤。这个方案的好处是零依赖、速度快,缺点是夜间色偏严重时容易漏检或误检。另一种做法是直接训练一个 YOLO 检测模型来定位车牌,精度更高,但你需要标注几百张真实车牌图片,还要解决检测模型本身的环境配置问题。对一个仿真软件来说,HSV 方案是第一版的首选,稳定又不需要额外标注。
字符分割则在拿到车牌矩形后做。标准蓝牌的字符排列是固定的:第一个字是省份简称(汉字),第二个是发牌机关字母,后面是 5 位字母数字混合。分割的核心是把灰度图二值化,然后做垂直投影——统计每一列像素的黑色像素数量,字符之间的投影会出现明显的低谷,按这个低谷切分即可。难点在于铆钉、边框、车牌底色不均匀都会在投影里产生干扰,所以分割前还需要做去边框和去铆钉的预处理。
字符识别才轮到卷积神经网络上场。你要把分割后的单个字符归一到统一尺寸(常见 32×32),送入一个对字符类别做分类的 CNN。输出层神经元数等于字符类别总数:省份汉字约 31 类,字母数字合计 34 类(字母去掉 I 和 O,数字 0-9),总计 65 类。这个分类问题对 CNN 来说是轻量任务,用三层卷积加两层全连接就能达到 98% 以上精度,完全不需要上 ResNet 这种重模型——重模型在这个场景下训练慢、体积大,识别率提升却微乎其微,属于典型的杀鸡用牛刀。
2.2 用 PyTorch 搭一个轻量 CNN 字符分类器:网络结构与参数说明
PyTorch 是这里最顺手的框架,比 TensorFlow 轻,而且 Windows 下安装不需要额外折腾。网络结构不用复杂,我一般用三层卷积。输入是 3 通道 32×32 图像(也可以直接读灰度单通道,但三通道能保留车牌底色信息,对识别某些颜色敏感字符有帮助)。
import torch.nn as nn class PlateCharCNN(nn.Module): def __init__(self, num_classes=65): super().__init__() self.features = nn.Sequential( # 第 1 层:3通道32x32 -> 32通道16x16 nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 第 2 层:32通道16x16 -> 64通道8x8 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 第 3 层:64通道8x8 -> 128通道4x4 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))这里几个参数值得说明。卷积核用 3×3 + padding=1,是因为输入只有 32×32,大卷积核会过早压缩空间信息。BatchNorm2d 一定要加,它能让训练收敛稳定很多,尤其是当你的训练集是用脚本合成、分布不完全均匀的时候;不加的话后期 loss 容易出现小幅震荡。Dropout 放在全连接层之间,取值 0.3 而不是常见的 0.5,因为分类任务本身不复杂,Dropout 过大反而欠拟合。最后一个 Linear 的 num_classes 按你数据集的类别数传,如果只做字母数字识别(不识别省份),这个数就是 34;如果省份汉字也要识别,就是 31 + 34 = 65。
训练环节的优化器我推荐 Adam,学习率 0.001 起步,每 20 个 epoch 乘 0.1 衰减。损失函数用 CrossEntropyLoss,适合多分类。训练 40 到 60 个 epoch 就能收敛,单张 1060 级别的显卡几分钟跑完;没有 GPU 用 CPU 也只需要十几分钟,因为合成数据集单张字符图很干净,模型很容易学。
import torch from torch.utils.data import DataLoader from torchvision import transforms transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) dataset = PlateCharDataset(root="data/synth_chars", transform=transform) loader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=0) model = PlateCharCNN(num_classes=65) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss()注意 DataLoader 里 num_workers 在 Windows 下必须设 0,否则多进程数据加载会报 RuntimeError。Resize 到 32×32 后不要做随机旋转,车牌字符一旦旋转超过 5 度,后续识别率下降非常明显。Normalize 的均值和方差取 0.5 是因为图像归一化到 [-1,1] 区间会让 CNN 更容易训练,这与常见 ImageNet 的归一化参数不同,但在此场景效果更好。
2.3 识别结果为什么要落数据库:演示与扩展的刚需
标题里带“数据库”三个字,说明这个项目需要展示的不只是“识别出车牌号”,还要管理识别记录。常见的需求是:每识别一张图,把车牌号、识别时间、置信度、图片路径存下来,窗口里能按时间查历史记录。这个需求用 SQLite 就够了——它零配置、单文件存储、Python 标准库自带驱动,交作业时直接把 db 文件一起打包即可。
不推荐为这个项目引入 MySQL。原因有三条:一是很多同学电脑上根本没装 MySQL 服务,评委现场演示时环境起不来会非常尴尬;二是 MySQL 需要账号密码配置,多一层变量就多一个踩坑点;三是毕设验收通常只关心“数据持久化有没有做”,SQLite 和 MySQL 在这一层没有本质差别。只有当导师明确要求“使用 MySQL 数据库”时才迁移,迁移也就是改一下连接串和驱动库的事。
3. 从零跑通最小可用的车牌识别:数据、训练与 GUI 串起来
3.1 合成车牌数据集:没有真实图片时的可靠替代方案
训练字符模型需要大量字符图片,而真实车牌数据很难拿到。一个可靠的替代方案是用脚本合成:把文字渲染到模拟车牌背景上,再做过多种类扰动。这比从网上下载零散的真实图片更可控——类别均衡、数量任意、标注也不用手工做。
合成字符集的思路是这样的:预先准备三块材料——字符字体、背景模板、扰动参数。字体用 Windows 自带的 simhei.ttf(黑体),它会以 simhei.ttf 的形式存放在系统字体目录;背景模板按蓝牌底色的 HSV 范围生成纯色块,再叠加轻微的高斯噪声和明暗渐变;扰动参数包括随机平移、缩放、对比度调节和 2~3 度的轻微旋转。每个字符以 50 到 200 个样本为目标,总共生成 13000 到 19000 张字符图,按类别分目录存放。
python scripts/gen_chars.py \ --font C:/Windows/Fonts/simhei.ttf \ --out data/synth_chars \ --chars "京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新ABCDEFGHJKLMNPQRSTUVWXYZ0123456789" \ --per_char 200 \ --size 64生成脚本内部做的事是:先用 PIL 画字符到 64×64 透明画布,再贴到底色背景上,最后用 OpenCV 做高斯模糊和亮度扰动。size 设 64 而训练时 Resize 到 32,是为了让模型在缩小时见过多种抗锯齿情况,防止过拟合到单一渲染效果。运行结束后检查一下 data/synth_chars 下的子目录数量,应该和传入的字符集数量对上,少了就是字符集里有多余空格或字体不支持某些字符。
3.2 字符分割的简单可靠做法:垂直投影切割的正确姿势
整个系统里最容易翻车的就是字符分割。模型训练得再好,字符框切偏了,识别就是错的。标准做法是垂直投影切割,先看效果,再谈参数。
import cv2 import numpy as np def segment_chars(plate_img): # 输入是已经定位并矫正后的车牌彩色图 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 二值化:车牌字符是白色/黑色,背景是蓝色/绿色 _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 去边框:去掉上下左右各2像素的边框干扰 binary = binary[2:-2, 2:-2] # 垂直投影:统计每列白色像素数 col_sum = np.sum(binary == 255, axis=0) cols = np.where(col_sum > 0)[0] # 按列连续性切出字符区间 char_cols = [] start = cols[0] end = cols[0] for c in cols[1:]: if c - end <= 3: end = c else: char_cols.append((start, end)) start = c end = c char_cols.append((start, end)) # 过滤掉过宽或过窄的区间,宽度阈值按车牌长度自适应 plate_w = plate_img.shape[1] chars = [] for left, right in char_cols: w = right - left if w < plate_w * 0.05: # 太窄:多半是铆钉 continue if w > plate_w * 0.5: # 太宽:多半是背景干扰 continue chars.append((left, right)) return chars这个函数里有两个参数值得注意。第一个是c - end <= 3的连通阈值,它决定两列白色像素之间空 3 列以上才算断开的字符。阈值太小会把汉字“京”的笔画拆成两半;阈值太大又容易把相邻字符粘成一个。建议用 2 到 3,需要为不同字体微调。第二个是宽度过滤的 0.05 倍,车牌宽度的 5%,主要筛掉铆钉投影。实际车牌铆钉恰好位于字符间隙附近,垂直投影时铆钉往往和字符列粘连,所以定位阶段要去铆钉,不要完全依赖后置过滤。
3.3 把模型包装成可交互的仿真软件:PyQt5 界面的最小骨架
仿真软件的定位是“像产品一样能演示”,所以界面至少要有:图片选择区、识别结果显示、置信度显示、历史记录表格。PyQt5 是常见选择,它比 Tkinter 好看,且打包 exe 的工具链成熟。
import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QFileDialog, QTableWidget class PlateApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("车牌识别仿真系统") self.setGeometry(200, 200, 900, 600) self.btn = QPushButton("选择图片", self) self.btn.setGeometry(20, 20, 100, 40) self.btn.clicked.connect(self.open_image) self.result_label = QLabel("识别结果:", self) self.result_label.setGeometry(150, 20, 600, 40) self.table = QTableWidget(self) self.table.setGeometry(20, 80, 860, 480) def open_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.jpg *.png *.bmp)") if not path: return self.run_recognize(path) def run_recognize(self, path): plate = self.detect_and_recognize(path) self.result_label.setText(f"识别结果:{plate}") self.insert_record(plate)这个骨架里建议把detect_and_recognize拆成三个方法:locate_plate(定位)、segment_chars(切字符)、recognize_chars(模型推理)。这样答辩时老师问“你的识别流程是什么”,你能清晰说出来流程而不是指着代码说“反正就是调了个模型”。insert_record里写数据库插入逻辑,让界面操作和持久化联动。
4. 把数据库接进仿真软件:识别记录、车牌登记与统计
4.1 表结构设计:识别记录表与车牌信息表怎么分
数据库在这个项目里不是装饰,它承担两个功能:一是记录每次识别的过程数据(识别了哪辆车、什么时间、置信度多少),二是维护一个车牌信息台账(这个车牌号属于谁、车型是什么)。这两类数据放一张表里会显得冗余,拆成两张更合理,也方便答辩时讲“我设计了关系型结构”。
CREATE TABLE IF NOT EXISTS plate_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, plate_no TEXT NOT NULL, plate_color TEXT DEFAULT 'blue', confidence REAL NOT NULL, image_path TEXT, recognize_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS vehicle_info ( plate_no TEXT PRIMARY KEY, owner_name TEXT, phone TEXT, vehicle_type TEXT DEFAULT 'car', register_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );plate_records 每识别一次插入一条,是流水表。vehicle_info 以 plate_no 为主键,是台账表。两个表通过 plate_no 关联。这里有个设计细节:车牌号要不要作为外键?我建议不加 FOREIGN KEY 约束,因为识别系统完全可能识别出不在台账里的车牌,流水记录仍然要保留。加了外键反而会导致插入失败。关联查询在需要显示车主姓名时再做 JOIN 即可,这样也避免在识别主流程里频繁查台账。
4.2 在 GUI 里做增删改查:Python 标准库 sqlite3 的完整用法
往数据库写数据不引入 ORM,直接用 sqlite3 标准库。原因很简单:ORM 需要额外安装并配置,标准库零依赖、报错信息直观,作为毕设足够。
import sqlite3 DB_PATH = "plate_system.db" def init_db(): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.executescript(""" CREATE TABLE IF NOT EXISTS plate_records (...); CREATE TABLE IF NOT EXISTS vehicle_info (...); """) conn.commit() conn.close() def insert_record(plate_no, confidence, image_path): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( "INSERT INTO plate_records (plate_no, confidence, image_path) VALUES (?, ?, ?)", (plate_no, confidence, image_path) ) conn.commit() conn.close() def query_records_by_date(start_date, end_date): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( "SELECT plate_no, confidence, recognize_time FROM plate_records " "WHERE date(recognize_time) BETWEEN ? AND ? ORDER BY recognize_time DESC", (start_date, end_date) ) rows = cursor.fetchall() conn.close() return rows这里有一个很多人没注意的细节,叫“连接要短开”。每次操作都打开连接、用完关闭,而不是在程序启动时建一个长连接。原因在于 sqlite3 不支持多线程共享同一个连接,你的 GUI 在识别线程里写库、在主线程里刷新表格,两边用同一个连接大概率会报 ProgrammingError。短开短用虽然每次多几十毫秒开销,但稳定排第一。
日期查询的 BETWEEN 写法要对 recognize_time 做 date() 转换,否则时间格式不匹配会查不到数据。且传入的日期字符串必须是 YYYY-MM-DD 格式,这是 SQLite 默认识别的格式。
4.3 界面表格刷新与数据库联动的常见做法
GUI 里点一次识别,不仅要在标签上显示结果,还要让表格自动刷新。实现上就是在插入数据库后重新执行一次查询,再把查询结果填进 QTableWidget。这里需要注意:不要在 insert 之后立即在同一事务里查询,有些 SQLite 版本在事务未提交时查询不到自身写入的数据。正确顺序是 insert → commit → 重新查询。
表格刷新时设置表格行数为查询结果数,列数固定为“车牌号 / 时间 / 置信度 / 图片路径”四列。置信度建议显示为百分比字符串,例如 f"{confidence*100:.1f}%"。给表格设置整行选择模式,方便后续做“删除选中记录”功能时取当前行号映射到数据库记录 ID。
5. 常见问题与避坑:从训练到 GUI 的高频翻车点
5.1 模型训练集准确率高但 GUI 识别率低:训练与推理数据口径不一致
现象:训练时模型在验证集上准确率 99%,但放到 GUI 里识别真实车牌,十几个字错四五个。
原因:合成字符图片和真实图片的二值化、归一化方式不一样。合成时字符是干净的黑色笔画,真实车牌经过摄像头拍摄后字符边缘有锯齿、背景有污渍,模型没见过这种分布。
解决:在训练集上做更狠的增强——加高斯噪声、模糊、随机亮度扰动和对比度抖动。更有效的办法是让推理代码复用训练时的同一套预处理 pipeline。很多人的代码是训练时用 PyTorch 的 transforms,GUI 推理时用 OpenCV 手写了一套,两边 Resize 都 32×32,但 Normalize 的参数不一致,导致输入分布完全不同。建议把预处理封装成一个函数,训练和推理共用。
5.2 省份汉字总是识别错:字符类别混淆是有规律的
现象:模型把“湘”识别成“苏”、“沪”识别成“浙”,且错误相对固定。
原因:省份汉字笔画相近,在低分辨率 32×32 下区分度很低。尤其“湘”和“苏”在草书字体下轮廓非常接近。这个不是模型 level 的问题,是数据问题。
解决:三种策略叠加。第一,合成数据时不要所有省份汉字都按同一字体渲染,可以给每个省份换 2~3 种不同字体,增加笔画的多样性。第二,训练时将字符图 RandomResizedCrop 裁剪 80% 到 95% 后再 Resize 回 32×32,强制模型学笔画骨架而不是整体轮廓。第三,如果试了仍然不行,就对高频错分对单独做数据扩增,把“湘苏”对多加样本,模型会学到更细的差异。
5.3 绿色新能源车牌识别率比蓝牌低很多:字符颜色极性不同
现象:蓝牌识别成功率在 95% 左右,绿牌直接下降到 70%,甚至经常定位不到。
原因:蓝牌字符是白色,绿牌字符是黑色,二值化时 Otsu 自动找阈值没问题,但车牌定位阶段用的是蓝色 HSV 范围过滤,绿色车牌在蓝色阈值里也选不到几个像素。这是定位系统性的漏检。
解决:HSV 颜色过滤阶段不要只筛蓝色,把绿色的范围也加上。具体是 H 通道的 35~80 区间覆盖蓝绿两色。分割阶段注意绿牌的字符是深色、背景是浅色,二值化时极性不同需要反转——统一在二值化后判断字符区域白色像素比例,如果背景的白色像素占比超过 70%,就做 bitwise_not 反转。这一步一定要加,它决定绿牌字符能不能被正确投影分割。
5.4 字符分割时汉字被切烂:汉字的投影结构不是连续块
现象:像“京”“川”这种汉字在垂直投影中产生多处低谷,一个字符被切成两三段。
原因:汉字含左右结构或内部空隙,垂直投影天然会在笔画间隙处出现断点。直接用阈值切割会把一个汉字拆成多个块。
解决:分割时引入先验信息——车牌有固定 7 个字符(新能源绿牌是 8 位,前 7 位结构同蓝牌,第八位是单独区块)。按先验 7 段切分比按投影切分稳得多。实操中做法是:先做投影粗分割,统计得到的区间数;如果大于 7,就按车牌宽度除以 7 的期望字符宽度重新等分;如果小于 7,说明车牌子图像可能有粘连,此时可以做一次形态学腐蚀把字符间细连接断开,再重投影。这个逻辑在代码里就是先统计 len(char_cols),再和 7 比较做分支。
5.5 打包 exe 后数据库写不进去或程序闪退:路径问题
现象:源码运行时一切正常,用 PyInstaller 打包成 exe 后数据库文件没生成,或者点击识别直接崩溃。
原因:打包后当前工作目录变成 exe 所在目录,而代码里数据库路径用的是相对路径,正常运行入口时没问题,但 exe 被放到其他目录运行时就找不到路径。
解决:数据库路径要用绝对路径,并且基于sys._MEIPASS或os.path.dirname(sys.executable)做动态拼接。常见做法是取 exe 所在目录的绝对路径,把数据库放到这个目录下。另外 PyInstaller 打包时要注意把模型权重文件 .pth 加进打包数据里,否则推理时会报找不到文件。命令行加--add-data "models/plate_char_cnn.pth;models",分号前面是源路径,分号后面是打包后的目标相对路径。
6. 验证与交付的技巧:把项目从“能跑”做到“能答辩”
你辛辛苦苦把整个链路跑通后,还剩最后一步:怎么证明它真的好用。大部分毕设演示的翻车现场,不是系统不能用,而是没法稳定复现。下面给出一个非常实用的验证思路——用分类别统计的脚本替代“随手拿几张图试试”。
先做一个识别率统计脚本,放在项目里,演示前跑一遍生成统计报告。脚本读一个测试文件夹里的车牌图片,对每张图做完整识别,记录“字符级准确率”和“车牌级准确率”两个指标。字符级准确率是每个字符的预测正确率,车牌级准确率是整块车牌完全正确的比例。对评委来说,这两个数字是最有说服力的验收依据。
import os from collections import defaultdict def evaluate(test_dir): char_total, char_correct = 0, 0 plate_total, plate_correct = 0, 0 per_char_err = defaultdict(int) for filename in os.listdir(test_dir): label = filename.split("_")[0] # 文件名格式:京A12345_1.jpg pred = recognize_full_plate(os.path.join(test_dir, filename)) plate_total += 1 plate_correct += (pred == label) for c_label, c_pred in zip(label, pred): char_total += 1 if c_label == c_pred: char_correct += 1 else: per_char_err[c_label] += 1 print(f"字符级准确率: {char_correct / char_total:.2%}") print(f"车牌级准确率: {plate_correct / plate_total:.2%}") print("高频错分字符 TOP5:") for ch, cnt in sorted(per_char_err.items(), key=lambda x: -x[1])[:5]: print(f" {ch}: {cnt} 次")跑完这个脚本你会发现,即使字符级准确率有 98%,车牌级准确率可能只有 80% 上下。这不是 bug,而是 7 个字符必须全对的概率天然低于单个字符准确率。答辩时主动把这个数字讲出来,反而显得你理解系统局限——评委最反感的是“我这个系统准确率 100%”。
验证通过后,最后一件事是把系统从“训练依赖”里抽离。推理时直接加载训练好的 .pth 权重即可,不要每次启动 GUI 都重新训练。把模型路径、数据库路径、阈值参数全部提到配置文件,让代码看起来是一个可配置的“仿真软件”,而不是一坨训练脚本。导出 ONNX 格式也可以加分,但增加复杂度,除非导师问起,否则留着当口头扩展点就好。
我在做这类项目时养成的习惯是:先把分割结果可视化出来看一下,再做模型训练。分割是玄学,二值化阈值一调,之前写好的切割逻辑可能全部作废。宁可花一小时把分割调稳,也不要急着把训练跑完再回头查错误从哪来。带一个离线可视化脚本,把每张测试图的定位框、切割线、字符块保存成标注图,翻车时对着图排查,比对着控制台日志猜快得多。希望这篇能帮你在车牌识别这条路上少踩几个坑。
本文还有配套的精品资源,点击获取