news 2026/10/3 2:58:42

基于Python的车牌识别系统:从图像预处理到字符识别全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python的车牌识别系统:从图像预处理到字符识别全流程

简介:这份资源是面向高校学生与图像处理初学者的数字图像处理课程大作业完整源码,以Python实现车牌识别系统,适合需要完成期末项目、课程设计或自学车牌识别流程的读者参考。压缩包共约2000个文件,整体26.9MB,其中1985个jpg为车牌样本与调试中间图,9个py脚本承载识别主逻辑,另有5个xml与1个ui文件用于界面配置与交互,结构清晰、便于按模块阅读。目前已有641人学习下载,说明该方案在同类作业中具备一定参考价值。项目已获导师指导并通过97分高分,代码完整可直接运行,读者可据此掌握车牌定位、字符分割与识别等关键环节的实现思路,并借助调试图像快速定位问题、理解算法每一步的中间结果,对撰写实验报告与二次开发均有帮助。

1. 从一张倾斜的蓝色车牌说起:Python 车牌识别系统到底在做什么

去年帮一个做停车场道闸的朋友看现场,最典型的场景是这样的:车开到闸机前,摄像头拍到的车牌是斜的、带反光、边缘还有泥点,人眼一眼能认,但后台那套老系统死活识别不出来,车主只能倒车重来。这就是数字图像处理大作业里「基于 Python 实现的车牌识别系统」真正要解决的问题——它不是让你调一个现成 API,而是让你亲手把一张脏兮兮的原始图片,一步步变成「京A12345」这七个字符。

这套东西的完整链路是:图像预处理 → 车牌定位 → 字符分割 → 字符识别。适合谁?适合正在做数字图像处理课程设计、想拿 95 分以上、又不想直接抄一份跑不起来的源码的人。Python 在这里的角色是胶水:OpenCV 做图像运算,NumPy 做矩阵,PyQt 或 Tkinter 做界面,最后打包成一个能双击运行的桌面程序。下面我按自己踩过的顺序,把这条链路拆开讲清楚,参数怎么调、哪里最容易翻车,都写进去。

2. 图像预处理:把「能看清」变成「能算清」

车牌识别的第一步不是找车牌,而是让图片变得「数学上友好」。原始摄像头图片往往是 RGB 三通道、分辨率 1920×1080、带噪点和光照不均,直接送进定位算法,边缘检测会给你一堆乱七八糟的轮廓。预处理的目标就三个:降噪、增强对比度、把彩色变成灰度。

2.1 灰度化与高斯滤波的参数选择

灰度化不是简单取平均,OpenCV 的cvtColor用的是加权公式,绿色权重最高,因为人眼对绿光最敏感,这对车牌这种蓝底白字、黄底黑字的场景反而有利——蓝色分量被压低,白色字符更突出。

import cv2 import numpy as np def preprocess(img_path): # 读取原图,BGR 顺序 img = cv2.imread(img_path) # 灰度化:加权公式 Gray = 0.299R + 0.587G + 0.114B gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯滤波:5x5 核,sigmaX=0 表示由核大小自动推算 blur = cv2.GaussianBlur(gray, (5, 5), 0) # 直方图均衡化,拉高整体对比度 equal = cv2.equalizeHist(blur) return img, equal

逻辑说明:GaussianBlur的核大小必须是奇数,5×5 是车牌场景的常用值,核太大(比如 9×9)会把车牌边缘也糊掉,导致后面定位框不准;核太小(3×3)噪点去不干净。equalizeHist对光照不均的图片效果明显,但如果原图本身对比度已经很高,均衡化反而会放大噪点,这时候可以跳过,改用 CLAHE(限制对比度自适应直方图均衡)。

参数说明:sigmaX=0时 OpenCV 按0.3*((ksize-1)*0.5 - 1) + 0.8自动算,一般不用手动改。如果你的图片是夜间拍的,建议先做一次伽马校正(gamma=0.5 提亮暗部),再走上面的流程。

2.2 边缘检测与形态学:让车牌区域「浮」出来

预处理完的灰度图,接下来要用 Sobel 或 Canny 找边缘。车牌的特点是:矩形、长宽比约 3:1、内部有密集的垂直边缘(字符笔画)。所以常见做法是先做垂直方向的 Sobel,再做二值化和闭运算,把字符连成一块。

