基于Python和OpenMV的车牌识别系统:从边缘检测到上位机联调
2026/9/23 17:37:06 网站建设 项目流程

简介:这份资源面向嵌入式视觉与智能小车方向的初学者及竞赛选手,提供一套基于Python与OpenMV的车牌识别系统源码,用于解决摄像头实时采集小车车牌、记录拍照时间与车辆图片的实际问题。压缩包共12个文件,约316KB,包含2个py脚本、1个cascade级联文件与配套xml、若干jpg与png示例图片,以及README说明和license授权文件,其中cascade与xml用于Haar Cascades车牌检测,py脚本承担图像处理与识别主流程,图片则便于快速验证效果。资源描述中提到,发挥部分可存储100个以上车牌进行比对,适合作为课程设计、竞赛作品或入门视觉项目的参考骨架。目前已有1958人学习下载,读者可从中获取车牌检测与识别的完整代码结构、级联模型文件及示例素材,并据此理解车牌定位与内容识别的基本实现思路,为后续扩展识别精度与存储管理功能提供起点。

1. 从一块 OpenMV 摄像头说起:车牌识别为什么值得用 Python 重做一遍

很多人第一次接触车牌识别,是在停车场道闸或者小区门口,看到摄像头一闪,栏杆抬起,屏幕上跳出一串车牌号。真正自己动手做的时候才会发现,难点不在“识别”这两个字,而在“稳定地把车牌从画面里抠出来,再稳定地认对每一个字符”。用 OpenMV 做这件事,思路和传统 PC 端方案完全不同:它是一块跑 MicroPython 的嵌入式视觉模块,算力和内存都有限,但胜在体积小、功耗低、能直接串口输出结果,非常适合做边缘端的车牌检测与识别。

标题里的“基于 Python 和 OpenMV 车牌识别系统”,本质上是把一整套流程拆成两半:OpenMV 负责图像采集、颜色/形状定位、字符区域分割,Python 负责上位机端的二次校验、数据存储、界面展示,或者反过来,Python 端做模型训练,OpenMV 端做轻量推理。适合谁?适合已经会一点 Python 基础语法、想从“跑通一个 demo”过渡到“做一个能演示、能答辩、能接硬件的完整系统”的人。下面按理论、实现、联调、优化的顺序,把这条链路讲清楚。

2. OpenMV 端车牌定位:从 RGB 阈值到 ROI 裁剪的完整参数

2.1 为什么车牌定位优先用颜色阈值而不是深度学习

OpenMV 的算力决定了它跑不动 YOLO 这类检测网络,常见做法是利用车牌本身的颜色特征做粗定位。国内蓝底白字车牌在 RGB 空间里蓝色分量明显高于红绿,转成 LAB 或 HSV 后阈值更稳定。我一般会先用 OpenMV IDE 自带的阈值编辑器取色,再把结果写进代码。这里要注意:阈值不是固定的,白天和傍晚、室内和室外差别很大,所以代码里要留出可调参数,而不是写死一组数。

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() # 蓝色车牌的 LAB 阈值,L 控制亮度,A 负值偏绿、正值偏红,B 负值偏蓝 blue_threshold = (0, 60, -20, 40, -80, -20) while True: clock.tick() img = sensor.snapshot() blobs = img.find_blobs([blue_threshold], pixels_threshold=800, # 小于这个像素数的色块直接丢弃 area_threshold=800, merge=True) # 相邻色块合并,避免车牌被切成两半 for b in blobs: # 车牌长宽比大致在 2.5: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)) roi = img.copy(roi=b.rect()) # 裁剪出车牌区域供后续处理 print(clock.fps())

这段代码的逻辑是:先做颜色分割得到候选色块,再用长宽比和最小宽度做几何过滤,最后把 ROI 裁出来。pixels_thresholdarea_threshold是最容易踩坑的两个参数,设太小会把衣服、广告牌上的蓝色区域也框进来,设太大又会在车牌较远时漏检。实际调试时建议先把pixels_threshold降到 300 左右观察误检来源,再逐步往上加。merge=True在车牌有反光或字符把蓝色区域打断时特别有用。

2.2 字符分割:投影法在 OpenMV 上的轻量实现

拿到 ROI 之后,下一步是把车牌里的字符一个个切出来。OpenMV 上常用的是垂直投影法:把 ROI 转成灰度,做二值化,然后统计每一列的白色像素数量,字符所在列投影值高,字符之间的空隙投影值低,据此切分。这个方法比连通域分析省内存,代码也好懂。

