跳到主要内容

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 一定能加载它。

先分清三个步骤​

  1. 采图。 csi.CSI.snapshot() 返回本帧 image.Image。
  2. 推理。 model.predict([img]) 把图像转换成模型输入,执行模型。
  3. 解释输出。 分类模型读取类别分数;检测模型通过对应后处理器得到坐标与分数,再画到图像上。

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.lenint模型文件占用的字节数。
Model.ramint模型张量工作区(tensor arena)占用的字节数,不是全程序总内存。
Model.input_shapelist[tuple[int, ...]]各输入张量的形状。
Model.input_dtypelist[str]各输入的数据类型字符。
Model.input_scalelist[float]各输入的量化比例。
Model.input_zero_pointlist[int]各输入的量化零点。
Model.output_shapelist[tuple[int, ...]]各输出张量的形状。
Model.output_dtypelist[str]各输出的数据类型字符。
Model.output_scalelist[float]各输出的量化比例。
Model.output_zero_pointlist[int]各输出的量化零点。
Model.postprocessobject构造时传入的后处理器或 None。
Model.labelslist[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 板卡速度。