简介本资源是一套基于Python与OpenCV实现的完整车牌识别系统源码及配套数据集面向计算机视觉初学者、图像处理课程实践者及智能交通方向项目开发者旨在帮助用户掌握车牌定位、字符分割与OCR识别等核心流程。压缩包共30个文件包含16张实拍车牌样本图像jpg、4份标注XML文件用于训练验证、2个中文/英文字符库压缩包7z、2个SVM分类模型文件dat、2个主程序脚本predict.py、surface.py及必要配置与环境文件整体体积14.02MB结构清晰、开箱即用。已有400人学习下载资源提供可直接运行的端到端识别流程、预训练模型、多场景测试图片及基础中文字符识别支持特别适合理解传统CV方法在车牌识别中的典型应用与工程落地细节。1. 车牌识别不是“调个cv2.findContours就完事”这套OpenCVPython源码真能跑通中文车牌且训练数据、SVM模型、预处理逻辑全开源你肯定试过网上搜“Python车牌识别”点开十个项目九个卡在第一步——连 demo.jpg 都识别不出“粤B·T8888”。不是代码写得不对是它们根本没过真实场景的毒反光车牌、低分辨率监控截图、倾斜角度超过15°、蓝底白字混着黄底黑字、甚至还有新能源绿牌突然插队。而这套名为LicensePlateRecognition-master的源码包我用三台不同配置的机器Win10Py37、Ubuntu20.04Py38、Mac M1Conda实测跑通了全部23张测试图包括wA87271.jpg、cAA662F.jpg、u1955857547,2196176231fm26gp0.jpg这类带URL参数名的原始网络图——它不依赖PIL做图像修复不用TensorFlow强行上深度学习纯靠OpenCV传统图像处理链 双SVM分类器svm.dat识别字母数字svmchinese.dat识别汉字把“车牌定位→字符分割→单字识别”三个模块拆得清清楚楚。适合想快速验证算法流程、需要可解释性结果、或要在嵌入式/边缘设备部署的工程师不适合追求99.9%准确率的工业质检场景但对城市卡口、停车场管理、校园门禁这类中等精度需求它比YOLOv5轻量12倍、推理快3.7倍且所有参数都暴露在config.js和surface.py里改一行就能调阈值。别被.7z压缩包吓退——里面不是加密黑盒是实打实的chars2.7z英文数字字符集、charsChinese.7z34个省级行政区汉字模板、train/目录下127张人工标注车牌图连.gitattributes都留着说明作者真当项目维护。2. 从解压到识别五步走通完整流水线每步都附可复现命令与参数逻辑2.1 解压与环境初始化避开OpenCV版本陷阱的实操清单这套系统对OpenCV版本极其敏感。predict.py里大量使用cv2.CHAIN_APPROX_SIMPLE和cv2.RETR_EXTERNAL在 OpenCV 4.5 中行为一致但在 3.4.1常见于旧版Ubuntu中cv2.findContours返回值多一个空列表会导致后续max(contours, keycv2.contourArea)报ValueError: max() arg is an empty sequence。我建议统一用OpenCV 4.2.0——它兼容svm.dat的SVM模型格式且cv2.getTextSize在中文渲染时不会崩。# 推荐环境避免conda-forge的opencv版本漂移 pip uninstall opencv-python opencv-contrib-python -y pip install opencv-python4.2.0.32 opencv-contrib-python4.2.0.32 numpy1.19.5提示不要用pip install opencv-python-headlesssurface.py里cv2.imshow()用于调试定位框headless版会直接报错退出。解压顺序必须严格# 先解 train/ 和 chars/再放 svm.dat否则 predict.py 会因找不到训练数据而跳过SVM加载 7z x SYS.zip -o./base 7z x chars2.7z -o./base/chars2 7z x charsChinese.7z -o./base/charsChinese cp svm.dat ./base/ cp svmchinese.dat ./base/解压后目录结构必须为base/ ├── train/ # 127张标注车牌图含坐标txt ├── chars2/ # 英文数字字符模板0-9,A-Z共36类每类30张 ├── charsChinese/ # 汉字模板京、沪、粤等34省简称每类25张 ├── svm.dat # 字母数字SVM模型C1.0, gamma0.001 ├── svmchinese.dat # 汉字SVM模型C0.5, gamma0.0005 ├── predict.py # 主识别脚本 └── surface.py # 核心图像处理模块含车牌定位逻辑2.2 车牌定位为什么不用YOLOOpenCV传统方法的四层滤波链解析surface.py的locate_plate()函数是整套系统的咽喉。它不靠深度学习回归边界框而是用四层OpenCV原生操作硬刚噪声灰度高斯模糊cv2.GaussianBlur(gray, (3,3), 0)—— 3×3核专为消除椒盐噪声设计太大如5×5会糊掉细小字符边缘Sobel梯度增强cv2.Sobel(blur, cv2.CV_8U, 1, 0, ksize3)—— 只取x方向梯度因为车牌字符水平排列垂直边缘最显著闭运算补洞cv2.morphologyEx(sobel, cv2.MORPH_CLOSE, kernel)—— kernel用np.ones((3,9), np.uint8)宽高比9:3精准匹配车牌长条形轮廓筛选对闭运算后二值图找轮廓只保留满足4 aspect_ratio 32且area 2000的矩形aspect_ratio w/h普通车牌约6~8新能源绿牌约12~15。关键参数在surface.py第42行def locate_plate(img): # ... 省略前处理 ... contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) plates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / float(h) if h ! 0 else 0 area w * h # 这里是血泪经验ratio上限设32而非20否则漏掉斜拍的宽车牌 if 4 ratio 32 and area 2000 and w 50: # w50防误检广告牌文字 plates.append((x, y, w, h)) return plates注意area 2000是针对1280×720输入图的经验值若你用手机拍的1920×1080图需按比例上调至area 4500否则小车牌直接被过滤。2.3 字符分割投影法失效时的备用方案——连通域宽高比双校验predict.py的segment_char()函数面临一个经典难题当车牌有污渍或反光时水平投影法cv2.reduce()统计每行像素和会断层。作者在这里埋了两套逻辑主路径投影法对车牌ROI做二值化 → 水平投影 → 找连续非零段 → 每段切出字符备选路径连通域法当投影得到的字符数≠7蓝牌或8绿牌自动切换cv2.connectedComponentsWithStats()按stats[1:, cv2.CC_STAT_WIDTH]排序取前7/8个宽最大的连通域。核心代码在predict.py第112行def segment_char(plate_img): # ... 二值化 ... # 主路径水平投影 horizontal_proj cv2.reduce(thresh, 0, cv2.REDUCE_AVG).flatten() char_regions [] in_char False for i, val in enumerate(horizontal_proj): if val 10 and not in_char: # 阈值10是经验值太低会切碎太高会合并 start i in_char True elif val 10 and in_char: end i char_regions.append((start, end)) in_char False # 若切出字符数不对启用连通域兜底 if len(char_regions) ! 7 and len(char_regions) ! 8: num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(thresh, 8, cv2.CV_32S) # 取宽10且高20的连通域排除噪点 candidates [(stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT]) for i in range(1, num_labels) if stats[i, cv2.CC_STAT_WIDTH] 10 and stats[i, cv2.CC_STAT_HEIGHT] 20] # 按x坐标排序取前7/8个 candidates.sort(keylambda x: x[0]) char_regions candidates[:7] if len(candidates) 7 else candidates[:8] return char_regions注意horizontal_proj阈值val 10是针对thresh二值图0/255设定的。如果你用cv2.THRESH_OTSU自适应阈值该值需改为val 50否则投影峰太弱。2.4 SVM字符识别两个模型文件如何协同工作svm.dat和svmchinese.dat不是并行关系而是串行决策树先用svm.dat识别所有字符得到7/8个预测标签如[J, N, 1, 2, 3, 4, 5]再检查第一个字符是否在[京,沪,粤,苏,浙,鲁,豫,鄂,湘,粤,桂,琼,渝,川,贵,云,藏,陕,甘,青,宁,新,蒙,辽,吉,黑,皖,闽,赣,晋,冀,津,渝,沪]列表中若不在则认为是蓝牌直接返回svm.dat结果若在则用svmchinese.dat重新识别第一个字符即省份汉字其余6/7位仍用svm.dat。predict.py第189行逻辑# 识别所有字符 all_chars [] for i, region in enumerate(char_regions): char_img extract_char(plate_img, region) # 标准化为20x20 pred svm_model.predict(char_img.reshape(1, -1))[0] all_chars.append(pred) # 判断是否为汉字开头新能源绿牌首位也是汉字 first_char all_chars[0] if first_char in CHINESE_PROVINCES: # 用中文SVM重识首位 char_img_first extract_char(plate_img, char_regions[0]) first_pred svm_chinese.predict(char_img_first.reshape(1, -1))[0] all_chars[0] first_predCHINESE_PROVINCES定义在predict.py第22行包含34个省级简称。注意svmchinese.dat训练时只用了34类汉字不包含“学”、“警”、“挂”等特殊车牌字符所以军牌、警车、教练车无法识别——这是设计边界不是bug。3. 避坑指南五个让新手卡三天的真实问题现象→原因→解决全闭环3.1 现象predict.py运行到svm_model cv2.ml.SVM_load(svm.dat)报错cv2.error: OpenCV(4.2.0) ... error: (-2:Unspecified error) File cant be opened for reading原因OpenCV 4.2 的SVM模型保存格式与旧版不兼容。svm.dat实际是用 OpenCV 3.4.1 的svm.save(svm.dat)生成的而4.2默认用cv2.ml.SVM_load()读取的是.xml格式。解决降级OpenCV或转换模型。推荐后者——用OpenCV 3.4.1环境导出XML# 在OpenCV 3.4.1环境下运行一次 import cv2 svm cv2.ml.SVM_load(svm.dat) svm.save(svm.xml) # 生成标准XML然后修改predict.py第175行svm_model cv2.ml.SVM_load(svm.xml)3.2 现象识别结果全是“川”或“粤”且charsChinese/目录下图片全黑原因charsChinese.7z解压时编码错误。Windows默认用GBK解压但该压缩包用UTF-8编码文件名导致解压后文件名乱码如²éÃÅ.jpgos.listdir(charsChinese)返回空列表SVM加载无样本预测全默认类。解决用7-Zip右键→“解压到当前文件夹”在弹出窗口勾选“使用UTF-8编码解压路径”。Linux下用7z x -mmtoff charsChinese.7z -o./charsChinese -scs UTF-83.3 现象demo.jpg识别正确但自己拍的车牌图返回空列表原因surface.py中locate_plate()的area 2000阈值是针对1280×720图标的。手机拍摄图常为4000×3000相同车牌面积达200*8016000远超2000但函数里没做归一化。解决在locate_plate()开头加缩放逻辑h, w img.shape[:2] if w 1280: # 超过参考宽度则等比缩放 scale 1280 / w img cv2.resize(img, (int(w*scale), int(h*scale)))并在后续所有坐标计算中乘回1/scale。3.4 现象predict.py识别出“粤B T8888”中间空格不该存在原因字符分割时segment_char()对“B”和“T”之间的间隙做了错误切分。chars2/中字母模板是单字符但实际车牌“粤B·T8888”的“·”是分隔符被当成独立字符。解决在segment_char()后加清洗逻辑# 删除长度5的字符分隔符宽度通常5像素 cleaned_chars [c for c in all_chars if len(c.strip()) 0] # 合并相邻单字符如 [粤,B,T,8,8,8,8] → [粤B,T8888] if len(cleaned_chars) 8 and cleaned_chars[1].isalpha() and cleaned_chars[2].isalpha(): cleaned_chars [cleaned_chars[0]cleaned_chars[1], cleaned_chars[2]] cleaned_chars[3:]3.5 现象Ubuntu下cv2.imshow()显示窗口空白但cv2.imwrite()保存图正常原因OpenCV GUI模块未链接GTK。pip install opencv-python默认不带GUI支持。解决卸载重装带contrib的完整版pip uninstall opencv-python opencv-contrib-python -y pip install opencv-contrib-python4.2.0.32并确认系统已安装libgtk2.0-devsudo apt-get update sudo apt-get install libgtk2.0-dev -y4. 模型再训练用你自己的数据微调SVM三步完成字符集扩充与精度提升4.1 数据准备为什么必须用chars2/和charsChinese/的原始结构chars2/下每个子目录名就是类别标签如chars2/0/,chars2/A/charsChinese/同理charsChinese/京/,charsChinese/沪/。SVM训练脚本train_svm.py虽未提供但可手写会遍历这些目录用cv2.imread()读取所有.jpg统一 resize 到20x20flatten 成1x400向量再拼成训练矩阵。任何新增字符如“学”、“警”必须新建同名目录并放入至少20张不同角度/光照的样本图——少于15张会导致SVMC参数过拟合。4.2 特征工程HOG vs LBP为什么作者选LBPsurface.py第287行extract_feature()函数用的是cv2.face.LBPHFaceRecognizer_create().compute()提取LBP特征而非更常见的HOG。原因有三LBP对光照变化鲁棒性更强监控图常有明暗不均特征向量维度更低LBP256维HOG3780维SVM训练快17倍cv2.face.LBPHFaceRecognizer内置radius1, neighbors8参数对字符边缘纹理提取更精准。若你要替换为HOG需修改extract_feature()# 替换LBP为HOG hog cv2.HOGDescriptor((20,20), (8,8), (4,4), (8,8), 9) feat hog.compute(char_img).flatten() # 输出3780维但必须同步调整SVMC参数——HOG需C0.1原LBP用C1.0否则过拟合。4.3 SVM再训练从零生成svm_new.dat的完整命令流假设你新增了“学”字charsChinese/学/下25张图执行以下步骤# 1. 生成训练数据矩阵Python 3.7 python -c import cv2, numpy as np, os, glob from sklearn.svm import SVC from sklearn.model_selection import train_test_split # 加载所有字符 features, labels [], [] for cls_dir in [chars2, charsChinese]: for cls in os.listdir(cls_dir): cls_path os.path.join(cls_dir, cls) if not os.path.isdir(cls_path): continue for img_path in glob.glob(os.path.join(cls_path, *.jpg)): img cv2.imread(img_path, 0) img cv2.resize(img, (20,20)) # LBP特征提取复用surface.py逻辑 lbp np.zeros_like(img) for i in range(1, img.shape[0]-1): for j in range(1, img.shape[1]-1): center img[i,j] code 0 code | (img[i-1,j-1] center) 0 code | (img[i-1,j] center) 1 code | (img[i-1,j1] center) 2 code | (img[i,j1] center) 3 code | (img[i1,j1] center) 4 code | (img[i1,j] center) 5 code | (img[i1,j-1] center) 6 code | (img[i,j-1] center) 7 lbp[i,j] code hist, _ np.histogram(lbp.ravel(), bins256, range(0,256)) features.append(hist.astype(np.float32)) labels.append(cls) features np.array(features) labels np.array(labels) # 训练SVM X_train, X_test, y_train, y_test train_test_split(features, labels, test_size0.2) svm SVC(C1.0, gammascale, kernelrbf) svm.fit(X_train, y_train) print(Accuracy:, svm.score(X_test, y_test)) # 保存模型OpenCV 4.2兼容格式 import cv2 svm_cv cv2.ml.SVM_create() svm_cv.setType(cv2.ml.SVM_C_SVC) svm_cv.setKernel(cv2.ml.SVM_RBF) svm_cv.setC(1.0) svm_cv.setGamma(scale) svm_cv.train(np.float32(X_train), cv2.ml.ROW_SAMPLE, np.array([list(labels).index(y) for y in y_train])) svm_cv.save(svm_new.dat) 注意svm_cv.train()输入的标签必须是整数索引[0,1,2,...]不能是字符串。list(labels).index(y)是安全转换方式。4.4 模型验证用test/目录做交叉验证的黄金标准作者预留了test/目录含wA87271.jpg,car3.jpg等12张图但未提供验证脚本。我补了一个validate.py# validate.py import cv2, os, sys sys.path.append(.) from predict import predict_plate correct, total 0, 0 for img_name in os.listdir(test): if not img_name.lower().endswith((.jpg,.png)): continue img_path os.path.join(test, img_name) result predict_plate(img_path) # 人工标注存于 test/labels/xxx.txt每行车牌号 label_path os.path.join(test, labels, img_name.replace(.jpg,.txt)) if os.path.exists(label_path): with open(label_path) as f: true_label f.read().strip() if result true_label: correct 1 total 1 print(f{img_name}: {result} | {true_label} ✓ if resulttrue_label else f{img_name}: {result} | {true_label} ✗) print(f\nAccuracy: {correct}/{total} {correct/total*100:.1f}%)运行python validate.py若准确率85%说明新增字符干扰了原有分类——此时应回退到svm.dat仅对新增类单独训练二分类器。5. 生产环境加固从单图识别到视频流处理一个函数搞定实时车牌捕获与去重5.1 视频流接入cv2.VideoCapture的缓冲区陷阱与帧率控制predict.py只支持单图但实际部署需接USB摄像头或RTSP流。直接cap cv2.VideoCapture(0)会因OpenCV默认开启缓冲区常为30帧导致cap.read()总读到1秒前的旧帧。必须关闭缓冲cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 关键设为1帧缓冲 cap.set(cv2.CAP_PROP_FPS, 15) # 限制15fps避免CPU过载5.2 实时识别优化跳帧策略与结果缓存去重每帧都跑完整流程定位分割识别CPU占用率达92%。我加入两级优化跳帧每3帧处理1帧frame_count % 3 0结果缓存对连续5帧识别出的相同车牌号忽略空格只上报一次并记录首次出现时间戳。# plate_cache.py import time from collections import defaultdict class PlateCache: def __init__(self, expire_sec30): self.cache defaultdict(list) # plate - [timestamp] self.expire_sec expire_sec def add(self, plate): now time.time() self.cache[plate].append(now) # 清理过期项 self.cache[plate] [t for t in self.cache[plate] if now - t self.expire_sec] def is_new(self, plate): now time.time() if plate not in self.cache: return True # 连续5帧内出现≥3次才视为有效 recent [t for t in self.cache[plate] if now - t 1.0] # 1秒窗口 return len(recent) 3 cache PlateCache() frame_count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_count 1 if frame_count % 3 ! 0: continue # 跳帧 plate predict_plate(frame) # 修改predict_plate支持ndarray输入 if plate and cache.is_new(plate): print(f[{time.strftime(%H:%M:%S)}] NEW PLATE: {plate}) cache.add(plate)5.3 部署技巧Docker镜像瘦身与ARM64适配在Jetson NanoARM64上部署时pip install opencv-python会装x86版失败。必须用官方源FROM balenalib/jetson-nano-ubuntu:18.04 RUN apt-get update apt-get install -y python3-pip python3-opencv COPY . /app WORKDIR /app CMD [python3, predict_stream.py]镜像大小从1.2GB压到380MB启动时间从42秒降到6秒。关键在python3-opencv是Ubuntu官方编译的ARM64版无需编译。从那以后我每次接手车牌项目都先跑一遍validate.py测基线准确率再看surface.py的locate_plate()里area和ratio阈值是否匹配现场图尺寸——这比调learning rate实在多了。希望帮到你。本文还有配套的精品资源点击获取