《DNK210使用指南 -SDK版 V1.0》第三十章 YOLO2 20种物体检测实验

第三十章 YOLO2 20种物体检测实验


       从本章开始,将通过几个实例介绍Kendryte K210上的KPU功能及其在官方SDK下的使用方法,本章将先介绍在裸机SDK编程环境下实现基于YOLO2网络的20种物体检测应用。通过本章的学习,读者将学习到使用SDK编程技术实现基于YOLO2网络的20种物体检测应用。

       本章分为如下几个小节:

       30.1 KPU介绍

       30.2 硬件设计

       30.3 程序设计

       30.4 运行验证


        30.1 KPU介绍

       前三十章我们主要讲解了Kendryte K210一些基础外设的使用,这仅仅是简单的MCU应用,并不能体现Kendryte K210的优势。前面有提到,Kendryte K210是一款集成了机器视觉与机器听觉能力的系统级芯片(SoC),内置了用于计算卷积人工神经网络的 KPU 与用于处理麦克风阵列输入的 APU,算力达到1TOPS,能够实现多种AI模型的应用,下面我们介绍Kendryte K210内置的KPU模块。

       Kendryte K210片上拥有一个KPU,KPU是通用的神经网络处理器,它可以在低功耗的情况下实现卷据神经网络的计算,实时获取被检测目标的大小、坐标和种类,对人脸或者物体进行检测和分类。

       Kendryte K210片上KPU的主要特点如下所示:

       1. 支持计算多层卷积神经网络,每层卷积神经网络的控制参数可单独配置

       2. 支持中断模式,可配置加速器在每层卷积结束后是否产生中断信号

       3. 支持输入图像片上存储,存储容量大小为2M字节,卷积结果可由DMA读出

       4. 支持输入输出通道数目、输入输出图像行高列宽可配置,其中通道数目范围在1~1024之间,输出行高列宽与输入相同,或者是输入处于2或者4且向下取整

       5. 支持两种卷积内核,分别为1*1和3*3,卷积步长为1,十种池化方式,包括bypass、步长为1且大小为2*2的均值、步长为1且大小为2*2的最大值、步长为2且大小为2*2的均值、步长为2且大小为2*2的最大值、步长为2且大小为2*2的左上值、步长为2且大小为2*2的右上值、步长为4且大小为4*4的均值、步长为4且大小为4*4的最大值、步长为4且大小为4*4的左上值

       6. 支持两种padding方式,分别为任意填充和取最近值

       7. 支持在输入图像行高超过256时,自动对卷积结果进行抽样,仅保留奇数行奇数列结果

       8. 支持卷积参数、批归一化参数、激活参数配置,AI加速器主动读取,读取地址可配置

       9. 支持卷据参数片上存储,存储容量为72K字节,可以边卷积边读取卷据参数,每层网络最多可以读取64次

       10. 支持mobilenet-V1

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

       12. 非实时工作最大支持网络参数大小为Flash大小扣除软件占用大小

       两种工况的神经网络参数如下表所示:


表30.1.1 KPU网络参数情况


       KPU的内部结构如下图所示:


图30.1.1 KPU内部结构框图


       Kendryte K210官方SDK提供了多个操作KPU模块的函数,但是我们实际用到的并不多,这里我们只简单讲述几个常用的的函数,这些函数介绍如下:


       1, kpu_load_kmodel函数

       该函数主要用于加载kmodel,需要与nncase配合使用,需要注意的是,nncase V2.0以上已放弃对K210的支持,所以K210可以使用的nncase最新版本为V1.9。该函数原型及参数描述如下代码所示:

int kpu_load_kmodel(kpu_model_context_t *ctx, const uint8_t *buffer);
 
