基于OpenCV的车牌识别系统:从定位到识别的完整实战
发布时间:2026/10/1 5:02:33来源:尧图网络
简介这是一套面向计算机相关专业毕业设计、课程设计与期末大作业场景的Python车牌识别系统完整项目包基于OpenCV实现适合正在做毕设的学生以及需要项目实战练习的学习者使用。资源共包含47个文件以21张jpg与15张png图片作为车牌样本与测试素材6个py脚本承载主识别流程、字符分割、数学处理与调试等核心逻辑另有2个7z压缩数据包、2个dat模型文件及1个md说明文档整体约28.47MB目录结构清晰便于按模块查阅与二次开发。项目已通过严格调试可直接运行涵盖车牌定位、字符分割与SVM字符识别等关键环节读者可据此快速理解车牌识别系统的完整实现思路并在此基础上完成功能扩展或论文撰写。目前已有282人学习下载适合作为高分毕设参考与实战练手项目。1. 从一张糊到看不清的卡口图说起这套车牌识别系统到底能跑出什么结果很多人第一次做车牌识别都是被一张卡口抓拍图劝退的车是斜的、牌是脏的、光是一半过曝一半死黑拿 OpenCV 直接阈值一拉满屏噪点。这个标题里的「基于 OpenCV 的车牌识别系统」本质就是一套用传统图像处理链路把车牌从整张图里抠出来、再把字符一个个切出来识别的完整工程配套源码和全部数据适合拿来做毕业设计、课程设计也适合当图像处理入门到落地的第一个完整项目。它解决的不是「识别率吊打深度学习」这件事而是让你在没有 GPU、没有大模型、只有一台普通笔记本的环境下把「读图 → 定位车牌 → 校正 → 分割字符 → 输出号码」这条链路完整跑通并且每一步都能看到中间结果。适合谁正在找 OpenCV 图像处理项目练手的人、需要一份能讲清楚原理的毕业设计的人、以及想搞明白传统视觉方案边界在哪的从业者。下面按我实际搭这套东西的顺序讲参数和坑都给你标出来。2. 环境先立住Python、OpenCV 和那堆装不上的依赖2.1 版本怎么选别一上来就翻车这套方案对版本其实不挑但有几个组合是血泪经验踩出来的。Python 建议 3.8 到 3.10太新的 3.12 在部分老版本 OpenCV 上会出 wheel 不匹配的问题。OpenCV 用opencv-python就够不需要编译源码除非你要 CUDA 加速——但车牌识别这套传统链路根本用不上 GPU别被「opencv cuda prebuilt wheels」这类词带偏装了也是浪费。组件推荐版本说明Python3.8 ~ 3.103.12 部分依赖无预编译包opencv-python4.5 ~ 4.84.9 部分 API 有微调numpy1.21 ~ 1.24和 OpenCV 版本要匹配Pillow9.x读中文路径图片时更稳安装命令就一行但国内网络建议加镜像# 安装核心依赖-i 指定镜像加速 pip install opencv-python numpy pillow -i https://pypi.tuna.tsinghua.edu.cn/simple # 验证是否装好能打印版本号就说明环境通了 python -c import cv2; print(cv2.__version__)逻辑说明opencv-python是预编译包装完直接能用不需要 cmake 编译。参数说明如果你后面要跑视频流再补一个opencv-contrib-python但纯图片识别用基础包即可。验证那行很关键很多人ModuleNotFoundError: No module named opencv就是因为装到了另一个 Python 环境里用which python和pip -V对一下路径。2.2 项目目录怎么摆数据放哪拿到「源码全部数据」这种包第一件事不是急着跑而是把目录理清楚。常见结构是源码一个文件夹、测试图片一个文件夹、可能还有训练好的字符模板。我一般会整理成下面这样方便后面调路径plate_recognition/ ├── main.py # 主入口串起整条链路 ├── locate.py # 车牌定位 ├── segment.py # 字符分割 ├── recognize.py # 字符识别 ├── data/ │ ├── images/ # 待识别原图 │ └── templates/ # 字符模板如果用的是模板匹配 └── output/ # 中间结果和最终结果逻辑说明把定位、分割、识别拆成独立模块好处是每一步都能单独跑、单独看图出问题好定位。参数说明data/images里放原图命名别用中文和空格OpenCV 的imread遇到中文路径在某些版本会返回 None这是新手最常踩的坑之一。3. 车牌定位颜色和边缘两条路先跑通再谈准3.1 为什么先做定位HSV 比 RGB 好用在哪整条链路里定位是决定成败的一步。车牌定位主流两条路一是基于颜色的 HSV 阈值二是基于边缘的形态学检测。国内蓝牌底色饱和度很高HSV 空间里 H 分量对光照不敏感比 RGB 稳得多所以常见做法是先转 HSV 卡蓝色区间再用形态学把车牌连成一块矩形。import cv2 import numpy as np def locate_plate(img_path): # 读图失败直接返回避免后面全是 None 报错 img cv2.imread(img_path) if img is None: raise ValueError(图片读取失败检查路径是否含中文或文件是否存在) # 高斯模糊压噪点核大小取奇数 blur cv2.GaussianBlur(img, (5, 5), 0) # 转 HSV蓝牌的 H 大致在 100~124 区间 hsv cv2.cvtColor(blur, cv2.COLOR_BGR2HSV) lower np.array([100, 43, 46]) upper np.array([124, 255, 255]) mask cv2.inRange(hsv, lower, upper) # 开运算去小噪点闭运算把车牌区域连成块 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 c in contours: area cv2.contourArea(c) if area 2000: continue x, y, w, h cv2.boundingRect(c) ratio w / float(h) # 蓝牌宽高比大致在 2.5~5.5 之间 if 2.5 ratio 5.5: candidates.append((x, y, w, h)) return img, candidates逻辑说明先模糊再转 HSV是为了减少噪点对颜色阈值的干扰。inRange把蓝色区域变成白、其余变黑形态学闭运算把断裂的车牌连起来。参数说明lower/upper的 H 区间是蓝牌经验值S和V下限设 43 和 46 是为了排除灰白和过暗区域kernel的 (17,5) 是横向长条因为车牌是扁的面积阈值 2000 和宽高比 2.5~5.5 是过滤条件图片分辨率大就要相应调大。3.2 边缘检测兜底颜色失效时怎么办颜色法在阴天、车牌脏污、或者黄牌绿牌上会失效。这时候用 Sobel 做垂直边缘因为车牌字符区域垂直边缘密集再用闭运算连成块。常见做法是两条路都跑谁先出合理候选就用谁。def locate_by_edge(img_path): img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 垂直方向 Sobel突出字符的竖直边缘 sobel cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize3) # 二值化阈值可调 _, binary cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 横向闭运算把字符连成车牌块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) return img, contours逻辑说明Sobel 取 x 方向一阶导字符竖直笔画会形成亮边。OTSU 自动定阈值省得手调。参数说明ksize3是 Sobel 核大小越大边缘越粗闭运算核和颜色法保持一致方便对比。注意cv2.Sobel输出可能是负数被截断用CV_8U前最好先取绝对值否则边缘会丢一半这是很多人contourArea报错或结果为空的原因。4. 字符分割与识别把「京A12345」一个个抠出来4.1 校正和分割透视变换加垂直投影定位到的车牌往往是斜的直接分割字符会切歪。先做透视校正把车牌拉成正矩形再用垂直投影找字符间隙。这一步是整个链路里最考验参数的地方。def segment_chars(plate_img): # 转灰度并二值化字符为白底黑字或黑底白字要统一 gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(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 5: chars.append(binary[:, start:i]) return chars逻辑说明二值化后字符是白、背景是黑垂直投影每列白点数就是该列是否有字符。投影值从低跳高是字符起点从高落低是终点。参数说明阈值v 2里的 2 是经验值图小就调 1图大噪点多就调 3宽度 5过滤掉铆钉和噪点。注意二值化后要确认是白字黑底如果是反的投影逻辑要跟着反过来否则一个字符都切不出来。4.2 模板匹配识别简单但吃模板质量传统方案识别字符最省事的是模板匹配。把 0-9 和 A-Z 的字符模板存好分割出的字符逐个和模板算相似度取最高的。它不需要训练但模板质量直接决定识别率。def recognize_char(char_img, templates): best_char None best_score -1 # 统一尺寸否则匹配没法比 char_resized cv2.resize(char_img, (20, 40)) for label, tmpl in templates.items(): tmpl_resized cv2.resize(tmpl, (20, 40)) # 用归一化相关系数越接近 1 越像 score cv2.matchTemplate(char_resized, tmpl_resized, cv2.TM_CCOEFF_NORMED)[0][0] if score best_score: best_score score best_char label return best_char, best_score逻辑说明matchTemplate在单字符对单模板时结果矩阵只有一个值直接取[0][0]。参数说明尺寸统一到 20x40 是经验比例接近真实字符宽高比TM_CCOEFF_NORMED对亮度变化不敏感比平方差法稳。注意模板必须和待识别字符同色系都是白字黑底否则相似度会整体偏低识别全乱。5. 避坑与排查这套链路最容易翻车的 5 个地方5.1 图片读进来是 None后面全崩现象cv2.imread返回 None紧接着cvtColor报错。原因路径含中文、文件不存在、或者路径写成了相对路径但工作目录不对。解决用os.path.exists先判断中文路径改用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)读。5.2 定位框一大堆车牌混在里面找不到现象候选框几十个车牌只是其中之一。原因颜色阈值太宽把车身蓝色、天空都框进来了。解决收紧 HSV 的 S、V 下限加宽高比和面积双重过滤再按面积从大到小排序取前几个。5.3 字符粘连或断裂分割数量不对现象本该 7 个字符切出来 5 个或 9 个。原因二值化阈值不合适字符之间连在一起或笔画断开。解决换 OTSU 自动阈值或者分割前做一次开运算断开粘连投影阈值微调。5.4 模板匹配识别率低数字和字母混淆现象0 和 O、1 和 I、8 和 B 经常认错。原因模板字体和实际车牌字体不一致或者字符没做尺寸归一化。解决模板用真实车牌裁出来的字符统一尺寸和颜色必要时对易混字符加规则约束比如车牌第二位通常是字母。5.5 换一张图参数就失效现象调好的阈值换张图就崩。原因硬编码阈值只适配了当前光照。解决把 HSV 阈值、面积阈值做成可配置参数或者用 OTSU 这类自适应方法替代固定阈值别指望一套参数打天下。6. 想再准一点把识别结果做后验校验的几个技巧传统链路跑到最后识别率大概在七八成想再往上提靠的不是换更复杂的算法而是加后验校验。我一般会做三件事。第一用中国车牌规则约束普通蓝牌是「省份简称 字母 5 位字母数字」第一位必须是汉字第二位必须是字母后面五位是字母或数字凡是违反这个规则的识别结果直接标为可疑。第二对易混字符做定向修正比如把识别成 O 的第二位强制改成对应字母把 I 和 1 按位置规则区分。第三多帧投票如果你处理的是视频同一辆车连续几帧的识别结果取众数比单帧稳得多。# 车牌规则校验第一位汉字第二位字母后五位字母数字 import re def validate_plate(plate_str): # 简化规则实际省份简称可列全 pattern r^[\u4e00-\u9fa5][A-Z][A-Z0-9]{5}$ if re.match(pattern, plate_str): return True, plate_str # 不满足规则时尝试把常见混淆字符替换后再校验 fixed plate_str.replace(O, 0).replace(I, 1) if re.match(pattern, fixed): return True, fixed return False, plate_str逻辑说明正则先卡结构结构不对的直接判可疑避免把明显错误的结果输出。参数说明[\u4e00-\u9fa5]匹配汉字[A-Z0-9]{5}是后五位。注意这个规则只适用于普通蓝牌新能源牌位数不同要单独写一套。验证方法上我习惯准备一组 20 到 50 张不同光照、不同角度的图跑完统计定位成功率和整牌识别率别只看一张图的效果。定位成功率看候选框里有没有正确车牌整牌识别率看最终字符串对不对。这两个指标分开看才能知道是定位拖后腿还是识别拖后腿。最后说个我自己的习惯这套传统方案我从来不指望它上生产但它是我带新人理解图像处理链路的最好教具因为每一步都看得见、改得动。你要是拿它做毕业设计把定位和分割的中间结果图都存下来放进论文比只放一个最终识别率有说服力得多。参数别照抄拿你自己的图跑一遍再调这才是这套源码真正的用法。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网