☰
OpenCV车牌识别实战:定位、字符分割与模板匹配全解析
2026/9/28 8:54:29 网站建设 项目流程

简介:利用Python和OpenCV实现车牌识别的实战资源包,面向计算机视觉初学者与Python开发者,覆盖图像预处理、车牌定位到字符识别的完整链路,并结合百度API和HyperLPR提升识别准确率。压缩包内含139个文件,约73.86MB,其中18个py脚本承载核心识别逻辑,50张jpg、32张png和19张jpeg图片作为不同场景下的测试样本,另有svm.dat/svmchinese.dat模型、yml配置、Dockerfile及shell脚本,方便自行配置环境并跨平台复现。目前已有1608人学习使用。资源覆盖车牌搜索识别、车牌号对比、数据库认证、图文搜索、数据库搜索等系统,同时支持文件图片、网络图片地址、实时截图、摄像头拍照识别,并带图片自适应窗口与对比演示gif。通过这些可直接运行的示例代码和模块化结构,读者能快速理解多场景车牌识别应用的工程落地方式;得益于清晰的功能目录和封装好的工具接口,也可将其快速改造为二次开发基础框架。

1. 车牌识别这个题目,卡住新手的从来不是识别

车牌识别在 OpenCV 里是个特别有意思的题目:听起来是「识别」,但真正动手做一遍就知道,字符识别反而是整条链路里最简单的一环。真正让识别率从 90% 掉到 60% 的,是前两步——车牌定位和字符分割。光照不均匀、路面反光、车牌倾斜、铆钉和边框干扰,每一个都能让一套看起来很完整的代码翻车。用 Python 和 OpenCV 做车牌识别,适合两类人:一类是刚入门计算机视觉、想拿一个完整项目串起图像处理基本操作的开发者;另一类是有具体落地需求、想在门禁或停车场场景里做一个轻量级识别原型的工程师。这条路最实在的价值在于,几百行代码就能跑通一个端到端的流程,而且每个环节都能肉眼看到中间结果,方便定位问题。

2. 环境搭建与 OpenCV 版本选型:这步踩坑最少但最碎

2.1 为什么 OpenCV 版本会影响车牌识别结果

做车牌识别,OpenCV 的版本选择不是随便pip install opencv-python就完事的。不同版本之间,函数命名、参数默认值、甚至是 findContours 的返回值结构都有变化。比如 OpenCV 3.x 和 4.x 之间,findContours从返回 3 个值变成了返回 2 个值,早期很多教程的代码在 4.x 上直接报错。做车牌识别这种需要大量轮廓操作的流程,版本不一致会让你排查半天才发现是环境问题。

另一个要区分的是opencv-python和opencv-contrib-python。车牌识别用不到 SIFT、SURF 这些需要专利许可的算法,也不需要cv2.text模块里的 OCR 工具,所以装基础版就够了。如果装了 contrib 版,某些函数的加载方式会有细微差别,虽然不影响主流程,但没必要。

2.2 最小可用环境配置与安装命令

我习惯用虚拟环境隔离项目依赖,避免和系统 Python 环境的包互相污染。下面是最小可用的安装步骤:

# 创建虚拟环境(Windows 和 Linux 通用) python -m venv plate_env # 激活虚拟环境 # Windows: plate_env\Scripts\activate # Linux/macOS: source plate_env/bin/activate # 安装 OpenCV 基础版和 numpy pip install opencv-python==4.8.1.78 pip install numpy==1.24.3

安装完成后,用一段极简代码验证环境是否可用:

import cv2 import numpy as np print(cv2.__version__) print(np.__version__) # 生成一张 300x100 的黑色图像,模拟车牌画布 img = np.zeros((100, 300, 3), dtype=np.uint8) # 画一个蓝色矩形代表车牌区域 cv2.rectangle(img, (10, 20), (290, 80), (255, 0, 0), -1) cv2.imwrite("test_plate.png", img) print("OpenCV environment is ready.")

这里解释几个关键点:cv2.__version__输出 4.8.1.78 说明安装成功;np.zeros((100, 300, 3), dtype=np.uint8)是生成一张纯黑色图像,三个维度分别对应该高度、宽度和颜色通道数,dtype=np.uint8是图像像素的标准数据类型,取值范围 0 到 255,用其他类型会导致像素值截断或图像显示异常。cv2.rectangle的最后一个参数-1表示填充整个矩形,如果不传或者传 1,画的就只是边框。

