资讯动态

基于Python的OpenMV车牌检测:从颜色分割到字符识别

发布时间:2026/9/17 3:50:14 来源:尧图企业网站定制
简介一套基于Python与OpenMV的车牌检测毕业设计实践资源面向嵌入式视觉、图像处理方向的学生。资源围绕“摄像头识别小车车牌、存储拍照时间与小车图片”的题目要求并预留了扩展存储100个以上车牌进行比对的思路可作为课题起步与二次开发的参考。包内含11个文件包括Python脚本、OpenCV Haar级联文件、测试图片与说明文档整体约314KB结构紧凑其中cascade用于车牌定位py脚本实现检测流程与级联转换jpg/png图片便于直接复现效果。方案采用Haar Cascades进行车牌检测并整理了训练脚本完善、内存优化、动态画框等任务清单适合想了解传统视觉方法实现车牌识别原理的学习者。目前已有316人学习关注对快速搭建车牌检测原型有一定参考价值。1. 毕业设计里的 Python OpenMV 车牌检测先想清楚两道工序下载这个压缩包时它叫“基于Pythonopenmv车牌检测”解压后目录里通常会有三块东西OpenMV 摄像头里跑的 main.py、电脑端处理结果和界面的 Python 脚本、以及一份毕设文档。整套系统的实际结构是一条两段式流水线——OpenMV 负责图像采集、颜色分割和车牌区域定位Python 端负责串口接收、字符分割、识别和结果展示。为什么一块板子不能把所有事干完还要多一台电脑因为 OpenMV 的 STM32 主控在同时跑摄像头驱动、图像缓存和颜色分类之后已经没有余量再跑高精度识别模型反过来如果整条识别链路都放在电脑上“嵌入式”这个课题分量又没了。把任务按计算强度拆成两级是这个标题下真正值得写的技术内容。下文按做毕设时的实际顺序走先把 OpenMV 端的颜色阈值调好再定串口 JPEG 帧协议Python 端做字符识别最后讲验证和调参。2. OpenMV 端颜色分割LAB 阈值、白平衡固定和 blob 二次过滤车牌检测的第一步是找出“蓝色在哪里”。OpenMV 的find_blobs要求提供一组 LAB 阈值L、A、B 各给最小值和最大值共 6 个数。为什么要 LAB 而不是 RGBRGB 的三通道和光照强度强相关同一个蓝色在顺光、逆光、树荫下会被传感器放大成完全不同的数值LAB 把亮度单独放在 L 通道A/B 通道只表达颜色倾向蓝底车牌在 A/B 轴上的分布比 RGB 紧凑得多。配合固定白平衡颜色分割的阈值可以连续用好几个环境。很多人第一次跑通find_blobs后就把 RGB565 图像直接送识别结果发现换一盏灯就找不到车牌。问题不在识别而在采集端的自动增益一直在变阈值和画面亮度永远对不上。OpenMV 的图形识别文档里把这类问题的优先排查项列为“先固定摄像头参数再调颜色阈值”顺序不能反。2.1 锁死自动曝光和白平衡让阈值不再漂移理想环境是室内固定机位光照变化只来自窗帘和灯开关。这种场景下最省事的做法是关掉自动曝光、自动增益、自动白平衡用一组固定参数跑。下面是我在 main.py 里常用的初始化片段import sensor, image, time # 蓝色车牌的 LAB 阈值由 OpenMV IDE 阈值编辑器导出后微调 BLUE_LAB (18, 62, -32, -12, -40, -18) sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QQVGA) # 160x120兼顾颜色分割速度与面积 sensor.skip_frames(time2000) sensor.set_auto_whitebal(False, rgb_gain_db(43, 36, 25)) sensor.set_auto_gain(False) sensor.set_auto_exposure(False, exposure_us20000) while True: img sensor.snapshot() blobs img.find_blobs( [BLUE_LAB], pixels_threshold150, area_threshold120, mergeTrue, margin3) for b in blobs: # 这一步只做颜色粗筛后面再加形状过滤 img.draw_rectangle(b.rect())sensor.set_auto_whitebal(False, rgb_gain_db(43, 36, 25))里的三个数值是 R/G/B 通道增益单位 dB应取哪组值可以在光线均匀的白色纸片前读一帧图用 IDE 里的颜色直方图观察三通道均值后定。exposure_us20000等于固定曝光 20ms换算帧率上限是 50FPS实际跑分割后能到 20FPS 已经很理想。傍晚或室内偏暗时把 exposure_us 提高到 30000 能让图像变亮但物体运动会拖影演示阶段建议直接调整补光。这里有个容易踩的坑pixels_threshold与area_threshold的含义不同前者是色块至少包含多少个像素后者是色块有效面积阈值。QQVGA 分辨率下 150 起步比较合理如果把分辨率换成 VGA像素阈值要跟着放大四倍否则全图会碎成一大堆噪点色块。2.2 用 OpenMV IDE 阈值编辑器导出一组能用的 LAB 六元组手敲 LAB 阈值基本不现实。OpenMV IDE 自带阈值编辑器工具栏里打开 Threshold Editor选一张实拍照把显示模式切到 LAB拖动六个滑杆直到蓝底车牌显示为白色、背景显示为黑色右下角会同步刷新六元组数值。直接把它贴进BLUE_LAB是第一个可用版本。要注意的是编辑器看到的是静态单帧实时跑起来会有传感器噪声和运动模糊所以我一般会把导出的阈值稍作放松B 通道下界放宽 24 个数值用来吸收阴天下偏灰的蓝色A 通道保持编辑器数值因为它在区分蓝/绿时最敏感。若车牌是新能源的渐变绿色LAB 阈值单独建一组用find_blobs的多个阈值列表并传而不是改一套参数硬跑。2.3 长宽比、density 区域过滤拦掉不是车牌形状的蓝色块颜色分割后车牌的蓝色栅栏、蓝色路牌、深蓝衣服都可能是候选。车牌板的几何特征很固定宽高比约在 2.2 到 4.5 之间蓝色像素占包围矩形面积的比例通常高于 0.45。因此在循环里加三道判断先用参数表说明含义find_blobs 参数作用调试建议pixels_threshold色块包含的最小像素数QQVGA 下 150VGA 下 600 左右area_threshold色块面积下限滤掉细碎噪点略小于 pixels_threshold 即可merge是否合并相邻色块反光碎片必须合建议 Truemargin合并时邻接像素距离碎片间隙 2~5px设 3过滤代码如下MIN_AREA 600 MIN_RATIO 2.2 MAX_RATIO 4.8 MIN_DENSITY 0.45 for b in blobs: if b.area() MIN_AREA: continue ratio b.w() / b.h() if not (MIN_RATIO ratio MAX_RATIO): continue if b.density() MIN_DENSITY: continue # 通过形状过滤后才当成车牌候选 img.draw_rectangle(b.rect())b.density()是色块内有效像素占包围矩形面积的比例这一步能滤掉“L 形”“U 形”的高光反光区域它们虽然也是蓝色但内部大面积空洞计算出来比例很低。mergeTrue与margin3必须同时出现否则一个车牌因反光断成两段后每段的长宽比都会失真第二道判断直接误杀。到这里OpenMV 端能把车牌外框画出来了。下一步要考虑的是怎么把车牌区域交给 Python 端做字符级处理。3. 车牌字符分割与透视校正给识别端一张规整的灰度图OpenMV 定位出蓝色区域后最直接的做法是把坐标和尺寸通过串口发给电脑让电脑端用同样的坐标截图。这个方案在“固定机位、固定背景”的演示环境能跑通但换一个角度蓝框边界稍微变化电脑端就不知道从哪里下手。更常见的做法是OpenMV 直接按 blob 的位置裁剪出车牌小图压缩成 JPEG 后用串口帧协议发给 Python后续的透视校正、字符分割全部交给 OpenCV。JPEG 在 OpenMV H7 系列里有硬件编码压缩一张 60×25 的车牌小图只用几毫秒不拖慢主循环。3.1 裁剪前向外扩 10%避免字符边缘被切blob.rect()是蓝色像素的最小外接矩形而字符本身比蓝色背景小一圈直接裁剪会切掉字符边缘。我会在四个方向各外扩 10% 再img.copy(roi...)用compressed(quality85)编码。quality 低于 75 时字符边缘开始出现块状伪影高于 95 没有明显收益只增加串口传输量85 是一个稳妥的起点。def make_plate_jpeg(img, blob): x, y, w, h blob.rect() px, py int(w * 0.1), int(h * 0.1) x1 max(0, x - px) y1 max(0, y - py) x2 min(img.width(), x w px) y2 min(img.height(), y h py) roi (x1, y1, x2 - x1, y2 - y1) plate img.copy(roiroi) return plate.compressed(quality85)外扩有两个副作用一是可能把旁边其他字符或物体边缘带进来二是车牌本身有时是斜的外扩后依然斜。这两个问题都由 Python 端解决OpenMV 不必做耗时的几何变换。3.2 用 HSV 提取蓝色轮廓再按最小外接矩形做透视变换OpenCV 端收到 JPEG 后先用cv2.imdecode解成 BGR 图转 HSV 后取蓝色掩码。中国蓝底车牌的 H 范围大致在 100124S 和 V 分别在 120 以上。用cv2.minAreaRect算出掩码外接矩形得到四个角点后用cv2.getPerspectiveTransform把车牌映射到正面视角的 240×70 画布。import cv2 import numpy as np PLATE_W, PLATE_H 240, 70 # 输出画布尺寸字符分割前固定 def correct_plate(plate_bgr): hsv cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (100, 120, 120), (124, 255, 255)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((4, 4), np.uint8)) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect) box np.float32(sorted(box, keylambda p: (p[1], p[0]))) dst np.float32([[0, 0], [PLATE_W, 0], [PLATE_W, PLATE_H], [0, PLATE_H]]) M cv2.getPerspectiveTransform(box, dst) warped cv2.warpPerspective(plate_bgr, M, (PLATE_W, PLATE_H), flagscv2.INTER_CUBIC) return warped, maskkeylambda p: (p[1], p[0])是一种简化排序对小角度倾斜够用如果车牌被拍到接近 45 度的正侧面梯形失真较大四个角点的顺序会乱这时要按四个点与中心点的相对方位做排序而不是套用这条 lambda。MORPH_CLOSE用 4×4 的核把白色字符造成的蓝色空洞补上避免minAreaRect取到的是“包围字符的蓝色边框”而非整个蓝底区域。3.3 垂直投影分割字符把 7 个字符切成独立小图透视校正后的车牌是 240×70 的灰度图。字符分割最常用的方法是垂直投影统计每一列的非零像素数连续有字的部分当做一个字符段间隙处切断。下面是可直接沿用的函数def split_chars(gray, min_w12, gap4): # 输入是单通道灰度图自动用 Otsu 二值化黑底白字 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) h, w binary.shape proj (binary 0).sum(axis0) // 255 thresh int(h * 0.02) in_char False start 0 spans [] for x in range(w): if proj[x] thresh and not in_char: in_char True start x elif proj[x] thresh and in_char: in_char False if x - start min_w: spans.append((start, x)) # 合并汉字内部被噪声切断的紧凑段 merged [] for s, e in spans: if merged and s - merged[-1][1] gap: merged[-1] (merged[-1][0], e) else: merged.append((s, e)) return [binary[:, s:e] for s, e in merged]min_w是字符段最小宽度用来滤掉竖线、污点这类窄噪声gap决定多近的段要被合并。240 像素宽的车牌被切成 7 段时每段约 2434 像素字符间隙通常大于 10 像素汉字内部偏旁间隙约 24 像素所以gap4是比较常见的取值。若分割后经常出现“京”被切成两份就调大gap若出现两个字母粘连就调小min_w并检查透视校正是否把图像压得太扁。分割出的每个字符都要再cv2.resize到统一尺寸比如高度 48、宽高比保持不变后居中放进 24×48 画布这一步是后续模板匹配或 CNN 识别能否稳定的关键。识别之前不要省略这一步。4. Python 端串口接收与字符识别从帧协议到模型选择OpenMV 把车牌 JPEG 字节通过串口发出后Python 端最容易遇到的坑不是传不出去而是找不到帧的起点。JPEG 是流式数据里面的任何字节都可能出现 0xAA、0x55帧头不能用单字节。我一般会定义三字节魔数 0xAA 0x55 0x02后面跟两字节大端长度再接载荷最后放一字节校验和校验方式用所有载荷字节相加取低 8 位够用且实现简单。4.1 OpenMV 按帧发送 JPEGPython 端按帧拆包OpenMV 侧把 JPEG 封进帧分块写入串口。分块是必要的UART 的发送缓冲区不是无限大一次性写入几 KB JPEG 会被截断或丢数据。from pyb import UART uart UART(3, 115200, timeout_char1000) def send_frame(uart, jpeg): size len(jpeg) head bytes([0xAA, 0x55, 0x02, size 8, size 0xFF]) payload head jpeg checksum sum(payload) 0xFF frame payload bytes([checksum]) # OpenMV UART 缓冲有限分 64 字节一块写块间留出传输时间 for i in range(0, len(frame), 64): uart.write(frame[i:i 64]) time.sleep(1)Python 端用pyserial读同一帧。拆包的逻辑是滑窗找魔数找到后读两字节长度再按长度读取 JPEG最后校验和。示例import serial def read_frame(ser): window bytearray() while True: ch ser.read(1) if not ch: return None window.append(ch[0]) if len(window) 3 and window[-3:] b\xaa\x55\x02: break if len(window) 6: del window[:-3] size_bytes ser.read(2) if len(size_bytes) 2: return None size size_bytes[0] 8 | size_bytes[1] jpeg ser.read(size) if len(jpeg) size: return None crc ser.read(1) total sum(window[-3:]) sum(size_bytes) sum(jpeg) if crc and (total 0xFF) crc[0]: return jpeg return None注意这里有两处耗时操作逐字节ser.read(1)在高波特率下会拉低吞吐但毕设场景一帧约 2KB压力不大若嫌慢可以先用ser.in_waiting批量读入再在内存里滑窗找帧头。调试原始串口流时在终端跑python -m serial.tools.miniterm /dev/ttyACM0 115200能直接看到十六进制帧比写日志方便得多。提示如果毕设要求 OpenMV 与 STM32 主控通信UART 引脚接法是把 OpenMV 的 TXD 接到 STM32 的 RXD再经 STM32 转发给 PC。帧协议不变但两边要核对波特率是否一致。4.2 字符识别三选一模板匹配、Tesseract、轻量 CNN分割出的 7 个字符前一位是省份汉字后六位是字母和数字。“单条模型打天下”在车牌场景不一定成立常见分法是汉字用模板匹配字母数字用 Tesseract想刷高精度就用 CNN。三种路线的对比如下识别路线数据集成本识别正确率主要限制OpenCV 模板匹配每个字符几张样本即可字符越少越准对分割角度敏感字符归一化必须严格Tesseract免训练装语言包字母数字较好汉字弱需要黑白名单与--psm 7配合轻量 CNN单字符数据集千张起三种路线中上限最高需要准备数据并训练周期长Tesseract 在毕设中最多的是这样用把单个字符图交给image_to_string只识别字母数字避免让它猜汉字。代码里显式关闭中文识别用白名单限定字符集能显著提高准度import pytesseract from PIL import Image def ocr_char(char_img, char_whitelistABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789): pil_img Image.fromarray(char_img) text pytesseract.image_to_string( pil_img, configf--psm 7 -c tessedit_char_whitelist{char_whitelist}) return text.strip()--psm 7表示把整张图当作一行文本处理不推测多行结构适合单个字符。字母数字最常见的误判是 0/O、8/B、5/S白名单虽然能挡住一些非法字符但无法解决字符形状本身疑似。识别之前把字符图水平方向再收紧 2 像素去掉外围白边能减少这类混淆。汉字字符建议单独走模板匹配。省份也就三十来个字每个字准备 35 个不同缩放、轻微旋转的样本建一个模板库用cv2.matchTemplate算归一化相关系数取最高分样本越多越稳。这条路在数据集有限时比 Tesseract 的汉字识别可信得多。4.3 字符结果拼接与显示7 个字符按从左到右的顺序拼回字符串后把结果通过cv2.putText画在原始帧上。这里有一个容易忽略的细节字符分割返回的顺序依赖split_chars的扫描方向从x0到xw所以结果天然有序不要再做一次排序。调试时把每个分割出的字符单独cv2.imwrite到临时目录命名带索引比如char_2.png对定位“第几个字符识别错了”非常有用。5. 最后一公里用验证集统计和错误样本反推参数识别率不是调出来的是测出来的。没有验证集的调参会变成“调到最后连自己都不知道是变好还是变坏”。我一般会准备三类测试数据正面正向、小角度倾斜、逆光或暗光每类 1020 张全部是真机拍摄而不是从网上截图。三类数据分开统计能看出系统短板到底在哪一类场景。5.1 写一个统计脚本输出整体正确率和分类正确率脚本逻辑很简单读入真实车牌标签调用前面封装的整条流水线得到预测结果按场景目录分组计算字符级正确率。字符级正确指 7 个字符全部一致某一个字符错就整条记为错误。def eval_dataset(samples): total, correct 0, 0 per_scene {} for scene, path, true_label in samples: jpeg read_plate_from_image(path) # 复现流水线返回分割后的字符图 pred predict_plate(jpeg) # 模板匹配 Tesseract ok pred true_label total 1 correct int(ok) per_scene.setdefault(scene, [0, 0]) per_scene[scene][0] int(ok) per_scene[scene][1] 1 print(f整体准确率: {correct / total:.2%}) for scene, (c, n) in per_scene.items(): print(f{scene}: {c / n:.2%} ({c}/{n})) return per_scene整体正确率能过 90% 是一道坎场景细分表能告诉你剩下的误判集中在吃哪个方向。会摆在毕设答辩上的“准确率”就是这个数最好把测试集照片和脚本一起放进压缩包评委随时能复跑。5.2 从错误类型反推调参方向错误现象优先检查项调整动作车牌框不出来曝光不够或阈值区间过窄曝光提到 25000us 以上放松 B 通道下界框出大面积蓝色区域LAB 阈值过宽A/B 范围不贴合收紧 A 通道上界检查白平衡是否被自动模式覆盖字符分割多一段汉字偏旁间隙被噪声断开调大gap观察分割索引图字符连在一起透视校正后图像过扁调整 dst 的PLATE_H从 70 改为 78 再试字母数字混淆归一化不够或字符倾斜在灰度二值化前先做一次cv2.equalizeHist这些调参不是一次就能结束的。改完一项后回到 5.1 的统计脚本看分类正确率变化不要凭肉眼对着两帧图判断。5.3 演示时给检测加一个目标锁定省串口流量连续逐帧发送 JPEG 会把串口占满识别端也跟不上。常见做法是对上一帧已经识别成功的车牌建立一个临时锁下一帧若发现相同中心点附近的 blob就跳过发送等目标移出画面或位置变化超过阈值再触发新识别。这段逻辑放在 OpenMV 主循环里lock None while True: img sensor.snapshot() blobs img.find_blobs([BLUE_LAB], pixels_threshold150, area_threshold120, mergeTrue, margin3) for b in blobs: if lock and abs(b.cx() - lock[0]) 15 and abs(b.cy() - lock[1]) 15: continue # 同一车牌已识别过跳过发送 # 通过长宽比过滤后生成 JPEG 并发送 lock (b.cx(), b.cy()) break time.sleep(50)锁的失效设计要简单直接只要当前 blob 中心远离锁定位 15 像素以上就视为“新目标”并触发识别。这样既避免了重复发送带来的串口拥堵又能让演示画面保持在 5FPS 左右的可接受范围真到答辩现场把 OpenMV 靠近车牌或移开锁会立刻解除并重新识别效果比不做锁定的连续帧识别更顺畅。本文还有配套的精品资源点击获取

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

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

免费获取报价