OpenCV+Python车牌识别系统源码解析:从定位到字符识别
简介基于OpenCV与Python实现的车牌识别系统源码属于个人毕业设计项目答辩评审分达98分。面向计算机、通信、人工智能、自动化等专业的学生、老师或从业者可作为期末课程设计、课程大作业或毕业设计的直接参考也适合初学者从源码层面理解图像处理与字符识别。资源共48个文件压缩包约29.42MB包含6个Python脚本主程序、车牌定位、字符识别、图像处理等模块、2个SVM训练数据文件、2个中英文字符库压缩包、1份说明手册及大量测试图片目录结构清晰便于按模块阅读和二次开发。目前已有486人浏览学习。代码均经过调试测试可正常运行能帮助快速搭建车牌识别流程直观掌握基于OpenCV的定位、分割、特征提取与SVM分类思路有能力的开发者还可替换或扩展模块实现不同场景下的识别优化整体具有较高的学习借鉴价值。1. 车牌识别源码到手先别急着乱跑把这三个入口文件搞明白再动手毕业设计拿到一份“基于 OpenCV Python 的车牌识别系统源码”多数人第一反应是直接python main.py跑一张图看到框出车牌就以为完事了。但真正答辩、验收甚至后续改进时你会发现这套源码的核心不在main.py而在三个东西颜色定位模块、字符分割模块、字符识别模块。这套源码解决的是“静态图片或视频帧里的蓝底白字车牌自动定位与字符读取”适合正在做毕设、准备课程设计或想低成本入门 OpenCV 图像处理流程的开发者。反直觉的结论先放这这套传统视觉方案里改两个颜色阈值往往比换一个识别模型更能提升准确率。2. 车牌定位整个系统的地基九成准确率问题出在这一步车牌定位决定了后续能不能继续往下走。定位不准、框偏了后面分割和识别再努力也白搭。这一章把定位链路拆开讲颜色空间转换、形态学处理、轮廓筛选每一段都对应到源码里的具体函数和可调参数。2.1 颜色空间转换为什么 OpenCV 默认的 BGR 不好使源码里第一步一般是这样写的hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV)这句转换是整个定位的地基。OpenCV 默认读进来的图是 BGR 三通道但 BGR 对光照变化极其敏感——同一块蓝色车牌晴天和阴天拍出来的 BGR 数值差异很大你很难用固定范围把它筛出来。而 HSV 把颜色拆成色调、饱和度、明度三个维度其中色调 H 对光照相对不敏感筛蓝色车牌时主要锁 H 通道就能稳定圈定目标区域。源码里一般会用cv2.inRange()生成掩码常见取色范围是lower_blue np.array([100, 100, 50]) upper_blue np.array([130, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue)这套参数的物理含义是H 在 100 到 130 之间是蓝色系S 不低于 100 保证颜色够浓V 不低于 50 保证不是暗黑蓝。实际项目里这块非常玄学因为不同省份的车牌蓝色有细微差异——新能源车牌的渐变蓝、老车牌的深蓝、相机白平衡偏掉之后的淡蓝都会让固定阈值失效。我一般会把lower_blue里的 H 下限放宽到 90把 S 下限降到 80多牺牲一点精度去换召回率后续靠形态学操作和轮廓筛选去噪。2.2 形态学操作与轮廓筛选把候选区域从一堆蓝色噪点里抠出来颜色掩码拿到手之后画面里通常不只是车牌还有蓝色车身、蓝色路牌、蓝色衣服这时候需要形态学操作把分散的蓝色区域聚合成完整的块。源码里常见做法是先闭运算再开运算kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) opened cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel)内核大小写(17, 5)是有讲究的车牌是横向矩形横向用 17 像素可以把被字符镂空打断的蓝色区域重新连起来纵向用 5 像素避免把相邻车道的车牌也粘连进来。闭运算先补洞开运算再消掉细小噪点这一步做完车牌区域基本是画面里最大最完整的蓝色连通块。随后通过轮廓筛选定位车牌矩形这是源码里最值得调试的一段contours, _ cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h ratio w / h if area 2000: continue if ratio 2.5 or ratio 5.5: continue roi img[y:y h, x:x w]area 2000过滤掉小噪点ratio限定在 2.5 到 5.5 之间是因为标准车牌宽高比约为 3.14加上拍摄角度偏差2.5 到 5.5 是实测比较稳的区间。如果你发现定位框经常框到半个车身优先检查ratio上限如果小物体误检多优先调area下限和形态学内核。2.3 定位链路整合把上面三段串进一个函数源码的detect.py一般会把这些步骤封装成一个函数def locate_plate(img): hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, lower_blue, upper_blue) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) opened cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel) contours, _ cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w * h 2000 or not (2.5 w / h 5.5): continue return img[y:y h, x:x w] return None这段代码把颜色转换、形态学、轮廓筛选全部串成一条流水线。注意RETR_EXTERNAL只取最外层轮廓避免车牌内部字符边缘干扰。如果定位结果总是把蓝色车身一起框进来问题大概率出在 HSV 的 S 通道试试把[100, 100, 50]里的 S 提高到 120蓝车身通常饱和度不如车牌照鲜艳。3. 字符分割与识别定位框出来之后真正的细节战才刚开始定位拿到的是车牌的矩形区域可这个区域里除了字符还有边框、铆钉、车牌底色。字符分割的任务就是把这 7 个字符一个个切出来识别阶段再逐个判断。分割切歪或者切粘连识别准确率直接崩。3.1 二值化与水平投影先把车牌上下边缘和铆钉去掉定位出来的车牌图先转灰度再二值化通常用大津法自动算阈值gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)用THRESH_BINARY_INV是因为字符是深色、背景是浅色反相之后字符变白、背景变黑方便后续投影分析。紧接着做水平投影也就是统计每一行的白色像素个数row_hist binary.sum(axis1)这一步的目的是找到车牌字符区域的上下边界。车牌上方有白色边框、左右有时候有汉字和字母的干扰但最典型的问题是铆钉——位于车牌上下沿的金属小点二值化后经常形成两个亮斑水平投影会多出两段非零区间。解决方式是投影后只取最长的连续非零段或者直接按比例裁掉上下各 5% 的像素。源码里一般会这样处理rows_with_char [i for i, v in enumerate(row_hist) if v 0] top min(rows_with_char) bottom max(rows_with_char) binary binary[top:bottom, :]3.2 垂直投影分割把一排字符逐个切出来水平裁剪完之后再对每一列统计白色像素做垂直投影col_hist binary.sum(axis0) in_char False char_boxes [] start 0 for i, v in enumerate(col_hist): if v 0 and not in_char: in_char True start i elif v 0 and in_char: in_char False char_boxes.append((start, i))这段代码的逻辑是扫描每一列遇到白色像素就开始记一个字符起点遇到全黑列就认为一个字符结束。得到的每个(start, i)区间就是一个候选字符的左右边界。理想情况下会得到 7 个区间但实际很容易合并出 6 个或 8 个。最常见的翻车点是字符之间的间隙被噪声填满两个字符连成一个区间或者“1”和“7”这种窄字符中间断开被切成两截。应对办法是加宽度过滤测量所有区间宽度的中位数把大于 1.8 倍中位数的区间视为粘连尝试在中间最细处切一刀把小于 0.3 倍中位数的区间视为噪声直接丢弃。这部分代码在源码里通常叫split_chars()参数要细调因为汉字“京”“沪”结构复杂垂直投影往往比字母宽不少。3.3 字符识别模板匹配和 SVM 是这套源码最常用的两种方案字符切好之后进入识别环节。毕业设计源码里最常见的是模板匹配预先准备字符模板库把待识别字符缩放到模板同尺寸算相似度取最高分。核心代码长这样def match_template(char_img, templates): char_img cv2.resize(char_img, (20, 40)) best_score -1 best_label for label, templ in templates.items(): templ cv2.resize(templ, (20, 40)) score cv2.matchTemplate(char_img, templ, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(score) if max_val best_score: best_score max_val best_label label return best_label, best_score模板匹配不用训练代码量小适合毕设展示原理但弱点很明显字体稍微变化、模板图光照和实际图不一致得分就上不去。进阶版源码会用 HOG 特征加 SVM 分类器提取 HOG 特征向量后训练一个多分类器识别率会高不少代价是代码多了两百行。3.4 识别拼接从定位到输出车牌号的完整调用plate_box locate_plate(img) if plate_box is not None: chars split_chars(plate_box) result for c in chars: label, _ match_template(c, templates) result label print(识别结果:, result)这段把二、三章的东西串起来了。注意split_chars()返回的字符个数可能不是 7有时第二个字符是字母后面 5 个是数字或字母所以不要在字符个数上写死用列表长度判断一下不够 7 个就丢弃重来才是工程上稳妥的做法。4. 环境搭建与源码运行把依赖装明白避免在第一步浪费时间源码拿到手很多人卡在跑不起来。这一章讲环境配置和运行参数按步骤走完就能跑通 demo。4.1 环境依赖Python 版本和 OpenCV 的坑这个项目依赖非常少核心就是 OpenCV 和 NumPy。建议用 Python 3.8 到 3.10 之间再新或再老都可能碰到兼容问题。安装命令pip install opencv-python4.8.1.78 numpy1.24.3版本号要加因为新版 OpenCV 4.9 之后有些函数接口变了。装完立刻验证python -c import cv2; print(cv2.__version__)如果你之前装过opencv-contrib-python建议先卸载两个包共存会把cv2搞坏。另外 macOS 上跑这种项目偶尔会遇到cv2.error: OpenCV(4.6.0) ...的报错多半是读取视频文件时解码器问题图片识别不受影响。4.2 源码目录结构与运行入口这份源码一般会按模块分文件主入口长这样. ├── main.py # 主程序入口 ├── detect.py # 车牌定位 ├── segment.py # 字符分割 ├── recognize.py # 字符识别 ├── templates/ # 模板图片目录 │ ├── chars/ # 按字符命名的模板 │ └── digits/ ├── test_images/ # 测试图片 └── requirements.txt跑单张图片时python main.py --image test_images/鲁A12345.jpg源码里一般用argparse传参--image指定输入图路径。有视频检测版本的还会多一个--video参数。第一次跑通建议用源码自带的测试图不要一上来就用自己的手机照片手机照片的白平衡和压缩质量很容易让定位失败你会绕远路。4.3 关键参数速查表参数位置默认值作用调参方向lower_blue[100, 100, 50]HSV 蓝色下限偏色就放宽 H 到 90upper_blue[130, 255, 255]HSV 蓝色上限车牌发绿就上调 H 到 140形态学内核(17, 5)闭运算连通度车牌小就调小到(11, 3)轮廓最小面积2000过滤小噪点误检多就调大到5000宽高比区间2.5 ~ 5.5车牌长宽约束只检测新能源车牌可缩到2.8 ~ 3.5模板尺寸(20, 40)匹配前统一尺寸模板库清晰度差就改用(24, 48)调参时一次只动一个改完测 10 张以上图片再下结论不要拿一张图调到完美就收工。样本覆盖要包含白天、傍晚、阴天、近距离、远距离、轻微倾斜这几个场景各来两张比拿 20 张同一场景的图反复调更有效。5. 避坑指南五个高频问题每个都是血泪经验换来的这套系统看着简单跑起来全是坑。以下五个问题是我在这类项目里反复踩过、也帮别人排过的高频故障按“现象 → 原因 → 解决”来写你照着查能省半天时间。5.1 蓝色车身喧宾夺主定位框圈住一整面车现象检测结果里车牌没框住倒是一大片蓝色车身被当成候选区域宽高比还恰好落在区间里。原因蓝色车身在 HSV 色彩空间里和车牌同属蓝色系形态学闭运算又把小色块连成了完整的区域轮廓筛选中面积和宽高比全部通过。很多车的深蓝色车身在低光照下 S 通道低而车牌是反光材料S 通道明显偏高。解决把 HSV 的 S 通道下限从 100 提到 130优先把暗色车身穿过滤再配合面积上限过滤比单纯调 H 通道效果好。代码里加一个if area 100000: continue防止大块车身进入候选。这个参数对新能源浅蓝车牌不友好如果你要识别的多是新能源车反向把 S 下限降到 70 再用形态学去噪。5.2 铆钉干扰导致水平分割多出两个横条现象字符分割后第 1 个字符顶部或底部多出一条很窄的横条识别阶段把它当成独立字符处理最终输出结果多一个字符。原因车牌上下沿的铆钉二值化后是白色小圆点水平投影里它们会形成短的连续段跟真正的字符区域连在一起。解决水平投影后对每段非零区域做高度判断小于字符平均高度 1/3 的直接丢弃。另外可以在分割前先做一次高度裁剪——统计投影中各段高度取最大段为基准只保留高度在 50% 到 150% 范围内的区域。这两个办法能挡掉八成铆钉问题。5.3 光照不均导致二值化后字符断成碎块现象晴天车牌照在车身上半边车牌亮半边暗大津法算出的全局阈值把暗部的字符二值化成了黑色投影分割时字符中间断掉识别结果乱码。原因cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)是全局阈值整张图只有一个阈值光照不均时暗部会低于阈值。解决改用自适应阈值代码就一处变化binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 10)注意自适应阈值对字符笔画的粗细很敏感31是计算邻域大小10是常数校正。如果笔画变成空心或者边缘粗糙把邻域调到 51校正调到 7。代价是处理时间稍微变长图片识别场景完全感受不到差别。5.4 车牌定位框是歪的垂直投影切割全乱现象定位出的矩形区域是倾斜的字符上下不对齐投影法切出来的字符直接错位识别率暴跌。原因cv2.boundingRect()返回的是正矩形不含角度信息。车辆行驶中摄像头拍到的车牌经常有俯仰角或旋转正矩形框会同时框进车牌的空白背景。解决先用cv2.minAreaRect()拿到最小外接旋转矩形再根据角度把 ROI 旋转摆正。核心代码rect cv2.minAreaRect(cnt) angle rect[2] if 0 angle 45: angle angle elif 45 angle 90: angle angle - 90 M cv2.getRotationMatrix2D(center, angle, 1.0) rotated cv2.warpAffine(img, M, (w, h))角度归一是坑最多的地方OpenCV 返回的角度范围是 0 到 90不同姿势下车牌角度表现不一致需要先用实验数据摸清你的场景角度分布再定方向。一般倾斜不超过 5 度时不用摆正强行旋转反而引入插值噪声。5.5 模板匹配把“0”和“O”搞混“8”和“B”也分不清现象识别结果把字母 O 识别成数字 0B 识别成 8导致输出车牌号在车管所校验时对不上。原因模板匹配只看像素相似度字符“0”和“O”在统一缩放后肉眼都难分辨TM_CCOEFF_NORMED得分差距可能只有 0.02模板图稍有一点字体差异就会翻转结果。解决加入字符位置先验。车牌第二位的字符一定是字母第 6 和第 7 位必是数字在这个前提下做候选集约束比如第一位只从汉字模板里找第二位只从字母模板里找。代码里加一个映射表限定搜索范围比换任何识别算法都见效快。6. 再进一步批量识别与准确率统计让毕设从“跑通了”变成“立得住”答辩时老师最常问的一个问题是“你识别率到底多少”很多人答不上来因为从来没统计过。这一章就讲怎么给这套源码加一个批量评估脚本同时给一个模板匹配改进方向。先做最基础的事批量跑测试集把每张图的识别结果和真实车牌号对比统计准确率。新建一个 CSV 文件两列一列叫filename一列叫label写成这样# test_labels.csv 格式 filename,label test_images/1.jpg,京A12345 test_images/2.jpg,沪B67890然后写统计脚本import csv import cv2 from detect import locate_plate from segment import split_chars from recognize import match_template def calculate_accuracy(label_file, label_dict): correct 0 total 0 with open(label_file, newline, encodingutf-8) as f: reader csv.reader(f) next(reader) for filename, true_label in reader: img cv2.imread(filename) plate locate_plate(img) if plate is None: total 1 continue chars split_chars(plate) if len(chars) ! 7: total 1 continue pred .join(match_template(c, label_dict)[0] for c in chars) total 1 if pred true_label: correct 1 else: print(f{filename}: 预测 {pred}, 真实 {true_label}) return correct / max(total, 1) acc calculate_accuracy(test_labels.csv, templates) print(f整体准确率: {acc:.2%})这个脚本的工程价值在于你能快速看到哪些图片识别错了、错在哪个字符、是定位问题还是分割问题还是识别问题。我自己的经验是跑完这个脚本你会发现 60% 的失败发生在定位阶段字符识别反而是最省心的环节。再给一个模板匹配的小改进方案实测能把“0/O”“1/I”这类易混字符的错误率降一半。方法是做多模板投票每个字符准备两套以上不同字体的模板识别时让这个字符跟所有模板打分取最高分同时加入字符位置约束修正。比如某个字符位于第 2 位且得分最高的模板是数字直接丢弃从字母模板里重新找次高得分。实现位置在match_template函数里加一段判断逻辑if idx 1 and best_label.isdigit(): best_score -1 for label in letter_templates: score compute_score(char_img, letter_templates[label]) if score best_score: best_score score best_label label这套流程走了很多遍之后我的习惯是每次拿到类似的车牌识别源码不管任务多急都先强制自己把 10 张样例图跑一遍、写一遍批量评估、再调第一版颜色参数。整个过程不超过半小时但能省掉后面密集的盲目调参时间。车牌识别这类传统视觉项目最大的成本不是代码是数据分布摸不清楚时的反复试错。希望帮到你。本文还有配套的精品资源点击获取