如果import cv2时报ModuleNotFoundError: No module named 'cv2',优先检查是不是没激活虚拟环境,或者 pip 装到了系统环境。这一步看着基础,但很多人的车牌识别项目还没开始写,就死在了环境上。

2.3 一张车牌图片的标准处理管线

环境就绪后,不要急着写完整算法。先建立一个最基本的图像处理管线,把原始图片到灰度图再到二值图的转换跑通,后面每一步都在这个管线上迭代:

import cv2 import numpy as np def load_and_preprocess(image_path): # 读取图片,cv2.IMREAD_COLOR 表示按 BGR 三通道读入 img = cv2.imread(image_path, cv2.IMREAD_COLOR) if img is None: raise ValueError(f"Cannot read image: {image_path}") # 统一缩放到固定宽度,保持纵横比 # 后续形态学操作的核大小和图片尺寸强相关,统一尺寸方便调参 height, width = img.shape[:2] target_width = 800 scale = target_width / width new_height = int(height * scale) resized = cv2.resize(img, (target_width, new_height), interpolation=cv2.INTER_CUBIC) # 转灰度 gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) # 高斯滤波降噪,核大小选 5x5,过大模糊边缘,过小去不掉噪点 blurred = cv2.GaussianBlur(gray, (5, 5), 0) return resized, blurred # 测试函数 img, gray = load_and_preprocess("test_plate.png") print(f"Resized image shape: {img.shape}") print(f"Gray image shape: {gray.shape}")

这段代码里有个容易被忽略的参数:interpolation=cv2.INTER_CUBIC。缩放图片时默认用INTER_LINEAR,但车牌字符是细小结构,线性插值在缩小图片时会产生锯齿和伪影,用三次插值能保留更多边缘细节。另外注意cv2.resize的dsize参数是(width, height)而不是(height, width),和shape的返回顺序正好相反,这里写反了图片尺寸就乱了。

3. 车牌定位:用颜色筛选和形态学把车牌从画面里挖出来

3.1 两种主流的定位思路:边缘检测 vs 颜色筛选

车牌定位的常见做法有两条路线。第一条是边缘检测路线:车牌区域的字符密集,边缘响应强烈,先做 Sobel 或 Canny 边缘检测,再做形态学闭运算把相邻边缘连成块,最后用轮廓外接矩形的长宽比筛选。这条路对蓝底白字的传统车牌效果不错,但遇到车身本身有大量水平线条的车型,比如白色面包车、银色轿车,干扰会非常严重。

第二条是颜色筛选路线:利用车牌背景颜色(蓝、绿、黄)在 HSV 颜色空间中的分布范围,用inRange提取出候选区域。这条路对光照变化更鲁棒,因为 HSV 把亮度信息和色相分离了,阴影和反光的影响被压缩在 V 通道里。实际落地时两条路可以融合,先用颜色筛掉大部分背景,再用边缘和形态学精修。我一般会走融合路线,下面按这个思路展开。

3.2 HSV 颜色空间与车牌底色范围

HSV 三个通道的含义分别是色相 H(0 到 180)、饱和度 S(0 到 255)、明度 V(0 到 255)。OpenCV 的 H 范围是 0 到 180,不是通常理解的 0 到 360,这一点和很多教材不一样。

不同底色车牌在 HSV 里的范围大致如下:

车牌底色H 范围S 范围V 范围备注
蓝色100-130100-25550-255最常见,新能源绿牌的蓝色字不算
绿色35-8040-25540-255新能源渐变绿牌,S 偏低
黄色15-3580-25580-255教练车、工程车
白色0-1800-30180-255警车等,容易和车身混淆

实际项目中,绿色渐变车牌是最难处理的,因为渐变导致同一块车牌上的 S 和 V 值跨度极大,单一阈值很容易把车牌切成碎片。我的处理方式是放宽 H 和 S 的范围,再用后续的形态学闭运算把碎片连回去。

3.3 基于颜色筛选的车牌定位代码

