KPU#

KPU#

K210具备神经网络加速器 KPU ,它可以在低功耗的情况下实现卷积神经网络计算,时时获取被检测目标的大小、坐标和种类,对人脸或者物体进行检测和分类。

KPU 具备以下几个特点:

  • 支持主流训练框架按照特定限制规则训练出来的定点化模型

  • 对网络层数无直接限制,支持每层卷积神经网络参数单独配置,包括输入输出通道数目、输入输 出行宽列高

  • 支持两种卷积内核 1x1 和 3x3

  • 支持任意形式的激活函数

  • 实时工作时最大支持神经网络参数大小为 5.5MiB 到 5.9MiB

  • 非实时工作时最大支持网络参数大小为(Flash 容量-软件体积)

load_kmodel#

描述#

加载kmodel模型文件,从文件系统或者Flash加载模型

语法#

  • 从内存buffer加载

int load_kmodel(uint8_t *buffer, size_t size, const char *name = "default");
  • 从SD卡文件系统加载

int load_kmodel(fs::FS &fs, const char *name);
  • 从flash加载

int load_kmodel(uint32_t offset);

参数#

  • buffer 模型内存buffer,

  • size 模型大小

  • fs 文件系统,使用SD卡则为 FFat

  • name 模型文件路径

  • offset flash中模型存放地址

返回值#

0:成功,-1:失败

示例说明#

从SD卡加载模型

KPU_Base landmark;
landmark.load_kmodel(FFat, "/KPU/face_detect_with_68landmark/landmark68.kmodel");

从flash加载模型

KPU_Base kpu1;
kpu1.load_kmodel(0x300000);

run_kmodel#

描述#

加载模型之后,对输入图像进行推理计算

语法#

  • 使用r8g8b8内存中的图运行

int run_kmodel(uint8_t *r8g8b8, int w, int h, dmac_channel_number_t dam_ch = DMAC_CHANNEL_MAX);
  • 使用Image图运行

int run_kmodel(Image *img, dmac_channel_number_t dam_ch = DMAC_CHANNEL_MAX);

参数#

  • r8g8b8 r8g8b8格式源图像内存

  • w 图像宽

  • h 图像高

  • dam_ch DMA通道

  • img Image图像类输入

返回值#

0:成功,-1:失败

示例说明#

img_128x128 = new Image(128, 128, IMAGE_FORMAT_R8G8B8, true);
landmark.run_kmodel(img_128x128)

get_result#

描述#

获取模型运行结果

语法#

int get_result(uint8_t **data, size_t *count, uint32_t startIndex = 0);

参数#

  • src 源图像

  • dst 生成的图像

  • create 是否为生成的图像创建内存

返回值#

0:成功,其他值:失败

示例说明#

float *result;
size_t output_size;
landmark.get_result((uint8_t **)&result, &output_size)

end#

描述#

去初始化kpu,释放模型内存

语法#

void end();

参数#

返回值#

示例说明#

landmark.end();

KPU_Activation#

该类包含以下激活函数,用于处理模型运行推理后得到的数据。

sigmoid#

描述#

将数据归一化到[0, 1]范围

语法#

static float sigmoid(float x);

参数#

  • x 输入浮点数

返回值#

处理后的浮点数

示例说明#

float th = KPU_Activation::sigmoid(10.4);

softmax#

描述#

将数据归一化到[0, 1]范围

语法#

static void softmax(float *x, float *dx, uint32_t len);

参数#

  • x 输入浮点数组

  • dx 输出浮点数组

  • len 数组长度

返回值#

示例说明#

float fea[192]={0};
float sm_fea[192];
static void softmax(fea, sm_fea, 192);

KPU_Yolo2#

KPU_Yolo2 继承自 KPU ,用于yolo模型的使用。

begin#

描述#

初始化yolo

语法#

int begin(float *anchor, int anchor_num, float threshold = 0.5, float nms_value = 0.3);

参数#

  • anchor 锚点参数与模型参数一致,同一个模型这个参数是固定的,和模型绑定的(训练模型时即确定了), 不能改成其它值。

  • anchor_num 锚点参数数组长度。

  • threshold 概率阈值, 只有是这个物体的概率大于这个值才会输出结果, 取值范围:[0, 1],默认值为0.5

  • nms_value box_iou 门限, 为了防止同一个物体被框出多个框,当在同一个物体上框出了两个框,这两个框的交叉区域占两个框总占用面积的比例 如果小于这个值时, 就取其中概率最大的一个框,默认值为0.3

返回值#

0:成功,-1:失败

示例说明#

KPU_Yolo2 yolo2;
yolo2.begin(anchor, sizeof(anchor) / sizeof(float));

end#

描述#

