ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

车牌识别系统实战:从原理到OpenCV代码的完整链路

车牌识别系统实战:从原理到OpenCV代码的完整链路 简介这份PPT面向计算机相关专业的毕业设计学生与深度学习入门者系统讲解基于Python与OpenCV的车牌识别系统实现思路。内容围绕车牌定位与字符识别两大模块展开涵盖图像灰度化、HSV色彩分割、Sobel边缘检测、二值化与闭运算、漫水填充等预处理技术并详细给出两个结构相同的CNN网络参数包括输入层36x128、三层卷积与池化配置、全连接层设计及训练细节同时分析汉字识别、图像质量差异等难点与未来优化方向。资源包共1个pptx文件约8.57MB以图文并茂的答辩演示形式呈现结构清晰适合直接用于课程汇报或答辩参考。目前已有4787人学习可帮助读者快速掌握车牌识别系统的整体方案设计、CNN模型搭建逻辑与关键技术选型是完成同类毕业设计或入门深度学习项目的实用参考资料。1. 车牌识别系统详细讲解从一张 PPT 到能跑通的最小闭环很多人第一次接触车牌识别系统是在一份讲解 PPT 里看到「输入图片 → 预处理 → 定位 → 字符分割 → 识别 → 输出结果」这条流水线觉得逻辑清晰、不过如此。但真到自己动手把一张手机拍的停车场照片丢进去出来的结果往往是框歪了、字符粘在一起、或者干脆把「京」认成「津」。问题不在算法本身而在于 PPT 讲的是理想链路落地时要处理的是光照、角度、模糊、遮挡这些脏活。这篇笔记面向两类人一类是要做课程设计或技术分享需要把车牌识别系统讲清楚、还要能演示另一类是想把车牌识别接进自己业务停车场、门禁、车辆统计的工程师关心的是能不能跑、参数怎么调、坑在哪。我会按「先立住原理再动手复现最后讲边界」的顺序展开中间给可直接抄的代码和参数表。整套方案基于常见的 OpenCV 轻量检测/识别模型组合不依赖特定平台本地就能跑。2. 车牌识别系统的四段链路与选型逻辑车牌识别License Plate RecognitionLPR本质是一个「先找再读」的级联任务。把它拆成四段车牌检测定位、车牌矫正透视变换、字符分割、字符识别。每一段都有传统图像处理和深度学习两条路线选型决定了你后面调试的难度。2.1 为什么检测环节优先考虑轻量检测器而不是纯颜色分割传统做法用 HSV 颜色空间 边缘检测 形态学找矩形轮廓优点是零训练、CPU 就能跑缺点是对光照和背景极度敏感。蓝色车牌在强逆光下饱和度掉得厉害阈值一调就漏检黄色车牌和车身颜色接近时又容易误检。常见做法是检测阶段用一个轻量目标检测模型比如 YOLO 系列的小模型专门出车牌框识别阶段再走字符识别。这样检测的鲁棒性由数据决定而不是靠你反复调 HSV 阈值这种玄学。我一般会先用检测模型把车牌框出来再做透视矫正把倾斜的车牌拉正这一步能显著提升后续字符识别的准确率。选型上给一个判断标准如果你的场景是固定机位、光照稳定比如地库入口传统颜色分割够用且成本低如果是移动端、多场景、光照不可控直接上检测模型别在阈值上耗时间。2.2 字符识别CRNN 与模板匹配的取舍字符分割 模板匹配是老 PPT 里的经典方案把车牌切成 7 个字符块逐个和标准字符模板做匹配。它在标准正脸车牌上准确率不错但一旦字符粘连、有污损、或者新能源车牌那种 8 位字符分割就会崩错误会一路传导到识别。CRNNCNN RNN CTC这类序列识别模型跳过了显式分割直接对整行车牌文本做识别对粘连和不定长更友好。代价是需要训练数据且推理比模板匹配重。实际项目里我的选择是如果只是演示和教学模板匹配足够讲清楚原理如果要上线用 CRNN 或类似的序列识别模型把分割这步省掉少一个错误来源。下面这张表是我在选型时会对照的维度维度颜色分割 模板匹配检测模型 CRNN训练数据需求几乎不需要需要标注数据光照鲁棒性差好倾斜车牌需额外矫正检测框自带角度信息新能源 8 位车牌分割易错天然支持变长推理速度CPU快中等适合场景固定机位、教学演示多场景、上线业务2.3 一套可复现的最小链路检测 矫正 识别把链路串起来最小可运行版本分三步。第一步用检测模型拿到车牌四点坐标第二步做透视变换把车牌拉成正视图第三步送进识别模型出字符。这里给一个用 OpenCV 做透视矫正的函数这是整条链路里最容易被忽略但影响最大的一步。import 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)] # 左上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 def four_point_transform(image, pts): rect order_points(pts) (tl, tr, br, bl) rect # 计算目标宽度取上下边最大宽度 widthA np.linalg.norm(br - bl) widthB np.linalg.norm(tr - tl) maxWidth max(int(widthA), int(widthB)) # 计算目标高度取左右边最大高度 heightA np.linalg.norm(tr - br) heightB np.linalg.norm(tl - bl) maxHeight max(int(heightA), int(heightB)) # 目标正视图的四个角点 dst np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtypefloat32) M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped逻辑说明order_points解决的是「检测模型给出的四个点顺序不固定」这个问题。如果不排序透视变换会把车牌扭成麻花。排序依据是坐标的加减关系左上角 xy 最小右下角最大这个规律对绝大多数凸四边形成立。参数说明maxWidth和maxHeight取的是对边最大值保证矫正后不丢信息。如果你的检测框本身有噪声可以在warpPerspective前加一步cv2.GaussianBlur平滑但别过度模糊会让字符边缘变糊反而伤识别。矫正后的图建议统一 resize 到固定高度比如 48 像素宽度按比例缩放方便后续识别模型吃固定输入。2.4 把识别结果落成结构化输出识别模型吐出来的通常是一串字符业务要的是「车牌号 置信度 时间戳」。这一步别省很多演示 PPT 到「输出车牌号」就结束了实际接入时你需要判断置信度阈值低于阈值的丢弃或转人工。我一般会设一个 0.85 的置信度下限低于它的结果不直接开门或计费避免误识别造成纠纷。结构化输出用 JSON 最省事字段包括plate_no、confidence、bbox、timestamp方便下游消费。3. 用 OpenCV 在本地跑通车牌定位与矫正这一章把检测和矫正落到可执行步骤。假设你已经有一张车辆图片目标是拿到矫正后的车牌小图。检测模型可以用现成的轻量模型也可以用传统方法先跑通流程我这里两条路都给方便你按场景选。3.1 环境准备与依赖安装先把环境搭好避免版本问题导致 API 对不上。建议用虚拟环境隔离。# 创建虚拟环境 python -m venv lpr_env # 激活Linux/macOS source lpr_env/bin/activate # 激活Windows lpr_env\Scripts\activate # 安装核心依赖 pip install opencv-python numpy # 如果走检测模型路线再装推理框架按你选的模型来 pip install onnxruntime逻辑说明opencv-python提供图像处理和透视变换numpy做矩阵运算onnxruntime用于加载 ONNX 格式的检测模型CPU 推理够用。参数上注意opencv-python和opencv-contrib-python别同时装会冲突如果你需要 SIFT 等 contrib 模块只装 contrib 版本即可。3.2 传统颜色分割定位车牌的完整代码先给传统路线适合快速验证和教学演示。核心思路是转 HSV按车牌颜色蓝、黄、绿做掩膜再找轮廓。import cv2 import numpy as np def locate_plate_by_color(image_path): img cv2.imread(image_path) if img is None: raise FileNotFoundError(图片读取失败检查路径) # 转 HSV颜色分割在 HSV 空间更稳 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌掩膜范围可根据实际图片微调 lower_blue np.array([100, 80, 80]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 形态学闭运算填补字符间的空隙让车牌连成一块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 找轮廓 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h ratio w / float(h) # 车牌宽高比大致在 2.5 到 5.5 之间面积过滤掉小噪点 if 2.5 ratio 5.5 and area 2000: candidates.append((x, y, w, h)) # 按面积排序取最大的作为车牌 candidates.sort(keylambda b: b[2] * b[3], reverseTrue) return img, candidates img, boxes locate_plate_by_color(car.jpg) if boxes: x, y, w, h boxes[0] plate img[y:yh, x:xw] cv2.imwrite(plate_crop.jpg, plate) print(定位到车牌已保存裁剪图) else: print(未定位到车牌需调整 HSV 阈值)逻辑说明HSV 掩膜负责把车牌颜色区域抠出来闭运算把车牌上字符造成的空洞填上让整个车牌成为一个连通区域这样findContours才能拿到完整矩形。宽高比过滤是关键车牌是扁长矩形这个先验能滤掉大量误检。参数说明lower_blue和upper_blue是蓝色车牌的 HSV 范围H 通道 100 到 124 覆盖大部分蓝色。如果你的图片偏暗S 和 V 的下限可以降到 60。闭运算核(17, 5)是横向长条因为车牌是横向的横向闭运算更符合形状先验。面积阈值 2000 是经验值图片分辨率高时要相应调大。3.3 检测模型路线的接入方式传统方法跑通后如果发现漏检多换检测模型。接入方式是把图片预处理成模型输入尺寸推理拿到框再映射回原图坐标。import cv2 import numpy as np import onnxruntime as ort def detect_plate_onnx(image_path, model_path, input_size640, conf_thres0.4): img cv2.imread(image_path) h0, w0 img.shape[:2] # letterbox 缩放保持长宽比避免车牌变形 scale min(input_size / w0, input_size / h0) nw, nh int(w0 * scale), int(h0 * scale) resized cv2.resize(img, (nw, nh)) canvas np.full((input_size, input_size, 3), 114, dtypenp.uint8) canvas[:nh, :nw] resized # 转 RGB、归一化、NCHW blob canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 blob np.expand_dims(blob, axis0) sess ort.InferenceSession(model_path, providers[CPUExecutionProvider]) outputs sess.run(None, {sess.get_inputs()[0].name: blob}) # 这里 outputs 的解析依赖具体模型输出格式按你的模型调整 return outputs, scale, (w0, h0)逻辑说明letterbox 是检测模型的标准预处理用灰色填充到正方形保证缩放后车牌不变形同时记录缩放比例推理完把框坐标映射回原图。归一化到 0 到 1 是大多数模型的输入要求。参数说明input_size要和模型训练时一致常见是 640。conf_thres是置信度阈值0.4 偏宽松漏检多就降到 0.3误检多就升到 0.5。providers指定 CPU 推理有 GPU 可换成对应 provider。输出解析部分因模型而异YOLO 系列通常是[batch, num_anchors, 5classes]需要自己做 NMS这部分建议直接参考你所用模型的官方后处理代码别硬猜。3.4 矫正后统一尺寸再送识别不管走哪条定位路线矫正后都要统一尺寸。识别模型对输入尺寸敏感高度不一致会直接掉点。def normalize_plate(plate_img, target_h48): h, w plate_img.shape[:2] scale target_h / float(h) target_w int(w * scale) resized cv2.resize(plate_img, (target_w, target_h), interpolationcv2.INTER_CUBIC) return resized逻辑说明按高度归一宽度等比缩放保持字符不变形。INTER_CUBIC比默认的线性插值在放大时更清晰字符边缘更锐利对识别有帮助。参数说明target_h48是常见取值CRNN 类模型常用 32 或 48。如果你的识别模型输入是固定宽度比如 160那就改成按宽度归一高度等比缩放具体看模型要求。别同时固定宽高那会把车牌拉变形。4. 字符识别与后处理的参数怎么设定位和矫正做完进入识别。这一章讲识别模型的输入输出处理以及后处理里那些不设就会翻车的参数。4.1 CRNN 推理的输入构造CRNN 吃的是矫正后的车牌条图输出是字符序列。输入构造要注意通道顺序和归一化。import numpy as np import cv2 def build_crnn_input(plate_img, input_w160, input_h48): # 统一到模型输入尺寸 img cv2.resize(plate_img, (input_w, input_h)) img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 多数车牌识别用灰度 img img.astype(np.float32) / 255.0 # 归一化到 [-1, 1]这是很多 CRNN 训练时的预处理 img (img - 0.5) / 0.5 img np.expand_dims(img, axis(0, 1)) # NCHW return img逻辑说明转灰度是因为车牌字符识别主要靠形状颜色信息作用有限还能减少计算量。归一化到[-1, 1]是训练时常用的预处理推理时必须一致否则输入分布对不上识别率会明显下降。参数说明input_w160、input_h48要和模型训练配置一致这个不能自己拍脑袋定。如果你不确定去看模型的配置文件或训练脚本里的输入尺寸。归一化方式同理训练用的什么推理就用什么这是最常见的翻车点之一。4.2 CTC 解码与置信度计算CRNN 输出的是每个时间步的字符概率需要 CTC 解码成最终序列同时算置信度。def ctc_decode(logits, charset): # logits 形状: [T, num_classes] indices np.argmax(logits, axis1) # CTC 去重合并连续相同去掉 blank假设 blank 为 0 result [] prev -1 confs [] for t, idx in enumerate(indices): if idx ! prev and idx ! 0: result.append(charset[idx]) confs.append(logits[t, idx]) prev idx text .join(result) confidence float(np.mean(confs)) if confs else 0.0 return text, confidence逻辑说明CTC 解码的核心是「合并连续重复 去掉 blank」。blank 通常编号为 0代表「此处无字符」。置信度取所有保留字符概率的均值作为整条结果的置信度。参数说明charset是字符集要和训练时完全一致顺序都不能错否则解码出来全是乱码。置信度阈值建议设在 0.85 左右低于它的结果不要直接用于开门或计费。如果你的场景允许重试可以低置信度时触发二次识别或转人工。4.3 后处理车牌号格式校验识别出来的字符串不一定合法加一层格式校验能挡掉不少误识别。国内普通车牌是「省份简称 字母 5 位字母数字」新能源是 8 位。import re # 省份简称集合 PROVINCES set(京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新) # 普通车牌1 位省份 1 位字母 5 位字母数字 NORMAL_PATTERN re.compile(r^[ .join(PROVINCES) r][A-Z][A-Z0-9]{5}$) # 新能源车牌1 位省份 1 位字母 6 位字母数字 NEW_ENERGY_PATTERN re.compile(r^[ .join(PROVINCES) r][A-Z][A-Z0-9]{6}$) def validate_plate(text): if NORMAL_PATTERN.match(text): return True, normal if NEW_ENERGY_PATTERN.match(text): return True, new_energy return False, invalid逻辑说明正则校验能过滤掉长度不对、省份简称不对的结果。这一步成本极低但能挡掉相当比例的误识别尤其是把「京」认成「津」这类形近错误如果省份简称不在集合里就直接判非法。参数说明PROVINCES集合按实际省份简称维护别漏。正则里[A-Z0-9]是字母数字实际车牌里字母 I 和 O 通常不用避免和 1、0 混淆如果你的场景要求严格可以把 I、O 从字符集里排除进一步降低误识。5. 车牌识别落地避坑5 个真实踩坑记录这一章是我在实际项目里踩过的坑每条按「现象 → 原因 → 解决」写你对照自己的场景排查。5.1 白天识别正常晚上全挂现象同一套模型白天准确率 95% 以上晚上掉到 60% 以下大量漏检。原因夜间车灯直射造成局部过曝车牌区域高光溢出字符细节丢失同时环境光不足车牌整体对比度低。训练数据如果以白天为主模型没见过这种分布。解决训练数据里补充夜间样本尤其是带车灯眩光的推理前加一步自适应直方图均衡CLAHE提升局部对比度。CLAHE 的clipLimit设 2.0 到 3.0tileGridSize设 8x8别设太大否则噪声也被放大。5.2 新能源车牌识别成 7 位现象8 位新能源车牌识别结果总是少一位或者最后一位错。原因识别模型的输出长度或字符集没覆盖 8 位情况CTC 解码时把最后一位当成了重复字符合并掉。解决确认训练数据的标签包含 8 位车牌字符集完整检查 CTC 解码逻辑确认 blank 编号正确。如果模型本身不支持变长换支持变长的序列模型别在解码上硬凑。5.3 透视矫正后字符被拉扁现象矫正后的车牌字符明显变形识别率下降。原因order_points排序错误四个点顺序乱了透视变换把车牌扭了或者目标宽高计算时用了错误的边。解决在order_points后打印四个点坐标人工核对一次目标宽高用对边最大值别用单边。如果检测框本身有旋转考虑用cv2.minAreaRect拿带角度的矩形再做旋转矫正比四点透视更稳。5.4 置信度虚高误识别也报 0.95现象明显错误的结果置信度却很高阈值过滤失效。原因置信度是模型输出的概率均值模型在训练集上过拟合时对错误结果也会给高概率。单看置信度不可靠。解决置信度 格式校验双保险。格式不合法的直接丢弃不管置信度多高。另外可以加一个「字符级置信度」检查如果某个字符的置信度明显低于其他字符标记为可疑触发二次识别。5.5 CPU 推理太慢单张超过 500ms现象检测 识别串起来单张图推理超过 500ms达不到实时。原因检测模型输入尺寸太大或者用了大模型识别模型没做批处理逐张推理。解决检测模型输入从 640 降到 416速度能提升一倍左右精度损失可接受识别模型做批处理攒几张一起推理如果还慢考虑模型量化把 FP32 转成 INT8CPU 上能再快 2 到 3 倍。量化后要重新验证精度别直接上线。6. 把车牌识别做成可演示的 PPT 级方案做技术分享或课程设计时光有代码不够还得让人看明白。我的习惯是准备一套「对比演示」同一张图分别展示原图、检测框、矫正图、识别结果四张图并排。这样听众能直观看到每一步的作用比讲一堆公式管用。具体做法是用 OpenCV 把中间结果拼成一张大图加文字标注。下面这个函数把四个阶段拼成演示图import cv2 import numpy as np def make_demo_grid(origin, detected, warped, text_result): # 统一高度方便拼接 h 300 def resize_h(img): scale h / img.shape[0] return cv2.resize(img, (int(img.shape[1] * scale), h)) imgs [resize_h(origin), resize_h(detected), resize_h(warped)] # 识别结果用白底图展示文字 result_img np.full((h, 400, 3), 255, dtypenp.uint8) cv2.putText(result_img, text_result, (20, h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 0), 2) imgs.append(result_img) # 横向拼接 grid np.hstack(imgs) return grid逻辑说明把原图、检测结果、矫正图、识别文字横向拼成一张图高度统一到 300 像素保证拼接后不变形。识别结果用白底黑字单独一块视觉上突出。参数说明h300是演示图高度投影或屏幕展示够用。cv2.putText的字体大小 1.2 和线宽 2 按输出分辨率调分辨率高就加大。如果你的车牌是中文putText不支持中文需要用 PIL 的ImageDraw配合中文字体文件来画这是个小坑提前准备好字体。进阶一点的做法是把这个演示流程包成一个命令行工具输入图片路径输出演示图和识别结果 JSON。这样分享时现场跑比放录屏有说服力。参数上给几个默认值检测置信度 0.4识别置信度 0.85矫正高度 48这些是我在多数场景下验证过的起点你按自己的数据微调。最后说个我自己的教训早期做演示时我总想用最复杂的模型秀技术结果现场环境没 GPU推理慢到卡住场面尴尬。后来我固定用轻量模型 CPU 推理提前在目标机器上跑一遍确认速度再上台。演示方案的第一原则是「稳」不是「炫」。把这条链路跑通、参数调稳、坑填平比堆模型有价值得多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进