import cv2 import numpy as np def locate_plate_by_color(img): # 输入为 BGR 图像,转 HSV 进行颜色筛选 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌的 HSV 范围,参数需要按实际图片微调 # H 范围收窄到 100-124,S 下限提高以过滤浅蓝和白色车身 lower_blue = np.array([100, 80, 50]) upper_blue = np.array([124, 255, 255]) # inRange 生成二值掩膜,白色区域为候选车牌 mask = cv2.inRange(hsv, lower_blue, upper_blue) # 形态学闭运算,先膨胀再腐蚀 # 核大小和图片尺寸相关,800px 宽的图用 15x5 的横向核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5)) closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 再做一个开运算,去掉零散的小噪点 kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) opened = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel_open) # 找轮廓 contours, _ = cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) area = w * h # 过滤掉太小和太大的区域 if area < 2000 or area > img.shape[0] * img.shape[1] * 0.5: continue # 车牌标准尺寸 440x140,长宽比约 3.14:1 # 实际拍摄有透视变形,放宽到 2.0-4.5 aspect_ratio = w / h if 2.0 < aspect_ratio < 4.5: candidates.append((x, y, w, h)) # 按面积排序,返回最大的候选区域 candidates.sort(key=lambda r: r[2] * r[3], reverse=True) if candidates: x, y, w, h = candidates[0] return img[y:y+h, x:x+w], (x, y, w, h) return None, None

这段代码有几个参数值得单独说。kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))用的是横向矩形核,原因是车牌是宽扁结构,横向的闭运算能把字符之间的空隙连起来,竖向核会把上下边框的干扰也带进来。核的大小和图片尺寸强相关,如果输入图是 1920 宽,核可能要放大到 25x7。

RETR_EXTERNAL只取最外层轮廓,避免把车牌里的字符轮廓也当成独立候选。CHAIN_APPROX_SIMPLE压缩轮廓点,只保留端点,减少后续计算量。长宽比筛选用的是w / h,但注意如果车牌有较大倾斜,这个比例会偏离 3.14,所以放宽到了 2.0-4.5,宁可多留候选,后面再做精修。

3.4 定位结果的验证方法

定位写完后,不要着急往下走。先做一件事:把定位结果可视化输出,看车牌区域有没有被完整框住,框内有没有包含大量非车牌内容。

# 在原图上画框并保存 def draw_locations(img, rect, save_path="result.jpg"): x, y, w, h = rect cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2) cv2.imwrite(save_path, img) print(f"Saved visualization to {save_path}") # 假设我们有一张测试图 test_img = cv2.imread("car_photo.jpg") plate, rect = locate_plate_by_color(test_img) if rect: draw_locations(test_img.copy(), rect) else: print("No plate candidate found. Adjust HSV range or kernel size.")

这一步很重要。很多人直接往下写字符分割,结果后面全乱了,回头检查才发现是定位框偏了半个车身。可视化验证能帮你快速确认问题出在颜色范围还是形态学核大小。如果框住了车灯或车身反光条,优先调 HSV 范围;如果框偏了但颜色区域覆盖正确,优先调轮廓筛选的长宽比范围。

4. 字符分割与识别:把鲁A12345拆成单个字符

4.1 为什么字符识别不用重头训练模型

车牌字符识别有一个特殊性:字符集是封闭的。省份简称 31 个,英文字母 24 个(I 和 O 通常不用),数字 10 个,加起来不到 70 类。这个规模用模板匹配就能达到不错的准确率,不需要上深度学习。当然,如果车流量大或环境复杂,用 SVM 或轻量 CNN 会更好,但那已经是另一套方案了。这里先讲模板匹配的完整链路,它是理解整个识别流程最好的切入点。

4.2 字符分割的核心:垂直投影法

字符分割最常用的手段是垂直投影法。原理是把二值化后的车牌图像每一列的像素值累加,字符列因为笔画多、累加值高,字符间隙因为几乎全黑、累加值接近 0。根据这个投影曲线的谷底位置,就能切出每个字符的左右边界。实际处理中汉字区域会出现笔画断裂的情况,需要做额外的粘连处理。

