news 2026/9/28 20:31:29

OpenMV+STM32+YOLO11+PaddleOCR车牌识别系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenMV+STM32+YOLO11+PaddleOCR车牌识别系统实战

去年底帮一个做毕业设计的学弟救急,他买的OpenMV板子跑不动现成的YOLO车牌识别模型,识别一帧要卡好几秒,车都快开出画面了,车牌还没认出来。我重新给他拆了方案:OpenMV只负责图像采集和前端触发,STM32做控制中枢,真正的车牌检测和字符识别全部放到PC上处理,用YOLO11(社区里常叫YOLOv11)检测车牌区域,再用PaddleOCR读字符,识别结果通过串口回传给STM32控制道闸动作。这套方案实测小场景下单帧识别稳定在300ms以内,做毕设或者入门嵌入式AI完全够用。这篇文章就把整套环境配置、代码联调和踩过的坑完整写下来,适合正在做OpenMV+STM32项目、或者想把深度学习模型塞进嵌入式流程里的朋友直接抄作业。

1. 系统整体设计与方案选型

1.1 为什么不让OpenMV直接跑YOLO

先说一个很多人刚入坑时的误解:OpenMV看起来是个带摄像头的“AI开发板”,官方也放了一些人脸检测、数字识别的示例,但它的主控本质上是单片机级别的算力。拿OpenMV Cam H7 Plus来说,用的是STM32H7系列芯片,虽然有双核,但跑一个轻量级神经网络做分类还可以,真要跑YOLO系列的目标检测,每帧要几百毫秒到几秒,而且内存经常溢出。车牌识别又是个级联任务——先检测车牌在哪,再识别里面的字符,两个模型叠在一起,在OpenMV上根本跑不动。

所以我在设计时明确了三段式分工:OpenMV只做“眼睛”,负责采集图像、按需压缩、发送图像;STM32当“手和脚”,负责接收数据、控制道闸舵机、OLED显示、协调整个流程;PC才是“大脑”,运行YOLO11检测车牌位置,再用PaddleOCR识别车牌号码,最后把结果通过串口回传给STM32。这套方案把实时性要求不高的识别计算放到PC上,嵌入式端只做轻量任务,性能和开发成本都能兼顾。

1.2 数据链路与串口帧协议设计

整个系统的数据流是这样的:OpenMV上电后持续采集图像,当检测到触发信号(比如按键、定时器或外部传感器)时,将当前帧缩放到合适尺寸并编码为JPEG,通过UART发送给STM32;STM32收到图像数据后,一方面在OLED上显示状态,另一方面将数据通过另一个串口通道原样透传给PC;PC端Python程序收到图像后,先跑YOLO11检测车牌区域,把车牌区域裁剪出来,再喂给PaddleOCR识别,最终把识别出的车牌号和置信度通过串口回传给STM32;STM32收到结果后控制舵机模拟道闸抬起,并在OLED上显示车牌号。

这里最关键的链路是OpenMV到STM32的通信。直接用裸图像数据的话,一张320x240的RGB565图像就有150KB左右,OpenMV和STM32之间的串口一般用921600波特率,传一张图就要好几秒,体验极差。所以我在OpenMV端把图像压缩成JPEG格式,同样分辨率下通常只有10~20KB,速度能快一个数量级。实测在115200波特率下传一张压缩图需要1秒多,921600波特率下能压到200ms以内,基本可接受。

通信帧格式我用的是自定义的简易协议:帧头固定为0xAA 0x55,接着是1字节数据类型(0x01表示图像数据,0x02表示识别结果,0x03表示心跳包),然后是2字节数据长度(小端模式,低字节在前),最后是数据内容,尾部跟1字节累加和校验。累加和就是从数据类型开始到数据内容最后一个字节所有字节的和,只取低8位。这个协议虽然简单,但足以应对本项目的数据传输,而且用状态机解析也非常方便。

2. 软硬件准备与开发环境搭建

2.1 硬件清单与选购避坑

硬件清单我直接列一下,都是比较常见的型号,新手照着买不会出错:

  • OpenMV Cam H7 Plus:建议买带SD卡槽和RGB LED的版本,后期调试会方便很多。原厂价格偏高,国产兼容版也能用,但要注意看固件版本和OpenMV IDE是否兼容,我遇到过某个兼容版刷了原厂固件后USB握手不稳定,最后换了线才好。
  • STM32开发板:我用的是STM32F103C8T6最小系统板,蓝色圆片那种,便宜且资料多。如果后续要跑更复杂的控制逻辑或者接更多外设,可以上F407,价格贵一点但性能余量大。注意F103的串口要选USART1或USART2,这两个引脚引出比较方便。
  • USB转TTL模块:CH340或者CP2102都可以,用于STM32和PC之间的串口通信。注意有些模块供电能力不足,如果STM32板上还有其他传感器,建议单独用USB供电。
  • 舵机:SG90就行,用来模拟道闸的抬起和落下,接线简单,PWM控制周期为50Hz,0.5ms~2.5ms脉宽对应0°~180°。
  • OLED显示屏:0.96寸I2C接口的SSD1306即可,用来显示识别结果,调试时能看到信息流转到哪一步。
  • 其他:杜邦线若干、面包板、一个按键(用于触发拍照识别),有条件可以加一个HC-SR501人体红外传感器做自动触发。