def split_chars(roi): gray = roi.to_grayscale() # 白字蓝底,二值化后字符为白色 binary = gray.binary([(180, 255)]) w, h = binary.width(), binary.height() col_sum = [0] * w for x in range(w): for y in range(h): if binary.get_pixel(x, y): col_sum[x] += 1 # 找到投影的波谷作为切分点 chars = [] in_char = False start = 0 for x in range(w): if col_sum[x] > 2 and not in_char: in_char = True start = x elif col_sum[x] <= 2 and in_char: in_char = False if x - start > 5: # 宽度太小的段视为噪声 chars.append((start, x)) return chars

binary的阈值(180, 255)需要根据实际光照调整,太严会丢笔画,太松会把底色也算进去。col_sum[x] > 2这个判断里的 2 是经验值,目的是过滤掉单列噪声。切出来的字符段数量正常应该是 7 个左右(含中间圆点),如果明显多于或少于这个数,优先检查二值化阈值和 ROI 是否裁歪。这一步在 OpenMV 上跑 QVGA 分辨率大概能到十几帧,够用。

2.3 把识别结果通过串口发给 STM32 或上位机

OpenMV 和 STM32 通信是热词里出现频率很高的组合,常见做法是用 UART 发字符串。OpenMV 的UART类初始化时要指定波特率、TX/RX 引脚,注意电平是 3.3V,和 STM32 直连没问题,但和某些 5V 单片机连要加电平转换。

from pyb import UART uart = UART(3, 115200) # UART3,波特率 115200 def send_plate(plate_str): # 加帧头帧尾,方便接收端做协议解析 frame = "$" + plate_str + "#" uart.write(frame.encode())

接收端解析时按$#截取有效载荷即可。波特率 115200 在短距离下很稳,如果线长超过半米或者环境干扰大,降到 9600 更保险。发送频率不要太高,识别一帧发一次就够,否则接收端缓冲区容易溢出。

3. Python 上位机:串口收数、字符识别与可视化界面

3.1 用 pyserial 接收 OpenMV 数据并做协议解析

上位机第一件事是把串口数据读进来。Python 里用pyserial,安装命令是pip install pyserial。注意端口号在 Windows 上是COMx,Linux 上是/dev/ttyUSBx,代码里最好做成可配置。

import serial import re ser = serial.Serial('COM3', 115200, timeout=1) def read_plate(): buf = ser.read(64).decode('utf-8', errors='ignore') # 用正则提取 $ 和 # 之间的内容 match = re.search(r'\$(.+?)#', buf) if match: return match.group(1) return None

timeout=1保证读不到数据时不会一直阻塞。errors='ignore'是为了防止串口噪声导致解码报错。实际项目里建议把读取放在独立线程,主线程负责界面刷新,否则界面会卡。

3.2 字符识别:模板匹配和轻量 OCR 怎么选

OpenMV 端如果只做分割不做识别,字符识别就落到 Python 端。两条路:一是模板匹配,把 0-9 和各省简称的字模存成小图,逐个算相似度;二是用pytesseractcv2做 OCR。模板匹配准确率高但泛化差,换字体就废;OCR 泛化好但对图像质量要求高。我一般会先用模板匹配跑通流程,再根据实际场景决定要不要换。

import cv2 import numpy as np def match_char(char_img, templates): best, best_score = None, -1 for label, tpl in templates.items(): tpl = cv2.resize(tpl, (char_img.shape[1], char_img.shape[0])) res = cv2.matchTemplate(char_img, tpl, cv2.TM_CCOEFF_NORMED) score = res.max() if score > best_score: best, best_score = label, score return best, best_score

cv2.matchTemplateTM_CCOEFF_NORMED返回值在 -1 到 1 之间,越接近 1 越像。模板图要提前归一化到和待匹配字符一样大小,否则尺寸不一致会直接报错。如果某个字符最高分低于 0.6,建议标记为“不确定”而不是硬猜,这在演示系统里比认错更体面。

3.3 用 Tkinter 或 PyQt 搭一个能演示的界面

答辩或者演示场景下,一个能实时显示摄像头画面、识别结果和历史的界面很加分。Tkinter 是 Python 自带库,不用额外装,适合快速搭。核心是把 OpenCV 的帧转成 Tkinter 能显示的格式。

