
简介这是一套面向计算机视觉初学者与开发者的车牌号识别与提取完整源码基于OpenCV与Python实现可应用于智能交通、停车场自动收费、车辆管控等场景。资源包共43个文件以37张JPG车牌样本图片作为算法训练与测试素材2个Python源文件承载车牌定位、字符分割与识别等核心流程另含2个Markdown项目文档、1个readme说明及1个编译文件整体约2.47MB目录结构清晰便于按模块查阅与二次开发。目前已有434人学习下载。通过这套源码读者可掌握从图像预处理、车牌定位到字符识别的完整实现思路理解样本图片与模型参数之间的对应关系并可直接运行编译文件快速验证识别效果为后续优化光照、脏污、破损等复杂场景下的识别准确率提供可复用的工程基础。1. 车牌识别这套源码真正难的不是 OpenCV 而是后面那三步很多人第一次搜「基于 OpenCV 和 Python 的车牌号识别与提取设计源码」脑子里想的是一段能直接跑的代码读一张车尾照片框出蓝底白字打印出「京A12345」。真动手才发现OpenCV 只是把像素矩阵递到你手里真正决定成败的是车牌定位、字符分割、字符识别这三步的工程取舍。我见过太多 demo 在实验室照片上 95% 准确率换到地库逆光、雨天反光、斜 30 度拍摄就集体翻车。这套方案适合两类人一是课程设计/毕设需要完整可复现链路的同学二是想把它当图像处理练手项目、顺带理解传统 CV 流水线的工程师。它不依赖深度学习框架一台普通笔记本就能跑但你要接受它对拍摄角度和光照的敏感——这正是后面几章要逐个拆开的地方。2. 从一张图到车牌字符串整条流水线的拆解与选型2.1 为什么是「定位 → 校正 → 分割 → 识别」四段式传统车牌识别的经典流水线不是拍脑袋定的而是被车牌本身的物理特征逼出来的。中国蓝牌是 440×140 的固定长宽比底色饱和度高、边缘密集、字符区域垂直投影有规律。这四条特征分别对应四个处理阶段颜色和边缘用来定位几何变换用来校正倾斜投影和连通域用来切字符模板或简单分类器用来认字。选型上我一般坚持两个原则。第一定位阶段颜色和边缘双通道并行单靠颜色在灰白车身上会漏检单靠边缘在复杂背景里会误检。第二识别阶段优先用模板匹配或轻量 SVM而不是一上来就上 CNN——课程设计和练手场景里引入深度学习会把环境配置、训练数据、显存问题全带进来反而偏离了「用 OpenCV 理解图像处理」的初衷。等你把传统流水线跑通、知道每一步的误差怎么传递再换 CNN 做端到端识别才有对比基准。2.2 环境准备Python、OpenCV、NumPy 的最小安装先把环境搭干净别在系统 Python 里乱装。我习惯用虚拟环境隔离避免和系统包打架。# 创建虚拟环境Python 3.8 以上都行 python -m venv lpr_env # 激活Windows 用 lpr_env\Scripts\activateLinux/macOS 用下面这行 source lpr_env/bin/activate # 安装核心三件套opencv-python 是主包 pip install opencv-python numpy # 验证是否装好能打印版本号就说明没问题 python -c import cv2, numpy; print(cv2.__version__, numpy.__version__)这里有个高频报错要提前说ModuleNotFoundError: No module named opencv。九成情况是你装包和跑代码用的不是同一个 Python 解释器。解决办法是先which pythonWindows 用where python确认当前解释器路径再用python -m pip install opencv-python强制装到当前解释器下。VS Code 里还要检查右下角选中的解释器是不是你激活的那个虚拟环境这一步不确认后面所有 import 都会莫名其妙失败。参数上opencv-python是包含主模块的常规包够用如果你要做视频流或 GUI 显示它已经带了 highgui不需要额外装opencv-contrib-python除非你要用 SIFT 这类专利算法。NumPy 版本建议 1.21 以上老版本和某些 OpenCV 版本混用会在矩阵运算时抛类型错误。2.3 车牌定位HSV 颜色分割 形态学闭运算定位是整条流水线里最玄学的一步因为背景千变万化。我的做法是先转 HSV 空间做颜色掩膜再用形态学把碎块连成整块。import cv2 import numpy as np def locate_plate(img): # 转 HSV比 RGB 更容易分离颜色 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌在 HSV 里的经验范围H 在 100-124 之间 lower_blue np.array([100, 80, 80]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算把车牌内部被字符打断的区域连起来 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 找轮廓按面积和长宽比筛 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / float(h) area w * h # 蓝牌长宽比约 3.14放宽到 2.5-4.0 if 2.5 ratio 4.0 and area 2000: candidates.append((x, y, w, h)) return candidates, mask逻辑说明inRange生成二值掩膜白色是候选区域。闭运算的核尺寸 (17,5) 是横向长条专门用来连接被字符隔断的蓝色区域——核太小连不起来核太大相邻车牌会粘连。轮廓筛选用长宽比和面积双条件长宽比卡 2.5 到 4.0 是因为拍摄角度会让真实比例变形面积阈值 2000 是过滤掉噪点小块的底线具体数值要按你图片分辨率调。参数怎么改如果你的图是 4K 高清面积阈值要往上调到 8000 以上如果是手机随手拍的低分辨率图2000 可能还偏大。HSV 的 H 范围在不同光照下会漂移阴天偏暗时 S 和 V 的下限要降到 50 左右否则掩膜会大面积丢失。2.4 倾斜校正最小外接矩形 仿射变换定位出来的车牌经常是斜的直接切字符会切歪。校正思路是求最小外接矩形拿到角度再旋转回水平。def deskew(img, rect): x, y, w, h rect roi img[y:yh, x:xw] # 转灰度后做边缘检测用最小外接矩形求角度 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) coords np.column_stack(np.where(edges 0)) if len(coords) 10: return roi # 边缘太少放弃校正 angle cv2.minAreaRect(coords)[-1] # OpenCV 返回角度在 [-90, 0)统一换算 if angle -45: angle 90 angle # 只在角度明显时才旋转避免小抖动引入插值模糊 if abs(angle) 1: return roi (h2, w2) roi.shape[:2] center (w2 // 2, h2 // 2) M cv2.getRotationMatrix2D(center, angle, 1.0) rotated cv2.warpAffine(roi, M, (w2, h2), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE) return rotated逻辑说明minAreaRect返回的角度是矩形相对水平的偏转但 OpenCV 的角度定义有历史包袱落在 [-90, 0)所以要判断小于 -45 时加 90 换算成常规角度。warpAffine用三次插值保证旋转后字符边缘不糊边界用BORDER_REPLICATE复制边缘像素避免出现黑边干扰后续分割。参数说明Canny 的 50/150 是低高阈值光照均匀时可以收紧到 80/200 减少噪边abs(angle) 1这个判断很关键小于 1 度不旋转因为旋转本身会引入插值误差得不偿失。这是血泪经验早期我没加这个判断结果本来水平的车牌被反复插值搞糊了。3. 字符分割与识别把车牌切成七个字再认出来3.1 垂直投影法切字符的完整实现校正后的车牌是水平长条字符之间有空隙垂直投影每列白色像素数会出现明显的波峰波谷波谷就是切点。def split_chars(plate_bgr): gray cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) # 二值化Otsu 自动找阈值比固定阈值稳 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 垂直投影对每一列求和 col_sum np.sum(binary, axis0) / 255 # 找波谷投影值低于阈值的列视为字符间隙 threshold binary.shape[0] * 0.05 gaps col_sum threshold chars [] start None for i, is_gap in enumerate(gaps): if not is_gap and start is None: start i elif is_gap and start is not None: # 宽度太窄的当噪点丢掉 if i - start 5: chars.append(binary[:, start:i]) start None return chars逻辑说明Otsu 二值化能自适应不同亮度比手动定 127 靠谱得多。投影阈值取图像高度的 5%意思是这一列白色像素少于总高的 5% 就算间隙。宽度小于 5 像素的段直接丢因为车牌字符最窄的「1」也有十几个像素宽太窄的必然是噪点或边框残留。参数怎么改如果分割出来字符粘连两个字符连成一块说明投影阈值太低把 0.05 提到 0.1如果字符被切碎一个字切成两半说明阈值太高降到 0.03。这个参数没有万能值要拿你的实际图片试几轮。3.2 模板匹配识别字符归一化与匹配打分切出来的字符大小不一要先统一到固定尺寸再和模板比对。def recognize(chars, templates): results [] for ch in chars: # 统一缩放到 20x40和模板尺寸一致 ch_resized cv2.resize(ch, (20, 40)) best_score -1 best_char ? for label, tmpl in templates.items(): # 用归一化相关系数匹配对亮度变化不敏感 res cv2.matchTemplate(ch_resized, tmpl, cv2.TM_CCOEFF_NORMED) score res[0][0] if score best_score: best_score score best_char label results.append(best_char) return results逻辑说明TM_CCOEFF_NORMED是归一化相关系数返回值在 -1 到 1 之间越接近 1 越像。选它而不是平方差匹配是因为相关系数对整体亮度偏移不敏感模板和实际字符亮度不完全一致时也能匹配上。参数说明模板尺寸 20×40 是经验值字符宽高比约 1:2和真实车牌字符比例接近。模板库要覆盖省份简称、字母、数字每个字符准备 3 到 5 个不同字体的样本匹配时取最高分。模板质量直接决定识别率这一步偷懒后面全白搭。3.3 识别结果的后处理用规则纠错模板匹配一定会出错尤其是「0」和「O」、「8」和「B」、「1」和「I」这种形近字符。后处理用规则兜底。def post_process(chars): # 中国车牌规则第 1 位是省份汉字第 2 位是字母 # 后面 5 位是字母数字混合且不含 I 和 O if len(chars) ! 7: return .join(chars) fixed list(chars) # 第 2 位必须是字母把误识别的数字纠正 if fixed[1] in 0123456789: mapping {0: O, 1: I, 8: B} fixed[1] mapping.get(fixed[1], fixed[1]) # 后 5 位里不允许出现 I 和 O按形近替换 for i in range(2, 7): if fixed[i] I: fixed[i] 1 elif fixed[i] O: fixed[i] 0 return .join(fixed)逻辑说明这是把车牌编码规则当先验知识用。第 2 位是发牌机关代号一定是字母后 5 位序号里官方明确不使用 I 和 O避免和 1、0 混淆。所以识别出 I 或 O 基本可以断定是误识别直接替换。参数说明这个规则表要按你实际处理的车牌类型调整。新能源绿牌是 8 位规则不同黄牌大型车也有差异。如果你的场景是单一蓝牌这套规则能救回不少错误。4. 避坑与排查那些让识别率从 90% 掉到 40% 的细节4.1 现象地库或夜间图片定位全失败原因HSV 颜色范围是按白天自然光调的地库灯光偏黄偏暗蓝色车牌的 H 值会漂移S 和 V 大幅下降固定阈值直接失效。解决不要用固定 HSV 范围改成自适应。先统计整图的 HSV 直方图找到饱和度最高的色相区间作为候选再动态生成掩膜范围。或者更简单粗暴——定位前先做一次 CLAHE 直方图均衡化提升对比度再进 HSV。我一般两个都上先均衡再自适应地库场景能救回大半。4.2 现象车牌定位框到了但框偏大把车身也框进去原因闭运算的核尺寸太大把车牌周围和车身同色的区域也连进来了。或者面积阈值太低把大块车身误当车牌。解决闭运算核从 (17,5) 往回收试 (13,5) 甚至 (11,3)。同时加一个填充率判断——车牌区域里蓝色像素占比应该很高如果候选框内蓝色像素占比低于 60%直接丢弃。这个判断比单纯调核尺寸更稳。4.3 现象字符分割把「川」切成三块或者两个数字粘一起原因垂直投影法对笔画不连续的汉字川、湘天然不友好投影波谷不明显粘连则是字符间距太小或二值化后膨胀过度。解决汉字单独处理不靠投影切改用连通域分析把相邻的连通块按水平距离合并。粘连问题在二值化后加一次轻微腐蚀把字符间的连接断开腐蚀核用 (2,1) 横向核。注意腐蚀别过头否则细笔画字符会消失。4.4 现象模板匹配把「8」认成「B」「0」认成「D」原因模板库样本太少或者模板本身质量差字符在缩放归一化时形变严重。解决模板库每个字符至少 5 个样本覆盖不同字体和轻微倾斜。匹配前对字符做一次去噪和细化让笔画更清晰。另外可以引入多模板投票——每个字符和所有样本匹配取平均分最高的类别比单模板稳得多。4.5 现象整条流水线单张图跑 2 秒以上批量处理慢到无法接受原因findContours和matchTemplate在大图上开销大尤其是模板匹配对每个字符遍历所有模板。解决定位阶段先把图缩放到宽度 800 以内再处理定位到车牌后再从原图裁剪对应区域做后续步骤保证字符清晰度。模板匹配用cv2.matchTemplate的 ROI 版本或者把模板预计算成特征向量用矩阵运算批量比对比循环快一个数量级。5. 把识别率再往上抬几个我反复验证过的进阶技巧先说一个最容易被忽略的点预处理的质量决定上限后面的算法只是逼近这个上限。我做过对比同一套定位和识别代码只把输入图做一次自适应直方图均衡加双边滤波识别率能从 72% 提到 85%。双边滤波保边去噪对车牌这种边缘密集的区域特别友好参数用d9, sigmaColor75, sigmaSpace75起步噪点多就加大 sigmaColor。第二个技巧是多帧投票。如果你处理的是视频而不是单张图同一辆车连续几帧的识别结果做多数投票能干掉大部分偶发误识别。实现很简单维护一个最近 5 帧结果的计数器取出现次数最多的作为输出。这个技巧在收费站、停车场这种车会停留的场景里效果拔群单帧 80% 准确率五帧投票能到 95% 以上。第三个是模板库的持续迭代。别指望一次建好的模板库能吃遍所有场景。我的习惯是每次识别置信度低于 0.6 的字符把它的归一化图像存下来人工确认后加进模板库。跑一段时间模板库会自然覆盖你实际场景里的字体和光照变化识别率会自己往上爬。这比调任何参数都管用。最后给一个验证方法别只看单张图的输出。准备一个至少 50 张的测试集覆盖白天、夜间、地库、斜拍、模糊五类场景每类 10 张。跑完统计每类的定位成功率和识别准确率你会清楚看到瓶颈在哪一类。我早期就是靠这个发现地库场景定位成功率只有 30%才回头去改 HSV 自适应而不是盲目调识别参数。说个我自己的教训刚做这个项目时我花了两天调模板匹配的参数识别率死活上不去。后来把中间每一步的掩膜、校正图、分割结果都存下来看才发现问题出在定位阶段框偏了后面全是在错误的输入上做优化。从那以后我养成了一个习惯——每加一个处理步骤就把这一步的中间结果可视化存盘出问题先看是哪一步开始错的而不是从头到尾瞎调。这个习惯帮我省下的时间比任何算法技巧都多。希望帮到你。本文还有配套的精品资源点击获取