去初始化yolo,释放内存

语法#

void end();

参数#

返回值#

示例说明#

KPU_Yolo2 yolo2;
yolo2.end();

run#

描述#

加载模型之后,对输入图像进行推理计算

语法#

  • 使用r8g8b8内存中的图运行

    int run(uint8_t *r8g8b8, int w, int h, obj_info_t *info, dmac_channel_number_t dam_ch = DMAC_CHANNEL_MAX);

  • 使用Image图运行

    int run(Image *img, obj_info_t *info, dmac_channel_number_t dam_ch = DMAC_CHANNEL_MAX);

参数#

  • r8g8b8 r8g8b8格式源图像内存

  • w 图像宽

  • h 图像高

  • info 检测矩形框结果输出

#define REGION_LAYER_MAX_OBJ_NUM (10)
typedef struct
{
    uint32_t obj_number;

    struct
    {
        uint32_t x;
        uint32_t y;
        uint32_t w;
        uint32_t h;
        uint32_t class_id;
        float prob;
    } obj[REGION_LAYER_MAX_OBJ_NUM];
} obj_info_t;
  • dam_ch DMA通道

  • img Image图像类输入

返回值#

0:成功,-1:失败

示例说明#

obj_info_t info;
yolo2.run(img_ai, &info, DMAC_CHANNEL5);

KPU_Face#

KPU_Face 继承自 KPU ,增加了人脸识别要用到的相关方法。

使用该模块需要先引用以下头文件:

#include "KPU_Face.h"

calc_feature#

描述#

计算人脸特征值

语法#

int calc_feature(float *feature);

参数#

  • feature 传入一个数组指针,获取计算的特征值结果。

返回值#

0:成功,-1:失败

示例说明#

KPU_Face face;
float feature[192];
face.calc_feature(feature);

compare_feature#

描述#

比对两个人脸特征值,计算相似分数。

语法#

static float compare_feature(float *f1, float *f2, size_t length);

参数#

  • f1 参与比对的特征值数组。

  • f2 参与比对的特征值数组。

  • length 特征值数组长度。

返回值#

返回相似分数[0-100],越接近100则说明相似度越高。

示例说明#

float tmp_score = face.compare_feature(feature, persion_info_save[p].feature, FEATURE_LEN);

calc_affine_transform#

描述#

根据两组人脸关键点算出变换矩阵。

语法#

static int calc_affine_transform(uint16_t *src, uint16_t *dst, uint16_t cnt, float* TT);

参数#

  • src 需要修正的人脸关键点。

  • dst 对齐的目标人脸关键点。

  • TT 3*3二维数组 TT[3][3]

返回值#

0:成功,-1:失败

示例说明#

float T[3][3];
face.calc_affine_transform((uint16_t*)face_key_point, (uint16_t*)dst_point, 5, (float*)T);

apply_affine_transform#

描述#

相似变换修正人脸图。

语法#

static int apply_affine_transform(Image * src, Image* dst, float* TT);

参数#

  • src 需要修正的人脸图像。

  • dst 修正后的人脸图像。

  • TT 根据 calc_affine_transform 计算得到的3*3二维数组 TT[3][3]

返回值#

0:成功,其他值:失败

示例说明#

float T[3][3];
face.calc_affine_transform((uint16_t*)face_key_point, (uint16_t*)dst_point, 5, (float*)T);
face.apply_affine_transform(img_ai, img_128x128, (float*)T);

例程 - kpu_face_attribute.ino#

人脸属性检测

以下是一个基于K210芯片的人脸检测和属性识别应用。

它通过摄像头拍摄照片,运行YOLOv2模型进行人脸检测,并在检测到人脸的图像上裁剪缩放得到人脸抠图送给人脸关键点检测模型,通过得到的关键点进行仿射变换得到矫正图像送给人脸属性检测模型,最终在LCD上显示出检测结果。

该程序中包含了许多注意点,例如需要初始化LCD、文件系统、摄像头等设备;需要对图像进行裁剪、缩放和仿射变换等操作;还需要获取模型运行结果并进行解析和处理。此外,为了提高检测的准确性,程序还对检测框进行了扩展,并使用仿射变换矫正了人脸属性模型需要的输入图像。

#include "Arduino.h"

#include "Image.h"

#include "GC0328.h"
#include "OV2640.h"

#include "ST7789V.h"

#include "KPU.h"
#include "KPU_Face.h"

#include "FS.h"
#include "FFat.h"

using namespace K210;

// GC0328 cam;
OV2640 cam;
ST7789V lcd(240, 320);

Image *img_ai, *img_display, *img_128x128;

// 定义KPU对象
KPU_Base landmark;
KPU_Yolo2 yolo2;
KPU_Face face;