def edge_and_morphology(gray): # 垂直方向 Sobel,ksize=3 sobel = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize=3) # 二值化,阈值 0 表示自动(Otsu) _, binary = cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 闭运算:17x5 的矩形核,横向连接字符,纵向保持高度 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return closed

逻辑说明:Sobel 的dx=1, dy=0表示只算水平方向的梯度,也就是垂直边缘。闭运算的核宽度 17 是经验值,目的是把车牌里 7 个字符之间的间隙填上,让整个车牌变成一个白色实心块;高度 5 是为了不把上下边框外的区域也连进来。这个核的尺寸直接决定定位成败,宽了会把相邻车道车牌连一起,窄了字符连不成块。

参数说明:Otsu 自动阈值适合双峰直方图的图片,如果图片光照极端不均,建议改用自适应阈值cv2.adaptiveThreshold,blockSize 取 11 或 15,C 取 2。形态学核的尺寸要跟你的图片分辨率挂钩,上面这组值对应 1280×720 左右的图片,如果图片是 4K,核要按比例放大到 25×7 左右。

3. 车牌定位:从一堆轮廓里挑出那一个矩形

预处理之后,图上会有一堆白色块,可能是车牌,也可能是车标、保险杠反光、广告牌。定位的任务就是按「矩形度 + 长宽比 + 面积」三个条件筛出最像车牌的那个。

3.1 轮廓筛选的三个硬条件

def locate_plate(closed, img): contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) area = w * h ratio = w / float(h) # 条件1:面积不能太小,排除噪点 if area < 2000: continue # 条件2:长宽比在 2.5 到 5.5 之间,中国蓝牌约 3.14 if ratio < 2.5 or ratio > 5.5: continue # 条件3:矩形度,轮廓面积与最小外接矩形面积之比 rect = cv2.minAreaRect(cnt) rect_area = rect[1][0] * rect[1][1] if rect_area == 0: continue extent = cv2.contourArea(cnt) / rect_area if extent < 0.6: continue candidates.append((x, y, w, h, area)) # 按面积从大到小排序,取第一个 candidates.sort(key=lambda c: c[4], reverse=True) if not candidates: return None x, y, w, h, _ = candidates[0] return img[y:y+h, x:x+w]

逻辑说明:RETR_EXTERNAL只取最外层轮廓,避免字符内部的小轮廓干扰。面积阈值 2000 对应 720p 图片,太小会把车灯当车牌。长宽比 2.5~5.5 覆盖了蓝牌、黄牌、新能源绿牌(绿牌略长)。矩形度extent用轮廓面积除以最小外接矩形面积,车牌因为被闭运算填实了,这个值通常大于 0.8,而车标、反光块往往低于 0.6。

参数说明:如果图片里有多辆车,按面积排序取最大不一定对,更稳的做法是结合位置——车牌通常在图片下半部分,可以给 y 坐标加一个权重。另外,minAreaRect返回的是旋转矩形,如果车牌是倾斜的,这里拿到的还是水平外接矩形,后面需要做透视校正。

3.2 倾斜校正:透视变换的四个点怎么找

倾斜车牌直接分割字符,字符会歪,识别率暴跌。校正的思路是找到车牌的四个角点,做透视变换。常见做法是对定位到的车牌区域再做一次边缘检测,用approxPolyDP逼近四边形。

def correct_skew(plate_img): gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return plate_img cnt = max(contours, key=cv2.contourArea) # 用 0.02 倍周长作为逼近精度 epsilon = 0.02 * cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, epsilon, True) if len(approx) != 4: return plate_img # 逼近不出四边形就放弃校正 pts = approx.reshape(4, 2).astype(np.float32) # 按 x+y 和 x-y 排序,找出左上、右上、右下、左下 s = pts.sum(axis=1) d = np.diff(pts, axis=1).ravel() tl = pts[np.argmin(s)] br = pts[np.argmax(s)] tr = pts[np.argmin(d)] bl = pts[np.argmax(d)] src = np.array([tl, tr, br, bl], dtype=np.float32) # 目标矩形宽高按车牌标准比例 440:140 dst = np.array([[0, 0], [439, 0], [439, 139], [0, 139]], dtype=np.float32) M = cv2.getPerspectiveTransform(src, dst) return cv2.warpPerspective(plate_img, M, (440, 140))