选购时最该注意的是供电。OpenMV和STM32刚上电时瞬间电流不小,尤其是OpenMV给传感器供电那一下,如果用电脑USB直接供电,可能会出现电压跌落导致复位,我的经验是准备一个输出能力在1A以上的5V适配器,通过面包板统一供电,GND一定要共地,否则串口通信会乱码。

2.2 OpenMV端:IDE与固件烧录

OpenMV的开发环境是OpenMV IDE,到官网下载对应系统的版本安装即可。连接时用一根数据线把OpenMV接到电脑上,IDE会自动识别端口,然后点击左下角的连接图标,绿色说明连接正常。第一次连接可能会提示固件版本过旧,这时候需要烧录新版固件:先下载对应板型的固件文件(.dfu格式),然后在IDE里点击“工具→运行Bootloader”,OpenMV会进入固件升级模式,选择固件文件并烧录。

这里有几个坑必须要提:第一,烧录固件时千万不要断电,OpenMV的Bootloader一旦刷到一半断电是有可能变砖的,虽然可以用串口救回来,但折腾起来很麻烦。第二,如果IDE识别不到OpenMV,先检查数据线是不是只有充电功能的数据线,换一根能传数据的线再试。第三,如果是兼容版板子,建议先找商家要对应的固件版本,不要盲目刷原厂最新版,否则可能出现摄像头驱动不匹配、画面黑屏的问题。

OpenMV和STM32通信这块,OpenMV端只需要初始化UART,然后把JPEG图像数据按帧协议发出去。要注意OpenMV的UART默认使用的是P4(TX)和P5(RX)引脚,对应STM32的USART1或USART2,接线时记得TX接RX、RX接TX。

2.3 STM32端:Keil5芯片包、ST-Link与串口驱动

STM32端的开发环境我用的Keil MDK5,这也是大多数人最常用的。安装Keil后,如果新建工程时找不到STM32F103C8,说明芯片包还没装。现在Keil MDK5默认从包管理器安装芯片支持包,打开Pack Installer,在搜索框输入STM32F1,找到Keil::STM32F1xx_DFP那个包安装即可。如果网络不稳定装不上,可以直接去Keil官网下载离线包,双击安装后重新打开Keil就能识别到了。

然后是ST-Link驱动。如果你用的是ST-Link V2仿真器,插上电脑后如果设备管理器里出现黄色的感叹号,说明驱动没装。去ST官网下载STM32 ST-LINK Utility或者STSW-LINK009驱动包,安装后重新插拔,设备管理器里能识别出ST-Link就正常了。如果你用USB转TTL模块下载程序,那需要的是串口驱动,CH340就装CH340驱动,CP2102就装CP2102驱动,装好后在设备管理器里能看到对应COM口。

调试时还经常遇到一个情况:ST-Link能识别到,但Keil下载时报“No target connected”或者“RDDI-DAP Error”。这个大部分时候是因为板子上的BOOT0跳线或者复位电路没设计好。F103最小系统板一般有BOOT0和BOOT1跳线,正常烧录程序要把BOOT0跳线拨到0(低电平)。另外用ST-Link连接时,SWDIO、SWCLK、GND三根线必须接好,最好再补一根3.3V给仿真器做参考电平,减少烧录失败的概率。

2.4 上位机Python环境:Torch、YOLO11与PaddleOCR GPU版

PC端是整个识别系统的核心计算平台,我强烈建议用NVIDIA显卡跑GPU加速。我的环境配置如下:

  • Windows 11 / Ubuntu 22.04都可以,我这次用的是Windows 11。
  • Python 3.10或3.11,建议用Anaconda建一个独立环境,避免和系统Python冲突。
  • NVIDIA驱动版本不低于535,CUDA 12.4,cuDNN 8.9。
  • PyTorch 2.3+,从PyTorch官网选择对应的安装命令。
  • Ultralytics库(统一提供YOLO11支持)。
  • PaddlePaddle GPU版 + PaddleOCR 3.x。

创建虚拟环境的命令:

conda create -n plate python=3.10 conda activate plate pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 pip install ultralytics

PyTorch装好之后,先验证一下CUDA是否可用:

import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

输出True说明GPU正常。接下来装PaddlePaddle,这个很容易踩坑。PaddlePaddle的CUDA版本要和PyTorch的CUDA版本匹配,我用的是CUDA 12.4,所以安装命令是:

pip install paddlepaddle-gpu==3.0.0

注意PaddlePaddle 3.x版本对Python版本有要求,Python 3.10没问题,但如果用Python 3.12,可能会遇到没有对应预编译包的问题,装起来会比较痛苦。装完后跑一句python -c "import paddle; paddle.utils.run_check()"验证是否安装成功。

最后安装PaddleOCR 3.x:

pip install paddleocr

PaddleOCR 3.x和之前的2.x版本API有一些变化,不推荐再用老文章的ocr = PaddleOCR(use_angle_cls=True)这种写法,3.x版本直接初始化PaddleOCR实例然后调用predict或者ocr方法即可。我会在后面的章节给出当前版本可用的代码。

3. OpenMV采集端与STM32通信实现

3.1 OpenMV的JPEG图像压缩与发送

OpenMV采集图像的代码很简单,核心在于压缩比的控制和发送时机。默认的sensor.snapshot()返回的是RGB565图像,内存占用大,我使用img.to_jpeg(quality=80)把图像编码成JPEG字节流,质量因子80在清晰度和体积之间比较平衡。实验下来,把分辨率设为QVGA(320x240)就够了,车牌在画面里占一定面积,太高分辨率只会增加传输压力,识别精度提升有限。

触发方式我做了两种:一种是用按键,连一个引脚到OpenMV上,检测到低电平就拍一张;另一种是定时触发,每2秒自动采集一帧。实际测试时建议用按键,不然串口会一直被图像数据占满,不方便看日志。

OpenMV端发送代码的核心逻辑是这样的:

import sensor import ustruct import time from machine import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_vflip(True) sensor.set_hmirror(True) sensor.skip_frames(time=2000) uart = UART(3, 921600, timeout_char=2000) # 注意引脚下对应 uart.init(921600, bits=8, parity=None, stop=1) KEY_PIN = Pin("P0", Pin.IN, Pin.PULL_UP) def checksum(data): return sum(data) & 0xFF while True: if KEY_PIN.value() == 0: # 按键按下 img = sensor.snapshot() jpeg = img.to_jpeg(quality=80) data_len = len(jpeg) header = bytes([0xAA, 0x55, 0x01, data_len & 0xFF, (data_len >> 8) & 0xFF]) body = jpeg cks = checksum(body) frame = header + body + bytes([cks]) uart.write(frame) time.sleep_ms(500) # 防抖

注意OpenMV的UART构造函数参数,不同的板子引脚对应的UART编号不一样,H7 Plus如果不确定就把UART(3)换成UART(1)之类的试一下,或者查一下板子的引脚图。这里我用了921600波特率,如果PC端STM32透传过来不稳定,可以降到460800。

还有一个细节:uart.write()是一次性把整个字节串写出去,但OpenMV内部发送缓冲区不是无限大的,如果图像数据超过缓冲区,写操作可能会超时或者丢弃部分数据。稳妥的做法是分片写入:每512字节一个包,包之间加一个极小的延时。我在测试中直接整段写给STM32也没出问题,但如果你发现图像传输到PC端总是解不出完整JPEG,优先考虑分片发送。

3.2 STM32串口接收与数据帧解析

STM32这边的任务是接收OpenMV发来的JPEG帧,解析校验,再转发给PC。我用的是STM32F103C8T6的USART1(PA9/PA10)接OpenMV,USART2(PA2/PA3)接USB转TTL模块,两个串口波特率都设为921600。如果波特率太高导致数据错乱,可以两个串口都用115200,只是传输会慢一些。

接收端我用了串口中断逐字节接收,状态机解析帧头、长度、数据和校验。虽然DMA+空闲中断效率更高,但对于一帧10~20KB的数据,逐字节中断在921600波特率下CPU占用率很高,容易影响其他任务。所以如果后续要加传感器或显示,建议改成DMA接收。这里先给一个简洁的状态机判断版本:

#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define MAX_FRAME 65535 uint8_t rx_buffer[MAX_FRAME]; uint16_t rx_index = 0; uint8_t rx_state = 0; uint8_t rx_type = 0; uint16_t rx_len = 0; uint16_t rx_data_len = 0; void USART1_IRQHandler(void) { uint8_t byte = 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { byte = USART_ReceiveData(USART1); switch (rx_state) { case 0: if (byte == 0xAA) rx_state = 1; break; case 1: if (byte == 0x55) { rx_state = 2; rx_index = 0; } else { rx_state = 0; } break; case 2: rx_type = byte; rx_state = 3; break; case 3: rx_len = byte; rx_state = 4; break; case 4: rx_len |= (byte << 8); if (rx_len > MAX_FRAME - 1) { rx_state = 0; } else { rx_state = 5; rx_data_len = 0; } break; case 5: rx_buffer[rx_index++] = byte; rx_data_len++; if (rx_data_len >= rx_len) rx_state = 6; break; case 6: // byte 是校验和 uint8_t sum = 0; for (uint16_t i = 0; i < rx_len; i++) sum += rx_buffer[i]; if (sum == byte) { // 校验成功,转发到PC // 这里先清空状态,再调用处理函数 frame_ready = 1; } rx_state = 0; frame_ready = 1; break; default: rx_state = 0; break; } } }