// 人脸属性
const char* pos_face_attr[] = {"Male ", "Mouth Open ", "Smiling ", "Glasses"};
const char* neg_face_attr[] = {"Female ", "Mouth Closed", "No Smile", "No Glasses"};

// YOLOv2模型的anchor
static float anchor[9*2] = {0.1075, 0.126875, 0.126875, 0.175, 0.1465625, 0.2246875, 0.1953125, 0.25375, 0.2440625, 0.351875, 0.341875, 0.4721875, 0.5078125, 0.6696875, 0.8984375, 1.099687, 2.129062, 2.425937};

// 扩展矩形框
static void extend_box(rectangle_t *in, rectangle_t *out, float scale)
{
    uint32_t x = in->x;
    uint32_t y = in->y;
    uint32_t w = in->w;
    uint32_t h = in->h;
    float new_x1_t = x - scale * w;
    float new_x2_t = x + w + scale * w;
    float new_y1_t = y - scale * h;
    float new_y2_t = y + h + scale * h;

    uint32_t new_x1 = new_x1_t > 1 ? floorf(new_x1_t) : 1;
    uint32_t new_x2 = new_x2_t < 320 ? floorf(new_x2_t) : 319;
    uint32_t new_y1 = new_y1_t > 1 ? floorf(new_y1_t) : 1;
    uint32_t new_y2 = new_y2_t < 240 ? floorf(new_y2_t) : 239;
    out->x = new_x1;
    out->y = new_y1;
    out->w = new_x2 - new_x1 + 1;
    out->h = new_y2 - new_y1 + 1;
}

// 人脸关键点坐标
#define PIC_SIZE    (128)
static uint16_t dst_point[] ={
        int(38.2946 * PIC_SIZE / 112), int(51.6963 * PIC_SIZE / 112),
        int(73.5318 * PIC_SIZE / 112), int(51.5014 * PIC_SIZE / 112),
        int(56.0252 * PIC_SIZE / 112), int(71.7366 * PIC_SIZE / 112),
        int(41.5493 * PIC_SIZE / 112), int(92.3655 * PIC_SIZE / 112),
        int(70.7299 * PIC_SIZE / 112), int(92.2041 * PIC_SIZE / 112)
};
#undef PIC_SIZE

// 初始化
void setup()
{
    camera_buffers_t buff;

    Serial.begin(115200);
    while (!Serial) {
        ;
    }

    // 初始化LCD
    lcd.begin();
    // lcd.invertDisplay(1);
    lcd.setRotation(3);
    lcd.setTextSize(2);
    lcd.setTextColor(0x07E0);
    lcd.setCursor(0,0);
    lcd.fillScreen(0xFFFF);

    // 初始化文件系统
    if(!FFat.begin()){
        lcd.printf("FFat Mount Failed\n");
        Serial.printf("FFat Mount Failed");
        while(1) {}
    }

    // 初始化摄像头
    if(0x00 != cam.reset(FRAMESIZE_QVGA))
    {
        lcd.printf("camera reset failed\n");
        Serial.printf("camera reset failed\n");
        while(1) {}
    }
    cam.set_vflip(true);
    cam.set_hmirror(true);

    // 获取摄像头缓存
    cam.get_buffers(&buff);
    if((NULL == buff.disply) || (NULL == buff.ai.r))
    {
        lcd.printf("get camera buffers failed\n");
        Serial.printf("get camera buffers failed\n");
        while(1) {}
    }

    // 初始化图像
    img_ai = new Image(cam.width(), cam.height(), IMAGE_FORMAT_R8G8B8, buff.ai.r);
    img_display = new Image(cam.width(), cam.height(), IMAGE_FORMAT_RGB565, buff.disply);

    img_128x128 = new Image(128, 128, IMAGE_FORMAT_R8G8B8, true);
    if(NULL == img_128x128)
    {
        lcd.printf("new image 128x128 failed\n");
        Serial.printf("new image 128x128 failed\n");
        while(1) {}
    }

    // 加载人脸检测模型
    if(0x00 != yolo2.load_kmodel(FFat, "/KPU/yolo_face_detect/face_detect_320x240.kmodel"))
    {
        lcd.printf("load kmodel failed\n");
        Serial.printf("load kmodel failed\n");
        while(1) {}
    }

    // 初始化YOLOv2参数
    if(0x00 != yolo2.begin(anchor, sizeof(anchor) / sizeof(float)))
    {
        lcd.printf("yolo2 init failed\n");
        Serial.printf("yolo2 init failed\n");
        while(1) {}
    }

    // 加载人脸关键点模型
    if(0x00 != landmark.load_kmodel(FFat, "/KPU/face_attribute/ld5.kmodel"))
    {
        lcd.printf("load kmodel failed2\n");
        Serial.printf("load kmodel failed2\n");
        while(1) {}
    }

    // 加载人脸属性检测模型
    if(0x00 != face.load_kmodel(FFat, "/KPU/face_attribute/fac.kmodel"))
    {
        lcd.printf("load kmodel failed3\n");
        Serial.printf("load kmodel failed3\n");
        while(1) {}
    }

    lcd.setFrameBuffer(img_display);
}

