
简介这份基于 Python 与 OpenCV 的车牌识别项目源码包面向计算机视觉初学者、课程设计与毕业设计学生以及希望把传统图像处理用于实际项目的开发者重点解决静态图片和摄像头视频中的车牌检测与号码识别问题。压缩包共 16468 个文件、约 24.91MB主体为万余张 jpg 图像样本另含少量 py 源码、xml/dat 模型与配置数据分别用于训练测试、程序运行和参数调整便于复现实拍与离线识别场景。功能覆盖图片车牌号识别、GUI 界面操作、识别结果导出 Excel并加入 OpenCV 摄像头实时识别识别模块函数也做了优化以提升速度与准确率。已有 1844 人学习可据此梳理车牌定位、字符分割识别、SVM 模型调用、界面开发与实时视频处理流程并借鉴性能优化和项目组织思路也可作为课程实践与项目复现参考。1. 从一个停车场闸机的识别延迟说起我在一个园区停车场的出入口蹲了一下午观察闸机前面的车牌识别摄像头。十分钟里有三辆车被卡住一辆是因为车牌位置偏了被识别成半个一辆是逆光画面里车牌白成一片还有一辆是新能源绿牌识别系统字段直接读错位。负责维护的 A 同学说这套系统用 Python 加 OpenCV 先做车牌定位再做字符识别算法逻辑不算复杂但每换一个现场阈值和核尺寸就得重调一次。这个项目讲的就是这么一套东西基于 Python 和 OpenCV 的车牌识别系统把一张车辆照片或者一路视频流变成结构化车牌号。它适合两类人一类是想从零理解计算机视觉落地链路的后端或算法初学者另一类是手上有摄像头、想搭一套可改造识别流程的开发者。它不追求识别率天花板价值在于整条链路是开放的每一行都能改这比调一个黑盒接口对二次开发友好得多。2. 技术栈与项目骨架从一张图到一串字符的处理流水线2.1 技术选型为什么是 Python OpenCV 而不是深度学习框架这个项目的底色是传统图像处理没有一上来就搬 YOLO 或者 CRNN。理由很实际车牌识别在受控光照、固定机位下颜色分割加形态学处理就能拿到不错的候选区域整套流程依赖少、启动快、调试时可解释。Python 负责流程编排和字符串后处理OpenCV 负责像素级运算numpy 负责矩阵运算这是典型的轻量 CV 组合。我一般会把依赖收在一份 requirements 里版本上不需要追新OpenCV 4.x 的 API 基本稳定# 核心依赖版本按现场验证过的锁住 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install Pillow10.0.1opencv-python是主包不带 GUI 的服务器环境换成opencv-python-headless更省事。numpy 的版本要和 OpenCV 编译版本对齐否则在苹果芯片机器上偶发导入失败。Pillow 主要用来做部分字符模板比对时的图像读写不是核心链路必需。选传统方法还有一个好处出错的时候你能看到中间图。颜色分割出来的掩膜长什么样、形态学闭运算后车牌区域连没连成块、轮廓筛选卡在哪一步每个环节都能 dump 一张图出来看。用深度学习模型时出错往往只能看到最后一层输出错了前面到底哪里丢的不太容易看。2.2 目录结构模块怎么分层一份源码拿下来最先要看的是目录因为它直接告诉你作者是怎么切职责的。这个项目通常按处理阶段分目录下面是我拆解后整理出的典型结构plate-recognition/ ├── main.py # 入口串起单图/批量/视频三种模式 ├── config.yaml # 阈值、核尺寸、车牌尺寸范围等参数 ├── core/ │ ├── preprocess.py # 缩放、滤波、增强 │ ├── localizer.py # 颜色分割 形态学 轮廓定位 │ ├── segmenter.py # 字符分割 │ └── recognizer.py # 字符识别与结果拼装 ├── utils/ │ ├── image_io.py # 中文路径读写兼容 │ └── debug_view.py # 中间结果落盘 ├── templates/ # 字符模板或 OCR 字典 └── data/ ├── samples/ # 测试图 └── output/ # 识别结果与中间图分层的核心是把「找车牌」和「读车牌」彻底拆开定位错了就只调 localizer字符读错了就只调 segmenter 和 recognizer不需要全链路重跑。debug_view 这个模块在调试期价值很高它把每一步的中间图都保存下来相当于给流水线装了监控。config.yaml 把参数从代码里挪出来是为了后续能在不改代码的情况下切换现场。2.3 主流程串接一个请求从读图到出结果入口文件负责把四个阶段串起来。这段代码是整个系统的主干写清楚输入输出后面所有模块都围绕它展开import cv2 from core.preprocess import preprocess from core.localizer import locate_plate from core.segmenter import segment_chars from core.recognizer import recognize_chars def recognize_image(path, config): # 读图中文路径需走 imdecode 兜底 img cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR) if img is None: raise ValueError(f图片读取失败: {path}) # 阶段一预处理统一尺寸并增强对比度 normalized preprocess(img, config[max_side]) # 阶段二定位车牌返回候选框与原图坐标 plates locate_plate(normalized, config) results [] for plate in plates: # 阶段三字符分割返回从左到右的字符小图 chars segment_chars(plate, config) # 阶段四字符识别拼装成字符串 text recognize_chars(chars, config) results.append({box: plate[box], text: text}) return resultsrecognize_image本身不做像素运算只负责调度和容错。max_side控制缩放的最长边常见取 1000 到 1280 之间太大处理慢太小车牌字符糊成一团。locate_plate返回的 box 要保留原图坐标否则画框回显时会偏。异常处理这里直接抛出比静默返回空列表好因为批量跑的时候能立刻发现是哪张图坏了。segment_chars和recognize_chars之间传递的是裁剪后的小图列表不传整张车牌图是为了让识别环节只关心单字符方便替换成别的识别方式。这种解耦在后期改造时非常有用比如你想把字符识别换成一个小型分类模型只要保持入参出参一致其他代码一行不用动。3. 核心模块拆解定位、分割、识别三段主力代码3.1 车牌定位颜色分割加形态学的组合拳定位是整个链路里最容易出问题的环节。这个项目采用的思路是先按车牌底色做颜色分割再用形态学把字符间隙连成一个区域最后用轮廓的宽高比和面积做筛选。import cv2 import numpy as np def locate_plate(img, config): hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌掩膜上下界根据现场光照微调 lower_blue np.array(config[blue_lower]) upper_blue np.array(config[blue_upper]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算把字符之间的缝隙填平形成一个整块 kernel cv2.getStructuringElement( cv2.MORPH_RECT, (config[close_kx], config[close_ky])) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 开运算去掉零星噪点 mask cv2.morphologyEx( mask, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) contours, _ cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) plates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / float(h) area w * h # 车牌宽高比大致在 2.5 到 4.5 之间 if (config[ratio_min] ratio config[ratio_max] and config[area_min] area config[area_max]): plates.append({box: (x, y, w, h), img: img[y:yh, x:xw]}) return platesHSV 空间比 RGB 更适合做颜色分割因为 H 通道对光照变化不敏感。blue_lower和blue_upper在 OpenCV 里 H 取值范围是 0 到 180蓝色大概落在 100 到 124 之间具体要按现场实测调。闭运算的核尺寸close_kx一般取 17 到 25太小连不成块太大会把相邻两块车牌粘在一起。宽高比筛选是最有效的过滤手段因为车牌是固定规格的矩形。ratio_min和ratio_max我一般设成 2.5 和 4.5留足透视变形余量。面积上下限用来排除远处的小噪点块和整片背景的大色块。如果现场有新能源绿牌就要在这套掩膜之外再挂一套绿色通道两路结果合并去重否则绿牌会整条漏检。定位阶段失败时的排查顺序是固定的先把 mask 存盘看一眼如果掩膜里车牌区域是碎的问题在闭运算核太小如果掩膜整片泛白问题是阈值上界太宽如果车牌区域完整但没进候选就是宽高比或面积卡掉了。3.2 字符分割垂直投影找切分点拿到车牌小图后要先做二值化再用垂直投影找字符之间的空白列def segment_chars(plate, config): roi plate[img] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 自适应二值化应对车牌表面反光和污渍 binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, config[block_size], config[c_value]) # 垂直投影统计每列白色像素数量 projection np.sum(binary 0, axis0) threshold config[proj_threshold] chars, start [], None for i, val in enumerate(projection): if val threshold and start is None: start i elif val threshold and start is not None: # 过滤过窄的噪点列 if i - start config[min_char_w]: chars.append(binary[:, start:i]) start None return charsadaptiveThreshold比全局阈值稳因为车牌不同位置的光照差异很大。block_size通常取 21 到 31 的奇数c_value取 5 到 10用来微调二值化的松紧。垂直投影的思路是把每一列的黑点数量画成一条曲线字符所在的位置是高柱字符之间的间隙是谷底从谷底切开就得到字符块。THRESH_BINARY_INV让字符变成白色因为暗底亮字和二值化后白像素计数更直观。min_char_w用来剔除诸如螺丝孔、边框产生的小块。分割数量一般期望是 7 个字符加一个点如果拿到 5 个或 9 个通常说明二值化把相邻字符粘上了或者噪点被当成了字符。这时候把中间图存盘出来看能一眼看出问题出在哪一列。3.3 字符识别模板匹配与 OCR 两条路识别环节有两种常见落地方式项目里可以用模板匹配做基础版也可以用轻量 OCR 后端。模板匹配版是把分割出的字符缩放到统一尺寸跟 templates 目录下的字符模板逐个比对def recognize_chars(chars, config): text for i, ch in enumerate(chars): # 统一到模板尺寸避免尺度差异影响匹配分 resized cv2.resize(ch, (config[tpl_w], config[tpl_h])) best_score, best_label -1, ? for label, tpl in load_templates(config[tpl_dir]).items(): score cv2.matchTemplate( resized, tpl, cv2.TM_CCOEFF_NORMED)[0][0] if score best_score: best_score, best_label score, label # 分数过低视为无法识别避免强行输出错误字符 text best_label if best_score config[match_threshold] else ? return texttpl_w、tpl_h要和模板图尺寸一致常见取 20x40。TM_CCOEFF_NORMED对亮度变化不敏感比平方差法更适合车牌。match_threshold一般设 0.6 到 0.75低于阈值输出问号比硬猜一个字符更有实际价值因为后处理环节可以对问号做业务兜底。另一条路是接一个小型 OCR 后端把分割后的字符图传进去拿文本。这种做法省掉了模板收集的工作量对字体和污损更宽容代价是多一个依赖。我的习惯是先跑模板匹配版把整条链路跑通等识别率成为瓶颈了再把 recognizer 这一层替换掉前面定位和分割的代码不需要改动这就是前面强调解耦的意义。字符识别阶段还有一个容易被忽略的问题中文省份简称的模板数量比字母数字少得多容易在一堆英文里误判。如果业务只关心后六位可以在输出时做字段裁剪把省份字单独标记出来不做强匹配。这种取巧的做法在现场往往比死磕准确率更实用。4. 环境搭建与运行排障新手最容易翻车的几个点4.1 从零跑通命令行与参数配置先把项目拉到本地建虚拟环境装依赖用 samples 里的图跑一遍单图模式python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python main.py --mode image --input data/samples/car_01.jpg --debug--mode支持 image、batch、video 三种--input在 batch 模式下传目录video 模式传 0 表示本机摄像头。--debug开启后会把每一步的中间图存到 data/output 下调试期建议一直开着。配置文件里几个关键参数需要按现场改参数含义常见取值max_side图片缩放最长边1000~1280close_kx / close_ky闭运算核尺寸17~25 / 5~9ratio_min / ratio_max车牌宽高比范围2.5 / 4.5block_size自适应二值化窗口21~31 奇数match_threshold模板匹配接受阈值0.6~0.75参数改动后重新跑同一张图对比中间图的变化比反复读代码快得多。批量模式在跑之前先拿三五张典型图验证参数否则一次跑几百张全错定位问题时反而更乱。4.2 避坑与常见问题排查现象一中文路径图片读出来是 None。原因在于cv2.imread对非 ASCII 路径支持不稳定Windows 上尤其明显。解决办法是统一用np.fromfile读字节再cv2.imdecode或者封装一层 image_io把所有读图入口都走它别在不同模块里混着调。现象二白天能识别傍晚整片漏检。这是颜色阈值的典型翻车现场。傍晚色温偏移蓝色车牌的 H 值会往偏紫或偏青的方向漂。解决办法不是放宽阈值上界那会把背景里蓝色车辆也吃进来而是加一步白平衡或者 CLAHE 对比度增强把色偏拉回来再让阈值保持较紧。现象三字符分割拿到 9 个以上。通常是车牌边框的固定螺丝或者反光条被当成了字符。解决的顺序是先检查二值化是否把噪点放大了block_size 偏小会这样再在分割后加一条宽度过滤把明显窄于正常字符的块丢掉。如果还不行就在分割前把车牌图上下各裁掉 5%边框往往在最外侧。现象四绿牌识别成乱码。新能源车牌是 8 位且底色为渐变绿用蓝色掩膜根本定位不到。正确做法是单独挂一条绿色 HSV 通道两路掩膜取并集后再走轮廓筛选识别阶段按位数做分支处理长度 8 的按新能源字段解析。现象五批量跑几十张后内存只涨不降。OpenCV 的 Mat 在循环里如果没有显式释放引用计数容易拖住。排查方式是每轮循环结束把中间变量置 None并且不要在循环里创建 VideoCapture 却不 release。这个坑如果不注意跑几千张的批处理会在中途被系统杀掉。5. 二次开发与参数调优让识别链路适配你自己的现场5.1 模块替换与验证方法想把这个项目改造成自己的能力最省力的路径是按阶段替换而不是推倒重写。定位模块如果要换成分割更稳的方案只要保持返回[{box:..., img:...}]这个结构主流程一行都不用改。识别模块换成小型分类模型时同理入参是字符图列表出参是字符串。改造前先建一套回归样本。我的做法是从现场抓 50 到 100 张图人工标注车牌字符串放进一个 csv 里然后写个小脚本每次改动后批量跑一遍输出整体准确率和逐张错误明细import csv def evaluate(dataset_csv, recognize_fn): total, correct, errors 0, 0, [] with open(dataset_csv, encodingutf-8) as f: for row in csv.DictReader(f): pred recognize_fn(row[image_path]) total 1 if pred row[plate]: correct 1 else: errors.append((row[image_path], row[plate], pred)) # 准确率与错误明细一起输出方便定位是哪类图出错 print(facc{correct/total:.3f}, errors{len(errors)}) return errorsdataset_csv至少要有 image_path 和 plate 两列plate 里存人工核对后的真值。这个脚本的价值不在于那个准确率数字而在于错误明细它会告诉你到底是逆光图错、绿牌错还是某个字母总被认成另一个。改参数时只看总体准确率容易走偏看明细才知道该往哪个方向调。5.2 现场调参的一个实用技巧不要一次改多个参数。传统 CV 流水线里定位的闭运算核、二值化窗口、匹配阈值这三个东西互相影响同时改两个效果变好变坏都说不清是谁的功劳。我一般固定住两个只动一个跑一遍回归样本看错误明细的变化方向确认了再动下一个。另一个技巧是把失败样本按错误类型分桶。定位失败、分割失败、识别失败三类问题的处理方向完全不同定位失败要查颜色阈值和形态学核分割失败要查二值化和投影阈值识别失败才轮到模板库质量。分桶之后你会发现准确率提升不是靠调参玄学而是靠把某一类错误清零。还有一个容易被忽略的点是日志和可视化要一起留。debug_view 在调试期存中间图上线后至少要保留失败样例的原图和中间图否则出现问题只能靠猜成了黑匣子。我吃过这个亏某次现场反馈识别率下降结果中间图一张没存只能重新去现场抓图复现白折腾两天。从那以后我每次改识别链路都强制先跑一遍那套回归样本确认不是用一两个成功案例骗自己再上线。希望你上手这个项目时也先建这套小样本集后面会省很多事。本文还有配套的精品资源点击获取