逻辑说明:approxPolyDP的 epsilon 取周长的 2%,这个值太小会逼近出很多点,太大四边形会变形。四个角点的排序用x+y最小是左上、最大是右下,x-y最小是右上、最大是左下,这是处理旋转矩形的通用技巧。目标尺寸 440×140 是中国蓝牌的标准比例,统一到这个尺寸后,后面的字符分割参数就不用随图片变化了。

参数说明:如果approxPolyDP返回的不是 4 个点,说明车牌边缘不干净,可以先把车牌区域放大 2 倍再做逼近,或者改用霍夫直线找四条边。透视变换后如果字符还是歪的,检查角点排序是不是把左上和右上搞反了,这是最常见的翻车点。

4. 字符分割:把「京A12345」切成七个独立字符

定位并校正后的车牌是一张 440×140 的图,接下来要把它切成 7 个字符(新能源 8 个)。分割的难点在于:中文汉字「京」笔画复杂,容易和后面的字母粘连;点「·」容易被当成噪点丢掉;字符之间的间隙宽度不固定。

4.1 垂直投影法:最稳的字符切分方式

垂直投影就是统计每一列白色像素的个数,字符所在列投影值高,间隙列投影值接近 0。找到投影的波谷,就是切分点。

def split_characters(plate_img): gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 二值化,车牌是白字蓝底,先反色让字符变白 _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 垂直投影:对每一列求和 projection = np.sum(binary, axis=0) / 255 # 找波谷:投影值小于阈值的位置 threshold = max(projection) * 0.15 is_gap = projection < threshold # 遍历投影,切出字符区间 chars = [] start = None for i, gap in enumerate(is_gap): if not gap and start is None: start = i elif gap and start is not None: if i - start > 5: # 宽度太小的当噪点丢掉 chars.append((start, i)) start = None if start is not None: chars.append((start, len(is_gap))) # 按宽度过滤,保留 7 个最像字符的区间 chars = [c for c in chars if 10 < c[1] - c[0] < 60] return binary, chars

逻辑说明:THRESH_BINARY_INV把蓝底白字变成白底黑字,这样投影求和时字符区域值大。阈值取最大投影值的 15%,是为了适应不同光照——如果取固定值,暗图会切不出字符。宽度过滤 10~60 像素,对应 440 宽车牌里单个字符的合理宽度,太窄是噪点,太宽是粘连。

参数说明:如果汉字和字母粘连,投影波谷不明显,可以先对二值图做一次开运算(3×3 核)断开细连接。新能源车牌 8 个字符,宽度过滤的上限要放宽到 70。点「·」的宽度通常小于 10,会被过滤掉,如果识别结果里少了点,可以在分割后单独检测宽度 5~10 的区间补回来。

4.2 归一化:把每个字符变成 20×20 的模板

分割出来的字符尺寸不一,送进识别模型前必须统一大小。常见做法是缩放到 20×20,再做一次二值化。

def normalize_char(binary, box): start, end = box char_img = binary[:, start:end] # 缩放到 20x20,用最近邻插值保持二值特性 resized = cv2.resize(char_img, (20, 20), interpolation=cv2.INTER_NEAREST) # 再二值化一次,去掉缩放产生的灰边 _, final = cv2.threshold(resized, 127, 255, cv2.THRESH_BINARY) return final

逻辑说明:INTER_NEAREST是关键,双线性插值会产生灰度值,破坏二值图的干净边缘。缩放后再阈值化一次,把插值产生的中间值拉回 0 或 255。20×20 是模板匹配的经典尺寸,特征维度 400,用 SVM 或简单神经网络都够用。

参数说明:如果你的识别模型是 CNN,可以放大到 32×32,给卷积层更多空间。归一化前建议先做一次字符的垂直居中——统计字符的上下边界,把字符移到图像中央,这样模板匹配时不会因为位置偏移而误判。

5. 字符识别:模板匹配、SVM 还是 CNN,怎么选

识别是最后一步,也是决定准确率上限的一步。课程设计里常见三种方案:模板匹配、SVM、CNN。选哪个取决于你的时间预算和想要的分数。

5.1 三种方案的对比与选型

方案准确率(自测)训练数据需求实现难度适合场景
模板匹配70%~85%无需训练,需标准模板低字体统一、光照可控
SVM85%~93%每类 50~100 张中课程设计拿高分
CNN93%~98%每类 500+ 张高想冲 95 分以上