void loop()
{
    obj_info_t info;

    // 拍摄照片
    if(0x00 != cam.snapshot())
    {
        lcd.setCursor(0,0);
        lcd.printf("camera snapshot failed\n");
        lcd.refresh();

        Serial.printf("camera snapshot failed\n");
        return;
    }

    // 运行YOLOv2模型
    if(0x00 != yolo2.run(img_ai, &info, DMAC_CHANNEL5))
    {
        lcd.setCursor(0,0);
        lcd.printf("yolo2 run failed\n");
        lcd.refresh();

        Serial.printf("yolo2 run failed\n");
        return;
    }

    // 遍历检测到的目标
    for(int i = 0; i < info.obj_number; i++)
    {
        rectangle_t in, cut_rect;
        in.x = info.obj[i].x;
        in.y = info.obj[i].y;
        in.w = info.obj[i].w;
        in.h = info.obj[i].h;

        // 扩展矩形框
        extend_box(&in, &cut_rect, 0.08);
        lcd.drawRect(cut_rect.x, cut_rect.y, cut_rect.w, cut_rect.h, 0xf800);

        // 裁剪图像
        Image *img_cut = img_ai->cut(cut_rect);
        if(NULL == img_cut)
        {
            lcd.setCursor(0,0);
            lcd.printf("cut img failed\n");

            Serial.printf("cut img failed\n");
            break;
        }

        // 缩放图像
        if(0x00 != img_cut->resize(img_128x128, 128, 128, false))
        {
            delete img_cut;

            lcd.setCursor(0,0);
            lcd.printf("resize img failed\n");

            Serial.printf("resize img failed\n");
            break;
        }
        delete img_cut;

        // 运行人脸关键点检测模型
        if(0x00 != landmark.run_kmodel(img_128x128))
        {

            lcd.setCursor(0,0);
            lcd.printf("run landmark failed\n");

            Serial.printf("run landmark failed\n");
            break;
        }

        float *result;
        size_t output_size;

        // 获取人脸关键点坐标
        if(0x00 != landmark.get_result((uint8_t **)&result, &output_size))
        {
            lcd.setCursor(0,0);
            lcd.printf("get landmark result failed\n");

            Serial.printf("get landmark result failed\n");
            break;
        }

        uint16_t face_key_point[5][2];
        for (int j = 0; j < 5; j++)
        {
            face_key_point[j][0] = uint16_t(KPU_Activation::sigmoid(result[2 * j]) * cut_rect.w + cut_rect.x);
            face_key_point[j][1] = uint16_t(KPU_Activation::sigmoid(result[2 * j + 1]) * cut_rect.h + cut_rect.y);
            // 在LCD上标记人脸关键点坐标
            lcd.fillCircle(face_key_point[j][0], face_key_point[j][1], 2, 0x001f);
        }

        float T[3][3];
        // 计算仿射变换矩阵
        face.calc_affine_transform((uint16_t*)face_key_point, (uint16_t*)dst_point, 5, (float*)T);
        // 应用仿射变换
        face.apply_affine_transform(img_ai, img_128x128, (float*)T);

        // 运行人脸属性检测模型
        if(0x00 != face.run_kmodel(img_128x128))
        {
            lcd.setCursor(0,0);
            lcd.printf("run face failed\n");

            Serial.printf("run face failed\n");
            break;
        }

        // 获取人脸属性检测结果
        if(0x00 != face.get_result((uint8_t **)&result, &output_size))
        {
            lcd.setCursor(0,0);
            lcd.printf("get face result failed\n");

            Serial.printf("get face result failed\n");
            break;
        }

        // 显示人脸属性检测结果
        for (int j = 0; j < 4; j++)
        {
            float th = KPU_Activation::sigmoid(result[j]);
            if (th > 0.7)
            {
                lcd.setCursor(cut_rect.x + cut_rect.w, cut_rect.y + j * 16);
                lcd.setTextColor(0xf800);
                lcd.printf("%s", pos_face_attr[j]);
            }
            else
            {
                lcd.setCursor(cut_rect.x + cut_rect.w, cut_rect.y + j * 16);
                lcd.setTextColor(0x001f);
                lcd.printf("%s", neg_face_attr[j]);
            }
        }
    }

    lcd.refresh();
}