简介这份资源是一套基于Python与OpenCV的车牌识别完整项目源码面向具备一定图像处理基础、希望深入计算机视觉或自动驾驶方向的开发者与学习者。项目围绕车牌定位与字符识别展开涵盖灰度化、二值化、高斯滤波去噪、Canny边缘检测、形态学膨胀腐蚀、连通组件字符分割以及模板匹配与SVM分类等关键环节并配有训练与测试样本便于理解从图像预处理到号码输出的完整链路。压缩包共40个文件约18.93MB以jpg样本图、xml配置、py脚本、dat模型文件为主另含png截图、7z压缩包及少量工程配置与说明文件目录结构清晰便于按模块查阅。目前已有316人学习下载。读者可据此复现车牌识别流程掌握图像预处理、边缘检测与字符分割的实践方法并借助svm.dat等模型文件理解分类器训练与识别验证思路是入门计算机视觉项目的不错参考。1. 车牌识别.zip 里到底装了什么从一张过曝照片说起地下车库出口逆光车牌被 LED 补光灯打得一片死白。我拿着手机拍的那张照片丢进开源 OCR识别结果是一串乱码。这就是很多人第一次做车牌识别的真实起点——不是算法不够先进而是输入图像本身已经废了。车牌识别.zip 这个标题背后通常指的是一套完整的车牌检测加字符识别方案包含数据预处理、车牌定位、字符分割或端到端识别、后处理校验几个环节。它能解决的核心问题是把一张包含车辆的图片或一段视频流变成结构化的车牌字符串。适合谁适合想快速搭一个停车场出入口 Demo 的嵌入式工程师、想理解 OCR 落地链路的算法初学者以及需要给现有安防系统加车牌能力的后端开发者。这一章不聊虚的先把整条链路拆开看。车牌识别的技术栈大致分两条路。传统路线是「检测 分割 单字符分类」用 OpenCV 做边缘检测和颜色定位再投影分割字符最后送进一个小 CNN 分类。端到端路线是「检测 序列识别」检测出车牌框后直接用一个 CTC 或 Attention 模型输出整串字符省掉分割步骤。两条路各有适用场景后面会细说选型理由。先记住一个数字中国蓝牌是 440mm × 140mm这个物理尺寸决定了你在图像里做透视校正时的目标宽高比大约是 3.14:1。很多定位算法翻车就是因为没把这个先验用上。2. 车牌定位颜色先验和边缘检测怎么配合才不打架2.1 为什么纯颜色定位在黄昏会集体翻车蓝牌和绿牌的 HSV 范围是有明确区间的。蓝牌底色 H 大约在 100 到 124 之间S 和 V 在光照正常时都偏高。但黄昏时分环境色温骤降蓝色通道被压缩H 值会漂移到 90 以下直接掉出阈值。我最早写的纯颜色定位脚本白天召回率能到 92%一到傍晚就掉到 60% 出头。血泪经验是颜色只能做粗筛不能做最终判决。改进思路是颜色和边缘做与运算。车牌区域有一个很强的特征——字符和底色之间有密集的垂直边缘。用 Sobel 算 x 方向梯度再做二值化车牌区域的边缘密度远高于车身。把颜色掩膜和边缘掩膜做交集黄昏场景的误检能压下去一大半。具体参数上Sobel 核大小用 3 就够阈值不要固定用自适应阈值或者按图像均值动态调。import cv2 import numpy as np def locate_plate_candidate(img_bgr): # 转 HSV 做颜色粗筛 hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) # 蓝牌范围H 下限放宽到 95 应对色温漂移 lower_blue np.array([95, 80, 60]) upper_blue np.array([130, 255, 255]) mask_color cv2.inRange(hsv, lower_blue, upper_blue) # 灰度图做 Sobel x 方向梯度 gray cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) grad_x cv2.Sobel(gray, cv2.CV_16S, 1, 0, ksize3) grad_x_abs cv2.convertScaleAbs(grad_x) # 自适应阈值提取边缘 mask_edge cv2.adaptiveThreshold(grad_x_abs, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15, 2) # 颜色和边缘取交集 mask_combined cv2.bitwise_and(mask_color, mask_edge) # 形态学闭运算连接断裂区域 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask_closed cv2.morphologyEx(mask_combined, cv2.MORPH_CLOSE, kernel) return mask_closed这段代码的逻辑是先用宽松的颜色阈值拿到候选区域再用边缘信息剔除掉那些颜色接近但纹理平滑的区域比如蓝色车身。闭运算的核我设成 17×5横向长纵向短因为车牌是扁长形状这个核能有效连接字符之间的断裂同时不会把相邻的两个车牌粘在一起。如果你的场景里车牌倾斜严重这个核要改成旋转矩形或者先做角度校正。2.2 轮廓筛选面积、宽高比、矩形度三个硬指标拿到掩膜后找轮廓但轮廓数量可能上百个。必须用硬指标筛。面积太小的是噪点太大的是车身。宽高比偏离 3:1 到 4:1 的直接丢。矩形度用轮廓面积比外接矩形面积低于 0.7 的说明形状不规则。这三个指标一卡候选框通常能压到 5 个以内。def filter_contours(mask, min_area2000, max_area50000): contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: area cv2.contourArea(cnt) if area min_area or area max_area: continue rect cv2.minAreaRect(cnt) (w, h) rect[1] if w 0 or h 0: continue aspect max(w, h) / min(w, h) # 宽高比在 2.5 到 4.5 之间 if aspect 2.5 or aspect 4.5: continue # 矩形度 rect_area w * h extent area / rect_area if rect_area 0 else 0 if extent 0.7: continue candidates.append(rect) return candidatesmin_area 设 2000 是针对 1080P 图像里车牌大概占 200×60 像素的情况。如果你的摄像头分辨率更高或者车牌更远这个值要按比例调。max_area 设 50000 是防止把整个车头框进来。矩形度阈值 0.7 是个经验值低于这个值说明轮廓有大量凹陷通常是字符断裂或者掩膜噪声导致的。注意 minAreaRect 返回的角度信息要保留后面做透视校正时要用。3. 字符识别端到端 CTC 和分割分类的选型分水岭3.1 什么情况下必须上端到端模型分割分类路线的前提是字符之间能干净切开。但实际场景里车牌有铆钉、有污损、有字符粘连投影分割经常把「京」和「A」切在一起。我遇到过最离谱的是一张泥水糊住的车牌二值化后字符连成一片分割算法直接输出一个 8 字符的宽连通域后面分类器完全没法处理。这种场景下端到端 CTC 是唯一出路。CTC 的核心思想是不需要字符级对齐标注输入一个特征序列输出字符序列中间用空白符分隔。车牌识别里常用 CRNN 结构CNN 提特征RNN 或 Transformer 做序列建模CTC 解码。好处是训练数据只需要车牌字符串标签不需要每个字符的位置框。坏处是收敛慢对短序列有时会重复输出。我一般会在 CTC 解码后加一个规则后处理中国车牌第一位是汉字第二位是字母后面五位是字母数字混合不符合这个模式的直接丢弃或修正。3.2 用 PaddleOCR 做车牌识别的微调路径如果你不想从零训 CRNN最省事的路径是拿 PaddleOCR 的识别模型做微调。它的识别头本身就是 CTC支持自定义字典。车牌字符集很小汉字加字母数字一共不到 70 类微调收敛很快。# 车牌字符字典按实际需求增删 plate_chars [ 京, 津, 沪, 渝, 冀, 晋, 辽, 吉, 黑, 苏, 浙, 皖, 闽, 赣, 鲁, 豫, 鄂, 湘, 粤, 桂, 琼, 川, 贵, 云, 藏, 陕, 甘, 青, 宁, 新, A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V, W, X, Y, Z, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] # 写入字典文件每行一个字符 with open(plate_dict.txt, w, encodingutf-8) as f: for ch in plate_chars: f.write(ch \n)字典文件准备好后在 PaddleOCR 的配置文件里把 character_dict_path 指向这个文件同时把 num_classes 改成 len(plate_chars) 1加一是 CTC 空白符。微调时学习率不要设太大1e-4 起步batch size 根据显存调。我一般会冻结 backbone 的前几层只训后面的序列建模部分这样小数据集上不容易过拟合。训练数据至少准备 5000 张以上覆盖不同光照和角度否则模型会严重偏向训练集里的字体风格。3.3 后处理校验把识别结果从「可能对」拉到「确定对」模型输出的是概率序列直接取 argmax 会得到一些看起来像但实际不存在的车牌。后处理要做三件事。第一按中国车牌格式校验位数和字符类型。第二对易混淆字符做规则替换比如「0」和「O」、「1」和「I」、「2」和「Z」根据位置判断该用哪个。第三如果系统有历史记录可以做时序投票同一辆车连续多帧的识别结果取众数。import re from collections import Counter def postprocess_plate(raw_text, historyNone): # 去除 CTC 空白和重复 text raw_text.replace(-, ).strip() # 中国车牌正则省份汉字 字母 5位字母数字 pattern r^[\u4e00-\u9fa5][A-Z][A-Z0-9]{5}$ if not re.match(pattern, text): # 尝试修正常见混淆 text text.replace(O, 0).replace(I, 1) if not re.match(pattern, text): return None # 时序投票 if history and len(history) 3: counter Counter(history[-5:] [text]) text counter.most_common(1)[0][0] return text这个后处理函数里正则模式是硬约束不符合的直接返回 None宁可漏检不可误检。混淆替换只对数字位做因为汉字位和字母位有固定规则。时序投票窗口我设 5 帧取最近 5 次结果加当前结果的众数这样单帧的随机错误能被压下去。注意 history 要按车辆 ID 分开存不能把所有车的识别结果混在一起投票。4. 避坑与排查车牌识别落地时最容易翻车的五个地方4.1 现象白天识别率 95%晚上掉到 40%原因补光灯过曝导致车牌区域饱和字符和底色的对比度反而消失。HSV 里 V 通道全部拉到 255颜色掩膜失效。解决在预处理阶段加一个过曝检测如果车牌候选区域的平均亮度超过 240先做局部直方图均衡或者 Gamma 校正把对比度拉回来。Gamma 值取 0.6 到 0.8 之间具体看过曝程度。另外补光灯的角度要调不要正对车牌偏 15 到 30 度能减少镜面反射。4.2 现象新能源绿牌识别成蓝牌或者直接漏检原因绿牌的 HSV 范围和蓝牌差异大很多开源方案只调了蓝牌阈值。绿牌 H 大约在 35 到 85 之间S 和 V 的分布也不同。解决颜色掩膜要做双通道蓝牌和绿牌分别设阈值最后取并集。绿牌的字符是黑色底色是渐变绿二值化时要用自适应阈值而不是固定阈值。如果绿牌样本少微调时要做数据增强把蓝牌样本的颜色通道偏移到绿色域。4.3 现象模型在测试集上准确率 99%上线后一塌糊涂原因训练数据分布和实际场景不匹配。测试集是从训练集里随机切的同分布。上线后遇到的是新角度、新光照、新车型。解决训练集必须按场景分层采样每个摄像头点位、每个时间段都要有样本。验证集要单独从一个没参与训练的点位采集。我一般会留一个「魔鬼测试集」专门放各种极端样本每次模型更新都跑一遍看有没有退化。4.4 现象车牌定位框抖动严重视频里框一直在跳原因单帧检测没有时序平滑相邻帧的检测结果独立噪声导致框位置跳变。解决加一个简单的卡尔曼滤波或者滑动平均。对框的中心点和宽高分别做指数移动平均平滑系数取 0.3 到 0.5。如果车辆运动快系数取大一点减少延迟如果车辆静止系数取小一点让框稳定。注意平滑只用于显示和跟踪识别用的裁剪图还是要用原始检测框否则会把模糊引入识别环节。4.5 现象字符分割时「川」字被切成三竖原因投影分割对笔画不连续的汉字不友好。「川」的三个竖笔在垂直投影上是三个独立峰分割算法会当成三个字符。解决如果走分割路线分割后要做合并逻辑相邻连通域的高度和宽度如果符合单个字符的比例且合并后宽度不超过最大字符宽度就合并。更彻底的办法是直接上端到端模型绕开分割。这也是我现在新项目一律用 CTC 的原因省心。5. 把识别率从 90% 推到 98% 的两个进阶技巧第一个技巧是合成数据增强。真实车牌数据标注成本高但你可以用字体文件合成。用 PIL 把汉字和字母数字按车牌布局渲染到不同背景上加随机透视变换、高斯噪声、运动模糊、亮度抖动。我一般会合成 10 万张左右和真实数据按 3:1 混合训练。合成数据的关键是字体要多样不要只用一种黑体宋体、楷体、甚至手写风格都要加进去否则模型会对真实车牌的字体变化不鲁棒。透视变换的参数要覆盖实际摄像头可能的角度范围一般水平偏转正负 30 度垂直偏转正负 15 度就够了。第二个技巧是置信度阈值动态调整。固定阈值在不同场景下表现差异很大。我的做法是维护一个滑动窗口统计最近 100 次识别的置信度分布如果平均置信度下降说明当前场景难度升高自动把阈值调低宁可多识别一些再靠后处理过滤。如果平均置信度很高就把阈值调高减少误识。这个自适应逻辑用简单的 PID 控制就能实现比例系数取 0.1 左右积分系数取 0.01避免震荡。class AdaptiveThreshold: def __init__(self, init_thresh0.8, window_size100): self.thresh init_thresh self.window [] self.window_size window_size self.kp 0.1 self.ki 0.01 self.integral 0.0 def update(self, confidence): self.window.append(confidence) if len(self.window) self.window_size: self.window.pop(0) avg_conf sum(self.window) / len(self.window) # 目标平均置信度 0.85 error 0.85 - avg_conf self.integral error self.thresh self.kp * error self.ki * self.integral # 限制阈值范围 self.thresh max(0.5, min(0.95, self.thresh)) return self.thresh这个自适应阈值类的逻辑是如果最近的平均置信度低于目标值 0.85说明当前识别难度大降低阈值让更多结果通过反之则提高阈值。kp 和 ki 的取值需要根据你的模型置信度分布调如果模型本身置信度普遍偏高kp 要调小否则阈值会剧烈波动。integral 项用来消除稳态误差但要注意积分饱和所以加了阈值上下限。最后说一个我自己的习惯每次模型更新后不要只看整体准确率要按省份、按车牌颜色、按拍摄时段分别统计。我吃过亏整体准确率涨了 2%但某个省份的汉字识别率掉了 10%原因是那个省的样本在训练集里被稀释了。现在我固定会跑一个分维度报表任何一个维度掉超过 3% 就回滚模型。这个习惯帮我省了很多次线上事故。希望帮到你。本文还有配套的精品资源点击获取