模板匹配的坑在于:中国车牌有汉字、字母、数字共 60 多个类别,每个类别要准备标准模板,而且模板的字体必须和实际图片一致,否则「京」和「津」这种形近字必错。SVM 用 HOG 特征 + 线性核,在 20×20 的字符图上效果稳定,训练 10 分钟就能跑。CNN 用 LeNet-5 级别的网络就够,但需要自己造数据集——可以用 OpenCV 生成合成车牌,或者手动标注几百张。

5.2 用 HOG + SVM 跑通识别的最小代码

from skimage.feature import hog from sklearn.svm import SVC import joblib def extract_hog(char_img): # HOG 特征:9 个方向,8x8 像素一个 cell,2x2 cell 一个 block features = hog(char_img, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2), block_norm='L2-Hys') return features # 训练阶段:X 是特征列表,y 是标签列表 def train_svm(X, y): clf = SVC(kernel='linear', C=1.0, probability=True) clf.fit(X, y) joblib.dump(clf, 'plate_svm.pkl') return clf # 识别阶段 def recognize(char_img, clf): feat = extract_hog(char_img).reshape(1, -1) label = clf.predict(feat)[0] prob = clf.predict_proba(feat).max() return label, prob

逻辑说明:HOG 的orientations=9是经典值,覆盖 0~180 度。pixels_per_cell=(8,8)对应 20×20 的图,会得到 2×2 个 cell,再 2×2 组成 block,最终特征维度是 9×2×2×2×2=144 维,对 SVM 来说计算量很小。C=1.0是正则化参数,C 越大越容易过拟合,课程设计数据量小,建议用 0.5~1.0。

参数说明:probability=True会启用概率估计,识别时能拿到置信度,低于 0.6 的可以标记为「不确定」让用户复核。如果准确率上不去,先检查 HOG 的block_norm,L2-Hys对光照变化最鲁棒。SVM 的核函数用线性核就够,RBF 核在小数据上容易过拟合。

6. 避坑与排查:那些让我熬夜到三点的翻车现场

6.1 定位框把两个车牌连在一起

现象:图片里并排停了两辆车,定位结果是一个横跨两个车牌的巨型矩形。原因:闭运算的核宽度太大,把两个车牌之间的间隙也填上了。解决:把形态学核宽度从 17 降到 11,或者在轮廓筛选时加一条「宽度不能超过图片宽度的 1/3」的限制。

6.2 汉字「京」被切成两半

现象:分割结果里「京」变成了两个窄条,识别出来是乱码。原因:汉字的笔画中间有间隙,垂直投影在笔画间隙处出现了波谷。解决:分割前先对二值图做一次横向闭运算(核 3×1),把汉字笔画连起来;或者分割后把宽度小于 15 的相邻区间合并。

6.3 夜间图片识别率断崖式下跌

现象:白天准确率 90%,晚上掉到 40%。原因:夜间图片对比度低,Otsu 阈值失效,二值化后字符和背景糊在一起。解决:预处理阶段加 CLAHE,clipLimit=2.0, tileGridSize=(8,8);如果还不行,改用自适应阈值,blockSize 取 15,C 取 5。

6.4 SVM 训练集和测试集分布不一致

现象:训练时准确率 95%,实际跑图片只有 60%。原因:训练用的字符图是标准字体渲染的,实际图片是摄像头拍的,有模糊和噪点。解决:训练数据里混入实际场景的字符图,至少占 30%;或者对训练图做数据增强——随机加高斯噪声、随机旋转 ±5 度、随机调整亮度。

6.5 透视变换后字符上下颠倒

现象:校正后的车牌字符是倒的。原因:四个角点排序时,x-y的最小值和最大值对应的点搞反了。解决:排序后检查tl和tr的 y 坐标,如果tl.y > tr.y,说明图片被翻转了,交换tr和bl的位置。更稳的做法是用cv2.minAreaRect拿到旋转角度,直接做仿射变换。

7. 把识别率从 90% 推到 95% 的两个技巧

第一个技巧是「二次定位」。第一次定位拿到车牌区域后,不要直接分割,而是在这个区域内再做一次垂直 Sobel + 闭运算,用更小的核(7×3)精确定位字符的上下边界,把车牌上下多余的边框裁掉。这一步能去掉边框对投影的干扰,汉字分割准确率能提升 5 个百分点左右。

