简介:这是一份基于Python与OpenMV的车牌检测毕业设计资源包,面向计算机视觉、嵌入式方向的本科毕业生及入门开发者。项目围绕小车摄像头实时识别车牌展开,覆盖车牌定位与内容识别两条核心链路,并选用Haar级联作为检测方案。资源共11个文件、约314KB,包含Python主控脚本、Haar级联模型文件(cascade/xml)、图片样本与说明文档等,其中py脚本承担图像处理和检测流程,图像文件用于测试与效果展示,级联与XML提供训练好的检测模型,README可辅助快速上手。目前已有316人学习下载。借助这套资源,读者可以掌握OpenMV与Python配合的车牌检测基本思路,获得可运行的实验代码和模型文件;结合题目要求中存储拍照时间、小车图片以及100+车牌比对等发挥项,也便于在原始基础上扩展存储比对、性能优化等模块,适合作为毕业设计起步模板或课程项目参考。
1. 为什么选 Python + openmv 做车牌检测,而不是直接用电脑
车牌检测在毕业设计里属于常青树题目,但大多数同学一上来就想跑 YOLO、OpenCV + SVM,结果卡在环境配置和硬件选型上。openmv 是一块带摄像头、能跑 MicroPython 的嵌入式视觉板,型号常见的有 OpenMV4 H7 Plus,CPU 是 Cortex-M7,主频 480MHz,内存只有 1MB 左右,不能装 Linux,也不能直接 import cv2、torch。它的卖点是固件里内置了 image sensor、image processing、模板匹配等底层图像库,用 Python 脚本就能调,适合做规则明确的视觉任务。车牌识别恰好符合这个场景:车牌位置固定、背景相对简单、字符排布规律,不需要深度学习也能给出可用结果。
选 Python + openmv,本质上是把"图像采集 + 前端处理 + 算法验证"放在板端,把"数据标注 +参数调试"放在电脑端。你可以在电脑上用标准 Python 写算法原型,验证思路后再改写成 MicroPython 语法烧进板子。这样做的直接好处是:毕设答辩时能当场演示实时画面,老师看到的是"硬件在跑",而不只是 PPT 里的仿真图。另一个实际好处是成本低,openmv 板子几百块,加上舵机、显示屏、电源,整套硬件预算能控制在千元以内,适合学生课题。
这篇博文按"环境搭建 → 车牌定位 → 字符分割 → 识别与部署"的顺序展开,每步给可直接抄的代码,并标注参数含义和踩坑点。你不需要额外购买树莓派或 GPU 服务器。
2. 搭好开发链路:openmv IDE 与 PC 端 Python 环境
2.1 软件准备清单
去 openmv 官网下载 OpenMV IDE,这是官方集成开发环境,内置固件烧录、帧缓冲查看器、串行终端,Windows / macOS / Linux 都有对应安装包。IDE 自带 MicroPython 解释器,但你写的脚本最终是烧录到板子上的,不是在本机运行,所以不需要在宿主机单独装 MicroPython。PC 端 Python 环境主要用于做图像算法验证、生成阈值文件、批量处理数据集;如果只想跑通 openmv 端识别,PC 端可以临时不装任何额外库。
注意区分两套 Python:
- MicroPython:运行在 openmv 内部,import 的是
image、sensor、time等内置模块 - CPython:运行在你的电脑上,用来写验证代码,import
cv2、numpy、matplotlib
两套环境的语法 90% 相同,但库不同。写代码时不要混用,比如在板端千万别写 import cv2。
2.2 PC 端最小环境配置命令
Windows 用户建议安装 Anaconda,它自带 numpy、matplotlib,省去手动装包。在 Anaconda Prompt 里执行:
conda create -n plate python=3.8 conda activate plate pip install opencv-python==4.5.5.64 pip install numpy matplotlib提示:openv-python 版本不需要追新,4.5.5 足够稳定,很多教程的 API 示例基于这个版本。如果联网安装失败,换用
pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple走国内镜像。
安装完成后,在 Python 交互环境里验证:
import cv2 import numpy as np print(cv2.__version__) print(np.__version__)能打印版本号就说明图像处理环境就绪。这里再确认一下各参数含义:opencv-python是 OpenCV 的 Python 绑定包,包含核心算法和 I/O 模块;numpy是 OpenCV 底层数据容器ndarray的提供者,图像在 Python 里就是一个三维数组,后续所有操作都是改数组数值。
2.3 烧录固件与连接串口
打开 OpenMV IDE,用 USB 线连接 board,IDE 右下角会显示连接的串口号,比如 COM3 或 /dev/ttyACM0。首次连接建议先刷固件:菜单栏 Tools → Run Bootloader,选择最新固件 .dfu 文件,等待进度条完成。之后 IDE 会自动重启板子。
写一段最简测试脚本,验证链路通:
import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) clock = time.clock() while(True): clock.tick() img = sensor.snapshot() print(clock.fps())代码逻辑很直白,reset重置感光元件,set_pixformat设置像素格式为 RGB565,set_framesize把分辨率设为 320x240,snapshot抓一帧画面。把这段代码烧录到板子上之后,如果 IDE 右下角的串口终端不停输出帧率数字,比如 30 到 50,就说明摄像头、处理器、烧录链路全部正常。
clock.tick()和clock.fps()是用于测帧率的内置功能,不是图像处理必需的,但调试时建议保留,因为一旦预处理算法写重了,帧率会掉到个位数,此时就要考虑减小分辨率或简化算法。
3. 车牌定位:用色块与几何特征筛出车牌区域
3.1 车牌区域的特征假设
在实际场景里,车牌不一定在画面正中,角度也可能有倾斜,因此定位算法必须结合颜色特征和几何特征。中国蓝底白字车牌的主色调是蓝色,HSV 空间的 H 值集中在 100 到 130 度左右。普通环境下的天空、蓝色衣物也会落入该区间,所以光靠颜色不够,还需要叠加面积、纵横比、矩形度三个条件。
openmv 的image.find_blobs()函数就是干这个的,它会扫描图像,把满足颜色阈值范围的像素连通成色块(blob),然后返回每个色块的外接矩形、面积、质心等属性。我们不需要自己实现连通域标记,但需要理解它的参数含义,尤其是pixels_threshold和area_threshold这两个值,它们用来滤除噪声点。
3.2 单个色块的阈值调试脚本
先写一个只输出色块信息的脚本,把板子对着车牌,通过 IDE 帧缓冲窗口观察实时画面和色块框:
import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) # 蓝色阈值,格式为 (L_MIN, L_MAX, A_MIN, A_MAX, B_MIN, B_MAX) # 这里用的是 LAB 色彩空间 threshold_blue = (20, 80, 20, 70, -40, -10) while(True): img = sensor.snapshot() blobs = img.find_blobs([threshold_blue], pixels_threshold=200, area_threshold=200, merge=True) for b in blobs: img.draw_rectangle(b.rect(), color=(255, 0, 0)) img.draw_cross(b.cx(), b.cy(), color=(0, 255, 0)) print("area=%d, w=%d, h=%d, ratio=%.2f" % (b.area(), b.w(), b.h(), b.w()/b.h()))关键参数说明:
pixels_threshold=200:色块包含的像素数小于 200 时不返回,这个值滤掉细小噪点area_threshold=200:色块面积阈值,单位是像素面积,如果画面噪声多就调大到 500merge=True:把相邻且颜色相近的色块合并成一个整体,车牌被反光分割成两块时需要打开
b.rect()返回(x, y, w, h)元组,draw_rectangle会在帧缓冲上画红色框。当画面里出现蓝色框正好套住车牌时,记录此时打印的 w/h 比值,正常车牌宽高比在 2.5 到 3.5 之间。
3.3 加入几何校验的完整定位代码
只画框不算完成,要让板子自动确认"这是车牌",还得加判断条件。下面这版代码把面积、宽高比、矩形度三个条件同时放入:
import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) threshold_blue = (20, 80, 20, 70, -40, -10) def is_plate(blob): # 车牌宽高比通常在 2.5 ~ 3.5 ratio = blob.w() / blob.h() if ratio < 2.0 or ratio > 4.0: return False # 车牌面积占画面比例,假设画面分辨率 320x240 img_area = 320 * 240 if blob.area() < img_area * 0.05: return False # 矩形度:色块面积占外接矩形面积的比例 rect_area = blob.w() * blob.h() if rect_area == 0: return False solidity = blob.area() / rect_area if solidity < 0.4: return False return True while(True): img = sensor.snapshot() blobs = img.find_blobs([threshold_blue], pixels_threshold=100, area_threshold=100, merge=True) for b in blobs: if is_plate(b): img.draw_rectangle(b.rect(), color=(255, 0, 0), thickness=2) img.draw_string(b.x(), b.y()-10, "PLATE", color=(255, 0, 0))is_plate返回 False 的几种常见情况值得注意:ratio 小于 2.0 说明这是个竖条形色块,可能是蓝色垃圾桶;solidity 低说明色块内部有大量空洞或不规则,比如蓝色雨伞边缘;面积占比低则说明目标太远。这三个参数是定位的核心,调整它们比调颜色阈值更影响最终效果。
3.4 常见定位失败定位
- 蓝色阈值把你衣服也框进去了:把条件调严,增加面积过滤,只保留画面下三分之一区域
- 车牌反光导致中间断裂:把
merge打开,再加大色块合并距离,find_blobs会合并边缘距离小于 10 像素的同色块 - 误检率仍然高:把宽高比压缩到 3.0 到 3.3,很多 logo 和告示牌会因此被排除
定位完成之后,下一章做字符分割才有意义。如果定位不稳,不要急着往下走,先确认画面里红色框稳定跟随车牌。
4. 字符分割与识别:从外接矩形到逐字输出
4.1 分割前先做预处理
拿到车牌外接矩形后,要做三步预处理:旋转校正、裁剪、灰度化。openmv 的img.rotation_corr()可以修正倾斜,传入x_rotation参数,单位是度,正数逆时针旋转。但旋转会消耗大量 CPU 时间,实际毕设中如果拍摄距离固定、角度比较正,可以跳过旋转,只做裁剪。
裁剪代码模板:
# 假设上一步已经得到 b 色块对象 # 扩展 10% 边缘,避免字符贴边 x = max(0, b.x() - int(b.w()*0.1)) y = max(0, b.y() - int(b.h()*0.1)) w = min(320, b.w() + int(b.w()*0.2)) h = min(240, b.h() + int(b.h()*0.2)) # openmv 的 crop 方法返回新图像 plate_img = img.copy(roi=(x, y, w, h)) plate_img.to_grayscale() plate_img.threshold(0, 80) # 灰度值小于 80 的置为黑色,背景置为白色crop不开新画布,返回原图的引用,所以后面如果需要独立处理,必须调用copy();threshold是把灰度图二值化,这里的 0 到 80 是像素灰度范围,低于 80 的变黑,其余变白。这是字符分割的前提,二值化好坏直接决定投影分割是否干净。
4.2 投影法分割字符
二值化之后,遍历每一列统计黑色像素数量,得到一个数组。车牌字符之间有明显的间隔,黑色像素数会先高后低,形成连续峰和谷。从谷底切分就是字符分割的经典方法。
openmv 不直接提供投影函数,但用get_pixel()遍历即可,320 列逐列扫描,计算量可以接受:
def split_chars(img): chars = [] width = img.width() height = img.height() # 垂直投影 proj = [] for x in range(width): count = 0 for y in range(height): if img.get_pixel(x, y) == 0: # 黑色像素 count += 1 proj.append(count) # 找出黑色像素数大于阈值的列区域,切分 threshold_v = 5 in_char = False start = 0 for x in range(width): if proj[x] > threshold_v and not in_char: in_char = True start = x elif proj[x] <= threshold_v and in_char: if x - start > 5: # 字符最小宽度,过滤噪点 chars.append((start, x)) in_char = False return chars逻辑说明:proj[x]就是第 x 列的黑色像素总数,in_char用来标记是否处于字符区间;当连续多列的黑色像素数高于阈值,认为是一个字符的起点,低于时则切分。x - start > 5这一步很重要,如果不加,车牌上的一颗污点就会把字符区域切成几段,导致识别结果多出好几个"字符"。
4.3 模板匹配识别
字符分割完成后,把每个子区域归一化到固定大小,比如 20x32,然后和预先存在 SD 卡里的模板图片做像素级比对。openmv 的template.match()返回相似度分数,值越高越相似。
先做模板:
# 模板文件放在 SD 卡 /templates/0.pgm, 1.pgm ... 9.pgm # 以及 A.pgm, B.pgm ... Z.pgm templates = {} for name in "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ": templates[name] = image.Image("/templates/%s.pgm" % name)识别函数:
def recognize_char(img): best_score = 0 best_char = None for name, template in templates.items(): r = img.match(template, threshold=0.6) if r > best_score: best_score = r best_char = name return best_char这里的threshold=0.6是相似度阈值,从 0 到 1,高于它就认为匹配成功。实际调试时,如果经常把 8 认成 B,把 0 认成 O,要先把模板图片的字体和实际拍摄字体统一。不少车牌字体是特殊的防伪字体,与电脑里的系统字体差异很大,模板最好用真实车牌字符截图生成。
4.4 中文省份简称识别的一个替代方案
中文汉字"京、沪、粤、苏"等笔画复杂,openmv 的模板匹配效果不稳定,尤其是 20x32 的小图。常见做法是不在板端识别汉字,而是把汉字区域图片直接通过串口发送给上位机,由电脑端用 OpenCV 做模板匹配或直接让用户人工确认。
这属于合理的工程取舍,因为毕设核心是车牌检测整体流程,不是丝滑的全自动识别。方案设计时可以在 openmv 端只输出 "PLATE: 苏A12345" 里的后面的字母数字,汉字对应输出一个问号或直接跳过。
5. 部署到板端:帧率优化与串口输出验证
5.1 帧率优化核心参数
板端内存只有 1MB,如果把分辨率设在 VGA(640x480),一帧原始图像就占 614400 x 2 字节,直接报内存错误。所以部署阶段必须坚持 QVGA 甚至 QQVGA(160x120)。降低分辨率是帧率提升的第一手段。
常用优化方案如下:
sensor.set_framesize(sensor.QQVGA) # 160x120 sensor.set_pixformat(sensor.GRAYSCALE) # 灰度图,占用减半 sensor.skip_frames(time=3000)灰度图牺牲了颜色信息,但如果你确定车牌在画面中的位置固定,可以先用 RGB565 定位一次,再把整个画面切成车牌区域,后续帧只处理该区域,这样既保留了颜色定位能力,又减少扫描范围。
5.2 通过串口输出识别结果
识别结果可以直接打印到 IDE 终端,也可以发给串口。openmv 与 STM32、Arduino 通信是毕设里常见需求,使用uart模块:
from pyb import UART uart = UART(3, 115200) # 使用 UART3,波特率 115200 while(True): img = sensor.snapshot() plate_str = process_plate(img) # 假设这是定位+识别主函数 if plate_str: uart.write(plate_str.encode() + b'\r\n') print("Send:", plate_str)UART(3, 115200)第一个参数是串口编号,具体看板子串口丝印位置;波特率要与接收端一致,否则乱码。这里encode()把字符串转成字节流,\r\n是行结束符,方便接收端逐行解析。
如果要发给电脑端 Python 程序,电脑端用serial库接收:
import serial ser = serial.Serial('COM3', 115200, timeout=1) while True: line = ser.readline() if line: print(line.decode().strip())两边波特率必须一致,COM 口要换成实际识别出来的串口号。串口通信加上 CRC 校验更好,但简单毕设场景下,行协议已足够。
5.3 一个可用的验证脚本
把上面的逻辑合在一起,形成完整代码框架,方便你对照修改:
import sensor, image, time from pyb import UART, LED sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) uart = UART(3, 115200) def plate_detect(img): threshold_blue = (20, 80, 20, 70, -40, -10) blobs = img.find_blobs([threshold_blue], pixels_threshold=100, area_threshold=100, merge=True) for b in blobs: if 2.5 < b.w() / b.h() < 4.0 and b.area() > 1500: img.draw_rectangle(b.rect(), color=(255, 0, 0)) return "SUCCESS:%d,%d" % (b.cx(), b.cy()) return None while True: img = sensor.snapshot() result = plate_detect(img) if result: uart.write((result + "\r\n").encode())这个脚本只做定位和框选,字符分割识别需要自行嵌入第四章内容。实际调试时你会发现,b.area() > 1500这个值在 QVGA 画面里对应的是车牌占画面的合理比例,内置到is_plate会让代码更清晰。
5.4 部署后必须验证的三件事
- 帧率不低于 15fps,否则画面卡顿严重,串口输出延迟高
- 距离 0.5m 到 1.5m 内能稳定框出车牌,超出该范围自动不识别,不能误框
- 连续运行 30 分钟不跑飞,内存不增长,这需要观察 IDE 的实时内存占用
如果帧率不达标,优先把分辨率改为 QQVGA,其次关闭 Ide 窗口的 JPEG 压缩预览、减少膨胀腐蚀操作次数。字符识别可以放到车定位成功后,每隔 5 帧才执行一次,识别频率降低不影响演示效果,但能明显提高响应速度。
本文还有配套的精品资源,点击获取