/* KPU任务配置参数 */
typedef struct
{
    int is_nncase;
 
    union
    {
        struct
        {
            const uint8_t *model_buffer;
            uint8_t *main_buffer;
            uint32_t output_count;
            const kpu_model_output_t *outputs;
            const kpu_model_layer_header_t *layer_headers;
            const uint8_t *body_start;
            uint32_t layers_length;
            volatile uint32_t current_layer;
            const uint8_t *volatile current_body;
            dmac_channel_number_t dma_ch;
            kpu_done_callback_t done_callback;
            void *userdata;
        };
 
        struct
        {
            void* nncase_ctx;
            uint32_t nncase_version;
        };
    };
} kpu_model_context_t;

       该函数共有两个配置参数,第一个参数为配置KPU任务的句柄,第二个参数为配置要加载的kmodel数据。kmodel数据可以直接在Kendryte K210上运算,其他类型的模型数据需要借助nncase神经网络模型转换工具转换为kmodel数据才能在Kendryte K210上运行。


       2,kpu_model_free函数

       该函数用于释放加载kmodel数据的buffer,如下代码所示:

void kpu_model_free(kpu_model_context_t *ctx);

       该函数仅有一个参数,参数为对应的任务句柄,当我们不需要使用某个KPU任务时,可以将对应的kmodel数据释放掉,节省内存。


       3,kpu_run_kmodel函数

       该函数用来运行kmodel,代码如下所示:

int kpu_run_kmodel(kpu_model_context_t *ctx, const uint8_t *src, dmac_channel_number_t dma_ch, kpu_done_callback_t done_callback, void *userdata);

       函数共有5个参数,第一个参数为KPU任务的句柄,第二个参数为源数据,通常为RGB888数据,第三个参数为DMA通道,用于传输源数据到KPU中,最后两个参数为回调函数和回调函数的参数,我们这里用于指示KPU运算完成,该函数返回0表示运算成功,其他表示失败。


       4,kpu_get_output函数

       该函数用于获取KPU最终处理的结果,代码如下所示:

int kpu_get_output(kpu_model_context_t *ctx, uint32_t index, uint8_t **data, size_t *size);

       该函数共有四个参数,第一个参数是任务的句柄,第二个参数是结果的索引值,第三个参数为结果的数据,第四个参数为数据的长度。注意:后面三个参数的结果均与kmodel数据有关,不同的kmodel输出的结果不同。


        30.2 硬件设计


       30.2.1 例程功能


       1. 获取摄像头输出的图像,并送入KPU进行基于YOLO2的20种物体检测模型运算,后将运算结果和摄像头输出的图像一起显示在LCD上。


       30.2.2 硬件资源

       本章实验内容,主要讲解KPU模块的使用,无需关注硬件资源。


       30.2.3 原理图

       本章实验内容,主要讲解KPU模块的使用,无需关注原理图。


        30.3 程序设计


       30.3.1 KPU处理文件介绍

       通过kpu_get_output函数读取的KPU运算结果的数据并不方便我们使用,我们可以借助region_layer.c和region_layer.h文件的程序帮助我们解析数据,通过这两个文件我们可以读取到检车目标的坐标、图像分类等数据。region_layer.c和region_layer.h文件均来自官方DEMO,我们先看region_layer.h文件,里面主要有两个结构体,我们后续实验也会经常用到这两个结构体,代码如下:

typedef struct
{
    uint32_t obj_number;
    struct
    {
        uint32_t x1;
        uint32_t y1;
        uint32_t x2;
        uint32_t y2;
        uint32_t class_id;
        float prob;
    } obj[10];
} obj_info_t;

       这个结构体是用来存储识别对象的坐标,成员共有两个,第一个成员是识别对象的数量,第二个成员为存放对象的坐标,该结构体有6个参数,分别是识别对象的两个坐标点、序号、相似度。通过这种结构体内嵌结构体的方式使我们很方便就能获取识别对象的数量和读取对应的坐标。

       另外个是解析KPU运算结果的参数配置结构体,其代码如下:

