不靠深度学习,用OpenCV模板匹配就能实现车牌自动识别 简介一套完整的PythonOpenCV车牌自动识别实战源码包附带可运行主程序、车牌图片数据与字符匹配模板面向图像处理学习者、毕业设计选题学生以及智能交通、车辆管理等应用开发人员。项目从车辆图像输入出发依次演示灰度化、二值化、滤波去噪等预处理利用边缘检测、轮廓查找和形态学操作完成车牌定位再通过垂直投影法、滑动窗口法等实现字符分割最后与模板库匹配得出车牌号码能够帮助读者把完整算法链路落到代码层面。压缩包共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 两个包前者提供核心图像处理接口后者额外包含一些特征匹配和背景建模的扩展模块车牌识别项目用到的基础功能都在核心包里。这里有一个长期存在的坑如果你同时安装了两个包或者装过某个发行版自带的 OpenCVimport 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-9A-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-124S 大于 90V 大于 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(keylambda 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), interpolationcv2.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), interpolationcv2.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(clipLimit2.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% 的失败都发生在定位阶段——车牌区域被裁偏了后续所有环节跟着出错。从那之后我养成了一个习惯任何识别项目先可视化中间过程再动代码。这个习惯帮我避开了无数次盲调参数的弯路也推荐给你。希望帮到你。本文还有配套的精品资源点击获取