ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于OpenCV与CNN的Python车牌识别系统:从定位分割到深度学习识别全流程

基于OpenCV与CNN的Python车牌识别系统:从定位分割到深度学习识别全流程 简介这份资源是经过导师审核的Python车牌识别系统毕设项目源码评审分达95分以上面向计算机相关专业学生与从业者可用于毕业设计、期末课程设计或大作业参考。项目融合深度学习与OpenCV技术实现车牌检测与字符识别运行GUI.py即可启动完整系统适合希望掌握图像处理与深度学习落地应用的中级学习者。压缩包共47个文件约28.81MB包含6个py核心脚本如主程序、图像识别、数学处理与配置模块、21张jpg与15张png测试图片、2个7z字符集压缩包、2个dat模型文件及gitignore等覆盖从训练数据到推理代码的完整链路。目前已有297人学习下载。读者可获得一套可直接运行的毕设级方案理解车牌定位、字符分割与SVM识别等关键环节并借助内置测试图片快速验证效果为二次开发或论文撰写提供扎实基础。1. 车牌识别系统从一张糊图到结构化文本中间到底卡在哪地下车库出口摄像头拍到的车牌往往只有 90 像素宽还带着运动模糊和顶灯反光。很多人第一次做车牌识别系统会以为难点在「识别」这一步实际上真正让项目翻车的是前面的定位和字符分割——车牌都没框准后面 CNN 再强也是白搭。这套基于深度学习加 OpenCV 的 Python 车牌识别系统核心链路是「检测车牌位置 → 校正倾斜 → 分割字符 → 深度学习分类」最终把一张图变成「京A12345」这样的结构化文本。它适合两类人一类是拿它当 Python 毕业设计、需要完整可跑源码和清晰模块划分的学生另一类是想把车牌识别接进停车场、门禁、称重系统的工程师关心的是准确率边界和部署成本。下面按我实际搭过一遍的顺序把选型、代码、参数和踩过的坑讲清楚。2. 技术选型为什么是 OpenCV 做前处理、CNN 做识别2.1 传统图像处理与深度学习的边界在哪车牌识别拆开看是四个子问题车牌检测、倾斜校正、字符分割、字符识别。前三个用 OpenCV 的形态学、轮廓、透视变换就能覆盖大部分场景第四个才是深度学习的主场。原因是字符识别面对的是 65 类左右的分类问题省份简称、字母、数字类别多、字形相近0 和 O、8 和 B传统模板匹配在光照变化下掉点严重而 CNN 对这类细粒度分类天然占优。我一般把 OpenCV 定位成「把图变干净」的角色灰度化、高斯模糊、Sobel 边缘、闭运算把车牌连成一块白色矩形再用轮廓筛选出候选区域。这一步不追求 100% 召回追求的是把候选框压到个位数给后面的 CNN 减负。深度学习部分用一个小型 CNN 就够不需要上 ResNet——字符图只有 20×20 左右参数量大了反而过拟合。提示如果你的场景是固定机位、光照稳定纯 OpenCV 模板匹配也能到 85% 左右一旦有夜间、逆光、大角度就必须上 CNN这是分水岭。2.2 环境搭建Python、OpenCV、深度学习框架的版本对齐环境是新手第一个翻车点。ModuleNotFoundError: No module named opencv和python 下载 cv2这类搜索量一直很高本质是包名和导入名不一致。OpenCV 的 pip 包叫opencv-python导入时却是import cv2很多人卡在这。# 建议用虚拟环境避免和系统 Python 打架 python -m venv lpr_env # Windows 激活 lpr_env\Scripts\activate # Linux / macOS 激活 source lpr_env/bin/activate # 安装核心依赖版本对齐很关键 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install tensorflow2.13.0 # 或 pytorch二选一 pip install pillow scikit-learn逻辑说明opencv-python提供图像处理numpy是 OpenCV 的底层数组依赖深度学习框架二选一即可。参数上OpenCV 4.x 和 3.x 的 API 有差异比如cv2.findContours在 3.x 返回三个值、4.x 返回两个值网上老教程直接抄会报ValueError: too many values to unpack。TensorFlow 2.13 对应 Python 3.83.11Python 3.12 装不上这是硬约束。import cv2 import numpy as np print(cv2.__version__) # 确认 4.x img cv2.imread(car.jpg) print(img.shape) # (h, w, 3)None 说明路径错了如果img是None九成是路径含中文或反斜杠转义问题用cv2.imdecode(np.fromfile(path, dtypenp.uint8), -1)读中文路径更稳。2.3 数据集从哪来、怎么标注毕业设计常见做法是找开源车牌数据集再自己补拍。标注分两块车牌位置用矩形框VOC 的 XML 或 YOLO 的 txt字符识别用「字符图 标签」的文件夹结构。我一般把字符图统一缩放到 20×20 灰度按类别建文件夹省得写额外的标签文件。数据用途格式数量建议备注车牌检测JPG XML5001000 张含夜间、倾斜样本字符识别20×20 灰度 PNG每类 200 张以上65 类共约 1.3 万张验证集同上总数的 15%不要和训练集同源数据量不够时用旋转 ±10 度、亮度 ±30%、加高斯噪声做增强比盲目堆网络深度有效得多。3. 车牌定位与字符分割OpenCV 前处理的完整代码链路3.1 从原图到候选车牌框的六步处理这一步的目标是把彩色原图变成几个候选矩形。核心思路是利用车牌区域「边缘密集、颜色偏蓝或绿」的特性。import cv2 import numpy as np def locate_plate(img): # 1. 高斯模糊压掉噪点 blur cv2.GaussianBlur(img, (5, 5), 0) # 2. 转灰度 gray cv2.cvtColor(blur, cv2.COLOR_BGR2GRAY) # 3. Sobel 提取垂直边缘车牌字符垂直边缘密集 sobel cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize3) # 4. 二值化阈值可调 _, binary cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 5. 闭运算把字符连成整块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 6. 找轮廓并按长宽比筛选 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for c in contours: x, y, w, h cv2.boundingRect(c) ratio w / float(h) if 2.0 ratio 5.5 and w 80: candidates.append((x, y, w, h)) return candidates逻辑说明Sobel 的(1, 0)表示只算 x 方向导数因为车牌字符是竖直排列的垂直边缘最丰富。闭运算的核(17, 5)是宽扁形专门用来横向连接字符。参数上长宽比 2.05.5 覆盖了蓝牌和新能源绿牌w 80过滤掉小噪点。如果候选框太多把闭运算核调大如果漏检把阈值改成手动低值。3.2 倾斜校正透视变换的四个角点怎么定拍到的车牌往往有 515 度倾斜不校正会直接影响字符分割。常见做法是用cv2.minAreaRect拿到最小外接矩形再取四个角点做透视变换。def correct_plate(img, rect): # rect 是 minAreaRect 返回的 ((cx,cy),(w,h),angle) box cv2.boxPoints(rect) box np.int0(box) # 按 x 坐标排序分出左右两组 box sorted(box, keylambda p: p[0]) left sorted(box[:2], keylambda p: p[1]) right sorted(box[2:], keylambda p: p[1]) src np.array([left[0], right[0], right[1], left[1]], dtypenp.float32) # 目标矩形宽高按实际车牌比例 440:140 dst np.array([[0, 0], [440, 0], [440, 140], [0, 140]], dtypenp.float32) M cv2.getPerspectiveTransform(src, dst) return cv2.warpPerspective(img, M, (440, 140))逻辑说明boxPoints拿到四个角点后按 x 排序分成左右两组再各自按 y 排序得到「左上、右上、右下、左下」的顺序这是getPerspectiveTransform要求的对应关系。目标尺寸 440×140 是标准蓝牌比例统一尺寸后字符分割的参数才好固定。角度超过 20 度时minAreaRect的角点顺序可能错乱需要额外判断这是常见翻车点。3.3 字符分割垂直投影法的阈值怎么定校正后的车牌是 440×140字符之间有明显空隙用垂直投影就能切开。def split_chars(plate_gray): # 二值化字符为白 _, binary cv2.threshold(plate_gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 垂直投影每列白色像素数 projection np.sum(binary, axis0) / 255 # 找字符区间 chars [] in_char False start 0 for i, v in enumerate(projection): if v 2 and not in_char: in_char True start i elif v 2 and in_char: in_char False if i - start 8: # 过滤太窄的噪点 chars.append(binary[:, start:i]) return chars逻辑说明projection是每列的白色像素计数字符所在列数值高空隙列接近 0。阈值v 2是经验值太小会把噪点当字符太大会把细笔画比如「1」切掉。i - start 8过滤掉宽度不足 8 像素的干扰。分割后一般得到 7 个字符新能源 8 个如果数量不对先回去检查二值化是否干净。注意中文省份简称如「京」「沪」笔画复杂二值化后容易粘连建议对第一个字符单独放宽宽度阈值。4. 深度学习字符识别CNN 模型结构与训练参数4.1 为什么用小型 CNN 而不是直接上预训练大模型字符图只有 20×20 灰度信息量有限。用 ResNet50 这种大模型参数量几千万训练集才一万多张必然过拟合验证集准确率上不去。我一般用三层卷积加两层全连接参数量控制在 50 万以内训练快、部署轻CPU 上单张推理 5 毫秒以内。import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(num_classes65): model models.Sequential([ # 输入 20x20 灰度 layers.Input(shape(20, 20, 1)), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), # 关键防过拟合 layers.Dense(num_classes, activationsoftmax) ]) model.compile(optimizertf.keras.optimizers.Adam(1e-3), losscategorical_crossentropy, metrics[accuracy]) return model逻辑说明三层卷积的感受野足够覆盖 20×20 的字符结构paddingsame保证边缘信息不丢。Dropout(0.5)放在全连接前是防过拟合最有效的一招。优化器用 Adam学习率 1e-3 是起点训练 20 轮后如果 loss 不降降到 1e-4。类别数 65 对应 31 个省份简称加 24 个字母数字去掉易混的 I、O。4.2 训练参数与数据增强的实操配置from tensorflow.keras.preprocessing.image import ImageDataGenerator train_gen ImageDataGenerator( rescale1./255, rotation_range8, # 小角度旋转 width_shift_range0.1, height_shift_range0.1, brightness_range[0.7, 1.3], validation_split0.15 ) train_data train_gen.flow_from_directory( chars_dataset, target_size(20, 20), color_modegrayscale, batch_size64, class_modecategorical, subsettraining ) model build_cnn() history model.fit(train_data, epochs30, callbacks[tf.keras.callbacks.EarlyStopping(patience5)]) model.save(plate_char_cnn.h5)逻辑说明rotation_range8模拟拍摄倾斜brightness_range模拟昼夜光照差这两个增强对车牌场景最有效。batch_size64在显存 4G 以上都跑得动太小会导致 loss 震荡。EarlyStopping(patience5)是后悔药验证集 5 轮不提升就停避免白跑。训练完保存.h5推理时直接load_model。参数推荐值调整方向学习率1e-3loss 震荡降到 1e-4batch_size64显存不足降到 32epochs30配合 EarlyStoppingDropout0.5过拟合加重可到 0.64.3 推理阶段把分割字符喂给模型def recognize(chars, model, idx_to_char): result for ch in chars: # 缩放到 20x20归一化 ch cv2.resize(ch, (20, 20)) ch ch.astype(float32) / 255.0 ch ch.reshape(1, 20, 20, 1) pred model.predict(ch, verbose0) result idx_to_char[np.argmax(pred)] return result逻辑说明idx_to_char是训练时class_indices的反向映射必须和训练顺序一致否则识别结果全乱。verbose0关掉每张图的进度输出批量推理时快很多。如果某个字符置信度低于 0.6可以标记为可疑实际系统里会触发二次拍摄。5. 避坑与排查车牌识别最常见的 5 个翻车现场5.1 现象识别结果全是同一个字符原因idx_to_char映射和训练时的class_indices顺序不一致或者推理时输入没归一化训练用了rescale1./255推理忘了除 255。解决训练完立刻把train_data.class_indices存成 json推理时加载同一份归一化写成函数复用。5.2 现象白天准、晚上全错原因训练集几乎全是白天样本模型没见过夜间低对比度图。解决数据增强里加brightness_range[0.5, 1.5]再补拍 200 张夜间样本。如果还不行在定位前加一步 CLAHE 自适应直方图均衡cv2.createCLAHE(clipLimit2.0).apply(gray)。5.3 现象字符分割多切或少切原因二值化阈值不合适或者车牌校正后仍有残留边框。解决分割前先用cv2.morphologyEx做一次开运算去掉细边框投影阈值从v 2调到v 3试。中文省份字单独处理宽度阈值放宽到 12。5.4 现象OpenCV 报too many values to unpack原因网上教程基于 OpenCV 3.xfindContours返回三个值你装的是 4.x 只返回两个。解决改成contours, _ cv2.findContours(...)或者统一锁版本opencv-python4.8.1.78。5.5 现象模型训练准确率 99%实际用起来一塌糊涂原因训练集和验证集同源字符图都是从同一批车牌裁的分布太像。解决验证集必须来自不同时间、不同机位的车牌哪怕只有 100 张也比同源 1000 张有参考价值。这是血泪经验别省这一步。6. 进阶技巧把准确率从 90% 推到 96% 的三个动作第一个动作是「检测和识别解耦调优」。很多人把定位和识别串成一个端到端模型改一处影响全局排查困难。我习惯保持 OpenCV 定位 CNN 识别的两段式定位召回率调到 98% 以上宁可多框几个候选识别阶段再用置信度过滤。这样每段可以独立测定位不行就调形态学参数识别不行就补数据互不干扰。第二个动作是「字符级置信度回退」。实际系统里7 个字符全对的概率是各字符准确率的乘积单字 98% 的话整牌只有 87%。做法是对置信度低于 0.7 的字符用相似字符集做二次判断比如「0/O」「8/B」「5/S」建一个混淆表结合车牌规则第二位通常是字母做约束。这一步能把整牌准确率拉高 35 个百分点。第三个动作是「用真实业务数据做增量训练」。上线后把识别错误的样本自动存下来人工标一遍每周增量训练一次。我一般设一个阈值同一字符连续错 20 次以上就把它加进训练集。这个习惯坚持两个月模型对本地车牌的适应度会明显好于通用模型。优化动作预期提升成本检测识别解耦23%低改架构置信度回退35%中需混淆表增量训练5%高需标注流程最后说个习惯每次改完参数别只看总准确率把混淆矩阵打出来看错在哪些字符对上。我踩过最深的坑就是盯着 95% 的总数沾沾自喜结果发现「京」和「津」混淆率 30%换个城市直接崩。把错误拆到字符级才知道下一步该补什么数据。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进