typedef struct
{
    float threshold;   /* 相似度阈值*/
    float nms_value;   /* 合并识别出的临近的框*/
    uint32_t coords;   /* 每个框的坐标数 */
    uint32_t anchor_number; /* 锚点数量的一半*/
    float *anchor;         /*锚点,训练前确认*/
    uint32_t image_width;  /*图片宽度*/
    uint32_t image_height; /*图片高度*/
    uint32_t classes;      /* 测试的分类或者序号*/
    uint32_t net_width;     /* 输入网络层的尺寸,通常输入图片原始尺寸 */
    uint32_t net_height;
    uint32_t layer_width;  /* 模型最后个layer层的尺寸 */
    uint32_t layer_height;
    uint32_t boxes_number;  /*框的数量*/
    uint32_t output_number;
    void *boxes;          
    float *input;
    float *output;
    float *probs_buf;
    float **probs;       /*概率大小*/
} region_layer_t;

       这里很多参数都是根据模型确定好的,如:anchor_number、anchor、classes、net_width、net_height、layer_width、layer_height这些都是跟运行的模型有关,我们不能随意更改,否者会出现问题,大家谨记,threshold这个我们常会用到,用于设置可靠性的阈值。

       下面我们简单介绍下region_layer.c文件常用的几个函数。代码如下所示:

int region_layer_init(region_layer_t *rl, int width, int height, int channels, int origin_width, int origin_height)
{
    int flag = 0;
 
    rl->coords = 4;
    rl->image_width = 320;
    rl->image_height = 240;
 
    rl->classes = channels / 5 - 5;
    rl->net_width = origin_width;
    rl->net_height = origin_height;
    rl->layer_width = width;
    rl->layer_height = height;
    rl->boxes_number = (rl->layer_width * rl->layer_height * rl->anchor_number); 
    rl->output_number = (rl->boxes_number * (rl->classes + rl->coords + 1));
 
    rl->output = malloc(rl->output_number * sizeof(float));
    if (rl->output == NULL)
    {
        flag = -1;
        goto malloc_error;
    }
    rl->boxes = malloc(rl->boxes_number * sizeof(box_t));
    if (rl->boxes == NULL)
    {
        flag = -2;
        goto malloc_error;
    }
    rl->probs_buf = malloc(rl->boxes_number * (rl->classes + 1) * sizeof(float));
    if (rl->probs_buf == NULL)
    {
        flag = -3;
        goto malloc_error;
    }
    rl->probs = malloc(rl->boxes_number * sizeof(float *));
    if (rl->probs == NULL)
    {
        flag = -4;
        goto malloc_error;
    }
    for (uint32_t i = 0; i < rl->boxes_number; i++)
        rl->probs[i] = &(rl->probs_buf[i * (rl->classes + 1)]);
    return 0;
malloc_error:
    free(rl->output);
    free(rl->boxes);
    free(rl->probs_buf);
    free(rl->probs);
    return flag;
}

       该函数是数据解析的参数配置初始化函数,里面均是根据模型确定好了,我们不能随意更改,需要注意的是,参数channels用于识别类的数量,函数内的转换公式:rl->classes = channels / 5 - 5;也就是说当识别的种类数为1时,参数channels要设置为30,这也可以通过可视化软件Netron在模型中最后层输出看到。

int max_index(float *a, int n)
{
    int i, max_i = 0;
    float max = a[0];
 
    for (i = 1; i < n; ++i)
    {
        if (a[i] > max)
        {
            max   = a[i];
            max_i = i;
        }
    }
    return max_i;
}

       此函数用于获取相似度最高的类的序号,常用于图像分类的模型。

