简介:一套完整的Python+OpenCV车牌自动识别实战源码包,附带可运行主程序、车牌图片数据与字符匹配模板,面向图像处理学习者、毕业设计选题学生以及智能交通、车辆管理等应用开发人员。项目从车辆图像输入出发,依次演示灰度化、二值化、滤波去噪等预处理,利用边缘检测、轮廓查找和形态学操作完成车牌定位;再通过垂直投影法、滑动窗口法等实现字符分割,最后与模板库匹配得出车牌号码,能够帮助读者把完整算法链路落到代码层面。压缩包共2000个文件,其中1999个jpg样本、1个Python脚本,整体仅12.37MB,体量精简,适合对照源码逐步调试和替换图片测试。包内车牌样本覆盖多个省份,可作为不同省份车牌匹配效果的练习数据,也可为车辆追踪、违章查询、停车场管理等真实场景提供实现参考;读者逐行跟踪源码,可直观感受预处理参数与定位、分割结果的关系。已有3105人学习下载,适合希望从预处理到识别输出理解OpenCV图像处理流程的开发者。
1. 车牌自动识别的最小闭环:一套模板匹配方案就能落地
车牌自动识别(License Plate Recognition)在安防、停车场、高速公路收费场景里已经是很成熟的能力,但自己从零用 Python 和 OpenCV 实现一遍,依然是计算机视觉入门最值得动手的项目之一。这个标题里最关键的限定词是“字符匹配模板”——它明确告诉你,这条技术路线不依赖深度学习,而是用经典图像处理把车牌定位、字符分割、模板匹配三步串起来。对刚接触 OpenCV 的开发者来说,这条路理解成本低、依赖轻、调试直观,一台普通笔记本就能跑通,而且识别结果可解释:每个字符是怎么被认出来的,你都能在中间变量里看得一清二楚。本文就按这套最经典的方案,把源码结构、处理流程、参数设置和踩坑点一次性讲透。
2. 环境与项目结构:先把 OpenCV 的“地基”打稳,再谈识别
2.1 安装 OpenCV 时最容易翻车的三个细节
OpenCV 的安装本身不复杂,但很多人第一步就卡在环境上。常见做法是用 pip 安装 opencv-python 和 opencv-contrib-python 两个包,前者提供核心图像处理接口,后者额外包含一些特征匹配和背景建模的扩展模块,车牌识别项目用到的基础功能都在核心包里。这里有一个长期存在的坑:如果你同时安装了两个包,或者装过某个发行版自带的 OpenCV,import cv2 时可能加载到旧版本,导致 cv2.findContours 这类接口的返回值跟你查到的文档对不上。我一般会在安装后用一行命令确认版本:
pip install opencv-python opencv-contrib-python numpy matplotlib python -c "import cv2; print(cv2.__version__)"如果输出的是 4.x,那么 findContours 返回两个值(contours, hierarchy);如果是老教程里常见的 3.x,同样的函数返回三个值。这种接口差异是新手踩坑的重灾区,后面我们统一按 4.x 写代码。另外,numpy 是 OpenCV 的底层依赖,图像在 Python 里本质就是 numpy 数组,所以 numpy 的版本不要乱降,否则 cv2 在读取图像时会报 dtype 相关的错误。Ubuntu 用户如果不想用 pip,也可以走 apt 安装或源码编译,但源码编译一次要几十分钟,对初学项目来说性价比不高。
2.2 项目目录划分:源码、数据、模板三者分离
车牌识别项目虽小,但目录结构建议一开始就按“源码、数据、模板”分开,不然调试几天后你会发现自己都找不到某张测试图放在哪。我常用的结构是这样的:
plate_recognition/ ├── src/ │ ├── locate.py # 车牌定位模块 │ ├── segment.py # 字符分割模块 │ ├── recognize.py # 模板匹配识别模块 │ └── main.py # 主流程入口 ├── data/ │ ├── test/ # 测试图片,建议涵盖不同光照和背景 │ └── template/ # 字符模板图片(0-9,A-Z,中文省份简称) └── output/ # 中间结果可视化输出把字符模板单独放一个目录是有原因的:模板匹配的识别效果高度依赖模板质量,如果你后续要加新的省份简称或者替换模板风格,只需要动 template 目录,不用改代码。这个设计在项目迭代时会省下大量时间。源码文件按处理阶段拆分,也是为了让每一步的中间结果可以被单独验证——这在计算机视觉项目中非常关键,你总得知道是定位错了还是分割错了,才能动手修。
3. 车牌定位:颜色与形态学双重过滤,把目标从复杂背景里抠出来
3.1 为什么先做颜色过滤而不是直接找边缘
车牌定位的思路有很多种:基于边缘检测、基于颜色、基于纹理,甚至基于滑动窗口。实际项目中,最稳的组合是“颜色过滤 + 形态学操作 + 轮廓筛选”,因为车牌本身有极强的先验特征:蓝底白字(大陆标准车牌)、矩形、宽高比约 3:1、字符区域纹理密集。如果只用 Canny 边缘检测,建筑轮廓、路面标线、车窗反光都会产生大量干扰边缘,后续筛选的阈值会非常难调。而先在 HSV 空间做颜色过滤,能直接砍掉大部分非蓝色区域,计算量小且效果直观。
HSV 空间的选择不是玄学,而是因为车牌蓝色在 BGR 空间受光照影响太大——同一个蓝色车牌,在阴影下和阳光下拍摄,BGR 三个通道的值可能差出一倍以上。HSV 把色相(H)从亮度(V)里分离出来,只要色相落在蓝色区间,亮暗变化就不影响判断。这是颜色过滤类任务里通用的第一课。
3.2 定位模块的完整代码与参数说明
import cv2 import numpy as np def locate_plate(image): # 1. 统一尺寸:长边缩放到 600,保留宽高比,减少计算量 scale = 600 / max(image.shape[0], image.shape[1]) img = cv2.resize(image, (int(image.shape[1] * scale), int(image.shape[0] * scale))) # 2. 高斯模糊:核大小用 5x5,去掉传感器噪声,避免颜色阈值抖动 blur = cv2.GaussianBlur(img, (5, 5), 0) # 3. 从 BGR 转到 HSV,注意 OpenCV 中 H 范围是 0-180 hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV) # 4. 蓝色车牌阈值:H 在 100-124,S 大于 90,V 大于 80 # 这个区间覆盖了大部分标准车牌的蓝色,但避开深蓝和紫色 lower_blue = np.array([100, 90, 80]) upper_blue = np.array([124, 255, 255]) mask = cv2.inRange(hsv, lower_blue, upper_blue) # 5. 形态学闭运算:先膨胀再腐蚀,把车牌字符间的空隙填上 # 内核用 20x6 的矩形,因为车牌是横向长条,横向连接更重要 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 6)) closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 6. 找轮廓并筛选:面积、宽高比、填充率三重过滤 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: x, y, w, h = cv2.boundingRect(contour) area = w * h ratio = w / h # 车牌宽高比在 2.5-4.5 之间,面积至少占全图的 0.5% if area > img.shape[0] * img.shape[1] * 0.005 and 2.5 < ratio < 4.5: return img[y:y + h, x:x + w] return None代码逻辑不复杂,但每个参数都有说法。高斯滤波的核大小 5x5 是经验值,核太小去不掉噪点,核太大会把车牌边缘磨平,导致后续轮廓不闭合。HSV 的阈值区间是第一个要重点调的地方:不同省份的车牌蓝色深浅有差别,新能源车的渐变绿底又是另一套规则,如果你要兼容多种车牌,就得把颜色区间参数化,暴露给调用方调整。形态学内核的尺寸直接决定字符间隙能不能被填上,20x6 的意思是横向跨度 20 像素、纵向 6 像素,这是针对常规车牌字符排列密度反复试出来的值——内核太窄,字符间隙连不起来,车牌区域会分裂成好几块;内核太宽,容易把车灯、车窗等相邻蓝色区域并进来。
轮廓筛选的三个条件本质是在做“几何形状先验”约束,这也是图像处理里一个很重要的思路:不要指望单一特征精准定位,而是用一组弱约束层层过滤。宽高比是车牌最硬的几何特征,标准车牌宽 440mm、高 140mm,比例约 3.14,加上拍摄角度变化和边缘误差,2.5-4.5 是合理区间。如果你要处理倾斜严重的图片,单纯 boundingRect 的宽高比会失真,后面避坑章节会单独说倾斜矫正的问题。
3.3 定位效果不理想时的排查顺序
定位是整个识别流程里误差最大的环节,定位错了后面全错。当候选区域框不住车牌或者框住了半辆车时,我的排查顺序是:先把 mask 和 closed 的中间结果用 cv2.imwrite 存下来逐张看,用 OpenCV 的可视化能力做“过程审查”。如果 mask 里车牌区域已经是断裂的碎块,说明 HSV 下限太低或上限太高,蓝色没被完整选中;如果 mask 完整但 closed 后区域过大,说明形态学内核太大,把周围蓝色物体并进来了。如果 mask 和 closed 都没问题但最终没输出,那就是轮廓筛选条件太苛刻,把面积阈值或宽高比范围放宽再试。这个“先看中间结果、再调阈值”的习惯,比盲目改参数高效得多。
4. 字符分割与模板匹配:把车牌拆成单字,再用模板字典做识别
4.1 字符分割:垂直投影与轮廓查找两条路
拿到定位后的车牌区域,接下来要把它切成单个字符的图。字符分割的经典做法有两种。第一种是垂直投影法:把车牌灰度图做二值化后,统计每一列上白色像素的数量,字符列会有密集的白点,字符间隙列几乎是零值,这样就能按投影的波峰波谷切分。这种方法的优点是直观、计算快,但缺点是对倾斜和字符粘连非常敏感,一旦二值化效果不好,切分点就会跑偏。第二种是基于轮廓查找:对二值化后的车牌图找轮廓,按轮廓的宽高和位置过滤出字符区域,再按 x 坐标排序。轮廓法对轻微倾斜的容忍度更高,因为字符个体的轮廓不会因为倾斜消失,而且无需预先知道字符个数。
我在实际代码里选轮廓法,因为它的鲁棒性更好,并且天然能跳过车牌的螺丝钉和分隔符等小噪声。但轮廓法也有代价——如果两个字符粘连在一起,它们会形成一个轮廓,导致分割失败。针对这个问题,分割前通常要先做一次“字符区域裁剪”,只保留车牌中间高度 60% 的区域,这能挡住车牌上下边缘的干扰线。
import cv2 import numpy as np def segment_characters(plate): # 1. 转灰度再二值化:用 Otsu 自适应阈值,避免手工设阈值的麻烦 gray = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 2. 去掉上下边缘:只保留中间 60% 高度,避开车牌边框和装饰线 h, w = binary.shape cropped = binary[int(h * 0.2):int(h * 0.8), :] # 3. 找轮廓并过滤:按高度、宽高比筛选字符候选 contours, _ = cv2.findContours(cropped, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) chars = [] for contour in contours: x, y, w, h = cv2.boundingRect(contour) # 字符高度大约占裁剪区域高度的 70-95%,宽度不应超过高度 if h > cropped.shape[0] * 0.5 and w < h * 1.2: chars.append((x, cropped[y:y + h, x:x + w])) # 4. 按 x 坐标从左到右排序 chars.sort(key=lambda item: item[0]) return [char for _, char in chars]这里的二值化用了 Otsu 自动阈值,原理是让算法自动寻找一个灰度阈值,使得前景和背景的类间方差最大。为什么要用 Otsu 而不是固定阈值?因为车牌图片的光照变化太大,白天强光下字符灰度可能高达 200,傍晚弱光下可能只有 120,固定阈值很难一次覆盖所有场景。Otsu 虽然不保证每次最优,但至少让分割结果具备一致性,降低后续模板匹配的方差。字符过滤条件中,高度阈值为裁剪区域高度的 50% 是为了把螺丝钉和车牌的白色边框点排除掉,因为在二值化图像里,螺丝钉的反光常常会形成一个轮廓。
还有一个细节值得单独说:字符轮廓的排序必须严格按 x 坐标排序,但 Python 的 sort 是稳定的,所以可以直接用 x 作为 key。这里不要用轮廓本身的中心点排序,因为不同字符宽度不同,中心点位置会有偏差,而 x 坐标就是字符的起始位置,最可靠。
4.2 字符模板的制作:用真实字体渲染还是从样本里切
模板匹配的识别效果,很大程度取决于模板本身。常见的模板来源有两种:一是用系统字体直接渲染出字符图片,二是从真实车牌样本里手动裁剪出字符图片。用字体渲染的好处是干净、标准、制作快,但问题是车牌的字体是专用的(类似“公安交警字体”),普通字体如思源黑体、微软雅黑在笔画粗细和字间距上跟真实车牌有差异,实际识别时对边缘细节的响应会打折。从真实样本里裁剪模板更贴近实际,但模板本身带有拍摄噪声和透视变形,反而可能让匹配分数不稳定。
我的做法是两者结合:用字体渲染生成基础模板库,保证每个字符都有;再从测试集中补充特殊字符的真实样本,覆盖渲染字体跟实际车牌差异较大的字符。模板图片统一处理成宽高比为 1:2 的灰度图,因为模板匹配要求模板和被匹配区域尺寸尽量一致,后面的缩放到统一尺寸就是在做这件事。
制作模板的脚本不复杂,核心思路是遍历模板目录、读取图片、统一尺寸、存入字典:
import cv2 import os import numpy as np def load_templates(template_dir): templates = {} for filename in os.listdir(template_dir): if not filename.endswith('.png'): continue # 文件名就是字符本身,例如 "A.png"、"京.png" char_name = os.path.splitext(filename)[0] img = cv2.imread(os.path.join(template_dir, filename), cv2.IMREAD_GRAYSCALE) # 统一缩放到 40x80,后续匹配时也把候选字符缩到同样尺寸 img = cv2.resize(img, (40, 80), interpolation=cv2.INTER_CUBIC) # 归一化像素到 0-1,消除不同模板之间的亮度差异 img = img / 255.0 templates[char_name] = img return templates这里有一个容易被忽略的点:归一化到 0-1 是模板匹配前的重要预处理。因为 OpenCV 的模板匹配相关函数对绝对亮度敏感,两个字形完全一样、但一个偏亮一个偏暗的字符,匹配分数会差很多。除以 255.0 后,匹配只看形状结构,不看绝对亮度。尺寸统一到 40x80 不是固定标准,你可以根据车牌区域字符实际大小调整,但必须保证模板和候选字符用同一套缩放规则。
4.3 模板匹配识别:用归一化相关系数做决策
识别环节用的是 cv2.matchTemplate,配合 TM_CCOEFF_NORMED 模式,这个名字翻译过来是“归一化相关系数”,它的输出范围在 -1 到 1 之间,越接近 1 代表两幅图的形状结构越相似。为什么选这个模式而不是简单的平方差?因为相关系数模式的本质是“减去各自均值后再算相似度”,对光照变化天然不敏感——字符是否偏亮偏暗,不会显著影响匹配分数。这在车牌识别的真实场景里非常重要,因为你永远无法保证测试图片的光照跟模板制作时一致。
def recognize_character(char_img, templates): # 候选字符缩放到模板同尺寸 char_img = cv2.resize(char_img, (40, 80), interpolation=cv2.INTER_CUBIC) char_img = char_img / 255.0 best_score = -1 best_char = None # 遍历模板字典,逐个计算相关系数,取最高分 for char_name, template in templates.items(): result = cv2.matchTemplate(char_img, template, cv2.TM_CCOEFF_NORMED) score = result[0][0] if score > best_score: best_score = score best_char = char_name # 置信度低于 0.6 时直接返回 None,后续可触发人工复核 return best_char if best_score > 0.6 else None这段代码逻辑简单,但有一个决策细节值得展开:匹配分数阈值 0.6 的设定要看你自己的数据集。如果测试图片清晰、没有倾斜,0.7 甚至 0.8 都能跑得很好;如果测试图模糊或角度刁钻,0.5 可能是更合理的选择。这个阈值直接决定了识别的“拒识率”——阈值设太高,很多字符被拒绝导致识别失败;阈值设太低,容易把相似字符(比如 0 和 O、1 和 I)认错。实际项目中,我会输出所有字符的得分排序而非只取最高分,这样后续校验时能看到“第二候选是谁、跟第一名差多少”,这在排查误识别时是救命的信息。
4.4 主流程串联:从输入图片到输出车牌号的完整调用
定位、分割、识别三个模块各自独立,最后需要一个主流程把它们串起来。主流程做的事情很简单:读取图片、调用定位、调用分割、遍历字符逐个识别、拼接结果。但有一个环节不能省——在定位模块里把车牌区域裁出来后,最好先做一次透视矫正或至少是灰度化后的对比度增强,这样分割和识别的稳定性会大幅提升。
import cv2 from locate import locate_plate from segment import segment_characters from recognize import recognize_character, load_templates def main(image_path): templates = load_templates('data/template') image = cv2.imread(image_path) if image is None: print('图片读取失败,请检查路径') return plate = locate_plate(image) if plate is None: print('未定位到车牌区域') return # 增强对比度:用 CLAHE 限制对比度自适应直方图均衡 gray = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray) chars = segment_characters(enhanced) if len(chars) < 5: print(f'分割出的字符数异常:{len(chars)},请检查分割参数') return plate_number = ''.join([recognize_character(char, templates) for char in chars]) print(f'识别结果:{plate_number}')这段代码的注释里藏着两个关键决策:一是分割前先做 CLAHE 对比度增强,这是针对光照不均场景的常用手段,它会把局部对比度拉开,让字符边缘更清晰,代价是可能增强背景噪点;二是分割后检查字符数量,正常车牌是 7 个字符(汉字 + 字母 + 数字组合),如果分割结果少于 5 个,大概率是定位区域切歪了或者二值化失败,直接返回错误比硬识别好得多。这种“错误尽早暴露”的写法,在调试阶段能帮你省下大量时间去定位问题出在哪个环节。
5. 识别率翻车的 5 个典型坑:光照、倾斜、粘连与模板失效
5.1 蓝底车牌的 HSV 阈值在阴天和傍晚集体失效
现象:晴天测试准确率很高,一到阴天或者傍晚,定位框开始漂移,有时候框到车窗玻璃上,有时候直接找不到车牌。
原因:HSV 颜色空间虽然比 BGR 对光照更鲁棒,但“鲁棒”不等于“免疫”。阴天时车牌蓝色的饱和度(S)显著下降,而傍晚时亮度(V)大幅降低,两者叠加后,原本设定的 [100, 90, 80] 下限会把大量暗部蓝色像素过滤掉,导致 mask 区域不完整。另外,车窗玻璃反射天空的淡蓝色,在 HSV 空间里色相也可能落在 100-124 区间,形成干扰区域。
解决:不要固定一套 HSV 阈值。我在项目里把颜色区间做成配置文件,按“晴天/阴天/夜间”预设三组参数,调用时根据图片的平均亮度自动选择。更稳的方案是同时引入边缘检测作为补充——颜色过滤负责给出候选区域,Canny 边缘的密度用来确认这块区域是否有字符纹理。两者取交集,虚警率会大幅下降。
5.2 车牌倾斜超过 5 度,宽高比筛选直接失效
现象:图片里车身有轻微侧倾,车牌在画面里不是标准的矩形,而是平行四边形,定位模块的 2.5-4.5 宽高比条件经常把真正的车牌过滤掉,或者框出来的区域里包含大量背景。
原因:cv2.boundingRect 返回的是“外接正矩形”,不感知旋转。车牌倾斜 10 度时,外接矩形的宽高比可能从标准的 3.14 变成 2.2 甚至更低,直接跌破下限。而如果车牌有俯仰角度,外接矩形又会包含车灯、保险杠等无关区域。
解决:用 cv2.minAreaRect 替换 boundingRect,它会返回带旋转角度的最小外接矩形。拿到旋转角度后用 cv2.getRotationMatrix2D 做仿射变换,把车牌摆正后再重新裁剪。这一步对后续字符分割的收益极大——倾斜状态下的字符投影会互相干扰,垂直投影法几乎必错。实际操作时,旋转矫正的代码要放在定位和分割之间,并且旋转后要做一次边界填充,否则车牌边缘的字符会被切掉。
5.3 二值化把字符和背景粘成一团
现象:分割出来的“字符”比预期大很多,轮廓宽高比异常,字符数量少了,明显是两个字被并成一个轮廓。
原因:车牌的白色字符和蓝色背景在灰度图上的对比度,取决于光照和拍摄设备的动态范围。当强光直射车牌时,字符和背景的灰度差变小,Otsu 自动阈值选出的分割点可能刚好落在字符半腰的位置,导致字符边缘膨胀,相邻字符粘连。另一个常见原因是车牌表面有污渍或反光,局部区域的灰度分布畸变,Otsu 的全局阈值无法适应局部差异。
解决:在二值化之前对灰度图做一次 CLAHE 增强,这能有效拉开局部对比度。如果字符仍然粘连,可以尝试形态学开运算(先腐蚀后膨胀)切断细小的粘连点,但内核尺寸要控制得很小,否则会把字符笔画也腐蚀掉。还有一条路是改用自适应阈值 cv2.adaptiveThreshold,它对光照不均的适应性比 Otsu 更好,代价是参数多了一组,块大小和 C 值需要按你的图像分辨率重新调。
5.4 模板匹配把 0 和 O、8 和 B 认反
现象:数字和字母混排的车牌上,识别结果偶尔把数字 0 认成字母 O,或者 8 和 B 互相混淆。
原因:这两个字符对在结构上高度相似,TM_CCOEFF_NORMED 的分数差距可能在 0.05 以内,而阈值判断只取最高分时,一点噪声扰动就会让结果翻转。汉字部分更容易翻车——省份简称的汉字笔画差异大,模板稍有偏差,匹配分数整体都会偏低,导致很低的置信度。
解决:两个层面的处理。第一层是模板增强,给易混淆字符组准备多张样本模板,取多个模板中的最高分作为该字符的最终得分——字符 B 准备两三种不同笔画粗细的样本,能有效压住噪声扰动。第二层是规则约束,车牌字符的排列有固定规则:第一个是汉字,第二个是字母,第三到第七是字母或数字。在拼接结果后加一道语法校验,比如第二位出现数字就报警告,最后一位是汉字就直接判错,这类“业务规则兜底”在工程里比纯算法优化还常用。
5.5 新能源车牌和使馆车牌让预设规则失效
现象:蓝牌跑得好好的,突然出现一辆渐变绿底的新能源车牌,定位框完全对不上;或者遇到黑色底的白字车牌,二值化直接反转。
原因:新能源车牌是渐变绿色背景、黑色字符,和蓝底白字的灰度分布恰好是反的。而使馆车牌的底色、字符排列规则跟普通车牌不同。你基于蓝底白字设计的 HSV 阈值、二值化方向(THRESH_BINARY_INV)、宽高比范围,全部建立在“蓝底白字”这个前提下。
解决:接受一个事实——没有一套参数能通吃所有车牌类型。在设计阶段就要把“车牌类型”作为一个输入参数,支持扩展不同底色的检测分支。实现上,HSV 过滤可以按“蓝色/绿色/黑色”分别跑一遍,每个分支走自己的形态学参数和字符分割流程。这个扩展说起来简单,但做的时候要保持清醒:加一种车牌类型,就意味着你要为它单独准备一组模板和参数,并重复一遍调参踩坑的过程。对初学项目来说,先把蓝牌做到 95% 准确率,再考虑扩展,这是性价比最高的路线。
6. 验证识别效果的实操方法:用统计指标给系统做一次“体检”
模板匹配方案跑通之后,“看起来能出结果”和“真正能用”是两回事。我习惯在项目收尾时做一次量化评估:准备一批带标注的测试图,逐张跑识别,统计字符级准确率和整牌准确率。这个测试集不用很大,30-50 张覆盖不同场景就够,但每张图必须有 ground truth 文本文件,否则结果无从对比。
评估脚本的输出建议做三列:图片名、真实车牌、识别结果。每跑完一批,重点看两类错误:定位偏移导致的字符缺失,和模板匹配导致的单个字符误识别。前者是参数问题,后者是模板质量问题,处理方式完全不同。一张图上的多个字符中,只要有一个字符的置信度低于阈值,就把整牌标记为“待人工复核”,这种保守策略在实际交付中比硬着头皮输出一个可能错的结果更靠谱。
代码层面,验证脚本只用标准库就能实现:
import os import glob def evaluate(test_dir): results = [] for img_path in sorted(glob.glob(os.path.join(test_dir, '*.jpg'))): label_file = img_path.replace('.jpg', '.txt') # 每张测试图配一个同名 txt 文件,内容就是真实车牌号 with open(label_file, 'r') as f: true_label = f.read().strip() pred_label = main(img_path) char_acc = sum(1 for a, b in zip(true_label, pred_label) if a == b) / len(true_label) results.append((os.path.basename(img_path), true_label, pred_label, char_acc)) # 输出整体字符准确率和整牌准确率 total_char_acc = sum(r[3] for r in results) / len(results) plate_acc = sum(1 for r in results if r[1] == r[2]) / len(results) print(f'字符级准确率:{total_char_acc:.2%}') print(f'整牌准确率:{plate_acc:.2%}') return results这套评估逻辑还有一层进阶价值:它让“调参”从拍脑袋变成数据驱动。比如你想验证 CLAHE 增强是否真的有效,就分别跑一次带增强和不带增强的评估,对比字符级准确率的变化——数字会告诉你答案,而不是靠感觉。如果你是第一次接触这个项目,我建议把上面定位、分割、识别三个模块的中间结果都打印成图片存到 output 目录,跑完一遍后逐个打开看。你会直观地看到哪一步把图像处理坏了,这种体感对理解 OpenCV 的工作方式特别有帮助。
我做车牌识别这个项目最大的教训是:模板匹配方案的上限由模板质量和预处理决定,而不是由识别算法本身决定。有一阵子我反复调主流程代码,识别率一直卡在 85% 上不去,后来静下心把测试集里失败的样本逐一翻出来看,发现 80% 的失败都发生在定位阶段——车牌区域被裁偏了,后续所有环节跟着出错。从那之后我养成了一个习惯:任何识别项目,先可视化中间过程,再动代码。这个习惯帮我避开了无数次盲调参数的弯路,也推荐给你。希望帮到你。
本文还有配套的精品资源,点击获取