资讯动态

车牌识别实战:Python+OpenCV传统视觉方案完整解析

发布时间:2026/10/17 9:22:06 来源:尧图企业网站定制
简介面向学习OpenCV与图像处理的Python开发者这份车牌自动识别资料包包含可直接运行的源码、车牌图片数据集和字符匹配模板为解决车牌定位、字符分割与识别问题提供了完整示例。整个实战项目涵盖图像灰度化、二值化、滤波去噪边缘检测、轮廓查找、形态学操作以及垂直投影或滑动窗口分割、模板匹配识别等关键环节适用于智能交通、停车场管理、车辆追踪等场景。包内共2000个文件其中1999张jpg作为车牌及字符模板样本1个py文件承载核心识别逻辑整体12.37MB结构清晰便于对照学习。目前已有3105人学习下载适合想要通过具体项目掌握OpenCV图像处理流程的初中级开发者也可为相关应用开发提供可直接参考的代码与数据集。1. 车牌识别项目实战Python OpenCV 传统视觉路线的完整落地车牌自动识别在很多人印象里是深度学习的专属领域但这套基于 Python OpenCV 的源码项目走的是另一条路纯传统图像处理不依赖任何深度学习框架。它通过颜色筛选定位车牌区域用轮廓和投影做字符分割最后用字符匹配模板完成识别——整套流程在一台普通笔记本上跑一张图只需要几百毫秒而且源码结构非常清晰一个文件就是一个处理阶段特别适合想理解图像处理底层逻辑的开发者。这份资源不仅包含完整源码还带了测试数据和多省份字符匹配模板拿到手就能直接跑通整个识别管线。如果你是刚入门 CV 的开发者或者需要在离线环境快速部署一个轻量级识别模块这个项目的学习价值比它本身的识别率更值得关注。2. 系统设计与技术选型为什么车牌识别能用模板匹配搞定2.1 传统视觉方案 vs 深度学习方案的选择逻辑开始拆代码之前得先理解这个项目的一个核心判断车牌识别一定要用 CNN 吗这套源码给出的答案是否定的。车牌场景有一个天然优势——字符集合是封闭且有限的。国内民用车牌由省份汉字、字母和数字组成汉字约 30 多个字母数字 36 个加起来不到 70 个类别。在字符已经严格分割的前提下用模板匹配做分类完全够用而且不需要 GPU、不需要训练、不需要标注数据。深度学习方案的痛点在于数据。一个能上路的车牌识别模型至少需要数万张标注车牌图像采集和标注成本高训练周期长而且模型在陌生场景泛化时经常出现玄学问题——训练集里没见过的字体、没见过的倾斜角度可能直接把置信度拉垮。传统视觉方案没有这些烦恼模板匹配的本质是像素相似度计算它不吃数据只吃模板质量。这套项目的作者把多省份字符模板单独抽出来放在 templates 目录下换一个省份的规则只需要替换对应汉字模板不需要重新训练任何东西。整个系统的技术栈非常克制Python 3 OpenCV NumPy三个依赖解决全部问题。OpenCV 负责图像读写、颜色转换、形态学操作、轮廓查找和模板匹配NumPy 负责矩阵运算和数组切片。没有 Flask、没有 PyTorch、没有 TensorFlow这在项目部署时是巨大的优势——离线环境装一个 opencv-python 就够了。2.2 目录结构与模块职责划分这个项目的目录结构做得很规整几乎是一个标准的传统 CV 工程模板。拿到源码后先看整体布局核心模块都在根目录或者 src 目录下命名直白从文件名就能猜到它在处理流程中的位置。plate_recognition/ ├── main.py # 主入口串联整个识别流程 ├── preprocess.py # 图像预处理灰度化、滤波、边缘检测 ├── locate_plate.py # 车牌定位颜色筛选 轮廓查找 ├── segment_chars.py # 字符分割投影法 轮廓过滤 ├── recognize_chars.py # 字符识别模板匹配 ├── templates/ # 字符模板目录 │ ├── provinces/ # 省份汉字模板如 京、鲁、粤、苏 │ ├── letters/ # 字母模板 A-Z │ └── digits/ # 数字模板 0-9 ├── data/ # 测试图像数据 │ ├── blue/ # 蓝牌样例 │ └── green/ # 绿牌样例新能源 └── output/ # 中间结果输出目录方便调试每个模块只做一件事preprocess 不做定位locate_plate 不做分割这种解耦设计对于学习源码非常友好。调试的时候可以把中间结果写到 output 目录比如定位出来的车牌区域图、分割后的单个字符图一眼就能看出问题出在哪个环节。main.py 的流程组织也很直白不是复杂的 pipeline 框架就是一个顺序调用import cv2 from preprocess import preprocess_image from locate_plate import locate_plate from segment_chars import segment_chars from recognize_chars import recognize_chars def main(image_path): # 1. 读取原始图像 img cv2.imread(image_path) if img is None: print(f无法读取图像: {image_path}) return None # 2. 预处理降噪 边缘增强 processed preprocess_image(img) # 3. 车牌定位返回车牌区域的 ROI 图像 plate_roi locate_plate(processed, img) # 4. 字符分割返回排序后的单个字符图像列表 char_imgs segment_chars(plate_roi) # 5. 字符识别模板匹配返回最终车牌字符串 result recognize_chars(char_imgs) return result if __name__ __main__: result main(data/blue/鲁A12345.jpg) print(f识别结果: {result})代码逻辑非常清晰第一步读图做预处理第二步定位车牌区域第三步分割出每个字符第四步用模板匹配识别。这里有一个值得注意的参数设计——locate_plate同时接收processed和img两个参数前者是处理后的图像用于定位后者是原始图像用于裁剪 ROI因为预处理后的图像像素值已经被改变直接从中截取车牌区域会导致后续字符分割精度下降。这种细节在教科书里经常被忽略但在实际项目中是影响识别率的隐藏因素。3. 车牌定位实现拆解从边缘检测到轮廓筛选的完整链路3.1 预处理阶段为什么先做灰度化和高斯滤波定位是车牌识别最关键的一步如果车牌区域找错了后面的一切都是徒劳。这个项目的预处理阶段把传统 CV 的经典操作串了一遍灰度化、高斯滤波、Sobel 边缘检测、二值化。每一步都有明确的参数选择理由不是随便拍的。import cv2 import numpy as np def preprocess_image(img): # 转灰度去掉颜色信息减少计算量 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯滤波核大小 5x5sigma 设为 0 由 OpenCV 自动计算 # 作用去掉图像噪声避免边缘检测阶段出现大量假边缘 blurred cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 边缘检测只算 x 方向梯度 # 车牌区域字符和背景之间有强烈的水平方向纹理变化 sobel_x cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize3) sobel_x cv2.convertScaleAbs(sobel_x) # 自适应二值化局部阈值处理光照不均 _, binary cv2.threshold(sobel_x, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) return binary为什么用 Sobel 而不是 Canny因为 Canny 会产生完整的物体边缘轮廓车牌区域在这种检测下和车身融为一体很难单独拎出来。而 Sobel 只计算水平方向的梯度车牌字符密集排列水平纹理变化非常突出经过二值化后车牌区域会呈现一个高亮的矩形块和周围环境区分度很高。高斯滤波核大小定为 5x5 是经验值。核太小去噪效果差核太大比如 11x11会把车牌字符的边缘细节磨平后续定位时轮廓面积不够。OTSU 二值化在这里很关键——不同光照条件下 Sobel 输出的梯度幅值差异很大固定阈值很容易翻车OTSU 根据图像直方图自动算阈值属于「兜底型」策略也是这套代码能在多组测试图上稳定跑通的底气。3.2 轮廓筛选与矩形度校验预处理拿到二值图后定位逻辑进入核心阶段查找所有轮廓筛选出车牌候选区域。车牌区域在二值图中是一个宽高比约 3.14:1 的矩形面积相对较大内部纹理密度高。这些特征组合起来就能锁定位移。def locate_plate(binary_img, original_img): # 查找所有轮廓只取外轮廓压缩水平/垂直方向冗余点 contours, _ cv2.findContours( binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) # 按轮廓面积降序排序车牌区域通常面积较大 contours sorted(contours, keycv2.contourArea, reverseTrue) for contour in contours[:20]: # 计算轮廓的最小外接矩形 rect cv2.minAreaRect(contour) (x, y), (w, h), angle rect # 归一化宽高比车牌标准宽高比 440:140 ≈ 3.14 aspect_ratio max(w, h) / min(w, h) if 2.5 aspect_ratio 4.0: # 面积过滤太小的轮廓直接跳过 area w * h if area 1000: continue # 根据角度校正旋转矩形得四个顶点坐标 box cv2.boxPoints(rect) box np.int0(box) # 在原图上裁剪车牌区域 roi crop_plate(original_img, box) return roi return Nonecv2.minAreaRect是定位阶段的核心 API它返回一个旋转矩形能处理车牌在图片中略微倾斜的情况。宽高比范围定在 2.5 到 4.0 之间没有卡死在 3.14是因为车牌在透视角度下宽高比会发生形变留出余量可以提高召回率。这里有一个常见的误用行为得提醒一下sorted(contours, keycv2.contourArea, reverseTrue)之后直接取第一个轮廓是不靠谱的很多项目中车身侧面的大面积阴影区面积远大于车牌如果只取前三个结果大概率选错。代码里取前 20 个候选逐一过条件筛选宁可多算几次也不能漏掉正确区域这是一种典型的「用计算量换准确率」的思路。3.3 HSV 颜色筛选把蓝色车牌从复杂背景中扣出来轮廓筛选在背景复杂的图片上容易误判——车身贴纸、车窗反光、路牌都可能形成类似矩形。这个项目加了一道保险HSV 颜色空间下的蓝色区域检测。国内蓝牌的底色是特殊的蓝色在 HSV 空间中集中在一定的色相区间这是传统 CV 里最可靠的颜色特征。def filter_blue_region(img): # 转换为 HSV 色彩空间 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色的色相 H 范围大约在 100~124OpenCV 中 H 范围 0~180 # 饱和度 S 和明度 V 设下限过滤掉暗部和高光区域 lower_blue np.array([100, 80, 40]) upper_blue np.array([124, 255, 255]) # 生成二值掩码在蓝色范围内的像素置为 255 mask cv2.inRange(hsv, lower_blue, upper_blue) # 形态学闭运算填补车牌区域内字符造成的空洞 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return maskHSV 参数不是随便拍的。OpenCV 的 H 通道范围是 0 到 180和常规的 0 到 360 刚好差一倍所以蓝色的标准值 240 在 OpenCV 里对应 120。S 下限设 80 是为了排除白色和灰色区域——白底黑字的普通车身上没有高饱和度的蓝色不会被误判成车牌。V 下限设 40 是让过暗的车牌也能被检测到因为阴天或逆光环境下蓝色车牌的明度会显著降低。形态学闭运算是这一步的隐藏功臣。车牌字符是白色或黄色的它们嵌在蓝色底板上inRange出来的掩码在字符位置是空洞的直接用这个掩码去定位会得到一个「破碎」的车牌区域。用MORPH_CLOSE先膨胀再腐蚀空洞被填充掩码变为完整的矩形块后续轮廓查找就非常干净。实际调试时如果你发现掩码总是缺一块优先检查膨胀核尺寸是不是太小。4. 字符分割与识别从投影切分到模板匹配的完整实现4.1 投影法字符分割垂直投影的波峰波谷分析拿到车牌 ROI 图像后下一步是把车牌上的七个字符逐个切出来。这个步骤做不好会直接连累后面的识别——切歪一个字符模板匹配的相似度就会大幅下降。这个项目采用垂直投影法核心思想是利用字符和背景之间的像素分布差异。def segment_chars(plate_roi): # 转为灰度并二值化字符区域变白背景变黑 gray cv2.cvtColor(plate_roi, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 垂直投影统计每一列白色像素的个数 col_sum binary.sum(axis0) / 255 height binary.shape[0] # 寻找投影中的波峰区间连续非零列构成一个字符候选区 char_regions [] in_char False start 0 for i in range(len(col_sum)): # 当前列有白色像素认为是字符区域内的点 if col_sum[i] height * 0.1: if not in_char: in_char True start i else: if in_char: in_char False char_regions.append((start, i)) # 过滤过窄的区间噪声产生的零散白色列宽度远小于字符宽度 char_widths [end - start for start, end in char_regions] median_width np.median(char_widths) valid_regions [] for start, end in char_regions: width end - start # 宽度在中位数一半以下的区域视为噪声 if width median_width * 0.5: valid_regions.append((start, end)) # 分割结果按区间从原图裁剪出单个字符 char_imgs [] for start, end in valid_regions: char_img plate_roi[:, start:end] char_imgs.append(char_img) return char_imgs垂直投影的原理很直白车牌上的字符是离散排列的字符之间有小段空白这些空白列上没有白色像素投影值归零。从投影序列中找出连续非零的段就得到了每个字符的左右边界。这里有一个容易踩坑的细节代码里特意做了处理——col_sum[i] height * 0.1这个阈值。如果直接用col_sum[i] 0判断车牌边缘的铆钉、边框上的污渍产生的零星白点会被误判为字符边界导致一个字符被切成了两段。设一个高度比例的阈值只有白色像素占比达到 10% 的列才被认为是有效字符列这个比例过滤掉了绝大多数噪声。median_width过滤是第二道保险字符宽度中位数的一半以下区域不可能是合法字符。4.2 字符尺寸归一化模板匹配前的关键一步分割出来的字符图像尺寸各不相同——车牌在图像中的大小、拍摄距离都会影响字符像素尺寸。模板匹配阶段要求模板和待识别图像尺寸完全一致所以必须做归一化。这个项目把每个字符都 resize 到和模板一样的分辨率通常是 20x40 像素。def normalize_char(char_img, target_size(20, 40)): # 先转灰度 gray cv2.cvtColor(char_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 contours: x, y, w, h cv2.boundingRect(contours[0]) # 对轮廓坐标做边界保护防止越界 y1 max(0, y - 2) y2 min(gray.shape[0], y h 2) x1 max(0, x - 2) x2 min(gray.shape[1], x w 2) gray gray[y1:y2, x1:x2] # 等比缩放后填充到目标尺寸避免拉伸变形 h, w gray.shape scale min(target_size[0] / w, target_size[1] / h) new_w int(w * scale) new_h int(h * scale) resized cv2.resize(gray, (new_w, new_h)) # 创建白色背景画布把字符放在中心位置 canvas np.full(target_size, 255, dtypenp.uint8) x_offset (target_size[0] - new_w) // 2 y_offset (target_size[1] - new_h) // 2 canvas[y_offset:y_offset new_h, x_offset:x_offset new_w] resized # 二值化确保只有纯黑和纯白两种像素 _, canvas cv2.threshold(canvas, 127, 255, cv2.THRESH_BINARY) return canvas归一化的重点在于「不要拉伸」。直接把 30x50 的字符 resize 到 20x40宽度和高度缩放比例不同字符会产生形变模板匹配时相似度会显著下降。正确的做法是等比缩放后居中放置在画布上多出的区域保持白色背景。边框去除这一步比较隐蔽——如果字符图像中带着车牌边框或铆钉噪声匹配时这些干扰像素会被计算进相似度里。通过查找字符实际轮廓再裁剪相当于把字符从「背景混杂」的图像中单独抠出来匹配精度会提升一个肉眼可见的档次。边界保护代码max(0, y - 2)和min(gray.shape[1], x w 2)是防止裁剪区域越界抛异常这个问题在字符贴住图像边缘时会发生。4.3 模板匹配识别相似度计算与结果置信度判定最后一步是多对一的模板匹配。每个待识别字符要和所有模板做相似度计算得分最高的模板对应的字符就是识别结果。import os import cv2 import numpy as np def recognize_chars(char_imgs, template_dirtemplates): # 载入模板库字典结构 {字符: 模板图像} templates load_templates(template_dir) # 记录识别结果 result for char_img in char_imgs: # 归一化到标准尺寸 char normalize_char(char_img) best_char ? best_score -1 # 逐个模板计算相似度 for label, template in templates.items(): # matchTemplate 使用相关系数法值越大相似度越高 res cv2.matchTemplate(char, template, cv2.TM_CCOEFF_NORMED) score res[0][0] if score best_score: best_score score best_char label # 置信度过滤低于 0.5 的识别结果视为失败不输出 if best_score 0.5: result best_char else: result _ return result def load_templates(template_dir): templates {} for root, _, files in os.walk(template_dir): for fname in files: if fname.endswith(.png): label os.path.splitext(fname)[0] path os.path.join(root, fname) img cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 所有模板加载时就归一化到 20x40避免运行时反复 resize img cv2.resize(img, (20, 40)) templates[label] img return templatescv2.TM_CCOEFF_NORMED是相关系数法它把像素值做了均值归一化处理能一定程度上抵抗光照差异的影响。这是模板匹配最常用的方法之一计算成本低效果稳定。置信度阈值 0.5 是一个经验值这个项目在测试数据上调出来的结论。如果匹配分数低于 0.5说明当前字符和所有模板的相似度都不够高——可能是分割阶段出了问题也可能是遇到未收录的字符类型此时输出_而不是强猜一个错误结果。实际调试时你可能需要针对自己的图片集调整这个阈值车牌整体偏小时匹配分数会偏低需要下调阈值如果经常出现错字则要上调阈值提高判别标准。模板文件命名直接以字符本身作为文件名比如鲁.png、A.png、0.png这样载入模板时自动把文件名解析为字符标签不需要额外的映射表——这是很多入门项目做不到的「数据即代码」设计新增字符只需要把模板图片丢进对应目录不需要改代码。5. 项目运行与常见问题排查从环境搭建到调试实战5.1 环境搭建与运行命令这个项目的依赖极其精简标准的 Python 环境就能跑起来。推荐使用虚拟环境避免和系统的 Python 包冲突。# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate # 安装依赖 pip install opencv-python numpy # 运行项目识别测试图片 python main.py如果实测发现 OpenCV 安装慢可以用国内镜像源加速pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple这种方式能缩短大量等待时间。安装成功后可以用python -c import cv2; print(cv2.__version__)验证环境是否正常输出版本号说明安装没问题。运行后项目会在终端打印识别结果同时在 output 目录下生成中间过程图——预处理后的二值图、定位出的车牌 ROI、分割后的字符图都可以直接打开查看。建议把输出目录打开着跑每一步的处理效果一目了然。5.2 定位失败图像尺度变化导致的轮廓面积过滤误伤现象在项目自带测试图上识别正常换了自己拍的手机照片后车牌区域定位输出 None。原因手机照片分辨率高车牌在画面中的像素面积很大但轮廓宽高比可能因为拍摄角度发生偏移超出了 2.5~4.0 的范围。反过来如果车牌离得远轮廓面积小于 1000会被面积过滤直接丢弃。解决先看 output 目录里预处理后的二值图确认车牌区域是否清晰可见。如果可见把locate_plate.py中的面积阈值从 1000 下调到 300同时把宽高比范围放宽到 2.0~4.5。这两个参数是车牌定位的「召回率」控制旋钮调宽会引入更多误检区域调窄会漏检建议逐步调整而不是一步到位。5.3 字符分割粘连铆钉和边框噪声干扰投影现象分割结果中出现 8 个甚至 9 个区域第一个字符被切成了两段。原因车牌固定的铆钉在二值化后形成高亮点和第一个字符连在一起或者车牌边框在预处理阶段没有被完全清除投影时边框的像素和字符像素叠加导致字符边界判断错误。解决在segment_chars之前增加一道形态学操作——先腐蚀再膨胀可以把铆钉和边框噪声从字符区域分离。腐蚀操作让细小的突起断连膨胀恢复字符原有的形状代价是字符边缘会轻微变细。如果切割区域数量依旧不对把投影阈值从height * 0.1调到height * 0.2让边界判定更严格。5.4 模板匹配识别错误训练模板和实际图像字体风格不一致现象数字 8 被识别成 0字母 O 被识别成 0省份汉字频繁出错。原因模板匹配是像素级比对模板字体是标准印刷体但实际车牌上有多种字体变体尤其在省份汉字上差异巨大。数字 0 和字母 O 的形状几乎一致像素层面区分度极低模板匹配极易混淆。解决对 0/O、1/I 这类混淆字符对在模板匹配后增加后续规则——车牌第二位必须是字母所以如果模板匹配给出数字但第二位按规则必须是字母则强制从字母模板中重新选择。这是一种简单的「规则兜底」策略不需要修改模板本身就能把这类错误压制住。5.5 性能瓶颈单张图片处理速度慢现象处理一张 4000x3000 像素的照片耗时超过 2 秒。原因图像分辨率太高预处理和轮廓查找的计算量和像素数量成正比4000 万级像素的图在 Sobel 和findContours阶段需要大量时间。解决在main.py读取图片后立即缩放把长边限制在 800 像素以内。车牌识别不需要超高分辨率800 像素足够完成定位和分割耗时能降低到原来的十分之一。这是传统 CV 项目最常见的优化手段和算法本身无关但经常被忽略。提示findContours在不同 OpenCV 版本中的返回值有差异。OpenCV 3.x 返回image, contours, hierarchy三个值OpenCV 4.x 返回contours, hierarchy两个值。如果你的代码报「not enough values to unpack」错误说明版本和代码不匹配把contours, _ cv2.findContours(...)改成contours cv2.findContours(...)[0]即可兼容所有版本。6. 二次开发实战从静态图片识别到摄像头实时识别拿到这套源码后最常见的改造方向是把单张图片识别改成摄像头实时流识别。整个改造不需要动定位和分割模块核心是换掉数据输入源加一个帧循环。我给出的方案是直接在主流程前套一个 VideoCapture 读取层。import cv2 from preprocess import preprocess_image from locate_plate import locate_plate from segment_chars import segment_chars from recognize_chars import recognize_chars def process_frame(frame): # 复用原有处理流程frame 就是一张图像 processed preprocess_image(frame) plate_roi locate_plate(processed, frame) if plate_roi is None: return None char_imgs segment_chars(plate_roi) return recognize_chars(char_imgs) def run_camera(): # 0 代表默认摄像头局域网摄像头可以是 RTSP 地址 cap cv2.VideoCapture(0) # 降低缓冲帧数减少处理延迟 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 跳过前 10 帧摄像头初始化阶段画面不稳定 for _ in range(10): cap.read() while True: ret, frame cap.read() if not ret: break # 缩放画面摄像头分辨率高但车牌识别不需要全尺寸 frame cv2.resize(frame, (640, 480)) # 隔帧处理留给摄像头采集和显示的时间 result process_frame(frame) if result: cv2.putText(frame, fPlate: {result}, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow(Camera, frame) # 按 q 退出循环 if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() if __name__ __main__: run_camera()这个改造的核心逻辑在于process_frame函数直接复用了原有模块。注意我把帧缩放到了 640x480因为摄像头采集的 1920x1080 图像在预处理阶段的计算量过大实时性跟不上的概率很大。用CAP_PROP_BUFFERSIZE把缓冲降到 1可以让摄像头不积压旧帧读取到的始终是最新的画面。另一个有价值的改造方向是支持新能源绿牌。绿牌是 8 个字符比蓝牌多一位而且字符排列是「省份 字母 6 位数字字母混合」的格式。改造点集中在segment_chars的字符数量期望上——目前代码不限制字符数量8 个字符会正常切出来但recognize_chars的置信度阈值可能对绿牌字符不友好因为绿牌字符和蓝牌字符在字体上有细微差别。把绿牌模板加到 templates 目录并给normalize_char加一个可配置的目标尺寸参数对黄牌和绿牌这种字符密集度高的场景很有用。还想再提升识别率的话建议在recognize_chars里加多模板投票机制。对同一个字符准备三到五套不同字体的模板匹配时分别计算最后取得分最高的模板作为结果。这种方式能规避个别模板和实际字符风格偏差过大的风险代码改动量不大但识别效果提升明显。我一般会在改造前后保留同一组测试图每改一次就跑一遍做前后对比这样能快速发现参数调整是变好了还是变差了。车牌识别的调试过程很依赖可视化的中间结果建议养成把每一阶段的输出图都存下来的习惯——预处理后的图、定位出的车牌、分割出的字符一步一个黑匣子地拆开看问题定位会快很多。希望这个项目的源码和这篇拆解能帮你走通传统 CV 的完整流程做出自己的车牌识别应用。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