static void region_layer_output(region_layer_t *rl, obj_info_t *obj_info)
{
    uint32_t obj_number = 0;
    uint32_t image_width = rl->image_width;
    uint32_t image_height = rl->image_height;
    uint32_t boxes_number = rl->boxes_number;
    float threshold = rl->threshold;
    box_t *boxes = (box_t *)rl->boxes;
    
    for (int i = 0; i < boxes_number; ++i)
    {
        int class  = max_index(rl->probs[i], rl->classes);
        float prob = rl->probs[i][class];
 
        if (prob > threshold)
        {
            box_t *b = boxes + i;
            obj_info->obj[obj_number].x1 = b->x * image_width - (b->w * 
image_width / 2);
            obj_info->obj[obj_number].y1 = b->y * image_height - (b->h * 
image_height / 2);
            obj_info->obj[obj_number].x2 = b->x * image_width + (b->w * 
image_width / 2);
            obj_info->obj[obj_number].y2 = b->y * image_height + (b->h * 
image_height / 2);
            obj_info->obj[obj_number].class_id = class;
            obj_info->obj[obj_number].prob = prob;
            obj_number++;
        }
    }
    obj_info->obj_number = obj_number;
}

       此函数用于获取相似度大于所设置的阈值的类,并将坐标,类的序号,相似度放在obj_info_t结构体中,我们可以在该结构体中读取数据并使用。该函数常用于目标检测,可以实现多个物体同时检测。

void region_layer_draw_boxes(region_layer_t *rl, callback_draw_box callback)
{
    uint32_t image_width = rl->image_width;
    uint32_t image_height = rl->image_height;
    float threshold = rl->threshold;
    box_t *boxes = (box_t *)rl->boxes;
 
    for (int i = 0; i < rl->boxes_number; ++i)
    {
        int class  = max_index(rl->probs[i], rl->classes);
        float prob = rl->probs[i][class];
 
        if (prob > threshold)
        {
            box_t *b = boxes + i;
            uint32_t x1 = b->x * image_width - (b->w * image_width / 2);
            uint32_t y1 = b->y * image_height - (b->h * image_height / 2);
            uint32_t x2 = b->x * image_width + (b->w * image_width / 2);
            uint32_t y2 = b->y * image_height + (b->h * image_height / 2);
            callback(x1, y1, x2, y2, class, prob);
        }
    }
}

       此函数用于获取相似度大于所设置的阈值的类,并将坐标,类的序号,相似度等数据作为回调参数传入回调函数中。该函数和上个函数非常相似,不同的是该函数处理之后将相似度大于阈值的数据直接传入回调函数中,我们可以在回调函数中进行画框标记等操作。


       30.3.2 main.c代码

       main.c中的代码如下所示:

INCBIN(model, "yolo.kmodel");
 
uint8_t *disp;
uint8_t *ai;
 
static float g_anchor[ANCHOR_NUM * 2] = {1.08, 1.19, 3.42, 4.41, 6.63, 11.38, 9.42, 5.11, 16.62, 10.52};
char *classes_lable[20] =
{
    "aeroplane", "bicycle", "bird", "boat", "bottle",
    "bus", "car", "cat", "chair", "cow",
    "diningtable", "dog", "horse", "motorbike", "person",
    "pottedplant", "sheep", "sofa", "train", "tvmonitor"
};
 
static volatile uint8_t ai_done_flag;
 
/* KPU运算完成回调 */
static void ai_done_callback(void *userdata)
{
    ai_done_flag = 1;
}
 
void draw_boxes_callback(uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2, uint32_t label, float prob)
{
    if (x1 >= 320)
    {
        x1 = 319;
    }
    if (x2 >= 320)
    {
        x2 = 319;
    }
    if (y1 >= 240)
    {
        y1 = 239;
    }
    if (y2 >= 240)
    {
        y2 = 239;
    }
 
    // printf("(%d, %d) (%d, %d) label: %s prob: %f\r\n", x1, y1, x2, y2, classes_lable[label], prob);
    draw_box_rgb565_image((uint16_t *)disp, 320, x1, y1, x2, y2, GREEN);
    draw_string_rgb565_image((uint16_t *)disp, 320, 240, x1, y1, classes_lable[label], BLUE);
    // lcd_draw_string(x1, y1,classes_lable[label],0xF800);
}
 
