引言:在当今人工智能技术迅速发展的背景下,计算机视觉已成为嵌入式系统应用的重要组成部分。K230 CanMV提供了一个强大的AI Demo开发框架,专注于实现低功耗、高性能的边缘设备图像处理与AI推理任务。本篇文档以人脸检测任务为核心,详解框架的工作原理、开发流程和应用方法,为开发者提供快速上手和深入理解的指导。
目录
一、AI Demo开发框架
为了帮助用户简化 AI 部分的开发,基于 K230_CanMV 提供的 API 接口,我们搭建了配套的 AI 开发框架。框架结构如下图所示:
Camera 默认输出两路图像:一路格式为 YUV420SP (Sensor.YUV420SP),直接提供给 Display 显示;另一路格式为 RGBP888 (Sensor.RGBP888),则用于 AI 部分进行处理。AI 主要负责任务的前处理、推理和后处理流程。处理完成后,结果将绘制在 OSD 图像实例上,并发送给 Display 进行叠加显示。
接下来就是重点针对上述流程进行讲解了,ConfigPreprocess、PreProcess、Inference、PostProcess、DrawResultK。
1.工作原理流程讲解
在 K230 的 AI Demo 过程中,数据从 Sensor(传感器) 传递到 Display(显示设备) 是一个复杂的流水线。
1.1.Sensor 到 Frame
Sensor 数据采集
- 输入源:通常是摄像头传感器,如 OV5640、IMX415 等,用来捕获实时视频流或图片。
- 采样频率:根据应用场景设置帧率(如 30FPS 或 60FPS)。
- 输出数据:传感器会输出 RAW 图像数据或经过 ISP(图像信号处理器)处理后的 RGB/YUV 数据。
Frame 构建
- 传输接口:通过 MIPI 或其他接口将图像数据传递到处理器。
- 缓存帧:将传感器数据存储到 Frame Buffer(帧缓存)中,供后续模块使用。
1.2. Frame 到 AI
ConfigPreprocess
- 配置参数:加载和设置模型相关的预处理参数,如输入图像的尺寸、数据格式(RGB/BGR/YUV)、归一化范围(如 [0,1] 或 [-1,1])。
- 模型加载:初始化 AI 模型的配置,包括加载推理引擎(如 K230 支持的 NPU 加速)。
PreProcess
- 图像格式转换:
- 如果输入图像是 RGB 格式,但模型需要 BGR 格式,则需要进行通道交换。
- 如果模型需要固定大小的输入(如 224x224),则需要裁剪或缩放图像。
- 归一化:
- 将像素值从 [0,255] 映射到模型要求的范围。
- 例如,减去均值并除以标准差:
(pixel - mean) / std
。
- 数据排布:
- 将图像从 HWC(Height, Width, Channel)转换为 CHW(Channel, Height, Width),以适应模型输入。
Inference(推理)
- 输入数据:将预处理后的图像传入神经网络。
- 执行推理:利用 K230 的 NPU(神经网络处理单元)加速推理过程,输出模型结果。
- 检测任务:输出框坐标、置信度等。
- 分类任务:输出类别索引及其概率。
- 分割任务:输出像素级的分类图。
- 推理引擎:K230 的 NPU 支持主流框架转换的模型(如 TensorFlow、PyTorch 通过工具链转换)。
在 K230 的 AI Demo 过程中,数据从 Sensor(传感器) 传递到 Display(显示设备) 是一个复杂的流水线。以下是整个流程的详细分解
PostProcess
- 解析模型输出:
- 检测任务:使用非极大值抑制(NMS)去掉冗余框。
- 分类任务:根据概率排序,取最高的类别。
- 分割任务:对输出 mask 映射到原图像尺寸。
- 结果修正:对推理结果进行置信度筛选或几何校正。
DrawResultK
- 结果绘制:
- 在检测任务中,绘制检测框和类别标签。
- 在分割任务中,将 mask 叠加到原图。
- 在分类任务中,显示类别及概率。
- 显示格式:将结果转为适配屏幕分辨率和显示格式的图像。
1.3. AI 到 OSD
叠加信息(OSD)
- 将推理结果和辅助信息(如帧率、时间戳、设备状态等)叠加到图像上。
- 使用 K230 的硬件加速功能优化 OSD 的绘制效率。
- 典型信息:
- 检测框和标签:物体边界框及类别。
- 时间戳:帧捕获时间。
- 置信度:预测结果的可靠性。
1.4. OSD 到 Display
图像编码
- 将带有 OSD 信息的帧编码为显示设备支持的格式(如 RGB888 或 YUV420)。
- 如果是视频流输出,则可以选择编码为 H.264 或 H.265 格式。
显示输出
- 接口:通过 HDMI、MIPI DSI 或其他显示接口输出图像到屏幕。
- 显示刷新:将图像送入显示缓冲区,并根据显示器刷新率(如 60Hz)进行更新。
二、应用方法和示例
用户可根据具体的AI场景自写任务类继承AIBase,可以将任务分为如下四类:单模型任务、多模型任务,自定义预处理任务、无预处理任务。不同任务需要编写不同的代码实现,具体如下图所示:
关于不同任务的介绍:
任务类型 | 任务描述 | 代码说明 |
---|---|---|
单模型任务 | 该任务只有一个模型,只需要关注该模型的前处理、推理、后处理过程,此类任务的前处理使用Ai2d实现,可能使用一个Ai2d实例,也可能使用多个Ai2d实例,后处理基于场景自定义。 | 编写自定义任务类,主要关注任务类的config_preprocess、postprocess、以及该任务需要的其他方法如:draw_result等。 |
自定义预处理任务 | 该任务只有一个模型,只需要关注该模型的前处理、推理、后处理过程,此类任务的前处理不使用Ai2d实现,可以使用ulab.numpy自定义,后处理基于场景自定义。 | 编写自定义任务类,主要关注任务类的preprocess、postprocess、以及该任务需要的其他方法如:draw_result等 |
无预处理任务 | 该任务只有一个模型且不需要预处理,只需要关注该模型的推理和后处理过程,此类任务一般作为多模型任务的一部分,直接对前一个模型的输出做为输入推理,后处理基于需求自定义。 | 编写自定义任务类,主要关注任务类的run(模型推理的整个过程,包括preprocess、inference、postprocess中的全部或某一些步骤)、postprocess、以及该任务需要的其他方法如:draw_results等 |
多模型任务 | 该任务包含多个模型,可能是串联,也可能是其他组合方式。对于每个模型基本上属于前三种模型中的一种,最后通过一个完整的任务类将上述模型子任务统一起来。 | 编写多个子模型任务类,不同子模型任务参照前三种任务定义。不同任务关注不同的方法。 |
三、单模型任务详解
单模型任务的伪代码结构如下:
- 导入模块
- PipeLine:用于图像处理流程,负责从传感器获取图像帧、显示图像等操作。
- ScopedTiming:用于性能分析,测量代码块的执行时间,帮助调试和优化。
- AIBase:AI 任务的基类,封装了模型加载、预处理、推理和后处理的通用逻辑。
- Ai2d:K230 的图像预处理库,支持裁剪、缩放、平移等操作。
- nncase_runtime:推理引擎,负责加载和执行 kmodel 模型。
- ulab.numpy:用于数组操作的轻量级库,适合嵌入式环境。
- image:可能用于图像操作(代码中未直接使用)。
- gc:垃圾回收,释放内存。
- sys:处理异常、退出程序等功能。
- 自定义 AI 任务类
-
class MyAIApp(AIBase):
MyAIApp
类继承自AIBase
,是一个定制化的 AI 应用。-
初始化方法
-
def __init__(self, kmodel_path, model_input_size, rgb888p_size=[224,224], display_size=[1920,1080], debug_mode=0):
- 传入参数:
kmodel_path
:模型文件路径。model_input_size
:模型的输入分辨率(如[320, 320]
)。rgb888p_size
:传感器传递给 AI 的图像分辨率。display_size
:显示设备的分辨率。debug_mode
:调试模式开关。
- 分辨率对齐:
- 宽度按 16 字节对齐,满足硬件 DMA 的要求。
- 使用
ALIGN_UP
函数完成对齐操作。
Ai2d
初始化:- 创建
Ai2d
实例,设置输入/输出格式和数据类型。
- 创建
-
- 配置预处理
-
def config_preprocess(self, input_image_size=None):
- 检查是否传入自定义的输入图像尺寸(
input_image_size
),否则使用默认的rgb888p_size
。 - 配置 resize 操作,使用 TensorFlow 的双线性插值方法(
tf_bilinear
)和半像素模式(half_pixel
)。 - 构建预处理流水线,将图像从
ai2d_input_size
转换为模型输入大小model_input_size
。
-
- 后处理
-
def postprocess(self, results): pass
逻辑:此方法需要用户根据具体任务重写。
- 例如:
- 对检测任务进行非极大值抑制(NMS)。
- 解析分类任务的概率结果。
- 将分割任务的 mask 映射到图像上。
-
- 绘制结果
-
def draw_result(self, pl, dets): pass
逻辑:此方法也需要用户根据具体任务实现。
- 使用
PipeLine
绘制检测框、类别标签等。
-
- 主程序入口
-
if __name__ == "__main__":
- 显示模式配置:根据目标设备选择
hdmi
或lcd
显示模式,并设置对应分辨率。 - 初始化 PipeLine:
- 创建
PipeLine
实例,负责从传感器读取图像帧并显示结果。
- 创建
- 初始化 AI 任务:
- 加载自定义的 AI 应用
MyAIApp
。 - 调用
config_preprocess()
配置模型的预处理参数。
- 加载自定义的 AI 应用
-
- 主循环
-
while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total", 1): # 性能分析,记录单帧处理时间 img = pl.get_frame() # 获取当前帧 res = my_ai.run(img) # 推理当前帧 my_ai.draw_result(pl, res) # 绘制推理结果 pl.show_image() # 显示结果 gc.collect() # 垃圾回收
详细流程:
- 退出检查:
- 检查是否收到退出信号。
- 性能分析:
- 使用
ScopedTiming
记录总耗时,便于调试性能。
- 使用
- 帧处理:
- 调用
pl.get_frame()
从传感器获取当前帧。 - 将图像传入
MyAIApp.run()
进行推理。 - 绘制推理结果,叠加到图像上。
- 调用
- 显示图像:
- 使用
pl.show_image()
显示当前帧。
- 使用
- 垃圾回收:
- 使用
gc.collect()
清理未使用的内存。
- 使用
-
- 异常处理与清理
-
except Exception as e: sys.print_exception(e) # 打印异常信息 finally: my_ai.deinit() # 释放 AI 相关资源 pl.destroy() # 销毁 PipeLine
- 捕获异常并打印堆栈信息。
- 在程序结束时,释放 AI 应用和 PipeLine 占用的资源。
-
-
-
四、人脸检测详解
下面以人脸检测为例给出示例代码:
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
import ujson
from media.media import *
from time import *
import nncase_runtime as nn
import ulab.numpy as np
import time
import utime
import image
import random
import gc
import sys
import aidemo
# 自定义人脸检测类,继承自AIBase基类
class FaceDetectionApp(AIBase):
def __init__(self, kmodel_path, model_input_size, anchors, confidence_threshold=0.5, nms_threshold=0.2, rgb888p_size=[224,224], display_size=[1920,1080], debug_mode=0):
super().__init__(kmodel_path, model_input_size, rgb888p_size, debug_mode) # 调用基类的构造函数
self.kmodel_path = kmodel_path # 模型文件路径
self.model_input_size = model_input_size # 模型输入分辨率
self.confidence_threshold = confidence_threshold # 置信度阈值
self.nms_threshold = nms_threshold # NMS(非极大值抑制)阈值
self.anchors = anchors # 锚点数据,用于目标检测
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]] # 显示分辨率,并对宽度进行16的对齐
self.debug_mode = debug_mode # 是否开启调试模式
self.ai2d = Ai2d(debug_mode) # 实例化Ai2d,用于实现模型预处理
self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8) # 设置Ai2d的输入输出格式和类型
# 配置预处理操作,这里使用了pad和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/libs/AI2D.py查看
def config_preprocess(self, input_image_size=None):
with ScopedTiming("set preprocess config", self.debug_mode > 0): # 计时器,如果debug_mode大于0则开启
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size # 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
top, bottom, left, right = self.get_padding_param() # 获取padding参数
self.ai2d.pad([0, 0, 0, 0, top, bottom, left, right], 0, [104, 117, 123]) # 填充边缘
self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) # 缩放图像
self.ai2d.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]]) # 构建预处理流程
# 自定义当前任务的后处理,results是模型输出array列表,这里使用了aidemo库的face_det_post_process接口
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
post_ret = aidemo.face_det_post_process(self.confidence_threshold, self.nms_threshold, self.model_input_size[1], self.anchors, self.rgb888p_size, results)
if len(post_ret) == 0:
return post_ret
else:
return post_ret[0]
# 绘制检测结果到画面上
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
if dets:
pl.osd_img.clear() # 清除OSD图像
for det in dets:
# 将检测框的坐标转换为显示分辨率下的坐标
x, y, w, h = map(lambda x: int(round(x, 0)), det[:4])
x = x * self.display_size[0] // self.rgb888p_size[0]
y = y * self.display_size[1] // self.rgb888p_size[1]
w = w * self.display_size[0] // self.rgb888p_size[0]
h = h * self.display_size[1] // self.rgb888p_size[1]
pl.osd_img.draw_rectangle(x, y, w, h, color=(255, 255, 0, 255), thickness=2) # 绘制矩形框
else:
pl.osd_img.clear()
# 获取padding参数
def get_padding_param(self):
dst_w = self.model_input_size[0] # 模型输入宽度
dst_h = self.model_input_size[1] # 模型输入高度
ratio_w = dst_w / self.rgb888p_size[0] # 宽度缩放比例
ratio_h = dst_h / self.rgb888p_size[1] # 高度缩放比例
ratio = min(ratio_w, ratio_h) # 取较小的缩放比例
new_w = int(ratio * self.rgb888p_size[0]) # 新宽度
new_h = int(ratio * self.rgb888p_size[1]) # 新高度
dw = (dst_w - new_w) / 2 # 宽度差
dh = (dst_h - new_h) / 2 # 高度差
top = int(round(0))
bottom = int(round(dh * 2 + 0.1))
left = int(round(0))
right = int(round(dw * 2 - 0.1))
return top, bottom, left, right
if __name__ == "__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="hdmi"
# k230保持不变,k230d可调整为[640,360]
rgb888p_size = [1920, 1080]
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 设置模型路径和其他参数
kmodel_path = "/sdcard/examples/kmodel/face_detection_320.kmodel"
# 其它参数
confidence_threshold = 0.5
nms_threshold = 0.2
anchor_len = 4200
det_dim = 4
anchors_path = "/sdcard/examples/utils/prior_data_320.bin"
anchors = np.fromfile(anchors_path, dtype=np.float)
anchors = anchors.reshape((anchor_len, det_dim))
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create() # 创建PipeLine实例
# 初始化自定义人脸检测实例
face_det = FaceDetectionApp(kmodel_path, model_input_size=[320, 320], anchors=anchors, confidence_threshold=confidence_threshold, nms_threshold=nms_threshold, rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
face_det.config_preprocess() # 配置预处理
try:
while True:
os.exitpoint() # 检查是否有退出信号
with ScopedTiming("total",1):
img = pl.get_frame() # 获取当前帧数据
res = face_det.run(img) # 推理当前帧
face_det.draw_result(pl, res) # 绘制结果
pl.show_image() # 显示结果
gc.collect() # 垃圾回收
except Exception as e:
sys.print_exception(e) # 打印异常信息
finally:
face_det.deinit() # 反初始化
pl.destroy() # 销毁PipeLine实例
上述代码实现了一个基于人脸检测模型的应用,主要用于实时捕获视频帧进行人脸检测,并在画面上绘制检测结果。代码的核心分为几个部分:类定义、初始化和主循环。
1. 类定义:FaceDetectionApp
FaceDetectionApp
继承自基类 AIBase
,实现了模型加载、前处理、后处理以及检测结果的绘制。
1.1. 构造函数 (__init__
)
- 初始化模型路径、输入大小、置信度阈值、NMS阈值、锚点数据和其他参数。
- 配置 AI 数据预处理工具
Ai2d
,用于裁剪、填充和调整输入图像尺寸。 - 对分辨率进行 16 对齐操作(宽度必须为 16 的倍数)。
1.2. config_preprocess
- 配置图像预处理的流程,包括:
- 填充:通过
get_padding_param
获取填充值,并调用self.ai2d.pad
添加边缘填充。 - 缩放:使用双线性插值将图像调整为模型输入尺寸。
- 构建流程:将输入尺寸转换为模型适配的形状。
- 填充:通过
1.3. postprocess
- 使用
aidemo.face_det_post_process
进行后处理,返回人脸检测结果。- 后处理包括置信度筛选和非极大值抑制(NMS)以消除重叠框。
1.4. draw_result
- 在检测结果上绘制矩形框:
- 根据检测框的坐标,计算映射到显示分辨率的实际位置。
- 调用
draw_rectangle
方法绘制矩形框。
1.5. get_padding_param
- 计算图像在调整到模型输入尺寸时需要的填充量。
- 确保图像比例一致,避免拉伸失真。
2. 初始化部分
在 if __name__ == "__main__":
中:
- 设置显示模式为
HDMI
或LCD
,选择对应的显示分辨率。 - 加载人脸检测模型(
.kmodel
格式)和锚点数据(.bin
文件)。 - 初始化
PipeLine
管道,用于处理图像流(包括帧获取、显示等功能)。 - 创建
FaceDetectionApp
实例,并配置前处理流程。
3. 主循环
- 不断从
PipeLine
中获取视频帧。 - 调用模型进行推理,返回检测结果。
- 绘制检测框并更新显示画面。
- 定期调用
gc.collect()
进行垃圾回收以释放内存。
异常处理
- 使用
try...except
捕获可能的运行时错误,并在异常发生时打印信息。 - 在
finally
块中释放资源,包括反初始化模型和销毁管道。
4. 核心逻辑流程
- 模型加载:加载
.kmodel
文件和锚点。 - 图像预处理:通过裁剪、填充和缩放,将帧调整为模型输入格式。
- 模型推理:使用神经网络对帧进行推理,输出检测框和置信度。
- 结果后处理:通过置信度和 NMS 筛选最终检测框。
- 结果显示:在视频帧上绘制检测框并输出。
5. 关键库和模块
nncase_runtime
: 用于加载和运行神经网络模型。Ai2d
: 图像预处理模块,支持裁剪、填充、缩放等操作。PipeLine
: 管道处理视频流,封装了帧获取、显示等功能。aidemo
: 提供后处理函数(如face_det_post_process
)。