import cv2 import numpy as np def segment_characters(plate_img): # plate_img 是定位阶段裁剪出的车牌区域,BGR 格式 # 转灰度并放大,便于后续处理 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (440, 140), interpolation=cv2.INTER_CUBIC) # 二值化:车牌字符为白色,背景为黑色 # THRESH_OTSU 自动计算阈值,避免手动调参 _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 处理汉字笔画断裂:先闭运算连接断裂笔画 # 核大小要小,3x3 足够,太大字符会粘在一起 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 计算垂直投影 h, w = binary.shape # binary 是单通道二值图,按列求和得到投影值 vertical_projection = np.sum(binary, axis=0) / 255 # 找出所有空白列(投影值为0的列) blank_cols = [i for i, v in enumerate(vertical_projection) if v == 0] # 根据空白列切分字符:连续的非空白列构成一个字符区域 chars = [] in_char = False start = 0 for i in range(w): if vertical_projection[i] > 0 and not in_char: in_char = True start = i elif vertical_projection[i] == 0 and in_char: in_char = False end = i # 过滤掉过窄的区域(噪声或铆钉),宽度小于10的丢弃 if end - start > 10: chars.append((start, end)) # 第一个字符是汉字,第二个是字母,中间是小圆点分隔符 # 需要去掉过宽的区域(可能是边框) filtered_chars = [] for start, end in chars: char_width = end - start if 20 < char_width < 80: filtered_chars.append((start, end)) # 按位置排序,取前7个作为车牌字符 # 实际车牌是 1个汉字 + 1个字母 + 6个字母数字,共7个字符 filtered_chars = filtered_chars[:7] char_imgs = [] for start, end in filtered_chars: char_img = binary[:, start:end] char_imgs.append(char_img) return char_imgs, filtered_chars

这段代码里有两个常用技巧。第一是cv2.threshold配合THRESH_OTSU,它会在内部自动计算一个全局阈值,适合光照相对均匀的裁剪后车牌图。但如果车牌区域有强烈反光,Otsu 会失效,此时要改用局部自适应阈值cv2.adaptiveThreshold,不过这个函数对参数敏感,耗时也更大,不是首选。

第二是垂直投影的平滑处理。投影曲线不会像理想状态那样干净,字符内部会出现投影值为 0 的列,比如汉字「京」的中间横竖交叉处、字母「A」的镂空部分。这就是为什么代码里在二值化后做了 3x3 的闭运算——把笔画内部的细小空隙填充掉。如果闭运算后字符还是断的,可以加大到 5x5,但要注意别把字符间的缝隙也填上了。

4.3 模板匹配识别字符

import os import cv2 import numpy as np class TemplateMatcher: def __init__(self, template_dir): self.templates = {} # 遍历模板目录,文件名格式: "A.jpg", "鲁.jpg" for filename in os.listdir(template_dir): if not filename.lower().endswith(('.jpg', '.png')): continue label = os.path.splitext(filename)[0] path = os.path.join(template_dir, filename) # 模板统一缩放到 40x80,和分割出的字符做匹配 template = cv2.imread(path, cv2.IMREAD_GRAYSCALE) template = cv2.resize(template, (40, 80), interpolation=cv2.INTER_CUBIC) _, template = cv2.threshold(template, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) self.templates[label] = template def match(self, char_img): # 输入字符图像缩放到和模板一致 char_img = cv2.resize(char_img, (40, 80), interpolation=cv2.INTER_CUBIC) best_score = -1 best_label = None for label, template in self.templates.items(): # matchTemplate 计算相似度,TM_CCOEFF_NORMED 对光照和对比度鲁棒 result = cv2.matchTemplate(char_img, template, cv2.TM_CCOEFF_NORMED) _, score, _, _ = cv2.minMaxLoc(result) if score > best_score: best_score = score best_label = label return best_label, best_score # 使用示例 matcher = TemplateMatcher("./templates") plate_text = "" char_imgs, positions = segment_characters(plate_img) for idx, char_img in enumerate(char_imgs): label, score = matcher.match(char_img) plate_text += label print(f"Char {idx}: {label}, confidence: {score:.2f}") print(f"Plate number: {plate_text}")

