4. Python脚本基础
OpenMV Cam 的核心价值在于其内置的 MicroPython 解释器,它允许开发者用 Python 语言快速驱动摄像头、处理图像并控制外设。与传统的 C/C++ 嵌入式开发相比,MicroPython 极大地降低了视觉应用的开发门槛。然而,OpenMV 的 MicroPython 并非桌面版 Python 的完全体,它针对资源受限的微控制器进行了裁剪和优化。本章将深入剖析 OpenMV 脚本的语法特性、核心库的调用逻辑以及标准脚本结构,帮助学员建立从“能跑”到“会写”再到“写得好”的工程化思维。
上图展示了 OpenMV 脚本开发的三大支柱:脚本结构、核心库与语法特性。三者相互依存,结构决定了代码的组织方式,库提供了功能接口,而语法则是连接两者的桥梁。在实际交付中,脚本的规范性直接影响后续的调试效率与维护成本。
4.1 MicroPython 语法核心:从 Python 到嵌入式环境的适配
4.1.1 核心原理:解释执行与资源约束
MicroPython 是 Python 3 的精简实现,专为微控制器设计。它保留了 Python 的绝大多数语法特性,如动态类型、面向对象、闭包和生成器,但去除了桌面 Python 中重量级的模块(如 multiprocessing、tkinter)。OpenMV 的固件基于 MicroPython 1.9.x 分支,运行在 Cortex-M7 处理器上,主频 480MHz,拥有 1MB RAM 和 2MB Flash。这意味着:
- 内存敏感:一个 320x240 的 RGB565 图像占用 150KB,而 QVGA 灰度图仅占 76.8KB。脚本中创建的列表、字符串和缓冲区都会消耗宝贵的堆内存。
- 性能瓶颈:Python 字节码解释执行比编译型语言慢 10-20 倍。图像处理算法应尽量使用 OpenMV 内置的 C 实现,而非纯 Python 循环。
- 垃圾回收:MicroPython 采用标记-清除算法,当内存碎片化严重时,GC 耗时可能达到数十毫秒,导致帧率抖动。
关键差异对比表:桌面 Python vs MicroPython
| 特性 | 桌面 Python | MicroPython (OpenMV) |
|---|---|---|
| 整数类型 | 任意精度 | 31位有符号(溢出自动转浮点) |
| 字符串 | Unicode 全面支持 | 仅 ASCII,UTF-8 需手动编码 |
| 异常处理 | 完整 traceback | 精简 traceback,行号有限 |
| 标准库 | 庞大 | 仅 os、time、math、gc 等子集 |
| 文件操作 | open() 通用 | 仅支持 FAT12/FAT32,路径以 / 开头 |
| 多线程 | threading 模块 | 不支持,需用定时器中断模拟 |
4.1.2 判断方法:识别语法陷阱
在编写 OpenMV 脚本时,以下语法差异最容易导致交付现场“翻车”:
- 整数除法:Python 3 中
/返回浮点,//返回整数。MicroPython 同样遵循,但浮点运算较慢,建议用//处理像素坐标。 - 内存分配失败:当 RAM 不足时,MicroPython 抛出
MemoryError。例如在循环中反复拼接字符串会迅速耗尽内存。 - 字节 vs 字符串:OpenMV 的
uart.read()返回字节对象,与字符串比较需用decode()或直接比较字节。 - 全局变量:在函数内修改全局变量必须声明
global,否则会创建局部变量。
4.1.3 案例拆解:内存泄漏排查
背景:某物流分拣项目,OpenMV 需要连续运行 8 小时,但运行 2 小时后帧率从 30fps 降至 12fps,最终死机。
初始现象:串口打印的 free RAM 从 180KB 逐步降至 20KB。
分析过程:通过逐段注释代码,定位到以下问题代码:
# 错误示例:在循环中创建新列表
while True:
img = sensor.snapshot()
blobs = img.find_blobs(thresholds) # 每次返回新列表
data = str(blobs) # 字符串拼接
uart.write(data + "\r\n") # 产生临时字符串
根因:str(blobs) 和 data + "\r\n" 每次循环创建新字符串对象,旧对象等待 GC 回收。当 GC 频率低于分配速率时,内存碎片化加剧。
解决方案:
# 正确做法:复用缓冲区
import ujson
buf = bytearray(256)
while True:
img = sensor.snapshot()
blobs = img.find_blobs(thresholds)
# 使用 ujson 序列化到预分配缓冲区
n = ujson.dump(blobs, buf)
uart.write(buf[:n] + b"\r\n")
gc.collect() # 手动触发 GC
结果:内存稳定在 150KB 左右,连续运行 12 小时无异常。
风险提示:手动调用 gc.collect() 会阻塞主线程约 5-10ms,若对帧率有严格要求,应优先优化算法减少临时对象,而非依赖手动 GC。
4.2 OpenMV 常用库详解:sensor、image、time
4.2.1 sensor 库:摄像头传感器控制
sensor 模块是 OpenMV 的硬件抽象层,负责配置摄像头参数。其核心 API 如下:
| 方法 | 功能 | 典型参数 | 注意事项 |
|---|---|---|---|
sensor.reset() | 复位传感器 | 无 | 必须在其他配置前调用 |
sensor.set_pixformat() | 设置像素格式 | sensor.RGB565 / sensor.GRAYSCALE | RGB565 占用内存为灰度 2 倍 |
sensor.set_framerate() | 设置最大帧率 | 整数,如 30 | 实际帧率受曝光时间限制 |
sensor.set_windowing() | 设置 ROI 窗口 | (x, y, w, h) 或 (w, h) | 减小窗口可提升帧率 |
sensor.skip_frames() | 跳过若干帧 | time=2000 (毫秒) | 用于自动曝光收敛 |
sensor.set_auto_whitebal() | 自动白平衡 | True / False | 颜色识别时建议关闭 |
操作流程:标准初始化顺序为 reset() → set_pixformat() → set_framerate() → set_windowing() → skip_frames()。若跳过 skip_frames(),首帧图像可能过曝或偏色。
4.2.2 image 库:图像处理核心
image 库提供了丰富的图像处理方法。最常用的包括:
- snapshot():从传感器捕获一帧图像,返回 image 对象。这是所有视觉处理的第一步。
- find_blobs():颜色块查找,返回 blob 列表。每个 blob 包含中心坐标、像素面积、边界框等属性。
- draw_* 系列:在图像上绘制线条、矩形、圆形、文字,用于调试可视化。
- copy() / crop():图像复制与裁剪,常用于 ROI 提取。
- binary():二值化处理,配合阈值参数可快速分割目标。
经验提醒:find_blobs() 的 pixels_threshold 和 area_threshold 参数用于过滤噪声。在光照变化大的场景,建议先对图像进行 median_filter() 或 binary() 预处理,再查找色块,可显著提升稳定性。
4.2.3 time 库:帧率控制与时间戳
time 库在 OpenMV 中主要用于性能监控和延时控制:
import time
clock = time.clock() # 创建时钟对象
while True:
clock.tick() # 开始计时
img = sensor.snapshot()
# 处理图像...
fps = clock.fps() # 返回最近 1 秒的平均帧率
print("FPS:", fps)
time.sleep_ms(10) # 延时 10ms,降低 CPU 占用
clock.tick() 必须在每帧循环开始处调用,clock.fps() 返回浮点数。注意 time.sleep_ms() 会阻塞主线程,若需非阻塞延时,可使用 pyb.delay() 或定时器。
4.3 标准脚本结构:从初始化到主循环
4.3.1 核心原理:分层设计
一个规范的 OpenMV 脚本应分为四个逻辑层:配置层(硬件初始化)、算法层(图像处理函数)、通信层(数据收发)、主循环层(状态机调度)。这种分层便于单元测试和代码复用。
4.3.2 操作流程:标准模板
# 1. 导入库
import sensor, image, time, ustruct
from pyb import UART
# 2. 硬件初始化
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framerate(30)
sensor.set_windowing((320, 240))
sensor.skip_frames(time=2000)
# 3. 外设配置
uart = UART(3, 115200, timeout_char=1000)
# 4. 全局常量
THRESHOLD = (30, 100, 20, 80, 20, 80) # LAB 颜色阈值
LED_ON = True
# 5. 辅助函数
def find_target(img):
blobs = img.find_blobs([THRESHOLD], pixels_threshold=100)
if blobs:
return max(blobs, key=lambda b: b.area())
return None
# 6. 主循环
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot()
target = find_target(img)
if target:
img.draw_rectangle(target.rect(), color=(255, 0, 0))
data = ustruct.pack("<ii", target.cx(), target.cy())
uart.write(data)
fps = clock.fps()
if fps < 15:
print("警告:帧率过低")
4.3.3 案例拆解:多目标跟踪的脚本优化
背景:机器人巡线项目中,需要同时识别红色引导线和蓝色停止线,并通过串口发送坐标。
初始方案:在主循环中依次调用两次 find_blobs(),分别查找红色和蓝色。
问题:帧率仅 18fps,且当两种颜色相近时误检率升高。
优化过程:
- 将图像从 RGB565 转为 GRAYSCALE,并分别对红色和蓝色通道做阈值分割,减少颜色串扰。
- 使用
img.binary([red_thresh, blue_thresh])一次性生成二值图,再分别查找连通域。 - 将串口发送改为 DMA 模式,避免阻塞。
结果对比:帧率提升至 28fps,误检率从 12% 降至 1.5%。
复盘要点:多目标识别时,优先考虑颜色空间转换(RGB→LAB)和形态学操作(腐蚀/膨胀)来分离目标,而非单纯增加阈值数量。同时,串口通信应使用 uart.write() 配合 ustruct.pack() 发送二进制数据,避免字符串转换开销。
4.4 常见误区与调试技巧
4.4.1 误区清单
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 在循环内创建大列表 | 内存耗尽,死机 | 预分配数组或使用生成器 |
忽略 skip_frames() | 首帧过曝,颜色识别失败 | 初始化后等待 1-2 秒 |
使用 print() 调试大量数据 | 串口阻塞,帧率骤降 | 仅打印关键信息,或使用 uart.write() |
| 直接比较浮点数 | 判断不稳定 | 使用误差范围 abs(a-b) < 0.01 |
| 在中断中执行耗时操作 | 中断丢失,系统崩溃 | 中断仅置标志位,主循环处理 |
4.4.2 调试工具链
OpenMV IDE 提供了帧缓冲区查看器、直方图工具和串行终端。建议在脚本中增加调试开关:
DEBUG = True
if DEBUG:
img.draw_string(10, 10, "FPS:%.1f" % fps, color=(0,255,0))
print("blobs:", len(blobs))
交付时关闭 DEBUG,可减少约 5% 的性能开销。
风险提示:OpenMV 的 print() 函数通过 USB 虚拟串口输出,若连接 IDE 调试,会占用 USB 带宽。在量产部署时,务必断开 IDE 连接,改用物理 UART 通信,否则可能导致通信超时。
4.5 本章总结
掌握 MicroPython 语法是 OpenMV 开发的基础,但更重要的是理解其与桌面 Python 的差异,以及 OpenMV 库的调用约定。标准脚本结构应遵循“初始化→配置→主循环”的范式,将算法封装为独立函数,并严格控制内存分配。在实际交付中,建议建立脚本版本管理,记录每次修改对帧率和内存的影响。下一阶段,我们将基于这些基础,深入探讨颜色识别与 AprilTag 定位的具体实现。