
简介这是一套面向初学者的车牌识别课程案例源码包使用Python语言编写基于开放计算机视觉库与支持向量机等传统机器学习方法完整覆盖车牌定位、图像分割、边缘检测、形态学处理、字符识别与模型评估等环节。资源共二十四个文件包含脚本程序、训练模型数据、中文字符集压缩包、车牌样本图片以及说明文档压缩包整体大小约十五兆目录结构按训练、预测与测试模块划分便于对照学习。已有四百二十九人学习下载适合希望快速上手计算机视觉车牌识别项目、或用于课程设计与毕业设计的学生开发者。除核心源码外还提供了训练数据、字符数据集及示例截图读者可据此复现从图像预处理到车牌识别的完整流程并在此基础上尝试引入深度学习方法进一步优化识别精度。1. 车牌识别项目为什么值得你动手跑一遍一个“简单源码”的真实价值如果你刚入门 Python 图像处理手里这份“简单的车牌识别-python源码.zip”其实就是一份带你看完整流程的活教材。它要解决的不是百万级车流的智能交通系统而是“给定一张含车牌的图片输出车牌号字符串”这个明确定义的小目标。跑通它你就能把 OpenCV 里的颜色过滤、轮廓查找、二值化、模板匹配这些零散概念串成一条完整流水线往深走你还能把末尾的模板匹配换成轻量卷积网络体验一次真实的模型替换。适合三类人做课程设计的学生、想快速验证 CV 落地方案的工程师以及想拿图像处理当 Python 进阶跳板的新手。它的价值不在于识别率有多高而在于“麻雀虽小五脏俱全”你能在一小时内真正看懂每一行代码在干什么。2. 拆解一个 python 车牌识别源码定位、分割、识别三件套2.1 车牌识别的三条技术路线与简化版源码的定位现在做车牌识别业内主流方案已经分成了三层最底层是纯传统图像处理中间是手工特征加分类器最上层是端到端深度学习。一个“简单”的 Python 源码包走的几乎必然是第一条路因为它依赖少、逻辑直观、不需要 GPU也适合教学。路线典型方法精度依赖适合场景传统图像处理颜色/边缘定位 投影分割 模板匹配中等OpenCV NumPy固定场景、单张图片、学习实验机器学习HOG SVM 识别字符中高OpenCV scikit-learn字符集较小需要抗干扰深度学习YOLO 定位 CNN/CRNN 识别高TensorFlow/PyTorch GPU复杂场景、视频流、多类型车牌你手上的源码大概率属于第一类。它的核心思路是利用车牌底色蓝、黄、绿在 HSV 颜色空间里有稳定的分布范围先框出候选区域再在区域内做字符分割最后对单个字符做模板匹配或简单分类。整条链路不涉及网络训练跑通只需要 OpenCV这决定了它可以在老笔记本上飞快运行也注定它对强光照、大角度倾斜和极端模糊比较敏感。理解这一点很重要源码里没有复杂的模型文件也没有海量数据集你调参时盯住的是颜色阈值、轮廓面积、字符宽度这些可解释的参数。这正是新手最需要的——每一步失败都能从图像中间结果里看到原因而不是面对一个黑匣子。2.2 源码里的核心模块与流程从图片到车牌号的四步一个典型简化版源码的代码结构通常是这样main.py作为入口往下拆成plate_detect.py定位、plate_seg.py分割、plate_recognize.py识别。这四个模块的职责和常见实现手法如下。第一步是预处理。输入图像先转成 HSV 和灰度图因为 HSV 对颜色更友好而灰度图供边缘检测和形态学操作使用。通常会用高斯模糊降噪再用 Sobel 或 Canny 提取边缘目的是让车牌区域的垂直边缘密度显得突出。第二步是定位。简化版多半通过cv2.inRange过滤出车牌底色比如早 H 100 到 130 的蓝色区域再用形态学闭运算把分散的蓝色像素连成块最后用cv2.findContours找轮廓按面积和宽高比筛掉非车牌区域。这里有两个经典参数最小轮廓面积和宽高比范围蓝牌宽高比约 3.14黄牌更扁调不好就会把车灯、蓝色广告牌框进来。第三步是字符分割。拿到车牌区域后先做灰度、二值化再遍历每一列统计黑色像素的垂直投影投影为零的列就是字符间隔。这个过程最怕车牌倾斜和铆钉干扰所以源码里常常提前做一次霍夫直线校正或者用透视变换把车牌拉正。第四步是识别。分割出的单个字符统一缩放成固定尺寸比如 20×40 像素然后和预设的模板库计算匹配度取最相似的模板作为结果。模板库一般包含 31 个省份汉字、24 个字母和 10 个数字存在fonts/或templates/目录里。代码写出来大致长这样import cv2 import numpy as np def extract_plate_area(hsv_img, lower_blue, upper_blue): 通过 HSV 颜色范围提取车牌候选区域 lower_blue / upper_blue: 蓝牌颜色的 HSV 下界和上界 mask cv2.inRange(hsv_img, lower_blue, upper_blue) # 闭运算填补车牌颜色块中间的孔洞 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 按面积从大到小排取最大的候选 contours sorted(contours, keycv2.contourArea, reverseTrue) return contours[0] if contours else None这段代码体现了定位的核心逻辑。inRange把蓝色像素变成白色闭运算让断裂的蓝色区域连成一体findContours拿到外轮廓。需要注意的是RETR_EXTERNAL只取最外层轮廓避免把字符内部的空洞也算进来如果你发现车牌被截了一半可以检查kernel的尺寸是不是太小导致闭运算没能把左右两块的蓝色连接起来。2.3 为什么用 Python 而不是 C生态与调试优势从工程性能看C 的 OpenCV 运行速度确实快得多但这类简化源码选 Python核心原因是调试效率。图像处理 80% 的时间花在“看中间结果”上——处理完每一阶段你都要用cv2.imshow或plt.imshow看一眼确认是不是自己想要的。Python 的 REPL 和 Jupyter 允许你改一个阈值立刻看到效果不用经历编译链接的漫长等待。另外Python 在字符识别阶段接模板匹配或轻量 CNN 都极容易模板匹配就是 NumPy 数组比较轻量 CNN 可以直接套用 Keras 的几行 API。而如果你最终想把方案迁移到 COpenCV 的 Python 实现和 C 实现函数名几乎一致代码逻辑可以直接照搬。所以这类源码既适合跑通效果也适合当翻译成其他语言的蓝本。还有一点容易被忽略Python 的 OpenCV 在读取中文路径图片时存在历史问题源码通常会给图像路径加一个cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)包装。如果你解压后直接跑imread遇到读不了图先看看是不是这个原因。3. 跑通源码的完整流程环境、依赖与第一个 demo3.1 python 环境安装与虚拟环境配置附命令不管你在 Windows 还是 Linux 上第一步都是保证 Python 版本在 3.8 以上。macOS 自带的是 2.7 或 3.9不建议直接用最好去官网下载 3.10 或 3.11 的安装包安装时勾选“Add Python to PATH”这一步能避开后面python命令找不到的坑。我的习惯是任何项目都开虚拟环境避免这个源码需要 OpenCV 4.x另一个项目需要 3.x互相污染。在项目目录里执行# 创建虚拟环境名字叫 venv python -m venv venv # 激活Windows 用 venv\Scripts\activatemacOS/Linux 用 source venv/bin/activate venv\Scripts\activate # 升级 pip防止依赖装到旧版本 python -m pip install --upgrade pip激活后命令行前面会出现(venv)后面所有 pip 安装都装进这个独立环境删掉整个文件夹就完全卸载。新手最容易踩的坑是“装了两个 Python”导致python和pip指向不同版本在虚拟环境里不存在这个问题因为python和pip都指向虚拟环境里的同一条路径。3.2 安装 OpenCV 等依赖requirements.txt 与 pip 常见坑解压 zip 后通常根目录会有一个requirements.txt写着opencv-python、numpy之类。如果没有手动安装也行但版本要配对。目前车牌识别最简单组合是pip install opencv-python numpy这两个装完就能跑通传统路线。要注意的是opencv-python和opencv-contrib-python不能同时装否则会出现AttributeError: module cv2 has no attribute xxx。如果你装了多个 OpenCV 版本先卸载再重装pip uninstall opencv-python opencv-contrib-python -y pip install opencv-python另外如果源里用了imutils或scikit-image也一并装上。装完验证一下python -c import cv2; print(cv2.__version__)能输出版本号说明环境通了。如果输出ModuleNotFoundError先执行python -m pip list看包是否在再确认虚拟环境是否激活。Linux 用户还可能遇到缺少libGL.so.1的问题需要sudo apt install libgl1 libglib2.0-0这是 OpenCV 依赖系统图形库不是 Python 的问题。3.3 跑通最小 demo命令行参数与输出结果说明这类源码的入口往往长这样通过命令行参数传入图片路径然后在终端打印车牌号并弹窗显示中间结果。常见做法是用argparse代码结构如下import argparse import cv2 from plate_detect import detect_plate from plate_recognize import recognize_chars def main(): # 显式声明输入图片路径 parser argparse.ArgumentParser(descriptionsimple plate recognition) parser.add_argument(--image, requiredTrue, helppath to input image) parser.add_argument(--save, actionstore_true, helpsave visualization result) args parser.parse_args() img cv2.imread(args.image) if img is None: print(ERROR: cannot read image, check path, args.image) return plate_region, boxes detect_plate(img) if plate_region is None: print(No plate detected) return plate_text recognize_chars(plate_region, boxes) print(Plate:, plate_text) if args.save: cv2.imwrite(result.jpg, plate_region) if __name__ __main__: main()跑的时候用一条命令即可python main.py --image ./test.jpg --save参数说明--image是必须项图片换成你本地的路径--save是可选开关加上会把裁剪出的车牌区域存成result.jpg方便你对照源码的定位效果。这段代码的作用是串联整套流程detect_plate返回车牌区域图像和每个字符的框recognize_chars对框内区域识别并拼接字符串。如果识别结果顺序不对比如把“粤B12345”识别成“B粤12345”问题出在分割后字符没有按 x 坐标排序去代码里找sorted(boxes, keylambda b: b[0])这一行就能修。3.4 调整三个关键参数让识别更稳定HSV 范围、轮廓面积、字符尺寸跑通 demo 之后你会发现同一张图换个小分辨率或换个角度就翻车根源往往在三个参数上。第一个是 HSV 颜色范围。蓝牌的 H 大概在 100~130S 在 100~255V 在 100~255。但雨天、黄昏、路灯下都会变。最简单的方法是用 OpenCV 的cv2.createTrackbar写一个小工具实时调或者临时把inRange的上下界打出来用取色工具在图上采样几个点取均值。绿牌和新能源车牌是另一个色域黄牌又不一样源码里通常只写死一种你要根据自己的测试图扩充范围。第二个是轮廓面积阈值。findContours之后如果直接取最大轮廓很可能把整辆车最大的轮廓比如车身当成车牌。所以代码里往往写if cv2.contourArea(contour) 1000: continue之类的过滤。这个 1000 是经验值假设输入图是 1080p车牌区域通常占 5000~20000 像素如果摄像头离车远面积会很小需要把阈值下调到 500 左右否则直接跳过了。第三个是字符尺寸归一化。分割出的字符宽高比不一致模板匹配前要先resize成模板的尺寸比如 20×40。这里要注意cv2.resize默认是线性插值对二值化字符可以用cv2.INTER_CUBIC平滑边缘但不要随便改成INTER_AREA否则字符笔画会变粗和模板匹配的相似度反而下降。如果你发现“8”老被识别成“B”多半是归一化时没有保持宽高比导致字形被拉伸变形。4. 让识别精度从“能跑”到“好用”数据与模型训练4.1 数据从哪来公开车牌数据集与自制数据标注传统模板匹配的词典有限遇到字体变体或模糊就会翻车。想提升精度最直接的路径是换成数据驱动的分类器前提是你得有字符级别的数据。常见来源有三个高校公开数据集、爬虫抓取注意合规、自己用摄像头采集。对于训练字符分类器你并不需要整张带车牌的图片而是需要已经切好的单字符图片和对应标签。如果你手头只有整张车牌图片可以复用源码里的分割函数批量生成字符样本。写一个小脚本遍历所有车牌图调用detect_plate和字符分割把每个字符保存成label/字符_编号.png。我做过一次收集 1000 张车牌图能切出大概 7000 个字符样本省去手工标注的苦力。这里有个要点分割结果质量直接影响数据集质量宁可切错删掉也不要混入模糊样本否则训练出的模型泛化能力奇差。数据增强也很关键。字符图片二值化后可以用cv2.erode/cv2.dilate模拟笔画粗细变化用仿射变换模拟倾斜加椒盐噪声模拟污损。每张原图生成 5~10 张变体数据集就能扩到几万张。注意不要在字符上做随机裁剪过多否则会把“1”截成“l”。4.2 模板匹配改轻量 CNN代码迁移示例模板匹配的问题是它没有“学习”能力只能找最像的模板。换成 CNN 后字符识别变成一个标准的图像分类任务。这里不必上 ResNet一个两层卷积加全连接的轻量网络就足够因为字符本身是灰度、小尺寸、高对比度。迁移时保持源码里的分割模块不动只替换recognize_chars的内部实现。原来的模板匹配代码大概是把归一化后的字符和模板库做距离比较改成调用模型预测即可。如下是一个典型的替换示例# character_model.py from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout def build_char_cnn(input_shape(20, 40, 1), num_classes65): model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activationrelu), MaxPooling2D((2, 2)), Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) return model这段代码定义了一个 2 层卷积的 CNN。Conv2D的卷积核大小 3×3 适合字符笔画MaxPooling减小分辨率增强平移不变性Dropout防过拟合因为字符数据集往往不够大。num_classes65对应 31 个省份汉字 24 个字母 10 个数字字母 I 不去掉的话是 24严格来说 31241065如果你把汉字省份也当类别。训练时输入要先做灰度、归一化到 0~1并 reshape 成(batch, 20, 40, 1)否则模型会报维度错误。模型替换后recognize_chars就不再依赖模板目录而是从char_model.h5加载权重。原来的模板匹配函数可以保留当作模型预测结果的兜底两边一致性低时再人工干预。4.3 训练一个字符分类器并导出模型TF/Keras 示例数据准备好后训练过程非常简单。你需要把每个字符图片的路径和标签存成列表然后用ImageDataGenerator做数据增强并训练。下面是我常用的最小训练脚本import tensorflow as tf from tensorflow.keras.preprocessing.image import ImageDataGenerator # 按目录结构读取数据每个子目录名就是字符标签 data_gen ImageDataGenerator( rescale1./255, rotation_range10, width_shift_range0.05, height_shift_range0.05, validation_split0.2 ) train_data data_gen.flow_from_directory( char_dataset/, target_size(40, 20), color_modegrayscale, batch_size32, class_modecategorical, subsettraining ) val_data data_gen.flow_from_directory( char_dataset/, target_size(40, 20), color_modegrayscale, batch_size32, class_modecategorical, subsetvalidation ) model build_char_cnn() model.fit(train_data, validation_dataval_data, epochs10) model.save(char_model.h5)参数说明rotation_range10控制随机旋转角度不超过 10 度模拟车牌倾斜width_shift_range和height_shift_range做随机平移让模型学会容忍字符位置偏移target_size必须是 (40, 20)也就是宽 40 高 20和源码里的归一化尺寸保持一致。训练 10 轮后模型精度通常能到 95% 以上如果欠拟合把epochs加到 30如果过拟合把Dropout提到 0.6 或扩大数据集。导出char_model.h5后在识别模块里用tf.keras.models.load_model加载即可。注意训练集和验证集的划分比例不能太小20% 的验证集意味着每种字符至少要有 20 张以上样本。如果你的数据严重不均衡比如“粤”字出现 2000 次“翼”字只有 5 次模型会对高频类别过拟合低频类别几乎不认得这种情况下建议按字符类别抽样保证每个类别最少 50 张。5. 车牌识别的高频翻车现场与排查手册5.1 定位不到车牌光照与颜色范围问题现象输入图片里人眼很清楚地看到蓝色车牌但detect_plate返回None或者框出来的区域偏到车子挡风玻璃上。原因蓝牌的颜色在 HSV 空间里不是恒定值。晴天直射时蓝色偏亮阴天偏暗黄昏时偏灰而inRange的阈值是写死的只要 V 通道整体上移或下移你所设定的 S 和 V 下界就把车牌像素滤掉了。另外如果图片太大车牌被缩放后蓝色像素占比变少形态学闭运算的核尺寸相对显得过大把小色块融掉了。解决先做一次可视化把mask和原图叠着显示看蓝色区域是否被正确选中。如果 mask 里车牌是黑色说明 HSV 范围不对。我的做法是写一个滑动条调参脚本把lower_blue和upper_blue暴露给cv2.createTrackbar实时看 mask 变化。定好范围后再改回常量。另外对输入图做一次降采样将长边控制在 800~1000 像素既能减少计算量也能让车牌在图上占的比例更合理轮廓面积阈值更好设置。5.2 字符分割粘连二值化与倾斜校正现象切出来的字符连成一坨“M”和“1”没法分开或者同一个字符被切成两半比如“川”被纵向投影误切成两个“1”。原因二值化阈值选得不合适导致字符内部的低对比度区域被抹黑或断开。或者车牌有倾斜字符投影在水平方向上重叠纵向投影找不到干净的间隙。有时候车牌上面的铆钉和边缘也被框进区域投影时会形成假的竖直缝隙。解决分割前一先做霍夫直线检测找到车牌边框的倾斜角用仿射变换把车牌转正或者用cv2.minAreaRect求出包围车牌的最小外接矩形再按角度旋转。二值化方面不要用固定阈值改用 Otsucv2.threshold的THRESH_OTSU或自适应高斯阈值。我通常会在二值化前先做一次cv2.medianBlur降噪半径 3能有效减少字符边缘毛刺。如果投影法还是不稳定可以改用连通域分析按字符的连通区域划分但这种方法对同一个字符断成多块的情况无能为力所以形态学处理要把断裂处接回去。5.3 中文省份识别错误字符集不全与数据增强不足现象其他字符都能正确识别就是省份汉字频繁出错比如“京”识别成“津”“苏”识别成“禾”。原因模板匹配或 CNN 的训练数据里某些汉字的样本太少或者汉字字体差异大车牌字体、印刷宋体、手写误触。模板匹配的模板图片本身可能只有一种字体遇到路牌上稍微粗一点的“京”就错。CNN 虽然能泛化但如果训练时“津”的样本数量只有“京”的五分之一模型就缺乏区分细节特征的依据。解决先统计你在测试集上的错误矩阵看哪些字符容易被混淆。如果只有省份字符错单独扩充省份字符数据集每个省份汉字至少 100 个不同样本。做数据增强时对汉字做随机加粗和腐蚀让模型适应笔画粗细变化。还可以在训练时把汉字类别和字母数字类别分开建模分别训练两个分类器因为汉字的区分特征和字母数字完全不在一个尺度上。6. 把车牌识别做成可评估的验证方案指标、进阶与个人习惯6.1 用准确率与漏报率验证识别效果跑通和调参之后你需要一套客观指标来判断到底能不能用。最直观的指标是“整牌识别率”也就是字符序列完全正确的图片数占总测试图片数的比例。但只报这个太笼统我会拆成两个指标定位召回率正确框出车牌区域的图片比例和字符识别准确率所有字符里识别正确的比例。写一个简单的脚本统计import json from plate_detect import detect_plate from plate_recognize import recognize test_data [ {image: test/001.jpg, truth: 粤B12345}, {image: test/002.jpg, truth: 苏A8D0E}, ] correct_plate 0 total_char 0 correct_char 0 for item in test_data: pred recognize(item[image]) if pred is None: continue if pred item[truth]: correct_plate 1 total_char len(item[truth]) correct_char sum(a b for a, b in zip(pred, item[truth])) print(整牌正确率:, correct_plate / len(test_data)) print(字符准确率:, correct_char / total_char)注意zip会截断长短不一致的字符串所以统计前先判断长度否则漏字符会被误算。我习惯每次调完参数都跑一遍这组脚本如果整牌正确率提升但字符准确率下降说明可能在某张图上定位框更准但分割变差要把中间结果可视化找原因。6.2 从一个图片 demo 走向摄像头实时识别的进阶路线如果图片识别已经稳定下一步往往是接摄像头视频流。源码里main.py只处理单张图片改成实时时核心优化有两个。一是限制处理频率每 5~8 帧只处理一帧因为车牌变化缓慢没必要逐帧识别。二是先在全帧上检测车的水平边缘带缩小车牌搜索区域再对候选区域做精细定位这样能把处理耗时从 200ms 降到 50ms 以内。代码层面只要将读图改成cv2.VideoCapture(0)循环把每帧传给detect_plate然后显示结果框即可。但注意摄像头数据是实时流如果处理耗时超过帧间隔cap.read()会越积越多导致画面越来越卡。此时在循环里加一个简单的丢帧逻辑cap cv2.VideoCapture(0) frame_interval 5 count 0 while True: ret, frame cap.read() if not ret: break if count % frame_interval 0: result process_frame(frame) count 1同时为了不让调试时窗口堆积cv2.imshow要显示原图和结果图不要显示 10 个中间过程窗口。我吃过这个亏开了 8 个窗口最后要一个个用鼠标关实在浪费时间。把中间结果拼接成一张大图再显示是收益率非常高的调试习惯。我的个人习惯是每个项目都建一个test_images文件夹里面按“正常、倾斜、模糊、夜间、雨雪”分类每次调参后全量跑一遍而不是只测刚调成功的那张图。这种黑白双面的验证能帮你发现“A 图调好了、B 图又丢了”的跷跷板问题。车牌识别是图像处理里最适合练手的方向因为这个看似“简单”的源码把定位、分割、识别、调参、评估全部串了起来而且每个环节你都有充分的图像中间结果可以做判断。跑完这份源码再回头去看那些所谓的高性能车牌识别系统你会心里有数知道它们到底强在哪个模块上。希望这些经验能帮你少踩几个坑早日把自己调的车牌识别跑稳。本文还有配套的精品资源点击获取