cv2.matchTemplate的输出是一个相似度矩阵,cv2.minMaxLoc取矩阵中的最大值和位置。TM_CCOEFF_NORMED是归一化相关系数法,返回值范围是 -1 到 1,它对图像的线性变化(比如亮度整体提升 80%)不敏感,所以光照轻微变化时用这个最稳。如果模板和字符都是二值图,也可以考虑cv2.TM_SQDIFF_NORMED,但实际效果不如相关系数法。

一个重要的落地细节是模板来源。很多公开的模板库字体印刷感太强,和真实车牌上的冲压字体差距大。我见过一个翻车例子:字符「0」和「O」、「1」和「I」在真实图片上分辨率低时几乎无法区分,模板匹配很容易混淆。常见做法是从实际拍摄的车牌中抠出字符建立模板库,这样和真实场景的分布最接近。

4.4 一个必须处理的特殊字符:汉字

汉字的识别是模板匹配里的老大难。原因有两个:一是省简称的字形结构复杂,「京」「冀」「苏」这些字的笔画密度高,二值化后容易出现笔画粘连或断裂;二是汉字字体在不同批次的车牌上有细微差异,同一省份新老车牌的「鲁」字可能字形不完全一致。

处理汉字的常见做法是把汉字单独分类,不和其他字母数字混在一起。通常车牌第一位是汉字,所以分割后第一个字符直接用汉字模板库匹配,后面的才用字母数字模板库。另外汉字区域的宽度明显大于后面的字母数字,可以做宽度判断来确认第一个字符的位置。如果模板匹配得分低于 0.5,大概率是分割出了问题,优先回去看那个字符的二值化图像。

5. 车牌识别避坑:五个让识别率骤降的边界条件

5.1 ModuleNotFoundError: No module named 'cv2'

现象:import cv2直接报ModuleNotFoundError: No module named 'cv2'。原因:绝大多数情况是没激活虚拟环境,或者 pip 安装到了系统 Python 而不是当前解释器。排查方法:先执行which python或python -m pip list,看当前环境里有没有 opencv-python。解决方式:重新安装并用python -m pip install opencv-python而不是pip install,因为python -m pip能确保安装到当前 Python 解释器对应的环境里。

5.2 contourArea 报错「未定义标识符」

现象:cv2.contourArea(cnt)在 IDE 里被标红,或者在 Python 环境中提示找不到函数。原因:这通常是 IDE 的静态检查误报,或者是在cv2模块被局部变量覆盖时出现的真实报错。解决:检查代码里有没有写cv2 = ...这种赋值语句覆盖了模块名;如果是 IDE 误报,运行代码验证即可。注意 OpenCV 4.x 中findContours的返回值是(contours, hierarchy)两个值,如果按旧版本写成三个值,contours接收到的是hierarchy,后续调用contourArea自然出问题。

5.3 蓝牌识别正常,绿牌大面积漏检

现象:同一个场景,蓝牌车能识别,绿牌新能源车完全找不到车牌区域。原因:绿色渐变车牌的背景颜色从深绿过渡到浅绿,HSV 中 H 值跨度大,S 值变化剧烈,固定的inRange范围无法覆盖整个车牌。解决:针对绿色车牌单独设置一组范围,用两套 HSV 范围分别做掩膜,再把两个掩膜做按位或合并。代码里可以这样写:

lower_green = np.array([35, 40, 40]) upper_green = np.array([80, 255, 255]) mask_blue = cv2.inRange(hsv, lower_blue, upper_blue) mask_green = cv2.inRange(hsv, lower_green, upper_green) mask = cv2.bitwise_or(mask_blue, mask_green)

5.4 白色车身反光导致定位框漂移

现象:定位框框住了车身反光条或车窗边缘,真正的车牌区域没被选中。原因:白色车身在 HSV 中 V 通道很高,S 通道接近 0,饱和度和蓝色车牌的筛选范围有重叠区域;反光条会产生高亮的饱和区域,被误判为蓝色。解决:不要只依赖颜色筛选。在颜色筛选之后,对候选区域做边缘检测,计算区域内边缘密度,车牌区域的边缘密度显著高于车身平面。另一个辅助手段是检查候选区域的宽高比是否接近车牌的 3.14:1,车身反光条往往是狭长形,比例在 8:1 以上,可以直接排除。

5.5 字符分割把「京」拆成两半

