简介:这份资源面向嵌入式视觉与智能小车方向的初学者及竞赛选手,提供一套基于Python与OpenMV的车牌识别系统源码,用于解决摄像头实时采集小车车牌、记录拍照时间与车辆图片等需求。压缩包共12个文件,约316KB,包含2个py脚本、1个cascade级联文件与配套xml、若干jpg与png示例图,以及README说明和license,其中Haar Cascades负责车牌检测,脚本承担图像处理与数据存储逻辑。资源描述提到发挥部分可存储100个以上车牌进行比对,适合作为课程设计、电子竞赛或毕业设计的起步参考。目前已有1958人学习下载,读者可据此理解车牌定位与识别的基本流程,参考级联模型与Python代码的配合方式,并在此基础上扩展识别精度与存储管理功能。
1. 从一块 OpenMV 摄像头说起:车牌识别为什么值得自己搭一套
很多人第一次接触车牌识别,是从停车场闸机、小区门禁或者高速收费站开始的。商业方案一套动辄几千上万,而实际上,一块 OpenMV 摄像头加一块单片机,再配上一台跑 Python 的电脑,就能把「拍图—定位车牌—分割字符—识别字符」这条链路完整跑通。这正是「基于 Python 和 OpenMV 车牌识别系统」这个标题真正指向的东西:OpenMV 负责在边缘端做图像采集和轻量预处理,Python 负责在主机端做算法验证、字符识别和界面展示,两者通过串口通信串起来。
它适合三类人:一是想入门嵌入式视觉的学生,二是需要做课程设计或毕业设计的开发者,三是想给现有硬件加一个低成本识别模块的工程师。整套方案的核心难点不在「识别」本身,而在图像质量、车牌定位的鲁棒性,以及 OpenMV 与主机之间的数据协议设计。下面按理论、实现、联调、优化的顺序,把这条链路拆开讲清楚。
2. OpenMV 端图像采集与车牌定位的 Python 实现
2.1 OpenMV 固件与开发环境准备
OpenMV 本质上是一块跑 MicroPython 的摄像头模块,常见型号是 H7、H7 Plus、M7。它自带 IDE,也可以配合 VSCode 使用。刷固件是第一步,常见做法是通过 OpenMV IDE 的「工具—运行引导加载程序」完成,选对应型号的固件文件即可。刷完后在 IDE 里连上设备,能看到帧缓冲画面就说明环境正常。
如果你习惯用 VSCode,需要装 OpenMV 官方扩展,然后在设置里指定 OpenMV IDE 的安装路径,扩展会调用底层的调试工具。这一步容易踩的坑是串口被占用:OpenMV IDE 和 VSCode 扩展不能同时连接同一块板子,否则会报「设备忙」。另外,OpenMV 的 MicroPython 和桌面 Python 语法接近但不完全一致,cv2、numpy这类库在 OpenMV 上是没有的,图像处理要用它自带的sensor、image模块。
# OpenMV 端:初始化摄像头并抓取一帧 import sensor, image, time sensor.reset() # 复位摄像头 sensor.set_pixformat(sensor.RGB565) # 彩色格式,便于后续颜色阈值分割 sensor.set_framesize(sensor.QVGA) # 320x240,兼顾速度和精度 sensor.skip_frames(time=2000) # 等待感光元件稳定 clock = time.clock() while True: clock.tick() img = sensor.snapshot() # 抓取一帧 print(clock.fps()) # 打印帧率,评估实时性这段代码是所有后续处理的基础。set_pixformat选 RGB565 是因为车牌定位常用颜色阈值,灰度图会丢掉颜色信息;set_framesize选 QVGA 是速度和分辨率的折中,如果车牌在画面里占比很小,可以升到 VGA,但帧率会明显下降。skip_frames不能省,否则前几帧白平衡没稳定,颜色阈值会失效。
2.2 用颜色阈值和矩形检测定位车牌区域
车牌定位的常见做法有两类:一类是基于颜色的阈值分割,蓝色车牌找蓝色区域,绿色新能源车牌找绿色区域;另一类是基于边缘和形态学的矩形检测。OpenMV 上更实用的是颜色阈值加find_blobs,因为它的算力有限,跑复杂边缘检测会掉帧。
# 蓝色车牌阈值:(L_min, L_max, A_min, A_max, B_min, B_max) blue_threshold = (0, 100, -20, 40, -60, -10) while True: img = sensor.snapshot() blobs = img.find_blobs([blue_threshold], pixels_threshold=200, # 最小像素数,过滤噪点 area_threshold=200, # 最小面积 merge=True) # 合并相邻色块 for b in blobs: # 车牌长宽比大致在 2:1 到 5:1 之间 ratio = b.w() / b.h() if 2.0 < ratio < 5.5 and b.w() > 60: img.draw_rectangle(b.rect(), color=(255, 0, 0)) img.draw_cross(b.cx(), b.cy())find_blobs返回的是色块对象,pixels_threshold和area_threshold用来过滤小噪点,merge=True会把挨得近的色块合并成一个,这对车牌这种有字符间隔的区域很关键。长宽比过滤是核心:车牌本身是扁矩形,如果检测到的色块接近正方形,多半是误检。实际调试时,阈值不是拍脑袋定的,要在 IDE 的「帧缓冲区」里用阈值编辑器实时拖滑块,看着画面里车牌被框住为止。
提示:不同光照下蓝色阈值差别很大,室内暖光和室外阴天的 A、B 分量能差 20 以上。稳妥做法是存两套阈值,根据画面平均亮度切换。
2.3 把车牌区域裁剪出来并通过串口发送
定位到车牌后,OpenMV 要做的是把这块区域裁剪出来,压缩后通过串口发给主机。直接发原始图像会太慢,常见做法是发灰度图或者二值化后的图,分辨率压到 100x30 左右,足够主机端做字符分割。
import sensor, image, time from pyb import UART uart = UART(3, 115200) # 使用 UART3,波特率 115200 while True: img = sensor.snapshot() blobs = img.find_blobs([blue_threshold], pixels_threshold=200, merge=True) for b in blobs: ratio = b.w() / b.h() if 2.0 < ratio < 5.5 and b.w() > 60: plate = img.copy(roi=b.rect()) # 裁剪车牌区域 plate = plate.resize(100, 30) # 统一尺寸 gray = plate.to_grayscale() # 转灰度,减少数据量 uart.write(gray.compress(quality=70)) # 压缩后发送 breakroi参数是 region of interest,即感兴趣区域,copy会返回一个新的图像对象。resize到固定尺寸是为了让主机端不用再处理缩放。compress用的是 JPEG 压缩,quality 越低数据越小但细节损失越大,70 是个常用起点。串口波特率 115200 是 OpenMV 的常规上限,再高容易丢包。发送前最好加一个帧头,比如先发两个字节0xAA 0xBB,主机端靠帧头对齐数据,否则串口流里很难判断一帧从哪开始。
3. 主机端 Python 车牌字符分割与识别
3.1 串口接收与图像还原
主机端用 Python 的pyserial接收数据,用numpy和cv2还原图像。这里的关键是协议解析:OpenMV 发的是压缩后的 JPEG 字节流,主机端要先按帧头切分,再解码。
import serial import numpy as np import cv2 ser = serial.Serial('COM3', 115200, timeout=1) # Windows 下是 COMx,Linux 下是 /dev/ttyUSBx buffer = bytearray() def read_frame(): global buffer while True: data = ser.read(1024) if data: buffer.extend(data) # 查找帧头 0xAA 0xBB idx = buffer.find(b'\xAA\xBB') if idx == -1: continue # 帧尾用 0xCC 0xDD 标记 end = buffer.find(b'\xCC\xDD', idx) if end == -1: continue payload = buffer[idx+2:end] buffer = buffer[end+2:] arr = np.frombuffer(payload, dtype=np.uint8) img = cv2.imdecode(arr, cv2.IMREAD_GRAYSCALE) return imgpyserial的read是阻塞读,timeout=1保证没数据时不会卡死。buffer累积数据是因为串口一次读到的可能只是半帧。帧头帧尾的设计让解析变得确定:找到0xAA 0xBB就是帧开始,找到0xCC 0xDD就是帧结束。cv2.imdecode直接把字节流解码成灰度图,省去写临时文件。如果解码返回None,说明这帧数据不完整,直接丢弃重来。
3.2 用 OpenCV 做字符分割
拿到灰度车牌图后,字符分割是识别的前提。常见流程是:二值化、去噪、垂直投影、按投影谷值切分。
def split_chars(gray): # 自适应二值化,应对光照不均 binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 8) # 开运算去噪 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 垂直投影:统计每列白色像素数 projection = np.sum(binary, axis=0) / 255 # 找字符边界 chars = [] in_char = False start = 0 for i, v in enumerate(projection): if v > 2 and not in_char: in_char = True start = i elif v <= 2 and in_char: in_char = False if i - start > 5: # 过滤太窄的噪声 chars.append(binary[:, start:i]) return charsadaptiveThreshold比全局阈值更适合车牌,因为车牌常有反光和阴影。THRESH_BINARY_INV让字符变成白色前景,方便投影统计。projection是每列白点数量,字符所在列白点多,字符间隙白点少,谷值就是切分点。阈值v > 2是经验值,太小会把噪点当字符,太大会把细笔画字符漏掉。切出来的字符宽度要过滤,太窄的肯定是噪声。这套方法对标准蓝牌效果好,对倾斜或污损车牌需要先做透视校正。
3.3 字符识别:模板匹配与轻量 CNN 的取舍
字符识别有两条路:模板匹配和深度学习。模板匹配实现简单,把 0-9 和 A-Z 做成模板,逐个算相似度,适合字符规整、字体固定的场景。缺点是换字体或光照变化就崩。轻量 CNN 比如 LeNet 改版,准确率高很多,但需要训练数据和推理框架。
| 方案 | 准确率 | 依赖 | 适用场景 |
|---|---|---|---|
| 模板匹配 | 70%-85% | 无 | 课程设计、固定字体 |
| 轻量 CNN | 92%-98% | 训练数据 | 实际部署、多字体 |
| 第三方 OCR | 95%+ | 网络/大模型 | 快速验证 |
模板匹配的核心代码就是cv2.matchTemplate,把每个字符图和模板库逐个比对,取最高分。轻量 CNN 可以用 PyTorch 训练一个 3 层卷积的小网络,输入 20x40 灰度图,输出 34 类(10 数字加 24 字母,去掉 I 和 O 避免混淆)。训练数据可以自己用 OpenMV 采集,也可以找公开的车牌数据集。实际项目里,如果只是演示,模板匹配够用;如果要落地,CNN 是必须的。
注意:车牌里字母 I 和 O 通常不会出现,因为容易和 1、0 混淆。做分类时把这两类去掉,能减少误识别。
4. OpenMV 与 STM32 通信及系统联调
4.1 串口协议设计与 STM32 端接收
OpenMV 和 STM32 通信是这套系统里最容易出问题的环节。常见做法是 OpenMV 做识别,把结果字符串通过串口发给 STM32,STM32 负责控制舵机抬杆或者驱动显示屏。协议要简单明确,比如每帧固定 8 字节:帧头 2 字节、车牌字符 6 字节、校验 1 字节。
// STM32 端串口接收中断示例(HAL 库) uint8_t rx_byte; uint8_t frame[9]; uint8_t idx = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (idx == 0 && rx_byte != 0xAA) return; // 等帧头 frame[idx++] = rx_byte; if (idx >= 9) { if (frame[1] == 0xBB) { // 校验帧头 // frame[2..7] 是车牌字符,frame[8] 是校验和 process_plate(frame + 2); } idx = 0; } HAL_UART_Receive_IT(huart, &rx_byte, 1); // 重新开启接收 }STM32 端用中断接收,每来一个字节进一次回调。idx是状态机的核心,只有收到0xAA才开始记录,收到完整 9 字节后校验帧头再处理。HAL_UART_Receive_IT必须在回调末尾重新调用,否则只能收一次。校验和可以用简单的异或,也可以加 CRC8,看对可靠性要求。波特率两边必须一致,OpenMV 设 115200,STM32 也要设 115200,差一点都收不到正确数据。
4.2 联调时帧率与延迟的实测方法
联调阶段最常遇到的是「识别到了但延迟大」或者「偶尔丢帧」。测延迟的方法是在 OpenMV 端翻转一个 GPIO,主机端收到数据时也翻转一个 GPIO,用示波器看两个上升沿的时间差。没有示波器的话,可以在 OpenMV 发送前打印时间戳,主机端收到后打印时间戳,两者相减就是端到端延迟。
帧率方面,OpenMV 在 QVGA 下跑find_blobs大概能到 20-30 FPS,加上串口发送会降到 10-15 FPS。如果帧率不够,优先降分辨率到 QQVGA(160x120),或者缩小find_blobs的搜索区域,只在画面下半部分找车牌。丢帧多半是串口缓冲区溢出,主机端读取速度跟不上,解决办法是提高波特率到 921600,或者降低发送频率,比如每 3 帧发一次。
4.3 常见误检与漏检的排查清单
误检和漏检是车牌识别绕不开的问题。排查时按这个顺序看:先看 OpenMV 的帧缓冲画面,确认车牌有没有被框住;再看阈值是不是合适,光照变了阈值要跟着调;然后看长宽比过滤是不是太严,把真车牌滤掉了;最后看串口数据有没有完整到达主机。
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 框住非车牌区域 | 颜色阈值过宽 | 收紧 A、B 分量范围 |
| 车牌框不住 | 阈值过窄或光照暗 | 放宽阈值或补光 |
| 字符切分错位 | 二值化噪声多 | 加开运算或调阈值 |
| 串口收不到数据 | 波特率不一致 | 两端统一 115200 |
| 识别结果乱码 | 帧头对齐错 | 检查帧头帧尾逻辑 |
这张表基本覆盖了 80% 的现场问题。实际调试时,建议在 OpenMV 端加一个 LED 指示,检测到车牌就亮,这样不用连电脑也能判断定位有没有成功。
5. 提升识别率的几个实用技巧
5.1 用透视校正解决倾斜车牌
OpenMV 装在侧面时,拍到的车牌是梯形的,直接分割字符会错位。解决办法是找车牌的四个角点,做透视变换。OpenMV 的find_blobs返回的min_corners()能给出色块的最小外接四边形角点,用img.warp或者主机端cv2.getPerspectiveTransform校正。
# 主机端透视校正 def correct_perspective(img, corners): # corners 是四个角点,顺序:左上、右上、右下、左下 src = np.float32(corners) dst = np.float32([[0, 0], [100, 0], [100, 30], [0, 30]]) M = cv2.getPerspectiveTransform(src, dst) return cv2.warpPerspective(img, M, (100, 30))getPerspectiveTransform需要源点和目标点一一对应,顺序错了图会翻转。目标尺寸设成 100x30 是为了和前面的分割流程对齐。校正后字符变成竖直的,投影分割的准确率能提升 20% 以上。
5.2 多帧投票降低偶发误识别
单帧识别难免有偶发错误,比如把 8 认成 B。多帧投票的思路是连续识别 5 帧,取出现次数最多的结果作为最终输出。实现上用一个字典统计每个字符的出现频率,超过 3 次才确认。
from collections import Counter def vote(results): # results 是最近 N 帧的识别结果列表 counter = Counter(results) char, count = counter.most_common(1)[0] if count >= 3: return char return None # 票数不够,继续等Counter的most_common(1)直接给出最高频项。阈值 3 是经验值,帧率 10 FPS 时大概 0.3 秒出结果,对闸机场景够用。如果要求更快,可以降到 2,但误识别率会上升。这个技巧对固定摄像头场景特别有效,因为车牌在画面里的位置和角度基本不变,多帧结果高度一致。
5.3 把识别结果落到可视化界面
最后一步是把结果展示出来。用 Python 的tkinter或者PyQt做一个简单界面,左边显示 OpenMV 传来的车牌图,右边显示识别结果和历史记录。tkinter自带,不用装额外库,适合快速验证。
import tkinter as tk from PIL import Image, ImageTk root = tk.Tk() root.title("车牌识别系统") label_img = tk.Label(root) label_img.pack() label_text = tk.Label(root, text="等待识别...", font=("Arial", 20)) label_text.pack() def update(img, text): im = Image.fromarray(img) photo = ImageTk.PhotoImage(im) label_img.configure(image=photo) label_img.image = photo # 必须保留引用,否则图不显示 label_text.configure(text=text) root.mainloop()ImageTk.PhotoImage必须保留引用,否则会被垃圾回收导致界面空白,这是tkinter的经典坑。update函数在主循环里被调用,传入最新的车牌图和识别文本。界面不用做太复杂,能实时看到图和结果就够了。如果要加历史记录,用一个Listbox追加就行。整套系统跑通后,从 OpenMV 采集到界面显示,端到端延迟能控制在 200 毫秒以内,对大多数门禁场景足够。
本文还有配套的精品资源,点击获取