当frame_ready置1后,主循环就读取rx_buffer里的JPEG数据,通过USART2原样发给PC,同时开始等待识别结果。这里有个需要注意的地方:JPEG数据本身是二进制,里面完全可能出现0xAA 0x55这样的字节组合,但由于我的状态机在接收到帧头后才开始解析,数据段内部不会再重新检测帧头,所以不会误判。风险在于数据段如果丢了一个字节,后面的数据全部错位,这时累加和校验也过不了,整帧会被丢弃,程序不会死锁,只是图像丢了一帧。实际测试中,在921600波特率下丢帧概率很低,但如果加了过长杜邦线或者供电不稳,丢帧就会明显增加。

3.3 STM32控制外设:PWM舵机与OLED显示

STM32识别到结果后要做两件事:一是控制舵机模拟道闸抬起,二是在OLED上显示车牌号。这两块逻辑都不复杂,但涉及STM32的定时器外设,顺便把很多人问的“定时器PWM控制舵机”说清楚。

SG90舵机的控制信号是50Hz的PWM,即周期20ms,高电平脉宽在0.5ms到2.5ms之间对应0°到180°。STM32的定时器输出PWM时,需要设置预分频系数和自动重载值。以72MHz主频的F103为例,如果要得到50Hz的PWM,可以设置定时器预分频为71(即72分频),计数频率变成1MHz,每1us计一次数;自动重载值设为19999,这样计数器从0数到19999正好是20ms。此时脉宽就是比较值,舵机角度和比较值的换算关系是:0°对应比较值500,180°对应比较值2500,90°对应1500。初始化代码如下:

TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 假设用TIM2的CH1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseInitStructure.TIM_Period = 19999; TIM_TimeBaseInitStructure.TIM_Prescaler = 71; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 1500; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE);

后面想抬起道闸就设置TIM_SetCompare1(TIM2, 2000),落下就是TIM_SetCompare1(TIM2, 1000),一个平滑斜坡函数可以做开机自检的往返运动。OLED显示部分用的是I2C接口的SSD1306驱动,网上标准库代码很多,移植到自己的工程时主要注意改I2C引脚和初始化时把屏幕的地址设为0x78(7位地址0x3C)即可。

4. 上位机YOLO11车牌检测模型训练与部署

4.1 数据集准备与标注格式转换

车牌检测模型用的是YOLO格式的目标检测,必须准备一张张带车牌标注框的图片和对应的txt标注文件。公开数据集里最适合中文车牌的是CCPD(Chinese City Parking Dataset),有20万张左右的车牌图,标注包含车牌框和四个角点。不过CCPD的标注格式比较特殊,需要转换成YOLO的格式:每行是“class x_center y_center width height”,坐标是归一化后的值。

如果不想折腾转换脚本,也可以自己拍100~200张照片然后手动标注。标注工具我推荐LabelImg,安装后直接把图片目录打开,用矩形框框出车牌区域,保存为YOLO格式。这里有个经验:车牌检测的难点不在环境复杂,而在车牌本身可能倾斜、反光、被遮挡,所以自己采集数据时一定要覆盖不同角度、不同光照、近景和远景。训练集里至少要包含各种常见车牌颜色,蓝牌、绿牌(新能源)、黄牌,否则模型很容易只认识蓝牌。

数据集目录结构按YOLO标准格式:

plate_dataset/ images/ train/ val/ labels/ train/ val/

train和val的比例一般按8:2或9:1划分,每张图片的标注txt和图片同名。写一个简单的划分脚本,随机把图片和对应txt移到train或者val目录里,注意千万不要把同一张图既放训练又放验证,否则模型效果虚高。

4.2 训练YOLO11车牌检测模型

训练使用的是Ultralytics的YOLO11,模型权重文件从官方仓库下载yolo11n.pt(nano版)或yolo11s.pt(small版)。车牌检测目标不算特别小,nano版在精度和速度之间比较均衡,所以我第一次训练用的yolo11n.pt。

创建一个plate.yaml配置文件:

path: D:/plate_dataset train: images/train val: images/val nc: 1 names: ['plate']

然后执行训练:

yolo detect train data=plate.yaml model=yolo11n.pt epochs=100 imgsz=640 batch=16 device=0

如果是纯CPU环境,把device=0改成device=cpu,但训练速度会慢很多,一个epoch可能就要十几分钟。训练过程中需要注意看loss曲线,正常情况下训练集和验证集的box_loss、cls_loss都在下降。如果loss明显不降,先检查标注文件有没有问题,用脚本可视化一下标注框是否准确贴合车牌。

