资讯动态

基于OpenCV与深度学习的车牌识别系统开发实战

发布时间:2026/10/9 21:33:45 来源:尧图企业网站定制
简介这是一份基于Python与OpenCV并结合深度学习模型完成的车牌识别毕业设计完整资料包适合正在准备毕业设计、课程设计或期末大作业的本科学生也适合想通过实际项目入门图像处理与CNN应用的新手。压缩包共18个文件含5个Python源码、8张车辆与车牌图片样本、2个dat模型数据、1份演示PPT、1份说明文档及配置文件整体大小约4.73MB结构清晰便于按模块阅读和部署。内容覆盖车牌图像预处理、车牌区域定位、字符分割与深度学习识别等关键流程代码带有详细注释并有文档对实现思路逐段解释学习者可按文档完成环境搭建与系统运行。目前已有86人学习浏览作为曾在答辩中获得高分且受导师认可的项目它能帮助理解OpenCV在图像识别中的用法并可作为扩展其他识别任务的参考实现。1. 车牌识别毕业设计到底在做什么OpenCV负责「看」深度学习负责「认」如果你接到的毕业设计题目是「基于Python和OpenCV的深度学习车牌识别系统」第一反应往往是Python 和 OpenCV 都熟深度学习也学过一点但这三样怎么拼成一个能演示、能写论文、能在答辩时讲清楚的完整系统这个题目背后是一条完整的车牌识别管线OpenCV 负责在复杂背景里把车牌区域找出来深度学习负责把车牌上的字符逐个读出来Python 负责把这两段粘成一套可运行的软件。它的重点不在某个算法的极致精度而在于「系统设计」——每一段都有可展示的技术点有可写进论文的实验数据也有可复现的代码结构。这个方向适合绝大多数选题做车牌识别的同学也适合想快速搭一套可演示系统的从业者参考。要明确一点这不是一个纯深度学习项目也不是一个纯图像处理项目。它故意在传统视觉和深度学习之间做了分工目的是让答辩时你能回答「为什么传统方法还不够」「为什么不能一刀切端到端」。接下来的内容就是按这个系统从架构设计、数据准备、车牌定位、字符识别到调试避坑的完整落地路径展开的。2. 系统架构与数据准备把源码拆成能写论文的模块先把训练数据喂饱2.1 为什么选了「OpenCV定位 深度学习识别」的两段式而不是端到端常见的毕设选题里车牌识别无外乎三条路线纯 OpenCV 模板匹配、端到端检测识别一体、以及本标题采用的「OpenCV 定位 深度学习识别」。纯模板匹配对光照和角度极其敏感拍歪一点就报废论文里没有「自适应」的说服力。端到端方案比如检测识别一体的单阶段网络精度和速度都好但训练需要大量标注数据答辩老师问「你怎么分析失败样本」时黑匣子式的网络很难讲出细节。两段式的好处是每段都有明确的技术点定位段能讲颜色空间、形态学、轮廓分析识别段能讲数据增强、网络结构、迁移学习论文的「创新点」和「系统设计」章节都有了素材。从代码组织上看这套系统也应该按模块拆而不是写一个几百行的脚本从头跑到尾。我一般会把工程分成这几个目录plate_recognition/ ├── config.py # 所有阈值、路径、类别映射集中配置 ├── data/ │ ├── make_synthetic.py # 合成车牌生成器 │ └── labels/ # 每张车牌图的标签 txt ├── modules/ │ ├── locate.py # OpenCV 车牌定位 │ ├── segment.py # 字符分割 │ └── recognize.py # 深度学习字符识别ONNX 加载 ├── train/ │ ├── train_char_cls.py # 字符分类网络训练脚本 │ └── export_onnx.py # 导出到 ONNX ├── main.py # 摄像头/图片/视频统一入口 └── docs/ # 开题报告、论文、答辩 PPT模块之间只通过函数接口通信locate.py返回候选框坐标segment.py接收车牌图返回字符子图recognize.py接收字符子图返回字符串。这样每一段都能单独测试出了错也知道去哪一段排查。毕设答辩时老师最爱问「这个模块你测过没有失败在哪」分模块的代码结构就是给你留的退路。2.2 训练数据从哪来开源数据集、自拍与合成数据的配比建议车牌识别最卡脖子的不是网络结构而是数据。网上能找到的开源车牌数据集大多针对蓝牌新能源绿牌的样本少黄牌更少。真实场景自拍又涉及隐私和采集成本一张张框出来标注足够几百个字符做分类器是可行的但要撑起定位模型的训练量就不现实了。行业里最常见的做法是合成数据打底用代码生成几千张不同字体、不同颜色、不同背景噪声的车牌字符图用来训练字符分类网络。因为字符分类器只关心「切好的字符长什么样」不需要背景所以合成成本极低。下面这个脚本就是我常用的合成数据生成器它解决的是「怎么低成本凑出分类网络需要的几万张字符样本」# data/make_synthetic.py from PIL import Image, ImageDraw, ImageFont import numpy as np import cv2 import random import os PROVINCES 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁 CHARS ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 # 车牌字母不含 I、O def random_plate_text(): 生成 7 位车牌字符串省份 发牌机关代码 5 位编号 p random.choice(PROVINCES) c random.choice(ABCDEFGHJKLMNPQRSTUVWXYZ) n .join([random.choice(CHARS) for _ in range(5)]) return p c n def render_plate(text, size(440, 140)): # 440x140 对应真实车牌 440mm x 140mm 的宽高比字符排布参考国标 img Image.new(RGB, size, (36, 36, 130)) # 蓝底 draw ImageDraw.Draw(img) font ImageFont.truetype(simhei.ttf, 72) # 必须用支持中文的字体 x 12 for ch in text: draw.text((x, 14), ch, fontfont, fill(255, 255, 255)) x 52 return np.array(img) def distort_plate(img): 模拟真实拍摄亮度扰动、噪声、模糊、透视形变 img img.astype(np.float32) img * random.uniform(0.6, 1.3) # 随机亮度 img np.clip(img, 0, 255).astype(np.uint8) if random.random() 0.5: img cv2.GaussianBlur(img, (3, 3), 0) # 随机模糊 rows, cols img.shape[:2] pts1 np.float32([[0, 0], [cols, 0], [0, rows], [cols, rows]]) dx, dy random.uniform(-0.03, 0.03) * cols, random.uniform(-0.03, 0.03) * rows pts2 np.float32([[dx, dy], [cols - dx, dy], [dx, rows - dy], [cols - dx, rows - dy]]) M cv2.getPerspectiveTransform(pts1, pts2) img cv2.warpPerspective(img, M, (cols, rows)) return img def save_sample(save_dir, idx): text random_plate_text() img render_plate(text) img distort_plate(img) # 注意PIL 生成的是 RGBcv2.imwrite 要求 BGR直接写会蓝白反转 img_bgr cv2.cvtColor(img, cv2.COLOR_RGB2BGR) cv2.imwrite(os.path.join(save_dir, f{idx:05d}.jpg), img_bgr) with open(os.path.join(save_dir, f{idx:05d}.txt), w) as f: f.write(text) if __name__ __main__: os.makedirs(data/synthetic_chars, exist_okTrue) for i in range(5000): save_sample(data/synthetic_chars, i)这段代码里有两个参数值得细说。(440, 140)是模拟真实车牌的比例字符横向间距x 52是按国标字符间距换算到 440 宽的近似值生成出来的图片字符分布均匀训练出的分割模型不容易被非均匀字符干扰。透视扰动的dx/dy控制在 ±3% 以内超过这个幅度字符会严重变形分类网络学到的特征就偏了。如果你只训练字符分类器用上面这种「纯车牌图 随机形变」就够了如果还要训练一个端到端检测器需要把车牌随机贴到街景背景上工作量会大一个量级。2.3 标注格式与目录组织别把标签散落在三个地方很多同学做这个题目时标签信息会出现在三个地方数据文件夹里的 txt、训练脚本里写死的类别列表、识别结果映射用的字典。一旦类别顺序改了某处漏改训练和推理的编号就对不上。我的习惯是所有标签字典收敛到config.py这一个文件数据文件夹的 txt 只存字符文本本身不存类别编号。字符转编号的逻辑由config.py统一提供。# config.py import json CHAR_MAP {ch: i for i, ch in enumerate( 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁 ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 )} NUM_CLASSES len(CHAR_MAP) def text_to_labels(text: str) - list[int]: # 把 京A12345 转成 [编号, 编号, ...]字符不在表内直接抛异常 missing [ch for ch in text if ch not in CHAR_MAP] if missing: raise ValueError(f未知字符: {missing}) return [CHAR_MAP[ch] for ch in text] def labels_to_text(labels: list[int]) - str: idx_to_char {v: k for k, v in CHAR_MAP.items()} return .join(idx_to_char.get(i, ?) for i in labels)这样做最大的好处是训练和推理共用一套映射不会出现「训练时把「渝」排在索引 5推理时字典顺序变了导致输出乱码」这种低级错误。字符表里故意去掉了I和O因为车牌标准里就不允许出现这两个字母去掉它们能直接消除一部分混淆类别数量是 31 个省份汉字 24 个字母 10 个数字 65 类。3. 车牌定位OpenCV 图像处理管线的五级筛选与参数调优3.1 从 BGR 到候选框HSV 阈值、形态学闭运算、轮廓过滤一段到位定位是整个系统里最依赖「经验值」的模块也是最容易翻车的模块。它的任务不是理解图像而是用颜色、形状、纹理这些低层特征把「长得像车牌」的区域筛出来。常规思路是这样的先转到 HSV 颜色空间用固定阈值框出蓝色区域再做形态学闭运算把字符间隙补上然后找轮廓最后用宽高比和面积过滤。每一步都有值得调的参数下面这段代码是把这条管线串起来的参考实现# modules/locate.py import cv2 import numpy as np def locate_plate_candidates(img_bgr, cfg): 返回候选框列表 [(x, y, w, h), ...]按面积从大到小排序 # 1. HSV 颜色阈值蓝牌蓝色范围H 在 100~124 区间 hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) lower np.array([cfg[h_lo], cfg[s_lo], cfg[v_lo]]) upper np.array([cfg[h_hi], cfg[s_hi], cfg[v_hi]]) mask cv2.inRange(hsv, lower, upper) # 2. 形态学闭运算横向核把字符之间的缝隙填上避免一个车牌被拆成多块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (cfg[morph_w], cfg[morph_h])) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 3. 找外轮廓并过滤 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h aspect w / h # 车标准确长宽比约 3.14容忍透视形变后放宽到 2.0~4.5 if area cfg[min_area] and cfg[aspect_min] aspect cfg[aspect_max]: boxes.append((x, y, w, h)) boxes.sort(keylambda b: b[2] * b[3], reverseTrue) return boxes对应的配置参数我在config.py里会写成这样不同分辨率下只需改min_areaLOCATE_CFG { h_lo: 100, h_hi: 124, # 蓝色色相区间 s_lo: 60, s_hi: 255, # 饱和度过低会带进灰蓝色背景 v_lo: 60, v_hi: 255, # 亮度过低是夜间反光翻车主因 morph_w: 15, morph_h: 5, # 横向闭运算核15 能覆盖字符间隙 min_area: 6000, # 假设输入 1280x720车牌约占 300x90 aspect_min: 2.0, aspect_max: 4.5, }这里每个参数都可能成为「玄学」现场。s_lo设到 60 以上阴影里的蓝色车牌会被直接滤掉设到 40 以下蓝色广告牌、蓝色车衣都会进候选框。morph_w15是按车牌 7 个字符的排列尺度设计的核太小字符间隙补不上核太大车身蓝色区域会连片粘连成巨型轮廓。我的建议是先把 min_area 和 aspect 范围放宽宁可多带回几个误检也不要漏检后面的识别模块会自动把误检框的置信度打低。3.2 透视矫正与字符区域的精细切分转正是一切后续操作的前提拿到候选框之后不能直接切图送给分割模块因为现实场景里很少有正对着车头的拍摄角度。倾斜超过 10 度的车牌垂直投影会全乱——字符的笔画斜着分布在投影轴上原本应该分开的字符粘连在一起。这也是很多同学「分割模块写得没问题但结果全错」的根源问题不在分割在定位段输出的图是歪的。# modules/locate.py 续 def warp_plate(img_bgr, contour, cfg): # 用最小外接矩形求旋转角度把车牌仿射转正 rect cv2.minAreaRect(contour) angle rect[-1] # 返回角度范围 [-90, 0) if angle -45: angle 90 # 统一转成水平方向 M cv2.getRotationMatrix2D(rect[0], angle, 1.0) rotated cv2.warpAffine(img_bgr, M, (img_bgr.shape[1], img_bgr.shape[0])) x, y, w, h cv2.boundingRect(contour) plate_crop rotated[y:y h, x:x w] # 统一 resize 到 440x140后续分割和识别都基于这个固定尺度 return cv2.resize(plate_crop, (440, 140), interpolationcv2.INTER_CUBIC)angle 90这一步很关键minAreaRect返回的角度在处理垂直方向拉长的物体时容易让人困惑统一加 90 度修正后不管车牌是左倾还是右倾转正逻辑一致。resize 到 440x140 的意义在于分割模块的投影阈值、识别网络的输入尺寸都跟这个分辨率绑定固定尺度能去掉一大半「图像大小不同导致参数失效」的问题。实际使用时如果发现车牌上下边框太粗挤占了字符区域可以在 resize 后按比例裁掉上下沿各 5% 的行。3.3 误检的两个主要来源蓝色背景干扰与轮廓粘连定位模块最常见的两个误检来源是蓝色广告牌和车身蓝色区域。广告牌的蓝色通常是大面积均匀色块没有字符纹理车身蓝色区域往往和车灯、车窗粘连成不规则的形状。应对误检不一定要上深度学习可以先用两个轻量规则做二次筛选边缘密度把车牌区域转灰度后做 Canny 边缘检测计算边缘像素占比。真车牌因为字符笔画密集边缘密度通常落在 8%~25% 之间纯色广告牌边缘密度极低。字符纹理验证对候选区域做第 4 章要讲的垂直投影如果切出来的字符块数在 6~8 之间就认为是候选车牌否则丢弃。这个规则同时还能排除掉「边框和背景形成的伪轮廓」。这两条规则实现成本低而且能写进论文里作为「基于纹理特征的候选框验证方法」。如果加了这两条之后依然有误检那就是颜色阈值放得太后门导致的回头收紧s_lo和h_hi的范围比继续加规则更见效。4. 字符分割与深度学习识别把黑匣子拆成可解释的分类任务4.1 字符分割垂直投影切出 7 个字符还要处理汉字断笔定位和转正之后拿到的是一张 440x140 的车牌图。字符分割的任务是把它切成 7 个单字符子图供分类网络使用。标准的做法是对灰度图做二值化然后沿列方向统计非零像素个数获得垂直投影再根据「有字符的列像素多、字符间隙列像素少」的分布切分。看起来简单实际写起来要处理汉字「川」「湘」这类笔画断裂的问题# modules/segment.py import cv2 import numpy as np def segment_plate(plate_bgr): gray cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) # OTSU 自动找阈值白字车牌二值化后字符为 255 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) col_sum np.sum(binary, axis0) # 垂直投影每列的白色像素数 segments [] # 存 (x_start, x_end) in_block False start 0 for i, v in enumerate(col_sum): if v 0 and not in_block: start, in_block i, True elif v 0 and in_block: segments.append((start, i)) in_block False if in_block: segments.append((start, len(col_sum))) # 修正 1过窄片段通常是汉字断笔或噪点并入相邻段 min_w 14 segments [s for s in segments if s[1] - s[0] min_w] # 修正 2段数 7 说明汉字被拦腰切断把最窄的段合并到邻居 while len(segments) 7: narrow min(range(len(segments)), keylambda i: segments[i][1] - segments[i][0]) # 与左侧合并若已在最左则与右侧合并 if narrow 0: segments[1] (segments[0][0], segments[1][1]) segments.pop(0) else: segments[narrow - 1] (segments[narrow - 1][0], segments[narrow][1]) segments.pop(narrow) return [plate_bgr[:, s[0]:s[1]] for s in segments]min_w14是经验值——440 宽的图分 7 个字符平均每个字符约 55px汉字里最窄的笔画块也不会低于 14px。而「川」这种字的三竖之间会有明显的投影低谷如果阈值设置不当就会被切成三段所以合并逻辑是必需的。反过来如果段数只有 5~6 个说明字符之间粘连了常见原因是二值化后边框和字符连成一片这时候优先检查转正时上下边框是否裁干净而不是改分割代码。分割模块的输出是分类网络的口粮这块做不干净后面的识别网络再强也白搭。4.2 识别网络用轻量 CNN 做字符分类为什么不用 LSTM/CTC切好的字符是一张张独立的单字符图这次任务从「序列识别」退化成了「图像分类」这是两段式结构带来的最大简化。很多网上代码直接上一套 CRNN CTC数据量不够时训练周期长、调参复杂毕业设计没必要冒这个险。单个字符的分类任务足够简单3 层卷积加 2 层全连接就能达到可演示的精度。我用 PyTorch 写的参考网络结构如下# train/train_char_cls.py import torch import torch.nn as nn class PlateCharNet(nn.Module): 轻量字符分类网络输入 1x32x64 灰度图输出 65 类 def __init__(self, num_classes65): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), # 64x32 - 32x16 nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), # 32x16 - 16x8 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d((4, 4)) # 不管输入多大都池化到 4x4 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))为什么不用更大更深的网络车牌字符是印刷体笔画结构规整类别内差异小MobileNet级别的网络在这个任务上提升有限反而会带来过拟合和显存压力。AdaptiveAvgPool2d((4, 4))让网络对输入尺寸不那么敏感即使分割出来的字符宽高比不规则也能正常前向推理这个细节在调试时能省很多事。训练时的增强策略要注意随机亮度扰动 ±20、高斯模糊 σ0.5、小角度旋转 ±5 度、随机平移 2 像素这些都是模拟真实拍摄条件的低成本增强不要做随机裁剪——字符被裁掉关键笔画反而是帮倒忙。训练超参我一般是 batch size 64、初始学习率 1e-3、SGD 动量 0.9 或 Adam训练 20~30 个 epoch在验证集上做早停patience 设 5 个 epoch。每次训练完要把「混淆矩阵」打印出来看一眼重点关注相似字符对0/O、1/I、8/B、5/S。如果混淆集中出现在某个字符对上先别急着调网络回数据里看看这个字符的样本够不够、增强有没有把它弄成另一个字形。4.3 把模型接进识别管线导出 ONNX用 OpenCV DNN 做推理训练好的 PyTorch 模型不能直接塞进识别管线里用因为演示环境不一定装了 PyTorch而且每次启动都要等模型加载。更稳的做法是导出成 ONNX用 OpenCV 的 DNN 模块推理这样整套识别代码只依赖 OpenCV 和 NumPy部署和演示都轻量。# train/export_onnx.py import torch from train_char_cls import PlateCharNet model PlateCharNet(num_classes65) model.load_state_dict(torch.load(plate_char.pth, map_locationcpu)) model.eval() x torch.randn(1, 1, 32, 64) # 模拟一张 32x64 的灰度字符图 torch.onnx.export( model, x, plate_char.onnx, input_names[input], output_names[logits], opset_version11, dynamic_axes{input: {0: batch}} ) print(导出完成plate_char.onnx)推理侧用 OpenCV 加载# modules/recognize.py import cv2 import numpy as np from config import labels_to_text class CharRecognizer: def __init__(self, onnx_path, char_map): self.net cv2.dnn.readNetFromONNX(onnx_path) self.char_map char_map def predict_char(self, char_img): # 统一成 32x64训练时用的是灰度图blobFromImage 会自动转灰度 resized cv2.resize(char_img, (64, 32), interpolationcv2.INTER_AREA) blob cv2.dnn.blobFromImage( resized, scalefactor1.0 / 255.0, size(64, 32), mean(0,), swapRBFalse, cropFalse ) self.net.setInput(blob) logits self.net.forward() # shape: [1, 65] label int(logits.argmax(axis1)[0]) score float(logits.max()) return label, score注意blobFromImage的size(64, 32)顺序是 (width, height)网络的输入是 (1, 32, 64)即高 32、宽 64这里写反了图像会被压成竖长的精度直接崩。swapRBFalse是因为字符图本身是灰度图转不转通道没差别但统一写成显式参数比靠默认值放心。 每个字符推理一次7 个字符也就是 7 次前向单张图整体耗时在毫秒级实时演示完全够用。最后把 7 个字符的 label 依次送入labels_to_text输出形如「京A12345」的字符串。5. 常见问题排查与避坑五个最容易让系统翻车的实操踩坑记录5.1 现象cv2.putText 把中文写成了问号合成数据全是乱码不止一个同学在生成合成数据时直接用cv2.putText写「京」「沪」这类汉字出来的图片字符全部变成??。原因很简单OpenCV 的 putText 只内置了 Hershey 矢量字体不支持中文字符集遇到非 ASCII 字符就渲染成问号。解决方法是改用 PIL 的ImageFont.truetype加载系统里的中文字体比如simhei.ttf来绘制。这里还有第二个隐藏坑PIL 生成的是 RGB 图像而cv2.imwrite按 BGR 写入直接保存的话红色和蓝色通道互换白字蓝底会变成蓝字白底。所以代码里写img_bgr cv2.cvtColor(img, cv2.COLOR_RGB2BGR)那一步不能省。5.2 现象HSV 阈值在室内测得好好的拿到室外黄昏场景全部漏检白天晴天识别率 95%傍晚一试候选框直接消失。原因是 HSV 空间里蓝色对亮度和饱和度非常敏感黄昏时色温偏暖蓝色车牌的 H 值会偏移同时画面整体变暗导致 V 值跌破阈值。我调过的参数组合里s_lo从 60 降到 40、v_lo从 60 降到 40能覆盖大部分光线变化。如果还不行在定位前给整张图做一次 CLAHE 自适应直方图均衡化把暗部细节拉开。要注意 CLAHE 会改变颜色表现只能作用于灰度通道不能直接作用于 BGR 图的每个通道否则色调会明显跑偏。5.3 现象字符分割把「川」字切成了三段后续识别全错位「川」的结构是三竖垂直投影在竖与竖之间会出现接近零的低谷如果低阈值设得太严格就会被切成三截。表现出来就是分割结果从「川A12345」变成「川」占三个字符位后面所有字符的左右边界整体错位识别结果成了乱序字符串。解决思路前面 4.1 的代码里已经写了切完先统计段数超过 7 段就把最窄的相邻段合并直到恢复到 7 个字符。但更根本的问题是「先切再数」这种后处理只能兜底真正有效的方法是在切割前把弱低谷的判定阈值调高——连续低于 3 列的投影低谷才算字符间隙单个列上的低谷算断笔。5.4 现象蓝牌识别正常新能源绿牌和黄牌一进来就废原因非常简单定位模块的 HSV 阈值只写了蓝色区间绿牌和黄牌在第一步inRange就被整体过滤掉了。很多网上的开题报告会把「自适应多颜色车牌识别」写进创新点但代码里依然只有一个蓝色阈值。要补的话在config.py里把LOCATE_CFG改成多组颜色区间定位时分别对蓝色、绿色、黄色各做一遍掩码再把候选框合并去重。绿牌还有另一个不同点它是 8 位字符分割模块的「段数调整到 7」逻辑会把第 8 个字符硬并掉这种和结构相关的改动要一起放进配置里做成plate_type感知的分割。5.5 现象答辩演示现场摄像头过曝识别率断崖式下跌我见过一个学弟的演示脚本本地测试了 30 张图片都正常现场接摄像头时被台灯直射车牌反光一片白识别率直接归零。原因有两层一是现场灯光不可控二是测试时从来没用过实时视频流。这类问题的稳妥解法是演示脚本里同时保留「图片识别」「本地视频识别」「摄像头实时识别」三个入口演示时优先展示录制好的视频再切实时。实时路径里对定位到的车牌 ROI 额外做一次直方图均衡化能救回一部分过曝区域。另外不要用 1080p 的摄像头裸流跑全流程把帧缩到 640x480速度提升一倍不止误检率反而更低——小分辨率下小尺寸的蓝色背景区域更容易被面积阈值过滤掉。6. 整合与调试让整套代码在演示时不翻车的三个习惯6.1 分模块日志与中间结果快照错误定位到 10 分钟以内系统整合后最大的问题不是「跑不通」而是「跑出错误结果但不知道错在哪一段」。我的做法是给每个模块加一个debug开关打开后把中间结果按步骤落盘# main.py 中的调试片段 if cfg[save_debug]: cv2.imwrite(fdebug/{frame_id:04d}_1_locate.png, plate_crop) cv2.imwrite(fdebug/{frame_id:04d}_2_segment.png, segment_vis) cv2.imwrite(fdebug/{frame_id:04d}_3_chars.png, chars_concat) print(f[{frame_id}] 候选框数{len(boxes)} 字符块数{len(chars)} f识别结果{text} 平均置信度{avg_score:.3f})日志里最重要的是「候选框数 → 字符块数 → 平均置信度」这条链路。候选框数是 0说明问题在定位段字符块数不是 7问题在分割段字符块数对但置信度低问题在识别段。调试的原则是永远先确认上一段的输出是对的再排查下一段不要跨段猜。把中间结果可视化之后错误定位通常 10 分钟就能完成。6.2 一个入口贯穿所有模式图片、视频、摄像头统一跑同一套流程演示现场最怕的是「这个功能在图片模式正常切到摄像头就崩」。我习惯把main.py写成一个入口通过参数切换输入源但内部走的永远是同一条locate - wrap - segment - recognize管线python main.py --source image --path test.jpg python main.py --source video --path demo.mp4 python main.py --source camera --id 0管线内部对输入做统一处理先把帧缩放到 640px 宽再进定位模块。摄像头模式下还有一个「防抖」技巧连续 3 帧在同一个候选框位置识别出合法车牌字符串才输出结果避免单帧误检导致结果闪烁。这个逻辑实现起来很简单但对演示观感提升非常明显。6.3 训练与演示参数分离演示前一天不要动模型我把所有训练超参数和演示参数放在两个配置文件里config.py管演示train_config.py管训练。演示用的模型一旦导出了 ONNX就冻结不再重新导出。答辩前一周不要再调模型和阈值把所有精力放在跑通流程、准备备用视频、测试不同光线环境上。这套系统里最容易出错的是 HSV 阈值最不该临时改的也是 HSV 阈值——它太依赖具体图像了换一个环境可能全变。我当年做完这套系统论文改了三遍真正把所有模块串起来跑顺是答辩前一周才完成的那周做的事只有一件把「定位→分割→识别」每一段的中间结果落成图一遍遍看是哪一段干掉了我的识别率。这个习惯我后来做其他项目也一直用希望能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