现象:分割结果中汉字被切成了两个部分,比如「京」的「亠」和「口」分离;或者反过来,「川」「苏」这类左右结构的字中间被误切了一刀。原因:垂直投影法天然对左右结构的汉字不友好,汉字内部笔画稀疏的地方投影值会降到 0,从而产生虚假分割点。解决:在分割后用字符宽度一致性检查来合并碎片。正常车牌字符宽度在 20-40 像素之间(440 宽标准图),如果某个片段宽度只有 5-10 像素,大概率是被误切的,尝试把它和相邻片段合并后再匹配模板:

# 合并过窄的字符片段 merged_chars = [] for start, end in chars: if not merged_chars: merged_chars.append([start, end]) else: last_start, last_end = merged_chars[-1] if start - last_end < 5 or (end - start) < 10: # 与上一个片段间隙小于5像素,或本身过窄,合并 merged_chars[-1][1] = end else: merged_chars.append([start, end])

这个合并逻辑的阈值不是死的,需要根据你的实际图片分辨率调整。判断标准是合并后的片段宽度应该在所有片段宽度的均值附近,偏差过大就要重新审视是不是合并策略出了问题。

6. 进阶:用透视校正和投影精修把识别率再拉一截

前面的流程跑通后,你大概会发现一个问题:倾斜车牌的识别率始终上不去。原因很直接——分割和匹配都假设字符是水平排列的,一旦车牌旋转 10 度以上,垂直投影的波谷被抹平,字符边界定位就会出错。这一章的技巧解决的就是这个问题。

车牌倾斜分为水平倾斜和垂直倾斜。水平倾斜指车牌整体旋转了一个角度,垂直倾斜指车牌在垂直方向有透视变形,比如拍摄时相机略高于车牌,导致车牌上宽下窄。处理水平倾斜用仿射变换,处理垂直倾斜用透视变换。

先看水平倾斜的检测方法。拿到定位出的车牌区域后,可以用霍夫直线检测找出车牌上下边缘的直线角度:

def get_plate_angle(plate_img): gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50, 150) # 概率霍夫直线检测,只取长线段 lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=100, minLineLength=100, maxLineGap=10) if lines is None: return 0.0 angles = [] for line in lines: x1, y1, x2, y2 = line[0] angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi # 只保留接近水平的直线,角度范围控制在 ±30 度内 if abs(angle) < 30: angles.append(angle) if not angles: return 0.0 return np.median(angles)

拿到角度后用cv2.getRotationMatrix2D构造旋转矩阵,再调用cv2.warpAffine旋正。注意旋转后车牌四个角不再是矩形,切割时边缘会出现黑边,要去掉黑边只保留中央有效区域。这个处理很值得做,因为很多停车场的相机都是斜着安装的,识别率能差出 10 个百分点。

垂直倾斜的处理要复杂一些,常见做法是提取车牌上下边缘的四个角点,做透视变换映射到标准矩形。这里有一个比较实用的偏方:先不做透视变换,而是在字符分割时改用倾斜投影法。把垂直投影的求和方向从竖直改为沿倾斜方向加权,这需要手动指定倾斜角度,但实现简单,适合角度范围固定的场景。

字符识别阶段也可以做一次精修。模板匹配的得分通常不会超过 0.9,如果你发现某一类字符总是混淆,比如「5」和「6」、「B」和「8」,可以通过增加后验规则来修正。车牌第二位是字母,最后五位是字母数字混合但第二位和倒数第一位通常是字母,这些规则可以组合成简单的约束逻辑,在模板匹配结果上做一轮校正:

# 简单的车牌规则校验 def validate_plate(plate_text): if len(plate_text) != 7: return False # 第一位是省份汉字,第二位是字母 # 汉字范围检查用简单的 Unicode 区块判断 if not plate_text[0] >= '\u4e00' or not plate_text[0] <= '\u9fff': return False if not plate_text[1].isalpha(): return False return True

最后的建议是集齐测试样本。把不同光照、不同角度、不同底色的车牌图片各存 20 张,每次改完参数跑一遍全量测试,记录识别率变化。我习惯把每张测试的中间结果(定位框、分割后字符、识别文本)都保存下来,识别错了能直接看出是哪个环节出了问题。这套流程做完,你的车牌识别项目才算真正能扛住真实场景的考验。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询