训练结束后,模型会保存在runs/detect/train/weights/目录下,best.pt是验证集效果最好的权重,last.pt是最后一轮的权重。做推理建议用best.pt。

4.3 模型推理、筛选结果与保存裁剪区域

模型训练好之后,上位机Python程序里加载权重,对OpenMV传来的图像做一次检测。这里有个容易踩的坑:检测结果是相对坐标(0~1之间),要恢复成像素坐标,需要乘上图像的宽和高。然后还要做一个置信度筛选,把conf低于0.5的框丢弃,否则背景区域很容易被误判成车牌。同时可以用NMS参数控制重叠框的数量,Ultralytics默认已经做了NMS,一般不用额外调。

推理代码核心部分:

from ultralytics import YOLO import cv2 import numpy as np model = YOLO('best.pt') def detect_plate(img_bgr): results = model.predict(img_bgr, conf=0.5, imgsz=640, verbose=False) boxes = [] for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int) score = float(box.conf[0]) boxes.append((x1, y1, x2, y2, score)) return boxes

拿到车牌区域后,我会把这块区域裁剪出来,稍微向外扩一点(一般是10像素),再送给OCR。为什么要外扩?因为YOLO检测出来的框可能紧贴着车牌边缘,如果车牌字符有倾斜,直接裁剪容易把边缘字符切掉,OCR识别就会漏字或错字。外扩之后,OCR还能借助车牌边框的蓝色/绿色来判断字符边界,识别率会高一些。

关于“保存推理结果”,Ultralytics自带results.save()可以把检测框画在图上保存,但车牌识别场景我更推荐把裁剪出来的车牌区域保存成单独文件,方便后续人工核查。比如:

for j, (x1, y1, x2, y2, score) in enumerate(boxes): crop = img_bgr[y1-10:y2+10, x1-10:x2+10] cv2.imwrite(f'plate_crops/{time.time()}_{j}.jpg', crop)

这样即使OCR偶尔识别出错,也可以从保存的车牌图去核对是检测框偏了还是OCR本身的问题。

4.4 小目标检测优化:输入尺寸、Mosaic与损失函数

车牌在整张图中的占比通常不大,尤其是OpenMV传回来的是320x240的JPEG,在640x640的推理输入下,原图里的车牌可能只有20~30个像素宽,属于典型的小目标问题。我实际测试中发现,直接拿原图跑YOLO11n,小尺寸车牌的召回率不高,经常漏检。

优化手段有几种,但不用一上来全上,按性价比排序。第一是提高推理输入尺寸,把imgsz从640改成960或者1280,小目标对应的特征图尺寸变大了,检测效果会有明显提升,代价是推理时间变长。车牌识别是离线任务,300ms和600ms差别不大,所以我直接用了imgsz=960。第二是训练时开启Mosaic增强,Ultralytics默认开启了,它会将多张图拼接成一张,变相增加了小目标样本的比例,让模型对小目标更敏感。第三是修改损失函数,YOLO11默认使用CIoU损失,有人用PIoUv2等方式替代,对小目标检测的定位精度会有改善,但需要改源码,新手不建议一上来就在这一层折腾,先调输入尺寸和训练轮数把基础效果拉起来,再考虑损失函数的改进。

我自己测试的一个对比数据:在自采的1000张图片上,imgsz=640时验证集mAP50只有0.82,改成imgsz=960后mAP50升到了0.89,推理单帧从260ms增加到了450ms,但准确率提升非常明显。对于车牌这种要求高召回的场景,这个时间成本完全可以接受。

5. PaddleOCR车牌识别与全链路联动

5.1 安装PaddlePaddle与PaddleOCR 3.x的关键细节

PaddleOCR 3.x的安装比2.x顺利很多,但仍然有几个关键点需要注意。第一,PaddlePaddle GPU版的版本号要和CUDA版本匹配,比如CUDA 12.4对应的是paddlepaddle-gpu==3.0.0系列,你可以用pip install paddlepaddle-gpu==3.0.0按官方索引安装。第二,PaddleOCR当前版本对Python版本有要求,3.10或3.11比较稳,Python 3.12某些依赖可能编译不过去。第三,如果遇到libiomp5md.dll报错或者MKL相关的冲突,很可能是机器上同时装了其他深度学习框架,比如PyTorch带了自己的MKL,和PaddlePaddle的MKL冲突。Linux下可以在运行前设置KMP_DUPLICATE_LIB_OK=TRUE,Windows下则建议在环境变量里加上这个变量名。

GPU版装好之后,用paddle.utils.run_check()验证,输出类似PaddlePaddle is installed successfully! Let's start deep learning with PaddlePaddle.就是正常的。验证CUDA有没有真正生效,可以跑一句paddle.device.is_compiled_with_cuda(),返回True说明编译时带上了CUDA。