import tkinter as tk from PIL import Image, ImageTk import cv2 root = tk.Tk() label = tk.Label(root) label.pack() cap = cv2.VideoCapture(0) def update(): ret, frame = cap.read() if ret: img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(img) imgtk = ImageTk.PhotoImage(image=img) label.imgtk = imgtk label.configure(image=imgtk) root.after(30, update) update() root.mainloop()

root.after(30, update)相当于每 30 毫秒刷新一次,约 33 帧。label.imgtk = imgtk这行不能省,否则图像对象会被垃圾回收导致界面空白。如果要做更复杂的布局和控件,PyQt 更合适,但学习成本高一些,看项目时间决定。

4. 联调与排错:OpenMV 和 Python 端最容易翻车的几个点

4.1 串口收不到数据时的排查顺序

联调阶段最常见的问题是上位机读不到任何东西。排查顺序建议固定下来:先确认 OpenMV 的 TX/RX 有没有接反,TX 接对方 RX,RX 接对方 TX,这是新手最容易搞错的地方;再用 OpenMV IDE 的串口终端看有没有数据发出;然后确认波特率两边一致;最后检查上位机端口号是否选对。Windows 下可以在设备管理器里看端口,Linux 下用ls /dev/ttyUSB*。如果这些都对了还是没数据,把发送端改成固定字符串循环发,排除识别逻辑本身的问题。

4.2 识别率上不去的三类原因和对应调法

识别率低通常不是单一原因。第一类是光照,逆光或夜间补光不足会让颜色阈值完全失效,解决办法是加补光灯或者改用灰度+边缘检测做定位。第二类是角度,车牌倾斜超过 15 度时投影法切分就会乱,可以在 ROI 裁剪后先做透视校正。第三类是运动模糊,车速快或者摄像头抖动会导致字符糊成一团,这时候要么提高快门,要么在算法端做多帧投票取众数。

现象可能原因调整方向
色块框到非车牌区域阈值过宽收紧 LAB 的 B 通道下限
车牌被切成两段反光打断色块开启 merge 并调大 margin
字符切分数量不对二值化阈值不合适在 150-200 之间微调
串口数据乱码波特率不一致两端统一为 115200 或 9600
界面卡顿读取和刷新同线程串口读取放独立线程

4.3 用日志和截图把偶发问题固定下来

偶发问题最难查,因为复现不了。我的习惯是在 OpenMV 端把每次识别的 ROI 截图存到 SD 卡,上位机端把每次收到的原始串口数据写进日志文件。这样出问题时可以回看当时到底拍到了什么、发了什么。OpenMV 存图用img.save("/sd/roi.jpg"),注意要先挂载 SD 卡。上位机写日志用 Python 标准库logging,设置level=DEBUG,把时间戳和原始字节都记下来。这套组合能解决大部分“有时候对有时候不对”的问题。

5. 进阶技巧:让车牌识别系统在 OpenMV 上跑得更稳的几个细节

第一个细节是分辨率取舍。QVGA(320x240)在车牌占画面三分之一以上时足够,如果车牌较远,可以临时切到 VGA 再切回来,但频繁切换会掉帧,建议根据实际安装距离固定一个分辨率。第二个细节是曝光控制。OpenMV 默认自动曝光在光线变化时会来回调整,导致阈值失效,可以在初始化后手动锁定曝光:sensor.set_auto_exposure(False, exposure_us=10000),具体数值根据现场亮度试出来。第三个细节是字符识别的置信度过滤。不管是模板匹配还是 OCR,都设一个最低分阈值,低于阈值就输出“未识别”,而不是输出一个错误车牌,这在真实系统里比多认几个更重要。

第四个细节是 Python 端的缓存去重。同一辆车连续多帧识别结果一样时,没必要每帧都刷新界面和写数据库,可以做一个简单的滑动窗口,连续 3 帧结果一致才确认输出。代码很短但效果明显:

from collections import deque history = deque(maxlen=3) def confirm(plate): history.append(plate) if len(history) == 3 and len(set(history)) == 1: return history[0] return None

deque(maxlen=3)自动只保留最近 3 个结果,set(history)长度为 1 说明三帧完全一致。这个逻辑能过滤掉大部分单帧误识别,代价是输出延迟 2 帧,在 30 帧的流程里几乎感觉不到。最后,如果项目需要长期运行,记得在 OpenMV 端加看门狗或者定时重启逻辑,嵌入式设备跑几天后内存碎片化是常态,主动重启比崩溃后手动断电体面得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询