拓冰建站拓冰建站
首页 / 资讯中心 / 正文

OpenCV+Python车牌识别实战:图像处理与字符识别全流程

简介计算机视觉作为人工智能的重要分支其核心技术在于通过图像处理手段提取有效信息。OpenCV作为业界标准的图像处理库配合Python的简洁高效为开发者提供了强大的图像处理与模式识别能力。从灰度化、边缘检测到形态学操作与轮廓分析每项技术都在实际项目中发挥着关键作用。以车牌识别为例其流程涵盖车牌定位、透视矫正、字符分割与模板匹配等环节完整呈现了从图像预处理到分类识别的技术链路。该方案不依赖深度学习框架在普通CPU上即可高效运行适用于课程设计、毕业设计及入门实战项目帮助开发者快速掌握计算机视觉的核心方法论。 车牌识别这个东西算是计算机视觉入门到进阶的一个典型项目了。OpenCV加Python的组合既不用烧显卡也不用上复杂的深度学习框架纯靠图像处理就能把车牌从图片里抠出来再把字符一个个认出来。这套流程跑通了你对图像处理的基本功——灰度化、边缘检测、形态学操作、轮廓分析、模板匹配——理解会上一个台阶。我最早接触这个项目时也觉得挺玄乎真的动手做了一遍才发现核心逻辑拆开就三步车牌在哪、字怎么切开、每个字是什么。这篇文章把我完整的实现思路、参数调优经验、踩过的坑都记录下来适合学完Python基础、想找个实战项目练手的同学也适合课程设计、毕业设计需要快速出成果的朋友参考。1. 项目整体设计与技术选型思路1.1 为什么选择OpenCVPython方案先聊聊选型。车牌识别系统在工业界其实已经很成熟了停车场道闸、电子警察、高速收费用的基本都是专用设备加深度模型。但作为学习项目或个人作品OpenCV加Python依然是最合适的组合。原因很实在OpenCV把所有底层图像处理算法都封装好了C接口性能最好但Python接口开发效率高代码量少调试方便。同样是做一个原型验证C可能写两百行Python五十行就搞定了。对于学习理解算法原理来说Python的简洁性让我能更关注算法本身而不是纠结指针和内存管理。而且OpenCV在图像处理这块儿几乎是事实标准它的cv2.findContours、cv2.Canny、cv2.dilate、cv2.matchTemplate这些函数随便哪个拿出来都是经过大量工业场景验证的稳定实现。自己从头造轮子去写一个边缘检测或者模板匹配不仅性能比不上精度也差得远。车牌识别本身对实时性要求其实不高——识别一张静态图片用OpenCV的传统图像处理方法在普通CPU上跑也就几十毫秒完全够用。如果用深度学习方案单是环境配置、模型训练、数据集标注这一套下来就要花掉大量时间。对于入门者和学生项目来说把传统方法吃透比直接套用现成的YOLO模型更有价值。1.2 车牌识别系统的完整工作流程整个系统的流程可以拆成四个核心模块我用一张流程图来概括这里用文字描述输入图片 → 车牌定位 → 车牌矫正 → 字符分割 → 字符识别 → 输出结果第一个环节是车牌定位目标是从一张复杂的图像中找到车牌所在的区域。这是整个系统中最关键的一步因为后续所有操作都依赖于这一步的准确性。如果车牌都没找对后面做得再好也没用。第二个环节是车牌矫正。实际拍摄的车牌往往是有倾斜的如果直接分割字符会导致切歪、切错。所以要先对定位到的车牌区域做透视变换把它矫正成正面视角。第三个环节是字符分割。把矫正后的车牌图片中的每个字符切出来变成一张张单独的字符图片。这个过程需要处理边框、铆钉、噪点等干扰。第四个环节是字符识别。对每个分割出来的字符图片进行分类识别出它是哪个汉字、哪个字母、哪个数字。这里可以用模板匹配也可以用简单的卷积神经网络。1.3 技术方案选型传统视觉还是深度学习在做车牌识别时有个绕不开的问题用传统图像处理还是深度学习我的建议是都别偏科。传统方法的核心优势在于可控性强、可解释性强。每一步做了什么、为什么这样做、参数为什么这么设都清清楚楚。出了问题也能一步步回溯排查。这对于学习理解图像处理的基本原理非常有帮助。而深度学习的优势在于对复杂场景的鲁棒性更强。比如夜间拍摄、车牌严重模糊、角度特别刁钻等场景传统方法可能就翻车了但深度学习模型经过充分训练后能扛住更多干扰。实际项目中的做法是两者结合用传统方法做预处理和车牌定位速度快、稳定性好用深度学习做字符识别精度高、泛化能力强。我的最终方案就是这种混合架构OpenCV负责定位和分割Python加一个轻量级CNN模型负责字符识别。这样兼顾了速度和精度代码量也不会爆炸。2. 环境准备与基础搭建2.1 Python与OpenCV的安装与配置在做任何代码之前先得把环境跑起来。这里我用的Python版本是3.9OpenCV版本是4.8.0这两个版本兼容性很好不会有奇怪的报错。安装OpenCV最简单的命令是pip install opencv-python但这里有个坑直接用pip install opencv-python装的是OpenCV的CPU版本对入门学习完全够用。但如果后续要做视频流处理或者实时识别建议再装一个opencv-contrib-python这个包包含了更多扩展模块。我的建议是用虚拟环境来隔离项目依赖避免不同项目之间的包版本冲突。创建并激活虚拟环境的命令如下python -m venv lpr_env # Windows lpr_env\Scripts\activate # Linux/Mac source lpr_env/bin/activate pip install opencv-python numpy matplotlib scikit-learn2.2 项目依赖库清单整个项目我用到的主要依赖库如下库名版本建议用途说明opencv-python4.8.0核心图像处理、轮廓检测、模板匹配numpy1.24.0数组运算、图像矩阵操作matplotlib3.7.0图像可视化、调试展示scikit-learn1.3.0制作数据集、训练分类器可选tensorflow / pytorch按需安装轻量级CNN字符识别可选如果你只做传统方案的车牌识别其实只需要前三个库就够了。深度学习部分可以后续再加。这里特别说一下numpy。OpenCV的图像本质上是numpy数组理解这一点很重要。一张彩色图片在OpenCV里的形状是(H, W, 3)的数组H是高度W是宽度3是BGR三个颜色通道注意OpenCV用的是BGR而不是RGB。整个图像处理过程说白了就是在操作这些数组的数值。2.3 开发调试环境的配置要点我这里推荐用VS Code配合Python扩展来做开发简单轻量调试方便。有一些小配置能显著提升开发效率第一设置OpenCV显示窗口的大小自适应。如果你的屏幕分辨率不够高OpenCV弹出的窗口可能会超出屏幕边界看不到完整图片。可以在代码里加上这个cv2.namedWindow(result, cv2.WINDOW_NORMAL) cv2.resizeWindow(result, 800, 600)第二配置好图像保存路径。调试过程中经常需要把中间结果保存下来查看建议在项目根目录建一个output/文件夹专门存放中间处理结果。这样既能追踪算法每一步的输出也方便在最终效果不好时排查是哪一步出了问题。第三善用cv2.imwrite而不是cv2.imshow来做输出。在无图形界面的服务器上调试或者批量处理图片时cv2.imshow会直接报错但cv2.imwrite可以正常使用。养成写文件而不是弹窗口的调试习惯能少踩很多坑。3. 车牌定位从原图到目标区域3.1 图像预处理灰度化、去噪与边缘检测车牌定位的第一步是做图像预处理。一张原始图片可能有各种颜色背景也可能很复杂——树、墙壁、行人、其他车辆什么都有。直接在这张图上找车牌是不现实的要先做减法。我采用的预处理流程是灰度化 → 高斯模糊 → 边缘检测 → 二值化。首先看灰度化。车牌的颜色信息蓝色底白字、黄色底黑字、绿色底白字等在定位阶段其实并不是必须的反而会让计算量增大。把它变成灰度图就是去掉颜色信息只保留亮度信息gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)接着做高斯模糊。这一步的作用是去除图像中的噪声和细节纹理。车牌边缘相比于复杂的背景纹理来说是大尺度的特征模糊掉小纹理能减少干扰blurred cv2.GaussianBlur(gray, (5, 5), 0)高斯模糊的核大小(5, 5)是我在实测后觉得比较合适的核太小去噪效果差核太大又容易把车牌的边缘细节也磨掉。然后是边缘检测我用的是Canny算子。这是OpenCV里最经典的边缘检测算法它能在保持边缘连续性的同时抑制噪声edges cv2.Canny(blurred, 50, 150)Canny的两个阈值参数也需要根据实际图片调整。低阈值50、高阈值150是我在晴天、光线充足的车牌照片上调出来的经验值。如果图片中车牌边缘太弱、检测不到可以把阈值调低到30~40反之如果检测出太多噪声边缘则适当调高。3.2 形态学操作让车牌的轮廓连成一片边缘检测做完之后车牌的轮廓已经能看到一些了但边缘往往是断断续续的——因为车牌上的字符、边框和背景之间有些地方的梯度不够明显Canny检测出来的是断开的碎片。这时候就需要形态学操作登场了。我做的是两步先膨胀再闭运算。膨胀操作让边缘变得更粗、更连续kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) dilated cv2.dilate(edges, kernel, iterations2)闭运算是先膨胀后腐蚀作用是把车牌区域内部的小孔洞填上让整个车牌区域变成一个实心的连通块closed cv2.morphologyEx(dilated, cv2.MORPH_CLOSE, kernel)3.3 轮廓提取与候选区域筛选形态学处理完车牌区域已经变成一个明显的亮色块状区域了。接下来用cv2.findContours提取轮廓然后根据车牌的特征来筛选候选区域contours, hierarchy cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)提取到轮廓之后核心工作就是筛选。标准车牌的宽高比大约是3.14:1440mm : 140mm这个比例是非常稳定的特征。我设定的筛选取值范围是for cnt in contours: x, y, w, h cv2.boundingRect(cnt) aspect_ratio w / float(h) area w * h # 车牌宽高比约3~4面积占图片总面积比例在0.0004~0.02之间 if 2.5 aspect_ratio 4.5 and 0.0004 area / (image.shape[0] * image.shape[1]) 0.02: candidate cnt break这个范围不是我随便拍的。标准车牌宽高比3.14但实际拍摄时因为角度、透视的影响这个值会在2.5到4.5之间波动。面积比例则是根据我的测试图片尺寸约1920x1080车牌在图中约占几百到几千像素来估算的。如果你自己的图片里车牌占比更小或更大需要相应调整这个范围。如果一次筛选出多个候选区域我的策略是按照面积从大到小排序优先选择面积最大的那个——因为在实际拍摄中面积最大的那个轮廓通常就是距离最近、最清晰的车牌。筛选完成后cv2.boundingRect得到的是最小正矩形但如果车牌有倾斜正矩形包住的区域会包含很多背景。这时候就要用到最小外接矩形rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect) box np.int0(box)cv2.minAreaRect返回一个旋转矩形包含中心坐标、宽高和旋转角度这个角度在下一步矫正中非常关键。3.4 透视变换矫正倾斜的车牌实际拍摄的车牌几乎不可能是绝对正对着镜头的多多少少有倾斜。如果直接分割字符会存在严重的误差。矫正的方法是用透视变换把倾斜的车牌映射成一个正面的矩形。先获取车牌四个角点映射到一个标准的车牌尺寸上# 假设box是车牌区域的四个角点按照一定顺序排列 src_pts np.array(box, dtypefloat32) # 标准车牌尺寸以宽度440px、高度140px为例 dst_pts np.array([[0, 0], [440, 0], [440, 140], [0, 140]], dtypefloat32) # 计算透视变换矩阵 M cv2.getPerspectiveTransform(src_pts, dst_pts) # 执行透视变换 warped cv2.warpPerspective(image, M, (440, 140))这里要注意src_pts角点的顺序必须和dst_pts一一对应否则变换结果会错乱。cv2.boxPoints返回的角点顺序不一定是我们想要的需要手动排个序——按左上、右上、右下、左下的顺序排列。排序的代码可以这样写def order_points(pts): rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] # 左上角xy最小 rect[2] pts[np.argmax(s)] # 右下角xy最大 diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] # 右上角x-y最小 rect[3] pts[np.argmax(diff)] # 左下角x-y最大 return rect这一套流程跑完拿到的warped就是一张规规矩矩的车牌正面图了。4. 车牌字符分割把牌照拆成单个字符4.1 二值化处理与去噪车牌区域拿到之后下一步是分割字符。字符分割的质量直接决定了识别精度——如果字符切歪了、切残了后面识别做再好也没用。先对矫正后的车牌做预处理。首先是二值化把车牌图片变成黑白两色warped_gray cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)这里我使用了Otsu自适应阈值它能根据图片的灰度分布自动计算最佳的二值化阈值比固定阈值比如127鲁棒得多。因为不同环境下拍摄的车牌亮度和对比度差异很大固定阈值很容易翻车。二值化之后还要去噪。车牌上常见噪声来源有两个一是铆钉车牌四角的小圆点二是边框。铆钉和边框如果在二值化图片上残留会干扰字符分割。去除铆钉和边框的方法有很多我用的是形态学开运算 边框裁剪的组合kernel np.ones((3, 3), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)接着把车牌最外圈的几行、几列像素直接裁剪掉因为边框在二值化后通常会连成一条白线h, w binary.shape binary binary[5:h-5, 5:w-5]4.2 字符分割的核心方法字符分割我尝试过三种主流方法垂直投影法、连通域法、轮廓检测法。垂直投影法的大致原理是把二值化后的车牌图片的每一列的白像素数量做一个统计列与列之间形成波峰和波谷。字符所在区域白像素多波峰字符之间的间隙白像素少波谷。根据波峰波谷的分界就能把字符切出来。这在很多教程里是首选方法但我实测下来它对噪声和粘连字符非常敏感只有当车牌特别干净、对比度特别高的时候才稳定。稍微有点噪点就切不对。连通域法是把图片中连通的白色区域找出来。字符和字符之间是分开的属于不同连通域按x坐标排序后就能依次切出。这比垂直投影更稳定但也会把噪点、铆钉误判成字符。轮廓检测法和连通域法原理类似但用的是cv2.findContours。我最终选用的就是轮廓检测法配合宽高比和面积过滤contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) char_regions [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h ratio h / float(w) # 字符宽度约占车牌宽度的10%~20%高度约占40%~90% if 0.1 * binary.shape[1] w 0.25 * binary.shape[1] \ and 0.4 * binary.shape[0] h 0.95 * binary.shape[0] \ and area 50: char_regions.append((x, y, w, h)) # 按x坐标从小到大排序 char_regions.sort(keylambda r: r[0])4.3 字符归一化处理分割出来的字符大小不一在做识别之前需要归一化成统一的尺寸。标准车牌字符的宽高比大约是0.5左右比如字符宽45mm、高90mm我这里把每个字符都缩放成20x40像素resized cv2.resize(char_img, (20, 40), interpolationcv2.INTER_AREA)INTER_AREA是缩小图片时效果最好的插值算法不会产生明显的锯齿。放大时则用INTER_CUBIC效果更好但因为我们是在缩小车牌字符通常远大于20x40像素所以用INTER_AREA。归一化之后每个字符就变成了一组长度80020x40的特征向量后面无论是做模板匹配还是训练CNN输入格式都统一了。分割完了还要做一步验证标准车牌是7个字符第一位是省份汉字简称第二位是字母后面五位是字母和数字混排所以正常情况下应该分割出7个字符。如果少于6个或多于8个说明分割出问题了需要回到前面调整参数。5. 字符识别从模板匹配到轻量级CNN5.1 模板匹配的实现原理字符识别最简单的方案就是模板匹配。思路很朴素准备一套标准字符模板各省简称汉字、大写字母A-Z、数字0-9然后把分割出来的字符图片和每个模板做相似度比较取最相似的作为识别结果。OpenCV里用cv2.matchTemplate实现模板匹配再配合cv2.minMaxLoc取最佳匹配位置result cv2.matchTemplate(char_img, template_img, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(result)TM_CCOEFF_NORMED是归一化相关系数匹配法输出值在-1到1之间越接近1表示匹配度越高。模板匹配的优势是简单、快、不需要训练。它的劣势也很明显对字体变化、字符变形、模糊特别敏感。如果实际车牌用的字体和模板字体差太多识别率会直线下降。我做模板匹配时踩过最大的坑是没有提前做二值化统一。如果分割出来的字符是灰度图而模板是二值图匹配结果就会很差。后来我把字符和模板都统一转成相同尺寸的二值图识别率才有了明显提升。模板匹配适合做一个快速原型验证或者作为最终方案的baseline。5.2 基于CNN的轻量级识别方案如果追求更高的识别精度我推荐用一个极简的卷积神经网络来做字符分类。不需要很大很复杂的模型车牌字符数量和类别是固定的省份汉字约30个实际用到的没有30个排除一些未使用的大写字母24个去掉I和O数字10个总共约60~70个类别。这个分类规模很小用一个三层卷积加全连接的轻量网络就够了。我用PyTorch或TensorFlow/Keras搭了个结构很简单的小网络import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) self.relu1 nn.ReLU() self.pool1 nn.MaxPool2d(2, 2) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.relu2 nn.ReLU() self.pool2 nn.MaxPool2d(2, 2) self.fc1 nn.Linear(64 * 5 * 10, 128) self.relu3 nn.ReLU() self.fc2 nn.Linear(128, num_classes) def forward(self, x): x self.pool1(self.relu1(self.conv1(x))) x self.pool2(self.relu2(self.conv2(x))) x x.view(x.size(0), -1) x self.relu3(self.fc1(x)) x self.fc2(x) return x输入是1x20x40的灰度图经过两层卷积池化后特征图尺寸变成64x5x10然后接全连接层输出类别概率。训练数据可以自己造用电脑里的字体生成各种车牌字符图片加上一些旋转、噪声、缩放等数据增强。我实际训练时用了3000张左右的字符图训练20个epoch验证集准确率能到99%以上。在实际使用中对单张字符图推理一次大概只需要几毫秒完全满足哪怕视频流的实时性要求。5.3 识别结果的后处理与校验拿到字符识别结果后还要做一个后处理校验。车牌的字符排列是有固定规律的第一位必须是省份汉字简称京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新第二位是大写字母对应发牌机关代号通常排除I和O第三到七位是字母或数字混排但通常也会排除I和O为了避免和数字1、0混淆。所以我可以在输出结果时加一个规则校验province_chars 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼 digits 0123456789 letters ABCDEFGHJKLMNPQRSTUVWXYZ result .join(predicted_chars) # 校验规则 if result[0] not in province_chars: print(第一位应为省份简称) if result[1] not in letters: print(第二位应为字母) for ch in result[2:]: if ch not in digits and ch not in letters: print(后五位应为字母或数字)这套校验能帮我们滤掉一部分明显的识别错误。比如识别结果第一位是数字那肯定是错了可以回到字符分割环节检查是不是把车牌的外框或噪声当成字符切进来了。6. 实操过程中的常见问题与避坑指南6.1 环境安装与报错排查OpenCV的环境安装问题可能是初学者遇到最多的我在实践中也碰到过不少。最典型的一个报错是ModuleNotFoundError: No module named cv2这个错误的原因很简单OpenCV没装成功或没装到当前使用的Python环境里。常见解决方法是pip uninstall opencv-python pip install opencv-python --user还有一类报错是用cv2.imshow时直接崩了或者白屏尤其是在远程服务器或Docker容器里cv2.error: The function/feature is not implemented ...这是因为当前环境中没有图形界面支持OpenCV无法创建显示窗口。解决办法是改用cv2.imwrite保存图片到文件然后用其他工具查看。我在之前的项目里就是这么调试的——把处理中间结果全部保存成图片一张张看。6.2 图像质量问题导致识别失败实际拍摄的车牌图片如果是在阴天、傍晚或者背光场景拍的车牌区域可能比较暗对比度不足。我实测后发现直接用Canny边缘检测会漏掉很多轮廓。这时候可以先做直方图均衡化提升对比度gray_eq cv2.equalizeHist(gray)cv2.equalizeHist能把灰度分布拉开让暗的地方变亮、亮的地方保持亮是低对比度场景下非常实用的预处理手段。需要注意的是直方图均衡化只对灰度图有效对彩色图需要先转成YCrCb之类的色彩空间只在Y通道上做均衡化否则会破坏颜色信息。另一个常见问题是车牌的蓝色在灰度图里和深色背景区分度不高。如果检测不出来可以考虑增加颜色过滤的路子——直接在HSV空间里筛选蓝色区域作为边缘检测的补充hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) blue_mask cv2.inRange(hsv, (100, 100, 100), (130, 255, 255))蓝色车牌对应的H通道范围大致在100~130之间。加上这层颜色过滤定位的成功率会大幅提升。当然这也意味着系统只能识别蓝底车牌如果想要兼容黄底、绿底车牌需要把对应的颜色范围都加进去。6.3 分割不准确时的调整思路字符分割常见的失败表现有两个一个是切出来的字符粘连在一起一个是字符被拦腰斩断。粘连的原因通常是字符间距过小或者图像分辨率不够两个字符的二值化区域连成了一片。我的经验是优先检查二值化阈值是否合适。用Otsu自动阈值仍然粘连的话可以试试自适应阈值cv2.adaptiveThreshold它对光照不均的处理更好binary cv2.adaptiveThreshold(warped_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)字符被切断则往往是因为边缘检测或形态学操作参数太激进把字符内部的笔画当成了背景。可以把膨胀迭代次数从2降为1或者把闭运算的核尺寸调小。如果分割结果不稳定我还有一个调试技巧把每一张分割出的字符图单独保存到一个目录里按顺序用数字命名。这样一眼就能看出分割是否错位、是否多了或少了字符。这也是我前面说的output/目录的用途之一。6.4 性能优化建议虽然静态图片识别不需要太关注性能但如果要用在视频流或嵌入式设备上就有优化空间了。首选的优化方向是降低处理分辨率。车牌识别对分辨率的敏感度没那么高——车牌本身是高频信息集中的区域只要车牌区域在图片中足够大就好。我的做法是如果原始图片是1920x1080先缩放到1280x720再处理。实测下来精度几乎不受影响速度提升却能接近一倍。第二个优化方向是缩小搜索范围。摄像头固定场景下比如停车场入口车牌通常出现在画面的中下区域、固定的几个车道位置。可以先做一条ROI感兴趣区域设定只在这个区域里做车牌定位能省掉大量背景干扰也大幅减少计算量。第三个优化方向是避免重复计算。如果在做视频流识别相邻帧之间车牌位置变化很小可以用上一帧定位到的位置作为当前帧的初始搜索区域只在这个小区域内重新定位。这种跟踪式的思路能大幅提升帧率。7. 项目完整代码框架下面是我项目的完整代码框架你可以直接参考或在此基础上扩展。完整的代码我按模块组织方便调试和复用。主程序main.pyimport cv2 import numpy as np from plate_locator import locate_plate from char_segmenter import segment_chars from char_recognizer import recognize_chars def main(image_path): # 1. 读取图片 img cv2.imread(image_path) if img is None: print(图片读取失败) return # 2. 车牌定位 plate_img locate_plate(img) if plate_img is None: print(未检测到车牌) return # 3. 字符分割 char_imgs segment_chars(plate_img) if len(char_imgs) 7: print(字符分割异常分割出 %d 个字符 % len(char_imgs)) return # 4. 字符识别 result recognize_chars(char_imgs) print(车牌号码, result) if __name__ __main__: main(test_car.jpg)车牌定位模块plate_locator.pyimport cv2 import numpy as np def order_points(pts): rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] rect[3] pts[np.argmax(diff)] return rect def locate_plate(image): # 预处理 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) blurred cv2.GaussianBlur(gray, (5, 5), 0) # 边缘检测 edges cv2.Canny(blurred, 50, 150) # 形态学操作 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) dilated cv2.dilate(edges, kernel, iterations2) closed cv2.morphologyEx(dilated, cv2.MORPH_CLOSE, kernel) # 轮廓检测 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) h, w image.shape[:2] best_area 0 best_box None for cnt in contours: rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect) box np.int0(box) roi_w rect[1][0] roi_h rect[1][1] if roi_w roi_h: roi_w, roi_h roi_h, roi_w aspect_ratio roi_w / float(roi_h) area roi_w * roi_h if 2.5 aspect_ratio 4.5 and area best_area: # 面积比例和宽高比都满足条件 if 0.0004 area / (h * w) 0.02: best_area area best_box box if best_box is None: return None # 透视变换 src_pts order_points(best_box) dst_pts np.array([[0, 0], [440, 0], [440, 140], [0, 140]], dtypefloat32) M cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(image, M, (440, 140)) return warped字符分割模块char_segmenter.pyimport cv2 import numpy as np def segment_chars(plate_img): gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 二值化 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 去除边框 h, w binary.shape binary binary[3:h-3, 3:w-3] # 轮廓检测 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) char_regions [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h ratio h / float(w) if w 0 else 0 if 0.1 * binary.shape[1] w 0.25 * binary.shape[1] \ and 0.4 * binary.shape[0] h 0.95 * binary.shape[0] \ and area 50: char_regions.append((x, y, w, h)) char_regions.sort(keylambda r: r[0]) char_imgs [] for (x, y, w, h) in char_regions: char binary[y:yh, x:xw] resized cv2.resize(char, (20, 40), interpolationcv2.INTER_AREA) char_imgs.append(resized) return char_imgs字符识别模块char_recognizer.pyimport cv2 import numpy as np import os class CharRecognizer: def __init__(self, template_dir, char_dict_path): self.templates [] self.labels [] # 加载模板这里支持从一个目录中读取所有模板图片 for label in os.listdir(template_dir): label_path os.path.join(template_dir, label) for file in os.listdir(label_path): img cv2.imread(os.path.join(label_path, file), 0) img cv2.resize(img, (20, 40), interpolationcv2.INTER_AREA) _, img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) self.templates.append(img) self.labels.append(label) def recognize(self, char_imgs): result for char in char_imgs: _, char cv2.threshold(char, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) best_score -1 best_label ? for template, label in zip(self.templates, self.labels): score cv2.matchTemplate(char, template, cv2.TM_CCOEFF_NORMED)[0][0] if score best_score: best_score score best_label label result best_label return result def recognize_chars(char_imgs): # 这里可以替换成CNN实现 recognizer CharRecognizer(templates/, None) return recognizer.recognize(char_imgs)这套代码框架虽然朴素但把整个流程跑通是没问题的。如果你后续接入CNN识别只需要修改recognize_chars函数的实现车牌定位和字符分割的代码可以完全复用。8. 个人的一些实操心得做这个车牌识别项目我最大的感受是算法这东西纸上谈兵是一回事跑起来是完全另一回事。你可能在理论上觉得车牌定位很简单——不就是找轮廓嘛——但实际一跑发现光照、角度、背景杂色都会把结果搞得一团糟。每一步的参数调整都是基于大量实拍的反复试错。我给准备做这个项目的朋友三个建议第一不要一开始就追求复杂方案。先跑通传统方法让整个链路work起来再逐步替换或进化某个环节。直接一上来就上YOLO加CNN代码是跑通了但你什么都没学到。第二一定要自己准备测试集。不要只用网上教程里的那几张图片做验证。自己拿手机拍各种场景——白天、晚上、晴天、阴天、远距离、近距离、不同颜色车牌——测试集越多样你对算法稳健性的理解就越深。第三做好可视化调试。把每一步的中间结果都保存成图片肉眼观察变化是最有效的调试方式。不要只盯着最后输出中间过程往往藏着问题的根源。车牌识别系统虽然看起来是个小项目但它覆盖了图像处理的大部分核心知识点。真正吃透这个项目你再去学人脸检测、物体识别、OCR这些方向会发现很多东西是相通的。这套从定位到分割到识别的思维框架在计算机视觉领域里可以复用到很多场景。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门