5.2 用PaddleOCR识别车牌字符

PaddleOCR 3.x的使用方式和2.x有区别,最直观的变化是调用接口简化了,不再需要单独指定det、rec、cls三个模型目录,直接初始化就能自动下载通用的中英文检测和识别模型。但车牌识别里有个优化点:我们只需要识别车牌字符,不需要通用场景里的行级文字识别,所以我在初始化时只保留最必要的组件,关闭不用的方向分类器(车牌是水平文字,用不到角度分类):

from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=False, lang='ch', show_log=False)

在3.x版本中,use_angle_cls=False这个参数的名称有没有变化,不同的小版本处理不太一样,如果初始化时报错说不认识这个参数,直接删掉,默认就是关闭的。调用识别时:

result = ocr.ocr(crop_bgr, cls=False)

result是一个嵌套列表,每个元素对应一行文字,result[0][0]形如[[box坐标], (识别文本, 置信度)]。注意这里传入的图片必须是BGR格式的numpy数组,如果用OpenCV读取JPEG,默认就是BGR,直接传即可。如果传了RGB,颜色通道反了,识别率会大幅下降。

实际测试中,PaddleOCR对标准蓝底白字车牌的识别效果很好,但有两个问题很常见。第一个是识别结果出现乱码,比如把“京”识别成“京”加一个空格,或者把字母“O”识别成数字“0”。这个和训练数据、字符集限制有关,通用OCR模型本身没有针对车牌做字符集约束。解决办法是把识别结果里的字符先统一转成大写,再过滤掉中文汉字之外的混乱字符,或者自己训练一个车牌字符分类器,但后者工作量偏大。我的处理方式是只保留省份简称+字母+数字这个模式,用正则表达式过滤:

import re plate_pattern = re.compile(r'^[\u4e00-\u9fa5][A-Z0-9]{5,6}$') def clean_plate(text): text = text.upper().strip() text = re.sub(r'[^A-Z0-9\u4e00-\u9fa5]', '', text) if plate_pattern.match(text): return text return None

第二个问题是低分辨率或倾斜车牌识别不准。OpenMV传过来的图本身是320x240,裁剪出的车牌区域可能只有40x15像素,直接识别几乎不可能。所以我在传给OCR之前,先做了两倍到三倍的双线性插值放大,让车牌区域的长边达到100像素以上。放大后识别率提升显著,代价是OCR耗时从20ms涨到50ms左右,完全可接受。

5.3 串口回传结果与道闸联动

PC端的识别结果需要通过串口发回给STM32,STM32才能控制道闸抬起。我用pySerial发送,串口号和波特率要和STM32那边对应好。发送的帧格式沿用自定义协议,类型字节填0x02表示识别结果,数据内容就是车牌号字符串。这里重要的一点是文本编码,建议用UTF-8编码,因为车牌里有中文,如果直接bytes发送,STM32那边接收后需要转成GBK或者直接在OLED库中映射。我为了省事,回传时把中文省份缩写映射成了拼音首字母,比如“京A12345”变成“J-A12345”,这样OLED上显示不会乱码,代码也简单很多。

Python端发送示例:

import serial import time ser = serial.Serial('COM5', 115200, timeout=1) plate_text = "京A12345" encoded = plate_text.encode('utf-8') data = bytes([0xAA, 0x55, 0x02, len(encoded) & 0xFF, (len(encoded) >> 8) & 0xFF]) + encoded data += bytes([sum(encoded) & 0xFF]) ser.write(data)

STM32端收到类型为0x02的帧后,解析出车牌字符串,如果非空,就通过PWM控制舵机抬起道闸,持续3秒后落杆,同时在OLED上显示车牌号。整个联动逻辑听起来简单,但第一次联调时特别容易出问题,尤其是PC串口发送后STM32没反应,多半是波特率不匹配或者GND没共地。

5.4 全链路测试步骤与效果验证

测试时我建议按下面的顺序分步验证,不要一上来就跑全流程,否则出了问题根本定位不到是哪一环。

第一步,用OpenMV IDE的串口终端工具,单独验证OpenMV能否把图像帧发出来。在IDE里打开串口终端,选择OpenMV对应的COM口,然后按下按键,如果能收到一大段乱码(raw JPEG),说明OpenMV端没问题。

第二步,把OpenMV和STM32连起来,STM32再接一根USB转TTL到电脑。在电脑上打开串口助手,选择USB转TTL对应的COM口,波特率设为921600,按下触发按键,看能否收到完整的JPEG数据流。这一步能验证OpenMV到STM32到PC的链路是否通畅。

第三步,用Python脚本直接读取USB转TTL串口,把收到的数据解析成JPEG,保存成文件或者用OpenCV显示出来。如果能正常显示图像,说明协议解析正确。

