简介面向高校计算机视觉课程期末项目的车牌识别系统源码基于Python与OpenCV实现涵盖车牌检测、字符分割、SVM识别等完整流程适合有Python基础、需要完成图像识别或机器学习方向大作业的学生参考与二次开发。压缩包共29个文件包含predict.py、surface.py等核心Python源码svm.dat与svmchinese.dat预训练模型文件charsChinese.7z等字符训练数据包以及大量测试用的车牌jpg图片和项目配置文件整体约13.87MB目录结构清晰便于按模块梳理代码逻辑。目前已有722人学习下载具备一定参考热度。代码经过严格调试可直接运行并配有测试样本和模型数据能够帮助读者快速复现车牌识别效果也可作为课程设计或毕业设计的基线项目在此基础上进行算法优化与功能扩展。1. 一个能跑通的车牌识别源码比“高分”更值钱打开这个压缩包之前先把期待放准你缺的不是一个“95分以上”的标签而是一套打开就能跑、跑完能跟老师讲清每一步思路的代码。基于PythonOpenCV的车牌识别系统核心链路就四步用颜色和形态学把车牌抠出来用投影法把字符切开用模板匹配把字符认出来再用规则修正结果。不需要GPU、不需要数据集、不需要训练几小时面对统一规格的蓝底车牌认真调参后识别率能做到90%以上。适合被大作业逼到墙角的同学也适合想用最小依赖验证CV流程的开发者。这篇笔记不讲获奖经验只讲怎么把它写出来、调得动、讲得清。2. 先看全链路OpenCV做车牌识别的四个关键环节与选型边界2.1 从图片到车牌号四步链路的数据流车牌识别系统说到底是把一张彩色图片变成一串文本。输入是车辆前脸或车尾的照片输出类似“京A12345”这样的字符串。整个处理链路在传统OpenCV方案里固定为四步每一步的产物都是下一步的输入我习惯先把这个数据流画清楚再动手写代码。第一步是车牌定位回答“车牌在哪”。常见做法是把图像从BGR转到HSV颜色空间把蓝色像素聚合出来再用形态学运算把离散的蓝色点连成一个连通的白色区块最后通过轮廓筛选得到候选区域。第二步是透视校正摄像头拍出来的车牌大多带角度不是正对镜头需要旋转或透视变换拉正。第三步是字符分割把校正后的车牌区域转成灰度图、二值化然后统计每一列的白色像素数白像素集中的列就是笔画列间空隙就是切分点。第四步是字符识别把切出来的小块跟模板库里每个字符做匹配得分最高的模板对应的文字就是识别结果。阶段输入核心方法常见瓶颈车牌定位彩色图像HSV颜色阈值 形态学 轮廓筛选光照变化、车身蓝色干扰透视校正候选区域minAreaRect或四点透视倾斜角度过大、透视变形字符分割校正后的灰度图二值化 垂直投影边框、铆钉、字符粘连字符识别字符块序列模板匹配或轻量OCR字形相似、模板字体不一致这条链路每一步都会影响最终结果。很多同学拿到源码第一件事是直接跑最终识别识别不对就开始玄学调参其实问题往往出在中间某一步。我一般会在每个环节后加一条imwrite把中间结果存成图片一旦结果不对直接看是哪一步的形状出了问题这比对着最终输出猜效率高得多。比如车牌定位如果框错了后边全错分割错位识别必错。所以做这个项目调试中间结果比调最终输出更重要。2.2 为什么不首选深度学习课程设计场景的性价比判断听到“车牌识别”现在很多人的第一反应是YOLO加CRNN检测车牌再用序列识别。这个方案准确率确实高复杂场景下能碾压传统CV但它有三个代价需要标注数据、需要GPU训练、需要推理环境。课程设计一共三四周时间如果数据没标够、显存不够大、答辩现场还要演示实时识别深度学习的试错成本很容易超过项目本身能承受的预算。传统OpenCV方案的好处是可解释性强。定位用了什么颜色空间、为什么选这个形态学核、分割为什么用垂直投影每一步都能用一两句话讲清楚答辩时老师很难把你问倒。而且它对硬件没要求集显笔记本跑实时视频流都够用。缺点也很明显对光照、倾斜、模糊、车牌颜色不规范等场景很敏感。所以我的建议是如果你的大作业要求是“在给定图片集上达到一定识别率”传统CV完全够用如果要求里明确包含夜间、雨雾、多类型车牌那再考虑换深度学习。不要把简单问题复杂化这是做课程设计最要紧的判断。对比维度传统OpenCV方案深度学习方案准确实率标准蓝牌场景90%左右复杂场景可到98%以上数据需求不需要训练数据需要几百到几千张标注图训练成本零训练需要GPU和几小时训练部署体积几十MB依赖少模型框架动辄几百MB答辩可解释性每步都能讲原理黑匣子只能讲“喂了数据”这张对比表不是否定深度学习而是说课程设计这个约束条件里传统CV方案的性价比最高。我自己做项目时也经常先跑通传统方案再考虑要不要升级因为传统方案是整个视觉流程的最佳教学范本。2.3 环境搭建Python与OpenCV版本搭配与最小依赖安装这个方案依赖很薄核心就是OpenCV、NumPy、Matplotlib后两个不是必须的但调试时画图看中间结果非常方便。最省事的方式是用虚拟环境单独建一个纯净空间避免把系统Python搞乱。安装命令如下python -m venv plate_env # Windows下激活plate_env\Scripts\activate # Linux/macOS下激活source plate_env/bin/activate pip install --upgrade pip pip install opencv-python numpy matplotlib这里有几个实际注意点。第一opencv-python这个包会自动带上NumPy单独写出来是为了提醒版本兼容OpenCV 4.x配NumPy 1.21以上的组合最稳太老或太新的NumPy版本偶尔会让cv2.findContours返回格式出意外。第二如果要处理视频用opencv-python就够了不要装opencv-contrib-python除非你明确需要contrib模块里的SIFT或其它专利算法这个包体积大且容易和基础包冲突。第三Python版本建议3.8到3.11之间的某个稳定版本新版OpenCV对3.12以上的Python做了兼容调整但有些预编译轮子可能还没跟上遇到ImportError时先别怀疑代码换回3.9或3.10试试。装完后打开Python环境验证一下import cv2 import numpy as np print(cv2.__version__) print(np.__version__)如果输出版本号且没有ImportError环境就绪。有一点要提前说明OpenCV的imread在读取中文路径时会失败这是Windows下最常见的坑。如果你的测试图片放在带中文名的文件夹里要么改路径要么用cv2.imdecode手动读入。后者的写法比较绕但遇到非用中文路径不可时这才是正解import numpy as np def imread_unicode(filepath): stream open(filepath, rb) data bytearray(stream.read()) arr np.asarray(data, dtypenp.uint8) return cv2.imdecode(arr, cv2.IMREAD_COLOR)注意代码里的图片路径最好全英文这是Windows下OpenCV的老问题不要等报错再改。如果项目要求支持中文路径就把上面这个函数放到公共工具模块里所有读取图片的地方都走它。3. 车牌定位用HSV颜色和形态学把车牌从背景里“挖”出来3.1 HSV阈值筛选为什么RGB阈值一看就碎定位车牌的第一步不是边缘检测而是颜色筛选。国内最常见的蓝底白字车牌它的蓝色在RGB空间里并不是一个稳定的数值不同光线下面一个像素的RGB可能从(0,0,255)飘到(120,180,200)直接设两个阈值很难覆盖所有情况。HSV空间把颜色的色调H、饱和度S、亮度V分开其中H值对光照变化相对不敏感所以更适合做颜色分割。这也是“先转HSV再inRange”成为标准做法的原因。常见做法是把BGR图片用cv2.cvtColor转到HSV然后用cv2.inRange设定蓝色的上下界。代码片段如下import cv2 import numpy as np img cv2.imread(car.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌的HSV阈值H在100-130之间S和V设下限滤掉暗区 lower_blue np.array([100, 100, 100]) upper_blue np.array([130, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) cv2.imwrite(mask.jpg, mask)这段代码做了三件事读图、转HSV、生成二值掩码。inRange的规则是每个像素的H、S、V三个通道都在lower和upper范围内时输出255否则输出0。生成的mask.jpg里白色区域就是算法认为“像蓝色车牌”的部分。这里有两个参数选择经验供参考。H的下界100和上界130覆盖的是标准蓝色如果测试图里车牌偏深或偏浅可以把下界放到90、上界放到140试试。S是饱和度下界100可以过滤掉灰蒙蒙的非彩色区域但也会误伤浅蓝色车身V是明度下界100滤掉阴影里的深色区域但如果图片整体偏暗要降到80甚至60。这三个值没有一个放之四海而皆准的组合最好的办法是把mask作为中间结果输出用几组阈值跑一遍肉眼挑最稳定的。为了让调试更直观我经常把mask叠加到原图上看一眼就知道阈值选得准不准mask_bgr cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) overlay cv2.addWeighted(img, 0.7, mask_bgr, 0.3, 0) cv2.imwrite(overlay.jpg, overlay)addWeighted把原图和彩色化的mask按0.7和0.3的权重叠加半透明的白色区域会覆盖在原图上。这样一眼就能看出车牌区域是不是被完整盖住同时还能看出哪些非车牌区域被误选。如果有大片车身被盖住说明阈值范围太宽如果车牌区域零零散散说明阈值太窄或S/V下界太高。3.2 形态学闭运算让字符区域连成一块“白斑”颜色筛选之后的mask通常不是一整块而是零散的白点字符笔画是白色的笔画之间有黑隙车牌的蓝色底面上也有各种噪点。如果直接在这张图里找轮廓找不到一个完整的车牌区块。这时要用形态学闭运算把相邻的白点连起来。闭运算是“先膨胀再腐蚀”。膨胀会把白色区域向外扩一圈原本离得近的白点会连到一起腐蚀再把扩出来的边界收回一部分既能填平字符间隙又不会让区域一大圈。针对横向长条的车牌我用的是横向矩形核kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) cv2.imwrite(closed.jpg, closed)getStructuringElement创建一个指定形状的核MORPH_RECT是矩形(17,5)表示宽度17像素、高度5像素。为什么要横向这么长因为车牌字符是横向排列的字符间距远远小于行间距用一个窄长核能把同一行的字符连起来又不会把上下两行文字粘连。iterations2表示做两次闭运算相当于把连通范围再扩大一圈。如果核太大车身贴纸、保险杠上的蓝色装饰会被一起连进来核太小字符间隙填不平。17和5是我在常见分辨率下的起点值你可以按比例缩放原则是核的宽度大于字符最大间距高度小于字符高度的一半。闭运算做完再做一个简单的连通域筛选计算所有轮廓的外接矩形把面积太小或形状不像车牌的直接丢弃。这一步能排除掉大量蓝色车贴、蓝颜色的小物品。按照我的血泪经验这一步宁可多留几个候选也不要漏掉真车牌。因为后面还有透视校正和字符分割它们会帮我们筛掉假货。3.3 轮廓筛选与透视校正从白斑到规整的车牌矩形拿到closed之后的二值图下一步用cv2.findContours提取轮廓。OpenCV 4.x里的写法是contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h if area 1000: # 太小的轮廓是噪声 continue ratio w / h if 2.0 ratio 5.0: # 中国蓝牌宽高比约3.1-3.4 boxes.append((x, y, w, h))这是一个非常典型的筛选逻辑。findContours从二值图里查找白色连续区域RETR_EXTERNAL只取最外层轮廓SIMPLE让轮廓点尽量少以省内存。对每个轮廓boundingRect算出外接矩形的左上角坐标、宽度和高度。area小于1000的直接扔掉因为正常单张车辆照片里的车牌面积远不止这个数ratio宽高比在2到5之间这是根据车牌物理尺寸留的余量。筛选出来的boxes列表就是候选车牌区域。如果boxes里不止一个候选我一般会按面积排序取最大的那个作为最终车牌同时保留第二大的作为备份boxes.sort(keylambda b: b[2] * b[3], reverseTrue) x, y, w, h boxes[0] plate_candidate img[y:yh, x:xw]接着做透视校正。如果车牌倾斜角度在30度以内用cv2.minAreaRect找到最小外接矩形再用仿射旋转把图片摆正即可。更稳的做法是用四点透视变换取最小外接矩形的四个角点映射到一个固定宽高的矩形上rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect) box np.int0(box) width 440 # 中国车牌标准宽度映射到像素 height 140 dst np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypenp.float32) M cv2.getPerspectiveTransform(box.astype(np.float32), dst) plate cv2.warpPerspective(img, M, (width, height))minAreaRect会返回一个中心点、宽高和旋转角boxPoints把它转成四个角点。getPerspectiveTransform根据源四点和目标四点计算透视变换矩阵MwarpPerspective用M把原图投射到目标尺寸。这里把车牌统一拉伸成440x140是为了后续分割和识别时字符大小相对固定减少模板匹配的缩放误差。如果车牌本身比较正也可以用getRotationMatrix2D按角度旋转但透视变换能一并处理前后俯仰角度鲁棒性更好。透视校正常常被忽略但它的价值在字符分割阶段才体现出来。如果车牌是歪的垂直投影的波峰会重叠字符切分必然错误。所以即使你的测试图片看起来挺正我也建议保留这一步它能让后面的工作轻松很多。边缘检测、轮廓筛选做得再漂亮车牌不正也白搭这是我踩过最深的坑之一。4. 字符分割与模板匹配识别让7个字符逐个“验明正身”4.1 二值化与垂直投影按像素分布切开字符车牌区域拿到手后先转灰度再做二值化。灰度图无法避免光照不均匀直接用固定阈值很容易把笔画融成一片或断成渣。我推荐用Otsu大津法它自动计算一个能把前景背景分得最开的阈值gray cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)threshold函数的第二个参数0会被忽略因为OTSU会自动算阈值第三个255是二值化后的最大值THRESH_BINARY表示大于阈值的置255、小于的置0。这样白字黑底方便做投影。但这里有个前提Otsu假设前景背景的灰度分布是双峰的如果车牌照到强反光整个图接近一片白Otsu会算出离谱的阈值。遇到这种情况我会先看二值化结果如果笔画之间黏成一团就改成自适应阈值cv2.adaptiveThreshold效果会好不少。字符分割最朴素的算法是垂直投影。思路很简单统计每一列里白色像素的个数得到一个数组。车牌的字符笔画密集字符所在列的白色像素明显多于字符间隙。于是连续“有值”的区域就对应一个字符h, w binary.shape col_sum np.sum(binary, axis0) # 按列求和 char_regions [] in_block False start 0 for i in range(w): if col_sum[i] 0 and not in_block: in_block True start i elif col_sum[i] 0 and in_block: in_block False char_regions.append((start, i)) char_regions [r for r in char_regions if r[1] - r[0] 8]这个循环从头到尾扫每一列遇到白色像素开始记录遇到空白列结束记录得到一个又一个的字符区间。过滤条件“宽度大于8像素”是为了去掉小白点噪声。正常车牌上会有7个字符加一个小圆点圆点通常又窄又矮投影上也会形成一个块可以在后续按宽度和高度的比例进一步剔除。垂直投影的优点是简单、可解释缺点是对字符粘连无能为力。如果车牌边框、铆钉和二值化噪声没有被清理干净投影会多出很多假区间。所以进分割之前我通常会先用连通域分析把不符合字符尺寸的小块去掉。具体做法是拿到二值图后用connectedComponentsWithStats标出每个连通域只保留高度在车牌高度50%到90%之间的区域其他全部涂黑num_labels, labels, stats, _ cv2.connectedComponentsWithStats(binary, connectivity8) for i in range(1, num_labels): x, y, cw, ch stats[i][0], stats[i][1], stats[i][2], stats[i][3] if ch binary.shape[0] * 0.5 or ch binary.shape[0] * 0.95: binary[labels i] 0这段代码遍历所有连通域根据外接矩形高度滤掉过高过矮的块。车牌字符的高度通常占车牌高度的60%到85%边框和铆钉要么很高上下边框要么很矮铆钉点都会被这一条规则干掉。做完之后再跑垂直投影假区间会少很多。不要一开始就上深度学习分割网络课程设计场景下把投影做好已经能覆盖大多数标准图片。4.2 模板匹配用归一化相关系数给字符“对号入座”分割出的每个字符块下一步要和预先准备好的字符模板做匹配。模板库怎么来两种常见做法一是用OpenCV自带字体渲染打印字符二是从网上收集标准车牌字符图片。第一种最简单但打印出来的字体跟车牌字体的笔画粗细不一致匹配率会受影响第二种更接近真实分布但需要花时间整理。我一般会准备一个模板目录每个文件名就是字符本身比如“京.jpg”“A.jpg”“5.jpg”方便映射。模板图片统一处理成灰度图并且缩放到同一个尺寸这是匹配稳定性的基础。加载模板的代码可以写成import os template_dir templates templates {} for fname in os.listdir(template_dir): if not (fname.endswith(.jpg) or fname.endswith(.png)): continue char_name os.path.splitext(fname)[0] tpl cv2.imread(os.path.join(template_dir, fname), cv2.IMREAD_GRAYSCALE) tpl cv2.resize(tpl, (20, 40)) # 统一模板尺寸 templates[char_name] tpl这里把所有模板都resize成20x40是因为切割出来的字符块高度不一统一尺寸后matchTemplate才不用处理多尺度问题。模板尺寸不要取太大否则匹配计算量会增加也不要太小否则细节丢失。20x40是我在440x140车牌下试出来的经验值按比例缩放即可。模板匹配的具体操作是把每个待识别字符缩放到跟模板一样大然后用matchTemplate做遍历匹配。这里有一个选择用哪种匹配算法。我常用TM_CCOEFF_NORMED它对图像整体明暗变化有归一化不会因为字符亮一点暗一点就大幅波动。代码示例def match_char(plate_char_gray, template_gray): # 统一尺寸避免大小不一致导致匹配分失真 plate_char_gray cv2.resize(plate_char_gray, (template_gray.shape[1], template_gray.shape[0])) result cv2.matchTemplate(plate_char_gray, template_gray, cv2.TM_CCOEFF_NORMED) _, score, _, _ cv2.minMaxLoc(result) return score best_char best_score -1 for char_name, tpl_gray in templates.items(): score match_char(char_block, tpl_gray) if score best_score: best_score score best_char char_name这段代码的核心是循环所有模板让待识别字符和每个模板比较取分数最高的作为结果。matchTemplate返回一个匹配图minMaxLoc取出最大值作为相似度分数。TM_CCOEFF_NORMED的分数范围在-1到1之间0.6以上才勉强可信0.8以上通常比较稳。如果最高分低于0.5宁可输出“”也不要硬给答案因为硬给错的比不给出更难看。我还会把每个字符的得分打印出来观察模板库哪里有冲突。比如字母“O”和数字“0”长得几乎一样车牌里一般不会出现字母O但模板库里如果有它就经常出来捣乱。处理办法就是干脆不把容易混淆的模板放进库里利用车牌规则排除掉。4.3 结构约束与后处理汉字在第一位字母第二位后面是数字字母中国民用车牌的结构是第一位是省份简称汉字第二位是发牌机关代号一个大写字母第三到第七位是字母和数字的混合一般不使用字母I和O。这个先验知识必须用上它能把识别率再拉高几个百分点也能纠正明显的低级错误。很多模板匹配方案翻车就是因为没有用这个约束让“京”字去和“A”比让“0”去和“O”比得分一乱就全错。结构约束的做法是在匹配阶段就对每个位置施加候选集限制。第一位候选只从模板库里的汉字列表查第二位只从英文字母列表查后面几位从大写字母加数字的列表查。代码实现时就是把上一节templates字典按位置分成三个子字典hanzi_templates {k: v for k, v in templates.items() if \u4e00 k \u9fff} letter_templates {k: v for k, v in templates.items() if k.isalpha() and k.isupper()} alnum_templates {k: v for k, v in templates.items() if k.isalnum() and k.upper() k} char_result for idx, block in enumerate(char_blocks): if idx 0: candidates hanzi_templates elif idx 1: candidates letter_templates else: candidates alnum_templates best_char, best_score match_in_candidates(block, candidates) char_result best_char这里用简单的条件判断分出三个候选库汉字按Unicode范围识别字母用isupper()数字字母混合用isalnum()。match_in_candidates就是上一节的循环匹配逻辑只是候选范围缩小了。这样即使某个汉字模板和英文字母模板长得像也不会被错误选中。识别完成后再做一个后置校验检查第二位是不是字母检查后面几位里有没有车牌禁用字母I和O有则替换为1或0。校验规则不复杂但能防止那种“整个车牌生成长得合法、实际上全错”的尴尬局面。模板匹配方案的上限取决于模板质量和字符块质量。如果分割出来的字符自带边框、铆钉噪声再好的模板也救不回来。所以出现识别错误时先怀疑分割再怀疑模板最后才怀疑匹配算法本身。5. 避坑指南车牌识别开发里最常见的5个翻车点5.1 车牌被车身蓝色贴膜“带偏”定位到车标上现象程序输出的候选框里车牌没框到反而框到了一块蓝色的车身贴纸或车标区域。原因HSV的阈值并不是只认车牌蓝而是认所有“颜色像车牌蓝”的像素车身贴膜的颜色稍深或稍浅时只要在阈值范围内就会被选出来。解决不要只依赖颜色加两条过滤规则——候选区域的宽高比必须接近车牌比例候选区域内部的纹理密度要高。颜色筛完后先做轮廓筛选再对候选框内做一次水平边缘检测统计边缘像素占比车牌区域的边缘密度明显高于平整车身。我通常把边缘密度阈值设在0.1以上低于这个值就认为候选区太光滑直接丢弃。5.2 字符分割时两个字符粘成一块投影波峰分不开现象垂直投影后只有一个大块包含两个甚至三个字符分割出来的块宽度明显超过单个字符。原因二值化后字符笔画太粗字符间隙里的噪声没有清干净或者车牌倾斜校正不到位导致相邻字符的投影重叠。解决先检查透视校正是否生效倾斜的车牌必须拉正再做投影。然后检查二值化阈值如果OTSU结果里间隙被白色噪声填满可以先用膨胀前先腐蚀一次或者对投影做平滑再根据波谷位置切分。实在不行用连通域分析先断开再按连通域外接矩形合并把投影法和连通域法结合起来用。5.3 模板匹配把“0”认成“O”“8”认成“B”现象识别结果里经常出现字母O、B等但车牌合法字符集里根本没有这些字母。原因模板库里同时放了字母O和数字0、字母B和数字8两者形态太接近归一化相关系数区分度不够。解决使用车牌结构约束在候选集里排除字母I和O如果必须保留就给模板匹配加一个“相似度惩罚”当两个候选字符得分差小于0.05时按车牌规则优先取数字。另外还可以做一次形态学细化让字符变成单像素宽再匹配把笔画粗细差异的影响降到更低。这个方法对“C”和“G”这类近似字符同样有效核心思路是别让模板库自相残杀。5.4 蓝色车牌的H阈值在黄昏和灯光下完全失效现象白天识别得好好的换到黄昏或夜间灯光的图片mask一片黑车牌完全找不到。原因HSV虽然比RGB稳定但H通道在中低亮度下也会漂移而且S会大幅下降蓝色车牌在黄光下看起来像灰色。解决不要死盯一组阈值。先做一次自适应亮度判断如果图像整体平均亮度低就把V下界调低、H范围放宽或者用两套阈值分别跑把两个结果求并集。更好的办法是增加一条灰度边缘检测通道蓝色定位失败时用Canny边缘加形态学找车牌外接矩形两条路互为备份。课程设计通常不需要做到全天候但如果你知道测试集里有傍晚的图这一条必须提前考虑。5.5 程序能跑但中文车牌显示成乱码现象识别结果打印到控制台或写入文件时“京”变成一堆乱码。原因OpenCV的imwrite不会乱码乱码出在Python控制台的编码问题以及Windows环境下终端默认编码是GBK而Python字符串是Unicode。解决打印时用sys.stdout.reconfigure(encodingutf-8)或者把结果写到UTF-8文件里如果用imwrite保存带中文名的图片需要先用imencode转成二进制再写入文件。这个坑不伤算法但会在答辩演示时显得很不专业值得提前处理。我建议把所有字符结果统一存成Unicode字符串输出时指定encoding不要用print裸打。6. 进阶操作用批量测试和参数调优把识别率从“能跑”做到“稳定”6.1 建一个最小验证脚本一键跑完整个目录并统计准确率单张图片跑通不算数课程设计答辩时老师会拿你没见过的图来测。所以从第一天起就把所有测试图放到一个目录里写一个循环批量识别最后打印准确率。这里有个小技巧文件名里写好真实车牌号识别结果跟文件名比对自动统计对错。这样每个参数改完跑一遍脚本就能看到量化变化不用肉眼盯着一张张图判断。import os true_plate None correct 0 total 0 for name in os.listdir(test_imgs): if not name.endswith(.jpg): continue true_plate name.split(.)[0] pred_plate recognize_single_plate(os.path.join(test_imgs, name)) total 1 if pred_plate true_plate: correct 1 print(ftotal{total}, correct{correct}, acc{correct / total:.2%})recognize_single_plate是封装好的主函数里面按定位、校正、分割、识别的顺序调用。注意这个统计是“完全正确”才算对实际项目中单个字符错也算错所以准确率会偏悲观。想更细就按字符维度统计分别计算汉字的准确率和数字字母的准确率这样你能知道问题出在哪个环节。6.2 三个关键参数的调整顺序与经验值调参不要乱试按下面顺序来。第一步调HSV阈值看mask里车牌是否是完整的白色区域不够就放宽H/S/V范围。第二步调形态学核大小核太小时字符间隙连不上核太大时容易引入干扰区域。第三步调字符分割的最小宽度阈值把误切出的噪声块过滤。经验上横向核取车牌宽度的1/10到1/15字符最小宽度取车牌宽度的1/25左右。不同分辨率下这两个值会变最稳妥的办法是先把车牌归一化到440x140再做分割所有宽度阈值就都固定下来了。调完一个参数就重跑批量脚本看准确率是升还是降不要凭感觉。6.3 从模板匹配走向轻量OCR后续扩展不推倒重来如果你的测试集里出现了模糊车牌、倒影、倾斜过大的照片模板匹配的准确率会明显下滑。这时候不需要推翻整个系统只需把第四步的模板匹配换成OCR识别模块。OpenCV自带了一个轻量级的端到端识别能力或者你可以在字符分割之后用一个小型CNN模型替换matchTemplate输入就是切好的字符块输出是字符类别。车牌定位、分割、校正的代码完全不用动改动只局限于“识别”这一个环节。这也是很多人做完大作业之后继续参加比赛或做毕设的扩展方向。我做车牌识别时养成了一个习惯每个环节的中间结果必须导出成图参数一改立刻对比前后两张图的差异。这个习惯比“95分以上”的标签管用多了因为真正支撑高分的不是代码本身而是你对每一步为什么这么做有把握。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?