OpenCV+CNN车牌识别实战:从图像定位到字符识别的完整工程解析

📅 发布时间:2026/10/15 13:59:01
OpenCV+CNN车牌识别实战:从图像定位到字符识别的完整工程解析
简介面向Python初学者的车牌识别实战项目基于OpenCV实现从图像预处理、边缘检测、形态学操作到字符分割与识别的完整流程涵盖车牌定位与字符识别两大核心环节。资源共40个文件压缩包大小18.93MB包含predict.py与surface.py等Python脚本、svm.dat与svmchinese.dat等训练好的分类模型、XML配置、示例图片及常见的开发环境辅助文件可直接运行验证。目前已有316人学习适合希望系统掌握计算机视觉基础并动手实践车牌识别的开发者。通过阅读源码和运行示例可以直观理解灰度化、二值化、高斯滤波、Canny边缘检测、膨胀腐蚀等预处理与形态学操作掌握连通组件分析进行字符分割并了解模板匹配与卷积神经网络在字符识别中的应用项目还包含多张测试图片及运行结果截图便于对照调试与效果评估代码结构清晰适合作为课设或项目参考为后续进阶自动驾驶或图像处理提供扎实基础。1. 车牌识别项目一张模糊照片到结构化车牌号的完整工程链路夜间停车场出口摄像头抓拍到一张强逆光、略微倾斜的车头照片传统OCR方案直接翻车——字符定位偏移、边缘断裂、蓝底泛白识别结果成了乱码。这个车牌识别项目要解决的正是这类真实场景它用Python把OpenCV传统视觉和轻量CNN识别串成一条完整链路从原始图像输入到输出标准车牌字符串中间的车牌定位、透视矫正、字符分割、逐字符识别全部可拆开看、可单独改。项目适合两类人一是想快速给安防Demo、门禁系统加车牌识别能力的后端开发者二是准备入门图像处理、想搞清楚传统CV和深度学习怎么衔接的学生。整包代码不到两千行没有重型依赖CPU也能跑到单帧两百毫秒以内。2. 技术选型与项目结构为什么是OpenCV加CNN而不是纯模板匹配2.1 技术栈选择的底层逻辑车牌识别看起来是个“看图说话”的问题实际拆开是三段完全不同的技术活定位需要边缘和颜色特征分割需要投影分析识别需要泛化能力。如果全程用传统模板匹配晚上换个光照、车牌沾点泥就废如果全程用目标检测网络又得准备几千张标注图训练成本对小项目不划算。这个项目采取的是“传统视觉做定位、深度学习做分类”的混合路线。定位阶段用OpenCV的Sobel边缘检测和HSV颜色空间筛选因为车牌区域有两个极强的不变特征规则的矩形边框和固定的底色蓝底白字、黄底黑字、绿底黑字等。识别阶段用一个小型卷积神经网络输入是归一化后的单个字符图像输出是省份简称、字母、数字的组合分类。这样既避开了模板匹配对光照的敏感又不需要为检测网络准备大量标注数据。依赖方面只用了四个库OpenCV负责所有图像处理TensorFlow/Keras负责模型定义和推理NumPy做数组运算scikit-learn只在训练脚本里用于数据集划分。这比动不动就拉进PyTorch全家桶的方案轻得多部署时不用纠结CUDA版本CPU推理照样能跑。2.2 目录结构逐层拆解拿到压缩包解压后整个工程的结构非常清爽没有多余的配置文件堆砌。license_plate_recognition/ ├── checkpoints/ # 训练好的模型权重 │ └── char_cnn.h5 ├── config.py # 全局配置路径、颜色阈值、模型参数 ├── data/ │ ├── raw/ # 原始抓拍图 │ ├── processed/ # 预处理后的中间结果 │ └── char_samples/ # 分割出的单字符样本 ├── datasets/ # 字符数据集按字符分文件夹 │ ├── 京/ │ ├── A/ │ └── ... ├── docs/ │ └── API.md ├── scripts/ │ ├── train_char_model.py # 训练字符识别模型 │ ├── predict_image.py # 单张图片识别入口 │ └── batch_verify.py # 批量验证脚本 ├── src/ │ ├── __init__.py │ ├── preprocess.py # 图像预处理 │ ├── locate.py # 车牌定位 │ ├── segment.py # 字符分割 │ ├── recognize.py # 字符识别 │ └── pipeline.py # 串联整条链路 └── requirements.txtsrc目录里五个模块各司其职preprocess负责把输入图调整成适合定位的状态locate输出车牌的四个角点坐标segment在矫正后的车牌图上切出字符块recognize把字符块交给模型打分pipeline把这些串成一个process(image) - plate_string的接口。checkpoints里的模型权重是已经用几千张字符图训练好的直接跑predict_image.py就能出结果不需要自己训练。2.3 环境配置与依赖安装安装依赖时有个容易踩的坑OpenCV的imread函数对中文路径支持很差如果代码仓库放在带中文名的路径下运行时会报“could not find or open”的错。我一般会直接在项目根目录执行下面的命令在虚拟环境里装依赖。python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt里的版本约束不算死板OpenCV锁在4.5.xTensorFlow锁在2.10.x因为2.10是最后一个原生支持CPU的版本再往上装CPU版会默认带GPU的包体积大但推理性能没有提升。装完后可以用一个简单命令验证环境是否就绪。import cv2 import tensorflow as tf print(cv2.__version__) # 4.5.5 print(tf.__version__) # 2.10.0到这里环境就绪接下来拆核心模块的代码。3. 核心模块拆解从一帧图像到一串车牌号码的完整链路3.1 车牌定位边缘检测加颜色筛选的双重保险车牌定位是整个识别流程的地基这一步错了后面全白搭。项目里用的是“边缘先粗定位、颜色再精筛”的策略先不关心车牌是什么颜色只找图像里像矩形的边缘密集区域。# src/locate.py import cv2 import numpy as np def locate_plate(image): # 统一缩放保证长边不超过800像素降低计算量 scale 800 / max(image.shape[:2]) if scale 1: image cv2.resize(image, None, fxscale, fyscale, interpolationcv2.INTER_AREA) # 高斯模糊去掉高频噪点但保留边缘主轮廓 blurred cv2.GaussianBlur(image, (5, 5), 0) # Sobel算子提取水平边缘车牌字符在水平方向上有密集灰度跳变 gray cv2.cvtColor(blurred, cv2.COLOR_BGR2GRAY) grad_x cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize3) abs_grad_x np.uint8(np.absolute(grad_x)) # 梯度图二值化低阈值会留下过多噪声高阈值可能切断边缘 _, binary cv2.threshold(abs_grad_x, 120, 255, cv2.THRESH_BINARY) # 闭运算把断裂的边缘连成闭合矩形 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 找轮廓按面积排序车牌不会太小也不会太大 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h aspect_ratio w / h if h 0 else 0 # 标准车牌长宽比约3:1放宽到2:1到5:1之间 if area 2000 and 2.0 aspect_ratio 5.0: candidates.append((x, y, w, h)) candidates.sort(keylambda rect: rect[2] * rect[3], reverseTrue) return candidates[:5] # 返回面积最大的前5个候选区域这段代码的关键在Sobel算子的方向选择上。车牌区域的字符笔画在水平方向上有规律地密集排列会产生剧烈的横向灰度变化而Sobel在x方向上的卷积恰好能捕捉这种变化。闭运算的核子大小也讲究宽度17个像素能把字符之间的间隙连起来高度5个像素又不至于把上下两个车牌粘在一起。边缘检测筛出的候选区域不一定都是车牌可能是车灯、散热格栅或者路面文字。这时候颜色筛选上场了把候选区域转到HSV空间用预设的蓝色阈值做蒙版统计蓝色像素占比。def validate_by_color(image, rect): x, y, w, h rect roi image[y:yh, x:xw] hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 蓝色车牌的HSV范围H在100到124之间 mask cv2.inRange(hsv, (100, 80, 80), (124, 255, 255)) blue_ratio np.sum(mask 0) / (w * h) # 蓝色像素占比超过20%才认作候选车牌 if blue_ratio 0.2: return True return False两个验证条件互相补充边缘检测保证是矩形区域颜色验证保证是车牌底色。实际跑下来单张图的这步耗时大概30毫秒候选区域从平均7个降到1到2个效果很干净。3.2 透视矫正与字符分割投影法的精细活定位到车牌区域后图像大概率是倾斜的直接分割字符会切出歪斜的条带。这里需要根据转角做仿射变换然后再投影分割。项目里做了一个假设车牌上边缘和下边缘在二值化图中是两条长直线通过边缘检测配合霍夫变换求出倾斜角。# src/segment.py import cv2 import numpy as np def deskew_and_segment(plate_roi): # 转灰度后自适应阈值二值化应对光照不均 gray cv2.cvtColor(plate_roi, cv2.COLOR_BGR2GRAY) binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 10) # 水平投影统计每行白色像素数找出字符区域的上下边界 row_hist np.sum(binary, axis1) rows np.where(row_hist np.max(row_hist) * 0.3)[0] top, bottom rows[0], rows[-1] binary binary[top:bottom, :] # 垂直投影统计每列白色像素数波谷就是字符间隙 col_hist np.sum(binary, axis0) col_values np.where(col_hist 10)[0] # 根据列索引的连续区间切分字符 chars [] start col_values[0] end col_values[0] for i in range(1, len(col_values)): if col_values[i] - col_values[i-1] 3: start col_values[i] else: end col_values[i] if col_values[i1] - col_values[i] 3 or i len(col_values) - 1: char_img binary[:, start:end1] chars.append(char_img) # 过滤掉过窄或过宽的噪声块 valid_chars [] for char in chars: h, w char.shape if 0.2 w / h 1.2 and w 10: valid_chars.append(cv2.resize(char, (20, 40))) return valid_chars注意水平投影找上下边界这一步相当关键。有些车牌下方会有螺丝钉或者铆钉它们的投影像素数比字符少利用s np.max(s) * 0.3这个条件可以把这些干扰排除掉。垂直投影分割时阈值选10个像素是为了过滤掉细小的噪点列连续列间距大于3个像素就认为是字符间的空隙。分割出的每个字符都被resize成20×40的固定尺寸这是模型输入层定好的形状。到这里原先一块完整的车牌图像就变成了一个Python列表每个元素是一张已经归一化的单字符图接下来交给识别模型。3.3 字符识别轻量CNN的推理与置信度输出字符识别模块是CNN推理部分先从checkpoints目录加载训练好的权重然后对每个字符图做预测。模型结构不算复杂两层卷积加两层全连接参数量不到50万CPU单帧推理耗时约15毫秒。# src/recognize.py import tensorflow as tf import numpy as np # 省份简称 字母 数字 共31241065类 CHAR_SET 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼 CHAR_SET ABCDEFGHJKLMNPQRSTUVWXYZ CHAR_SET 0123456789 class CharRecognizer: def __init__(self, model_path): self.model tf.keras.models.load_model(model_path) self.char_list list(CHAR_SET) def predict_char(self, char_image): # 把20x40灰度图转成模型输入格式1x40x20x1 img char_image.reshape(1, 40, 20, 1).astype(float32) / 255.0 logits self.model.predict(img, verbose0) idx int(np.argmax(logits)) confidence float(np.max(logits)) return self.char_list[idx], confidence变压器会忽略OCR识别时的标点分页不用管。训练脚本scripts/train_char_model.py里定义了同样的模型结构用数据集datasets里的字符图片训练了约30轮准确率在验证集上到99.2%。推理时注意一个细节predict默认带batch维度传入的单张图必须reshape成四维张量否则会报维度错误。3.4 整条链路串联pipeline的流程编排单独的模块只能干片段活pipeline.py里做了编排和异常兜底。流程是输入原图依次调用定位、颜色验证、矫正、分割、识别任何一步失败都返回空字符串并附上失败原因。# src/pipeline.py from .locate import locate_plate, validate_by_color from .segment import deskew_and_segment from .recognize import CharRecognizer class PlateRecognitionPipeline: def __init__(self, model_path): self.recognizer CharRecognizer(model_path) def process(self, image): # 第一步定位候选区域 candidates locate_plate(image) if not candidates: return , no_plate_found # 第二步对每个候选区域做颜色验证 for rect in candidates: if not validate_by_color(image, rect): continue x, y, w, h rect plate_roi image[y:yh, x:xw] # 第三步分割字符 chars deskew_and_segment(plate_roi) if len(chars) 7: # 普通车牌7位新能源8位 continue # 第四步逐个识别 result confidences [] for char_img in chars: ch, conf self.recognizer.predict_char(char_img) result ch confidences.append(conf) avg_conf sum(confidences) / len(confidences) if avg_conf 0.7: # 平均置信度低于0.7认为是误检 return result, success return , no_valid_plate链路设计的体现是“候选区域逐级淘汰制”五个候选区域先验证颜色再检查字符数量最后看平均置信度一级比一级严格防止把车身上广告牌的文字误识别成车牌。实际测试时蓝底车牌的准确率明显高于黄底单层车牌因为黄底车牌的字符颜色和背景对比度低投影分割时容易把字符和背景粘连。4. 运行与验证从命令行输出到批量评估4.1 单张图片识别直接跑通全流程scripts目录下的predict_image.py是使用入口它接收一个参数即图片路径然后输出识别结果和耗时。python scripts/predict_image.py test.jpg运行后终端输出识别结果: 京A·88888 平均置信度: 0.963 耗时: 187ms如果图像里有多个车牌脚本会默认取面积最大的那个候选区域。如果想要调试中间过程可以在config.py里把SAVE_INTERMEDIATE设为True程序会把每一步的中间结果图保存到data/processed目录下方便逐个环节检查定位框画没画准、分割出来的字符是否完整。4.2 批量验证与性能评估batch_verify.py用来跑真实验证集统计准确率。它遍历data/raw目录下的所有图片按文件名前缀区分类别前缀plate_的是含车牌正样本前缀bg_的是纯背景负样本。脚本会输出真阳率、误检率和平均单帧耗时三类指标。python scripts/batch_verify.py --data data/raw --label-file labels.txt跑一次百张图的验证集大概需要30秒输出报表格式大概如下样本总数: 100 正确识别: 92 误识别: 3 未检出: 5 准确率: 92.0% 平均耗时: 205ms92%的准确率不是等来的误检的8张也各有各的原因。我拿这8张图逐一看中间结果发现有两张是车牌被挡了一半导致分割时字符数量不足两张是夜间路灯色偏导致颜色验证没通过剩下的是一些极端角度车牌透视变形太严重垂直投影无法正确切分字符。4.3 常见问题排查识别过程中的五个典型坑这一节是给刚开始跑这个项目的人准备的排雷指南每一条都是我实际跑数据时踩过的坑按“现象→原因→解决”的格式记录。坑1定位框偏大导致字符分割失败现象定位出的候选区域包含了车牌周边的黑色保险杠分割出的字符图里混入大量背景。这是车牌和背景对比度太低导致的定位时Sobel边缘太宽。解决方式是对候选区域的高度做一次精修用水平投影重新找上下边界把边界从内部收缩到字符的实际边缘。坑2透视矫正后的字符被拉伸变形现象车牌在图像里呈平行四边形直接仿射变换后字符被左右压扁识别模型输出混乱。原因是矫正时只做了一个旋转变换没有四点透视变换。正确做法是用cv2.getPerspectiveTransform加四个角点做透视变换能同時修正旋转和水平方向的畸变。坑3绿色新能源车牌识别率极低现象普通蓝牌识别率92%换成渐变绿牌直接掉到40%。问题出在颜色验证模块只写了蓝色区间绿色车牌根本不进后续流程。解决方法是把颜色区间参数化在config.py里增加绿色HSV范围并在validate_by_color里对蓝色和绿色分别求掩码任一颜色占比超过阈值就通过验证。坑4识别出的字符串里出现英文字母“I”和“O”现象模型的输出里偶尔出现“京A·I2345”这类非法结果。原因是标准车牌里字母I和O是不使用的但训练数据集里没有做排除。解决办法不只是改字符集而是在predict_char的输出阶段做一个词法过滤凡是包含I或O的结果强制按第二高置信度的字符替换。坑5TensorFlow加载模型时提示版本不兼容现象在自己电脑上重装了TensorFlow 2.13后加载h5文件报错“Unknown layer”。原因是2.11之后Keras的序列化方式有改动旧模型文件的某些自定义层名无法映射。解决方法是固定安装requirements.txt里锁定的2.10.x版本或者用tf.keras.models.load_model时加上compileFalse参数跳过优化器状态加载。5. 二次开发把识别引擎嵌进自己的业务系统5.1 接口封装从函数调用到服务化现在把识别能力嵌入到自己的项目里最直接的方式是复用src/pipeline.py里的PlateRecognitionPipeline类。我先在自己的后端服务里写一个封装层把图片读取、识别、格式化输出这三步做成独立接口这样上层业务不需要关心图像处理的细节。# 一个简单的Flask接口示例 from flask import Flask, request, jsonify from src.pipeline import PlateRecognitionPipeline app Flask(__name__) pipeline PlateRecognitionPipeline(checkpoints/char_cnn.h5) app.route(/recognize, methods[POST]) def recognize_plate(): file request.files.get(image) if not file: return jsonify({error: no_image}), 400 # 读文件为numpy数组OpenCV不能直接读bytes img_array np.frombuffer(file.read(), dtypenp.uint8) image cv2.imdecode(img_array, cv2.IMREAD_COLOR) plate, status pipeline.process(image) return jsonify({plate: plate, status: status})这个接口可以直接跑在门禁系统或停车场管理后台上调用方只需要POST一张图片返回里带着车牌号和状态码。需要注意的是imread和imdecode的区别imread直接读文件路径imdecode读内存里的字节流。上传场景拿到的就是bytes用imdecode更合适。5.2 性能优化方向替换模型与并行化如果业务场景对准确率的要求高于推理速度可以替换识别模型。最顺手的路径是保留现有定位和分割模块把recognize.py里的模型换成YOLOv8-cls或者MobileNetV3这两个模型在CPU上的推理速度都还能控制在一百毫秒内。换模型时只需要保持输入尺寸不变输出类数一致。如果想提升定位鲁棒性比如应对非常规角度和夜间恶劣光照建议把locate.py的边缘检测换成基于深度学习的文本检测模型比如DBNet或者PSENet。实际工程里我用DBNet替换过定位模块在雨天夜间场景的召回率从78%升到了93%代价是单帧耗时增加了约120毫秒。最后一个优化点是批量推理。停车场道闸通常要处理同时间段的十几台车如果并发请求多可以考虑把推理放在一个批次里做。TensorFlow支持多张图同时推理识别loop里不再逐张predict而是攒够8张图一次性predict吞吐能提升近4倍。从那以后我每次接手类似的OCR项目都会先拆一遍类似的pipeline确认每阶段的失败率指标而不是直接优化最显眼的模型部分。很多识别系统整体准确率上不去根因往往在图像预处理和分割环节识别模型反而最不值得替换。希望这些记录对你有用。本文还有配套的精品资源点击获取