第四步,把YOLO11和PaddleOCR接进来,用一张真实的监控图像测试识别效果,先用本地图片测试,不用串口,等识别稳定后再接串口数据流。

第五步,全链路联调,按下OpenMV按键,观察STM32 OLED上是否出现车牌号,舵机是否抬起。如果一切正常,整个系统就算跑通了。

我在测试时遇到过一个很隐蔽的问题:OpenMV发送的JPEG数据传送到PC后,Python的串口读取如果一次性read(4096),读到的可能不是完整的一帧,因为串口是流式的,帧边界必须靠协议自己解析。所以Python端也要写状态机来组帧,不能简单假设一次read就是一帧。这个问题在串口助手测试时看不出来,因为串口助手只是把收到的字节流原样显示,但Python程序如果按帧解析就会失败。

6. 常见问题与排查技巧实录

6.1 串口通信乱码、丢包与连接失败

串口问题在这个项目里占了我调试时间的一半以上,我把典型问题整理成一个速查表:

现象可能原因解决办法
电脑收不到任何数据接线错误检查TX/RX是否交叉,GND是否共地
收到大量乱码波特率不匹配统一所有串口波特率,建议460800或115200
图像数据不完整发送端缓冲区溢出分片写入,每512字节延时1ms
偶发校验失败供电电压不足使用独立5V电源,减少杜邦线长度
STM32无故复位共地不良检查电源模块地和串口地是否连通
OpenMV无法识别数据线供电不足换带数据传输的双绞线

串口波特率这个坑,我的建议是如果你用921600和115200都试过还是乱码,那就提高容错度,改用460800。实际测试中,460800和921600在短距离下速度差不多,但460800对电路寄生电容和线缆长度更宽容。如果项目环境允许,甚至可以把识别链路改成异步消息驱动——OpenMV发送一张图后,等PC返回识别结果再继续发送下一张,这样就算偶尔丢帧,也不会出现多帧堆积导致协议错乱的问题。

6.2 YOLO11训练loss不降与识别精度低

模型训练阶段最常见的坑有两个。第一是数据标注质量差,标注框不贴合车牌,或者一张图里漏标了多个车牌。YOLO系列对标注质量非常敏感,一个坏标注会把整个方向的梯度带偏。检查方法是把训练集里随机抽几十张图,可视化标注框,确认每个框都紧贴着目标边缘。第二是数据分布不均衡,训练集里全是蓝色车牌,没有绿色和黄色车牌,那么绿色车牌识别率几乎为零。解决方法是扩充数据,或者用图像增强,把蓝牌的色调随机偏移到绿色/黄色来模拟其他类型。

推理阶段精度低,还有一个容易被忽略的原因:摄像头拍摄的图像和训练集图像风格差异大。OpenMV的摄像头色彩饱和度和手机摄像头不太一样,在CCPD上训练好的模型,直接拿到OpenMV图像上测试,精度通常会掉一些。解决方法是采集一部分OpenMV实际拍摄的图像加入训练集,或者对测试图像做一次简单的颜色校正。我自己在做毕业设计时,最后是把OpenMV拍的500张真实图像补充到训练集里,测试集mAP一下子从0.84涨到了0.91。

6.3 OpenMV连接失败与固件恢复

OpenMV IDE报“无法连接设备”时,不要急着刷固件,先按顺序排查。第一步,设备管理器里看有没有识别到端口,没有就换数据线、换USB口。第二步,如果有端口但IDE连接失败,可能是端口被占用,把上次运行过的Python脚本关掉,或者重启IDE。第三步,如果是兼容版板子,IDE里可能会显示一个未识别设备,需要去官网找对应的驱动。最后才考虑刷固件。刷固件时如果中途失败,OpenMV会进入DFU模式,可以用dfu-util或者官方工具恢复,但这个过程比较折腾,建议搜一下对应板型的救砖教程。

6.4 STM32烧录失败与芯片识别不到

STM32烧录失败的问题,我在帮几个学弟调试时发现,超过一半都是ST-Link接线或者BOOT跳线的问题。F103最小系统板用ST-Link烧录时,必须把BOOT0跳线拨到0,如果板子之前烧过会跑的程序,复位后可能一直停在原程序里,ST-Link连接会失败。另外,看看使用的ST-Link是不是盗版,盗版ST-Link在Keil MDK5新版本里可能驱动不兼容,一种变通办法是换成DAP-Link(CMSIS-DAP)或者USB转TTL串口下载。用串口ISP下载时,BOOT0要拉高,下载完再拨回低。很多人卡在这一步。

6.5 PyInstaller打包PaddleOCR的巨坑

如果你最后想把这个上位机程序发给别人,不要求对方装Python环境,那就得用PyInstaller打包。这里我想说一句扎心的话:PyInstaller打包PaddleOCR是我做过最折磨的事情之一,因为PaddleOCR引入了Paddle推理库,打包出来的exe动辄几个GB,而且不处理依赖的话运行时报一堆错。

