资讯动态

传统图像处理也能做车牌识别:OpenCV定位、分割、匹配全流程拆解

发布时间:2026/10/20 2:05:28 来源:尧图企业网站定制
简介基于Python与OpenCV的车牌自动识别实战资源面向图像处理初学者及智能交通、停车场管理等应用开发者提供可直接运行的源码与配套素材。项目完整覆盖图像灰度化、二值化、滤波去噪、边缘检测、轮廓查找、形态学定位、字符垂直投影分割及模板匹配识别等关键步骤能帮助读者从零搭建一套车牌识别流程。压缩包共2000个文件核心是1个.py主程序另含1999张jpg图片既有多角度车辆样本也有各地车牌的字符匹配模板整体仅12.37MB轻量易部署。已有3106人学习下载适合作为课程设计、毕业设计或技术预研参考。通过该资源可掌握OpenCV在图像预处理、区域提取与字符识别中的典型用法并可直接替换数据验证效果。1. 车牌识别不是玄学一个纯 OpenCV 也能跑通的小项目“车牌识别”三个字一出很多人的第一反应是得上深度学习、得调参、得有 GPU 才能做。但这份基于 Python OpenCV 的车牌自动识别项目走的是完全相反的路线不依赖任何神经网络只靠传统图像处理把“定位车牌、分割字符、匹配模板”三步走完配套的源码、车辆图片数据集、字符匹配模板全都齐了。在一台没有独立显卡的笔记本上就能跑出完整的识别结果。适合两类人一类是刚学完 OpenCV 基础、想看看图像处理怎么拼成完整应用的开发者另一类是需要在本地快速搭一个车牌识别 Demo、再往里加自己业务的工程实践者。我拆完这套代码后的感受是它不算惊艳但作为“从零到一”的落地范本链路极完整改起来也顺手。2. 架构设计与技术选型传统图像处理为什么还有生命力2.1 技术栈选择OpenCV NumPy不上模型也能识别整套项目以 OpenCV 为图像处理核心辅以 NumPy 做数组计算识别阶段用的是模板匹配而不是机器学习模型。这个选型决定了项目的几个底子依赖极轻核心运行环境只需要 opencv-python、numpy 两个库连深度学习框架都不装。可解释性强车牌是怎么被找出来的、字符是怎么被切开的每一个中间结果都能以图片形式输出调试时肉眼可见。速度够快单张图片从读入到输出结果在普通 CPU 上通常是毫秒级比动不动扛一个几百兆模型的做法轻量得多。这个选择也意味着它天然有边界模板匹配对字体变形、光照变化、车牌倾斜都比较敏感。如果目标是“识别精度无脑拉到 99%”那得上 YOLO 一类的检测模型如果目标是“先有一套能跑的识别流程并且每一步都能看懂、能改”传统图像处理反而更合适。2.2 核心模块划分定位、分割、匹配三个文件各管一段项目源码的模块划分非常清楚主目录下几个 Python 文件各司其职plate_recognition/ ├── main.py # 程序入口组装整条识别流水线 ├── plate_locator.py # 车牌定位模块颜色阈值 形态学处理 ├── char_segmenter.py # 字符分割模块二值化 垂直投影 ├── char_matcher.py # 字符识别模块模板匹配 置信度输出 ├── utils.py # 公共工具图像缩放、中间结果可视化 ├── data/ │ ├── car_images/ # 测试车辆图片含车牌 │ └── labels/ # 部分图片对应的真实车牌标注 ├── templates/ │ ├── chars/ # 字符模板数字、字母、省份简称汉字 │ └── plate/ # 完整车牌模板可选对比用 └── output/ # 识别结果与中间过程图输出目录从文件命名就能看出设计思路识别不是一个黑匣子而是三段式流水线。每段都有独立的输入输出改其中一段不影响另外两段。比如你觉得定位不准只需要动plate_locator.py分割和匹配的代码一行都不用碰。这种解耦方式是我比较认可的地方也是后续做二次开发最容易上手的原因。2.3 识别主流程从一张车辆图到车牌字符串的四步链路入口main.py里主流程按顺序执行四个阶段图像读取与去噪cv2.imread读图转 HSV 色彩空间为颜色筛选做准备必要时用高斯模糊降低噪点。车牌定位按蓝色或黄色车牌的颜色范围生成二值掩膜用形态学闭运算把断裂区域连接起来再用轮廓检测圈出候选车牌区域。字符分割把定位到的车牌区域转灰度、做二值化通过垂直投影找到每一列的像素分布据此切出单个字符。字符识别将每个字符缩放到固定尺寸依次与模板库里的字符模板做归一化相关匹配得分最高的模板对应字符就是识别结果。这段流程里最值得学习的地方在于“把大问题拆成三个小问题”它没有尝试一步到位直接用模板匹配全图找车牌而是先靠颜色和轮廓缩小范围再靠投影规则做分割最后才进入匹配环节。每一步都控制变量出问题时能快速定位到具体模块。我之前见过不少新手一上来就想用一个大模型解决所有问题反而在调试时无从下手。3. 核心代码拆解定位、分割、匹配的完整链路3.1 车牌定位HSV 颜色阈值 形态学闭运算 轮廓筛选车牌定位是整个识别链路里的第一道关卡也是最容易翻车的地方。项目里用的是颜色特征来定位国内蓝牌底色是蓝色在 HSV 颜色空间里蓝色有一个相对稳定的区间先用cv2.inRange把符合颜色范围的像素抠出来再做形态学处理最后用轮廓外接矩形筛选出车牌。import cv2 import numpy as np def locate_plate(img): # 统一转到 HSV 颜色空间方便按颜色做阈值筛选 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌阈值范围H 100~124S 90~255V 90~255 lower_blue np.array([100, 90, 90]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算先膨胀后腐蚀把车牌上断裂的字符区域连成整块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 7)) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 提取外轮廓并用面积和宽高比过滤掉干扰区域 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 面积太小直接跳过正常车牌宽高比在 2 到 4 之间 if w * h 3000 or w 2 * h: continue return img[y:y h, x:x w] return None这段代码有几点值得展开HSV 比 RGB 更适合做颜色筛选因为 RGB 下蓝色受光照影响很大同一块蓝牌在阳光和阴影里可能是完全不同的 RGB 值而 HSV 把色调 H 单独拆出来只要色相在蓝色区间内S 和 V 在一定范围内变化都能命中。闭运算的核大小(17, 7)是我试出来的经验值。车牌字符之间有空隙如果不做闭运算轮廓检测会把每个字符当成独立对象核太小连不起来核太大会把周围环境也并进来。这个值不是固定的图片分辨率变高时要按比例放大。w 2 * h这个条件是为了过滤那些面积不小、但形状不像车牌的误检区域。正常蓝牌的长度大约是高度的 2~3 倍如果轮廓细长或者接近正方形直接放弃。这里还有一个常见误区有些人会先转灰度再做边缘检测来找车牌但边缘检测在复杂背景下会产生大量干扰轮廓反而更难筛。颜色 形态学的方式在车牌底色明显的场景下更稳。3.2 字符分割大津二值化 垂直投影找切分边界定位到车牌区域后下一步是把“一整块车牌”切成“一个个字符”。项目用的方法是垂直投影把二值化后的车牌图像按列统计白色像素数量连续有白色像素的区间就是一个候选字符。def segment_chars(plate): # 车牌区域转灰度再用大津法自动确定二值化阈值 gray cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) h, w binary.shape # 统计每一列白色像素个数构成垂直投影 col_sum np.sum(binary 255, axis0) # 遍历每一列找到连续非零区间作为字符边界 chars [] in_char False start 0 for i in range(w): if col_sum[i] 0 and not in_char: start i in_char True elif col_sum[i] 0 and in_char: in_char False # 太窄的区间大概率是噪声过滤掉 if i - start 5: chars.append((start, i)) return chars这里cv2.threshold用的THRESH_OTSU是自动阈值它会根据图像的灰度分布算出最合适的二值化阈值省掉了手动调参的麻烦。不过它也有自己的脾气——如果车牌区域里有大面积阴影或者反光大津法可能把阈值算偏导致字符和背景分不清。后面避坑章节会专门讲这种情况。垂直投影的原理很简单但有几个细节会影响结果车牌边框如果车牌定位时把边框也圈进来了边框会形成一条横穿整行的白色带直接影响列方向的投影。常见做法是先把车牌上下左右各裁掉几个像素再去掉边框干扰。铆钉和螺丝车牌上的固定螺丝在投影图上会形成极窄的白色列可能被误判成字符。所以代码里用i - start 5过滤掉宽度太小的区间。汉字结构特殊比如一些省份简称汉字是左右结构内部笔画之间有较大的空白垂直投影可能把一个字切成两半。遇到这种情况可以基于“车牌共有 7 个字符宽度大致均匀”的先验知识在分割后做一次宽度校正。3.3 字符识别归一化模板匹配 置信度排序分割完成后每个字符都是一张大小不一的小图。识别模块的职责是把它和模板库里的模板逐一比较找出最像的那个。项目用的是cv2.matchTemplate匹配方法选的是TM_CCOEFF_NORMED也就是归一化相关系数值越接近 1 越像。def recognize_char(char_img, templates): best_score -1 best_name # 统一尺寸避免字符大小差异影响匹配结果 target_size (40, 80) char_img cv2.resize(char_img, target_size) for name, tpl in templates.items(): tpl cv2.resize(tpl, target_size) # 归一化相关性匹配结果越接近 1 说明越相似 res cv2.matchTemplate(char_img, tpl, cv2.TM_CCOEFF_NORMED) _, score, _, _ cv2.minMaxLoc(res) if score best_score: best_score score best_name name return best_name, best_score模板加载部分一般会在程序启动时完成遍历templates/chars/目录文件名就是字符本身比如A.png、7.png、以及代表省份简称的汉字模板。recognize_char每次调用只处理一个字符主流程里循环调用即可。这个方案的优点和缺点都写在脸上优点是不需要训练模板库里加一个字符识别能力就扩展一个字符模板的收集成本极低从车牌图片里截图就行。缺点是模板匹配对“形变”很敏感。同一个字符在不同字体、不同粗细、略有倾斜的情况下相关系数会明显下降。如果两个模板长得接近比如O和0、B和8很容易互相打架。实际做识别时我会额外打印每个字符的前三名匹配结果。比如某个字符的 Top1 是B、Top2 是8且两者分数只差 0.03那这个位置十有八九需要人工复核。这个“输出 Top3 而不是只输出 Top1”的习惯在调模板时非常救命。3.4 main 入口把三段流水线串起来main.py的职责比较薄但它的组织方式值得学习if __name__ __main__: img cv2.imread(data/car_images/test_001.jpg) plate locate_plate(img) if plate is None: print(未定位到车牌) exit(1) # 保存中间结果方便肉眼检查定位是否准确 cv2.imwrite(output/plate.jpg, plate) char_boxes segment_chars(plate) for (start, end) in char_boxes: char_img plate[:, start:end] char, score recognize_char(char_img, templates) result.append(char) print(识别结果:, .join(result))把中间结果output/plate.jpg落盘这个习惯我很推荐。很多开发者只盯着最终识别结果定位错了就闷头调参数。把每一步中间结果都存下来一眼就能看出是定位裁偏了、分割切碎了还是匹配选错了调试效率翻倍。4. 常见问题与排查跑不起来和识别不准的五个踩坑记录4.1 读图颜色全乱字符像是“鬼影”现象同一张车牌用脚本识别出来是乱码但人工看图片完全正常中间过程图里字符底色和文字颜色对调了。原因OpenCV 的cv2.imread读进来是 BGR 顺序而某些图像处理步骤用 RGB 顺序处理或者用matplotlib显示时忘记转换导致颜色通道错位。更隐蔽的是如果模板图片用PIL读取RGB再和 OpenCV 处理过的字符图BGR做匹配模板和待识别图在通道顺序上不一致匹配分数会非常低。解决全项目统一使用cv2.imread读图显示中间结果时显式执行cv2.cvtColor(img, cv2.COLOR_BGR2RGB)模板加载也走 OpenCV保持通道一致。从那以后我每次跑图像项目都强制检查一遍所有图片的读取入口。4.2 车牌定位框偏了半个字现象定位环节输出的plate.jpg里车牌左边被切掉一块或者右边多出一截导致分割时第一个字符残缺。原因最常见的是轮廓筛选条件太宽把车牌附近的车身部件也并了进来其次是形态学闭运算的核太大把车牌和旁边的干扰区域粘连成一个轮廓。解决先看轮廓筛选条件w 2 * h是否太松尝试加上面积上限闭运算核从(17, 7)改成(11, 5)再试。不要小看这个调整核的宽高比直接影响纵向粘连程度我踩过的坑是核高度设得过大结果把车牌上下的两道白边全包进来了。4.3 汉字被切碎一个字变成三个字现象分割结果里省份简称汉字被拆成好几段后续匹配全部失败。原因垂直投影对左右结构的汉字非常不友好。汉字内部左右部件之间的空隙在投影图上表现为白色像素骤降甚至为 0于是被误判成字符边界。解决两步补救。第一基于车牌字符数先验通常 7 个字符如果分割出的区间数量远大于 7说明有字符被切开第二按字符的平均宽度做合并把宽度明显小于均值的相邻区间合并成一个。我一般还会在分割前对二值图做一次膨胀把汉字内部缝隙填上能有效减少这种切碎问题。4.4 模板匹配速度慢像在“卡死”现象跑一张图没问题连续跑几十张图时明显变慢每张要几百毫秒。原因字符识别是串行遍历全部模板每个模板都要resize再matchTemplate模板库越大、图片分辨率越高耗时越线性增长。解决先把所有模板在启动时统一resize到目标尺寸并缓存避免每识别一个字符就 resize 一次。其次可以用尺寸过滤缩小候选范围——数字和汉字的大小差异明显先按字符宽高比粗筛一遍再对剩余模板做精确匹配。实测把模板预处理缓存后速度能提升一倍以上。4.5 换一批照片识别率骤降全家剩一个准现象在项目自带的数据集上识别率挺高换成自己拍的照片同一套参数的识别率掉了一大截。原因牌照颜色阈值和形态学核大小都是“经验值”带有一定运气成分。新照片的拍摄角度、光照、分辨率变了原来的参数就不再适配。解决明确一个验收流程——固定 5 张正常照片、5 张弱光照片、5 张远近不同的照片每次调参后都把这 15 张全跑一遍记录识别率而不是拿单张调“全对”就收工。调参的核心变量就两个HSV 阈值范围和形态学核大小其他参数尽量保持稳定否则变量太多很难定位问题。5. 二次开发改造把模板匹配引擎换成你自己的识别引擎5.1 替换识别层保留定位和分割只动一个模块这套代码的模块边界设计得比较好改造时可以明确“只换识别层”。具体做法是在char_matcher.py里新增一个识别函数输入仍是分割好的字符图像输出仍是字符和置信度然后在main.py中把recognize_char的调用改成新函数。这样定位和分割完全不动当天就能看到换引擎前后的对比结果。推荐两条路线。第一把模板匹配换成“轮廓特征 最近邻”提取每个字符的宽高比、笔画密度、轮廓点数用距离度量找最近邻模板鲁棒性比直接模板匹配好一点代码量也不大。第二直接换成小型 CNN把分割结果统一缩放到64x128用一个两三层卷积的小模型做分类训练数据从模板生成或从 label 数据里切。这条路线上限高但需要准备足够的字符样本。5.2 验证新引擎用 Top3 命中率和混淆矩阵说话替换后的验证不能只看“识别对了没”。我一般会在原来的测试图片基础上把输出改成 Top3统计真实车牌字符出现在 Top3 里的比例同时把最容易混淆的字符对比如B/8、O/0、D/U单独列一张表观察新引擎是否缓解了这些冲突。如果新引擎在易混字符上仍然频繁出错不要把时间花在调阈值上直接补样本更有效。模板匹配时代补的是“更像的模板”CNN 时代补的是“更多样化的训练样本”。这一步做完识别率往往能稳定往上走。5.3 顺手做一个小工具把识别结果和中间过程图一起导出二次开发时我还会给output/目录增加一个“过程图拼接”功能把原图、定位结果、分割后的每个字符、最终识别字符串拼成一张大图保存。这样无论是自己复盘还是给别人演示都能直观看到每一步发生了什么。这个工具代码不超过 30 行但价值极高。整套项目拆完我最深的感受是传统图像处理方案不是过时而是“确定性更强、调试更直观”。定位、分割、匹配三步你都能看到中间结果改一个参数就能立刻看到影响。这也养成了我一个习惯凡是做识别类项目先保证每一步中间结果可视化再去刷精度。希望这篇拆解能帮你把代码跑起来再按自己的场景改出想要的效果。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