ml — 机器学习
ml 在 OpenMV 上加载并运行 TensorFlow Lite 模型。对于 H7 Plus / 固件 5.0.1(OPENMV4P),推理由 Cortex-M7 执行,可使用 CMSIS-NN 内核;这块板没有 NPU,不能照搬 N6、AE3 的硬件加速模型或帧率结论。
本页整理 ml.Model、图像预处理,以及本课程需要的 FOMO 和 BlazeFace 结果处理。模型的算子、输入大小、量化方式和内存需求决定能否运行;“有 .tflite 文件”不等于 H7 Plus 一定能加载它。
先分清三个步骤
- 采图。
csi.CSI.snapshot()返回本帧image.Image。 - 推理。
model.predict([img])把图像转换成模型输入,执行模型。 - 解释输出。 分类模型读取类别分数;检测模型通过对应后处理器得到坐标与分数,再画到图像上。
model 在循环外加载一次;img 和推理结果每帧更新。上一帧检测到目标,不代表这一帧也有目标,不要把旧结果当作本帧新检测。
Model — 加载模型
ml.Model(path: str, *, postprocess: object = None) -> Model
| 参数 | 用法 |
|---|---|
path | 实际模型文件路径,或固件内置模型名称。H7 Plus 的 /rom 为只读模型资源,SD 卡挂载在 /sdcard。 |
postprocess | 可选的后处理对象,调用形式是 processor(model, inputs, outputs);没有时返回输出张量。 |
构造时加载模型并准备张量内存。若同目录存在同名 .txt,例如 model.tflite 对应 model.txt,每行标签会读入 model.labels;不存在时为 None。标签顺序必须与训练时的类别顺序相同,不能按文件名字母顺序重新排列。
import ml
# 先把自己的模型和同名标签文件放到 SD 卡
model = ml.Model("/sdcard/model.tflite")
print(model)
print(model.input_shape, model.output_shape)
print(model.labels)
model.tflite 是你需要准备的真实文件,不是本站附带的模型。先打印输入输出信息,确认它是分类、检测还是其他类型,再选择后续代码。
Model.predict()
Model.predict(inputs: list, *, callback: object = None) -> list
inputs 是列表,每个输入张量对应一项,单输入图像模型也要写 [img]。每项可以是:
| 输入类型 | 行为 |
|---|---|
image.Image | 自动包装成 ml.preprocessing.Normalization,转为模型所需图像张量。 |
ndarray | 形状须与对应 input_shape 一致;按该输入的 scale / zero point 量化,float32 输入直接传递。 |
| 可调用对象 | 接收 (bytearray, shape, dtype),负责把输入张量填入缓冲区。 |
没有后处理器时返回 ndarray 列表,每项对应一个输出张量,并已反量化为 float32。有后处理器时,把原始输出类型的张量交给它,最终返回它的结果。
callback 只替换本次调用的构造时后处理器,不会永久改掉 model.postprocess。官方签名标注 -> list,但后处理器可以返回其他对象,无检测结果时某些处理器返回 ();实际判断应与选用的处理器一致。
Model 属性
这些是属性,读取时不加 ()。
| 属性 | 官方类型 | 含义 |
|---|---|---|
Model.len | int | 模型文件占用的字节数。 |
Model.ram | int | 模型张量工作区(tensor arena)占用的字节数,不是全程序总内存。 |
Model.input_shape | list[tuple[int, ...]] | 各输入张量的形状。 |
Model.input_dtype | list[str] | 各输入的数据类型字符。 |
Model.input_scale | list[float] | 各输入的量化比例。 |
Model.input_zero_point | list[int] | 各输入的量化零点。 |
Model.output_shape | list[tuple[int, ...]] | 各输出张量的形状。 |
Model.output_dtype | list[str] | 各输出的数据类型字符。 |
Model.output_scale | list[float] | 各输出的量化比例。 |
Model.output_zero_point | list[int] | 各输出的量化零点。 |
Model.postprocess | object | 构造时传入的后处理器或 None。 |
Model.labels | list[str] | 同名文本文件中的标签;不存在时为 None。 |
类型字符为 'b'(int8)、'B'(uint8)、'h'(int16)、'H'(uint16)、'f'(float32)。例如图像输入形状 [(1, 96, 96, 3)] 表示一个输入张量,其批次为 1、高 96、宽 96、通道 3;这里是解释形状的例子,不是所有模型的固定尺寸。
Normalization — 图像预处理
ml.preprocessing.Normalization(scale: tuple[float, float] = (0.0, 1.0), mean: tuple[float, float, float] = (0.0, 0.0, 0.0), stdev: tuple[float, float, float] = (1.0, 1.0, 1.0), roi: tuple[int, int, int, int] = None)
Normalization.__call__(image: image.Image) -> Normalization
Normalization.__call__(buffer: bytearray, shape: tuple[int, int, int, int], dtype: int) -> None
scale 是浮点模型期望的范围,例如 (0.0, 1.0) 或 (-1.0, 1.0)。先缩放,再减 mean,最后除以 stdev;三个通道依次为 R、G、B,必须与训练预处理一致。灰度输入按 0.299*R + 0.587*G + 0.114*B 合成均值与标准差。
对于 int8 / uint8 图像输入,scale、mean、stdev 被忽略,直接写入整数像素并对 int8 作偏移;不要以为设置这些参数就改变了量化模型的训练约定。
roi=(x, y, w, h) 先截取图像区域,默认整图。随后按比例居中缩放,不足部分补黑边。输入形状需为 (1, H, W, C),C 为 1 或 3。
from ml.preprocessing import Normalization
# 仅适用于训练时确实使用 -1~1 输入范围的浮点模型
normalize = Normalization(scale=(-1.0, 1.0))
# 在已有 model 和 img 的采图循环中执行
outputs = model.predict([normalize(img)])
normalize(img) 返回绑定到这帧图像的新预处理对象,predict() 再调用它填充输入缓冲区;它不直接返回推理结果。
Fomo — Edge Impulse 目标检测
ml.postprocessing.edgeimpulse.Fomo(threshold: float = 0.4, w_scale: float = 1.414214, h_scale: float = 1.414214, nms_threshold: float = 0.1, nms_sigma: float = 0.001)
Fomo.__call__(model: ml.Model, inputs: list, outputs: list) -> list
适用于 FOMO 输出格式,不是所有 Edge Impulse 模型都用它。threshold 为保留检测的最低分数;w_scale、h_scale 在非极大值抑制前放大网格单元宽高,值大时相邻响应更容易合并。nms_threshold 是 IoU 阈值,nms_sigma 是 soft-NMS 的分数衰减参数。
结果按类别分组,形式为 [[((x, y, w, h), score), ...], ...]。空类别仍占位,外层下标与模型类别编号一致;无检测时文档说明可返回空元组 ()。FOMO 由网格响应得到目标位置,不应把输出框当作精确物体轮廓。
下面程序要求 SD 卡上已有可在本板运行的 FOMO 模型 model.tflite。摄像头只初始化一次,模型也只加载一次。
import csi
import ml
from ml.postprocessing.edgeimpulse import Fomo
cam = csi.CSI()
cam.reset()
cam.pixformat(csi.RGB565)
cam.framesize(csi.QVGA)
cam.snapshot(time=2000)
model = ml.Model("/sdcard/model.tflite", postprocess=Fomo(threshold=0.4))
while True:
img = cam.snapshot()
results = model.predict([img])
for class_id, detections in enumerate(results):
for rect, score in detections:
img.draw_rectangle(rect, color=(0, 255, 0))
label = str(class_id)
if model.labels is not None and class_id < len(model.labels):
label = model.labels[class_id]
print(label, rect, score)
先按外层找到类别,再按内层遍历该类目标;标签缺失时输出编号。threshold=0.4 是示例筛选值,需要根据实际场景验证,不代表准确率为 40%。
BlazeFace — 人脸位置与关键点
ml.postprocessing.mediapipe.BlazeFace(threshold: float = 0.6, anchors: ndarray | None = None, nms_threshold: float = 0.1, nms_sigma: float = 0.1)
threshold 控制分数筛选;anchors=None 使用处理器内置锚点配置;nms_threshold、nms_sigma 处理重叠检测。它只适配 BlazeFace 对应的输出布局,不能用于普通分类模型。
返回的是逐目标列表:[(rect, score, keypoints), ...],没有 FOMO 的外层类别列表。H7 Plus 官方示例使用 /rom/blazeface_front_128.tflite;先确认你的固件资源中确有该文件,再运行下面的程序。
import csi
import ml
from ml.postprocessing.mediapipe import BlazeFace
cam = csi.CSI()
cam.reset()
cam.pixformat(csi.RGB565)
cam.framesize(csi.VGA)
cam.window((400, 400))
cam.snapshot(time=2000)
model = ml.Model("/rom/blazeface_front_128.tflite",
postprocess=BlazeFace(threshold=0.4))
while True:
img = cam.snapshot()
for rect, score, keypoints in model.predict([img]):
img.draw_rectangle(rect, color=(0, 255, 0))
ml.utils.draw_keypoints(img, keypoints, color=(255, 0, 0))
print(rect, score)
该模型的六个关键点依次是右眼、左眼、鼻、嘴、右耳、左耳。这是人脸检测,不是身份识别或与样本人脸相似度比较。官方类页把关键点描述为点列表,H7 Plus 示例注释写为 (6, 2) 的 ndarray;本例将其直接交给官方绘制函数,不假定它具有列表专属方法。
utils — 量化与绘制
dequantize()、quantize()
ml.utils.dequantize(model: ml.Model, value: ndarray, index: int = 0) -> ndarray
ml.utils.quantize(model: ml.Model, value: ndarray, index: int = 0) -> ndarray
index 选择模型的输出张量,value 是 ndarray。反量化为 (value - zero_point) * scale;量化按 value / scale + zero_point 转换,输出类型为 float 时直接返回原值。例如原值 130、零点 128、比例 0.1,反量化后为 0.2。
默认 predict() 已返回反量化 float32 输出,不应再次反量化。自己编写后处理器时收到的是原始张量,才按该输出的参数处理。
draw_predictions()
ml.utils.draw_predictions(image: image.Image, boxes: list[tuple[float, float, float, float]], labels: list[str], colors: list[tuple[int, int, int]], scores: list[float] | None = None, format: str = "pascal_voc", font_width: int = 8, font_height: int = 10, text_color: tuple[int, int, int] = (255, 255, 255)) -> None
在 image 上原地画框和文字,返回 None。boxes、labels、colors 按目标一一对应;scores 可选,用于追加分数文字。font_width、font_height、text_color 控制标注外观。
注意坐标格式:默认 format="pascal_voc" 接收归一化的 (xmin, ymin, xmax, ymax);已有绝对像素 (x, y, w, h) 时传 format=None,format="point" 则在框中心画点。例如 ml.utils.draw_predictions(img, [rect], ["face"], [(0, 255, 0)], format=None)。
draw_keypoints()
ml.utils.draw_keypoints(image: image.Image, keypoints: ndarray, radius: int = 4, color: tuple[int, int, int] = (255, 0, 0), thickness: int = 1, fill: bool = False) -> None
在图像上原地绘制关键点,返回 None。keypoints 为每行含 (x, y, ...) 的 ndarray;radius 是圆半径,color 是 RGB 元组,thickness 是线宽,fill=True 为实心点。
运行前检查
| 现象 | 先检查什么 |
|---|---|
| 找不到模型 | path 和文件名是否真实存在;SD 卡路径是 /sdcard,内置模型路径以实际 /rom 内容为准。 |
| 模型加载失败 | 算子、量化类型与固件支持是否匹配;模型及工作区是否放得下。 |
| 输出全是异常类别 | 图像颜色、输入形状、归一化、标签顺序是否与训练一致。 |
| 框位置不对 | 后处理器是否对应这个模型;是否混淆 ROI 坐标、绝对像素和归一化坐标。 |
| 检测速度慢 | 输入大小、模型复杂度、摄像头分辨率、绘图和打印开销;在本板实际测量,不引用 NPU 板卡速度。 |