基于Python和OpenMV的车牌识别系统:从边缘检测到上位机联调

发布时间:2026/9/23 17:37:12
基于Python和OpenMV的车牌识别系统:从边缘检测到上位机联调 简介这份资源面向嵌入式视觉与智能小车方向的初学者及竞赛选手提供一套基于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(time2000) # 等摄像头自动增益稳定 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_threshold800, # 小于这个像素数的色块直接丢弃 area_threshold800, mergeTrue) # 相邻色块合并避免车牌被切成两半 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(roib.rect()) # 裁剪出车牌区域供后续处理 print(clock.fps())这段代码的逻辑是先做颜色分割得到候选色块再用长宽比和最小宽度做几何过滤最后把 ROI 裁出来。pixels_threshold和area_threshold是最容易踩坑的两个参数设太小会把衣服、广告牌上的蓝色区域也框进来设太大又会在车牌较远时漏检。实际调试时建议先把pixels_threshold降到 300 左右观察误检来源再逐步往上加。mergeTrue在车牌有反光或字符把蓝色区域打断时特别有用。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 charsbinary的阈值(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 上是COMxLinux 上是/dev/ttyUSBx代码里最好做成可配置。import serial import re ser serial.Serial(COM3, 115200, timeout1) def read_plate(): buf ser.read(64).decode(utf-8, errorsignore) # 用正则提取 $ 和 # 之间的内容 match re.search(r\$(.?)#, buf) if match: return match.group(1) return Nonetimeout1保证读不到数据时不会一直阻塞。errorsignore是为了防止串口噪声导致解码报错。实际项目里建议把读取放在独立线程主线程负责界面刷新否则界面会卡。3.2 字符识别模板匹配和轻量 OCR 怎么选OpenMV 端如果只做分割不做识别字符识别就落到 Python 端。两条路一是模板匹配把 0-9 和各省简称的字模存成小图逐个算相似度二是用pytesseract或cv2做 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_scorecv2.matchTemplate的TM_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(imageimg) label.imgtk imgtk label.configure(imageimgtk) 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 接对方 RXRX 接对方 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设置levelDEBUG把时间戳和原始字节都记下来。这套组合能解决大部分“有时候对有时候不对”的问题。5. 进阶技巧让车牌识别系统在 OpenMV 上跑得更稳的几个细节第一个细节是分辨率取舍。QVGA320x240在车牌占画面三分之一以上时足够如果车牌较远可以临时切到 VGA 再切回来但频繁切换会掉帧建议根据实际安装距离固定一个分辨率。第二个细节是曝光控制。OpenMV 默认自动曝光在光线变化时会来回调整导致阈值失效可以在初始化后手动锁定曝光sensor.set_auto_exposure(False, exposure_us10000)具体数值根据现场亮度试出来。第三个细节是字符识别的置信度过滤。不管是模板匹配还是 OCR都设一个最低分阈值低于阈值就输出“未识别”而不是输出一个错误车牌这在真实系统里比多认几个更重要。第四个细节是 Python 端的缓存去重。同一辆车连续多帧识别结果一样时没必要每帧都刷新界面和写数据库可以做一个简单的滑动窗口连续 3 帧结果一致才确认输出。代码很短但效果明显from collections import deque history deque(maxlen3) def confirm(plate): history.append(plate) if len(history) 3 and len(set(history)) 1: return history[0] return Nonedeque(maxlen3)自动只保留最近 3 个结果set(history)长度为 1 说明三帧完全一致。这个逻辑能过滤掉大部分单帧误识别代价是输出延迟 2 帧在 30 帧的流程里几乎感觉不到。最后如果项目需要长期运行记得在 OpenMV 端加看门狗或者定时重启逻辑嵌入式设备跑几天后内存碎片化是常态主动重启比崩溃后手动断电体面得多。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询