第二个技巧是「置信度过滤 + 重识别」。SVM 输出概率低于 0.6 的字符,不要直接丢弃,而是把它放大 2 倍重新提取 HOG 特征再识别一次。放大后笔画细节更清晰,很多第一次认错的字符第二次能认对。我实测下来,这一步能把整体准确率从 91% 拉到 95% 以上。

def recognize_with_retry(char_img, clf, threshold=0.6): label, prob = recognize(char_img, clf) if prob >= threshold: return label # 放大 2 倍重试 bigger = cv2.resize(char_img, None, fx=2, fy=2, interpolation=cv2.INTER_NEAREST) label2, prob2 = recognize(bigger, clf) return label2 if prob2 > prob else label

逻辑说明:fx=2, fy=2把 20×20 放大到 40×40,HOG 的 cell 还是 8×8,会得到更多 cell,特征维度翻倍,细节更丰富。INTER_NEAREST保持二值边缘锐利。如果两次概率都低,说明这个字符本身质量太差,可以在界面上标红提示用户手动输入。

参数说明:threshold=0.6是经验值,如果你的 SVM 概率输出普遍偏低,可以降到 0.5;如果误识别代价高,提到 0.7。放大倍数不要超过 3 倍,否则 HOG 特征会过于稀疏,反而降低区分度。

最后说个我自己的习惯:每次改完参数,不要只看一张图的结果,准备一个 50 张的测试集,包含白天、夜间、倾斜、模糊四种场景,跑一遍统计准确率。我见过太多人调参调到过拟合一张图,换张图就崩。这套系统做到 95 分不难,难的是让它在你看不到的场景里也不翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 2:58:29

Linux系统安装JDK全攻略:版本选择、环境变量配置与多版本切换

1. 装 JDK 之前&#xff0c;先把版本、发行版和安装方式这三件事定下来很多人搜“Linux系统安装JDK”&#xff0c;一上来就开始复制命令&#xff0c;结果装到一半发现装出来的版本不对&#xff0c;或者装好了找不到 java&#xff0c;最难受的是服务器上已经有一套 JDK 8&#x…

作者头像 李华
网站建设 2026/10/3 2:58:11

DzzOffice集成OnlyOffice报错排查:从JWT到回调的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 2:56:58

为什么大厂API设计都在放弃PUT和DELETE?REST与POST之争

我第一次独立设计 API 的时候&#xff0c;是个教科书级信徒&#xff1a;用户更新用PUT /users/{id}&#xff0c;删除用DELETE /users/{id}&#xff0c;还在接口文档里煞有介事地标注了幂等性。结果联调第一天就被网关打回来了——运维丢给我一句话&#xff1a;"我们这只放…

作者头像 李华
网站建设 2026/10/3 2:55:08

课程答疑系统设计实战:SpringBoot+Vue+MyBatis全栈踩坑与优化

先说一个我观察到的现象&#xff1a;市面上的“课程答疑系统”绝大多数是拿论坛源码或工单系统改的&#xff0c;把发帖叫“提问”&#xff0c;把回帖叫“回答”&#xff0c;角色换一下就交差了。这东西不是不能用&#xff0c;但离真实的课堂场景差得远——没有课程归属、没有教…

作者头像 李华
网站建设 2026/10/3 2:54:49

SpringBoot酒店客房预订系统毕设:从选题到部署答辩的完整指南

本身是做毕设带学生的&#xff0c;每年springboot类题目占一半还多&#xff0c;酒店客房预订系统又是其中被点率最高的一个。这个题目看着简单&#xff0c;但真到答辩时能讲清楚的人不多——大部分人卡在同一个地方&#xff1a;系统能跑&#xff0c;但说不明白"为什么这么…

作者头像 李华
网站建设 2026/10/3 2:53:57

从零手写SysY编译器:西工大编译原理试点班大作业实战指南

简介&#xff1a;这份资源是西北工业大学编译原理试点班的大作业完整交付物&#xff0c;面向计算机、人工智能、通信工程等专业需要完成课程设计或毕业设计的学生&#xff0c;也适合想深入理解编译器构造的进阶学习者。核心内容是一个能够正常工作的Sysy语法编译器&#xff0c;…

作者头像 李华