我的建议是尽量用conda环境打包,确保环境干净,再用pyinstaller --onedir而不是--onefile打包。--onefile虽然看起来清爽,但Paddle这种体积大、动态库多的库,用onefile启动时会先把一堆文件解压到临时目录,启动慢一倍不止,而且容易被杀毒软件拦截。打包命令大致是:

pyinstaller -D -w main.py --collect-all paddleocr --collect-all paddle

如果你运行打包后的exe报找不到paddle模块,尝试在代码开头手动指定paddle库路径。实在搞不定,可以选择用Nuitka替代PyInstaller来做编译打包,兼容性好一些,但配置复杂度也高。如果只是自己学习、毕设演示,我更推荐用绿色版的conda环境直接运行,省去打包的麻烦。

6.6 整体性能优化与稳定性建议

最后说几个关于系统稳定性的实用技巧。OpenMV端在每次发送JPEG后,串口缓冲区内可能会残留上一帧的数据,可以在发送前清空一次串口接收缓冲,避免上一帧的尾部干扰下一帧的解析。STM32端,在frame_ready置位但没有及时处理的情况下,下一帧可能已经到达,所以处理数据的逻辑要尽量放在主循环而不是中断函数里,中断只负责收数和置标志位。PC端,建议把串口读取放到一个独立线程里,识别主循环用队列接收图像,这样即使OCR偶尔耗时较长,串口数据也不会堆积丢失。

供电方面,我遇到过最诡异的一次问题是:当舵机转动时,OLED屏幕闪烁,识别结果偶尔回传失败。后来用示波器一量才发现,舵机转动瞬间电流尖峰导致5V电压跌落,STM32直接掉电复位了。解决方案是在舵机电源线上并联一个大电容,或者在舵机和MCU之间用独立电源隔离,这个经验在“STM32控制舵机”相关项目里非常通用。

最后再分享一个经验

我在整个项目里体会最深的,不是YOLO11训练得有多好,也不是PaddleOCR识别得有多准,而是“方案拆分”这件事本身。一开始学弟想把所有模型都塞进OpenMV,结果性能完全失控;后来把识别放到PC上,整个系统顿时顺了。做嵌入式视觉项目,很多时候不是算法不够强,而是计算资源摆错了位置。如果你也卡在“板子性能不够”的死胡同里,不妨想想能不能把重计算交给上位机,让单片机专心做控制。这套OpenMV+STM32+YOLO11+PaddleOCR的架构,既兼顾了嵌入式开发的完整链路,又让深度学习模型可以被真正使用起来,也是个很不错的毕设框架。后面如果还想继续扩展,可以试着把道闸替换成真实的路侧停车计时逻辑,或者给系统加上车辆品牌的识别,都是在这个架构上很容易长出来的功能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 20:29:13

2026 面试必问 Java 知识点|八股文完整整理,附详细答案

今年的行情&#xff0c;让招聘面试变得雪上加霜。已经有不少大厂&#xff0c;如腾讯、字节跳动的招聘名额明显减少&#xff0c;面试门槛却一再拔高&#xff0c;如果不用心准备&#xff0c;很可能就被面试官怼得哑口无言&#xff0c;甚至失去了难得的机会。 现如今&#xff0c;…

作者头像 李华
网站建设 2026/9/28 20:29:09

出货翻板偶尔卡在半开位置,拆开发现转轴套磨出了椭圆~YH

2026年Q2&#xff0c;一台设备的出货翻板出现了偶发卡滞问题——翻板打开后&#xff0c;偶尔卡在半开位置无法完全复位&#xff0c;导致下一次出货时商品被挡住。拆开翻板组件检查&#xff0c;发现转轴套已经磨成了椭圆形。一、现象&#xff1a;翻板偶发卡滞&#xff0c;出货失…

作者头像 李华
网站建设 2026/9/28 20:29:01

国内大型DD精密转台核心生产基地盘点

在DD精密转台领域&#xff0c;“大型生产基地”不仅代表产能规模&#xff0c;更意味着企业具备从底层核心部件自研、规模化量产到品控交付的全链路实力。结合2026年最新的行业数据&#xff0c;以下是国内在大型DD精密转台制造领域具备显著产能与技术优势的源头厂家&#xff1a;…

作者头像 李华
网站建设 2026/9/28 20:28:02

基础B(队列)(第?+1课)

1.前言 在前几分钟&#xff0c;我写完了关于栈的介绍具体情况请点这里。 毕竟&#xff08;也是&#xff09;一年前的知识了&#xff0c;有点遗忘&#xff0c;所以如有错误请大家在评论区指出&#xff0c;我会修改的。 趁着手感火热&#xff0c;赶紧再来写一篇关于队列的文章…

作者头像 李华