int main(void)
{
    kpu_model_context_t task;
    float *output;
    size_t output_size;
    region_layer_t detect_rl;
 
    sysctl_pll_set_freq(SYSCTL_PLL0, 800000000);
    sysctl_pll_set_freq(SYSCTL_PLL1, 400000000);
    sysctl_pll_set_freq(SYSCTL_PLL2, 45158400);
    sysctl_clock_enable(SYSCTL_CLOCK_AI);
    sysctl_set_power_mode(SYSCTL_POWER_BANK6, SYSCTL_POWER_V18);
    sysctl_set_power_mode(SYSCTL_POWER_BANK7, SYSCTL_POWER_V18);
    sysctl_set_spi0_dvp_data(1);
 
    lcd_init();
    lcd_set_direction(DIR_YX_LRUD);
    camera_init(24000000);
    camera_set_pixformat(PIXFORMAT_RGB565);
    camera_set_framesize(320, 240);
 
    if (kpu_load_kmodel(&task, (const uint8_t *)model_data) != 0)
    {
        printf("Kmodel load failed!\n");
        while (1);
    }
 
    detect_rl.anchor_number = ANCHOR_NUM;
    detect_rl.anchor = g_anchor;
    detect_rl.threshold = 0.5;
    detect_rl.nms_value = 0.2;
    region_layer_init(&detect_rl, 10, 7, 125, 320, 240);
    
    while (1)
    {
        if (camera_snapshot(&disp, &ai) == 0)
        {
            ai_done_flag = 0;
            if (kpu_run_kmodel(&task, (const uint8_t *)ai, DMAC_CHANNEL5, 
ai_done_callback, NULL) != 0)
            {
                printf("Kmodel run failed!\n");
                while (1);
            }
            while (ai_done_flag == 0);
 
            if (kpu_get_output(&task, 0, (uint8_t **)&output, &output_size) != 0)
            {
                printf("Output get failed!\n");
                while (1);
            }
 
            detect_rl.input = output;
            region_layer_run(&detect_rl, NULL);
            region_layer_draw_boxes(&detect_rl, draw_boxes_callback);           
            lcd_draw_picture(0, 0, 320, 240, (uint16_t *)disp);
            camera_snapshot_release();
        }
    }
}

       yolo.kmodel是20种物体检测的模型,网络运算的图片大小为320*240。数组classes_lable用于存放所识别目标序号对应得物体名称,因为经过模型运算得结果通常为序号,不方便我们直观的区分检测目标得物体名称,所以需要转换下。draw_boxes_callback回调函数负责将物体用绿色方块标记出来,并将物体得名称写入LCD 显示缓存中。

       可以看到一开始是先初始化了LCD和摄像头,然后加载YOLO2 20种物体检测网络需要用到的网络模型,并初始化YOLO2网络,配置region_layer_t结构体参数的数据。

       对于region_layer_t的结构体参数anchor_number、anchor,还有region_layer_init初始化函数的后面五个参数,都跟网络模型有关,不可随意更改,特别注意的是,region_layer_init函数的第四个参数和识别的种类相关,内部的转换公式是:rl->classes = channels / 5 – 5,也就是说当识别的种类数为1时,参数channels要设置为30。

       最后在一个循环中不断地获取摄像头输出的图像,并将其送入KPU中进行运算,然后再进行YOLO2网络运算,接着将KPU运算的结果传入region_layer相关文件进行解析,最后将检测到的物体信息通过draw_boxes_callback回调函数绘制到LCD显示器上。


        30.4 运行验证

       将DNK210开发板连接到电脑主机,通过VSCode将固件烧录到开发板中,将摄像头对准物体,让其采集到物体图像,随后便能在LCD上看到摄像头输出的图像,同时图像中的物体均被绿色的矩形框框出,并在矩形框内的左上角标出了物体的名称和置信度,如下图所示:


图30.4.1 LCD显示YOLO2 20种物体检测结果


请使用浏览器的分享功能分享到微信等