【MAX78000第二季】— 基于MAX78000的无线抄表
2023年MAX78000人工智能应用设计大赛第二季由ADI公司赞助电子森林举办。
标签
开发板
人工智能
边缘AI
YANG_1
更新2024-01-10
1180

前言:

   无线抄表的设计灵感来源于2023上海慕尼黑展上所看到的一个Demo,一个由珠海零边界展出的一个智能水表方案。在现实生活中,水表是一种常用的设备,一般需要工作人员去现场查看并且手工记录数据,整个过程繁琐枯燥,虽然现在物联网式水表正在取代,但是价格昂贵,所以此无线抄表目标是解决此费时费力的过程,低成本、低功耗、高效率无疑是此设计的重要点,所以ADI的MAX7800X系列很适合此设计,通过部署AI模型在MCU端,在MCU端进行推理,可以准确快速地识别读数,只需要将推理结果返回到云端,可以有效的保护用户的数据隐私,节省网络带宽,以便其他需要高带宽使用的情景。

FpjN9YpgglArZOE7au-J-_6TMDrp


整体设计:

   在前期准备中,最重要的数字识别功能方案有多种想法:

  1. 使用传统的模板匹配,通过摄像头进行数据采集,选择合适的阈值,图像二值化,基于图像灰度跳变频率定位数字区域,再将数字区域与模板库中进行比较计算相识值,值最大则为正确字符
  2. 先对于摄像头获取的数据进行固定裁剪,裁剪出一个个尺寸固定的数字再分别输入到神经网络中进行识别,也就是分类任务
  3. 使用YOLO、OCR和SSD等目标检测

最后确定的参考官方demo使用TinySSD,软件总体框架和代码分组如图:

Fjb3KTW2wXl2KxO2Zg9wgpvQu6kJFiXNW9NS5tCLq8Oytu1eFDMkrtQD


AI开发过程:

FgFx0dW11EmkcwTCVvW5kt_nZBgf

   环境搭建主要参考官方readme文件中的步骤,因为笔记本没有显卡,所以在整个开发过程中我选择的是服务器上进行的,我主要参考此篇文章https://www.eetree.cn/project/detail/1333,环境为PyTorch  1.8.1  Python  3.8(ubuntu18.04)  Cuda  11.1,然后使用Anaconda创建max7800所需的环境,在此不多赘述。


收集数据制作训练集和测试集:

   在此阶段中MAX78000FTHR上板载的CMOS VGA图像传感器(OVM769)进行图像的拍照采集,主要参考了官方demo中的“ImgCapture”,具体修改部分如下:

char id_c[10];
uint16_t id_car = 0;
int main(void)
{
// Initialization...
int ret = 0;
int slaveAddress;
int id;
g_app_settings.dma_mode = USE_DMA;
g_app_settings.imgres_w = IMAGE_XRES;
g_app_settings.imgres_h = IMAGE_YRES;
g_app_settings.pixel_format = PIXFORMAT_RGB565; // This default may change during initialization

/* Enable cache */
MXC_ICC_Enable(MXC_ICC0);

/* Set system clock to 100 MHz */
MXC_SYS_Clock_Select(MXC_SYS_CLOCK_IPO);
SystemCoreClockUpdate();

Bsp_Led_Init();
MXC_GPIO2->out_clr = MXC_GPIO_PIN_0;
MXC_GPIO2->out_clr = MXC_GPIO_PIN_1;
MXC_GPIO2->out_clr = MXC_GPIO_PIN_2;
sd_mount();

MXC_DMA_Init();
g_app_settings.dma_channel = MXC_DMA_AcquireChannel();

camera_init(CAMERA_FREQ);

slaveAddress = camera_get_slave_address();

ret = camera_get_manufacture_id(&id);
if (ret != STATUS_OK) {
printf("Error returned from reading camera id. Error %d\n", ret);
return -1;
}
printf("Camera ID detected: %04x\n", id);

g_app_settings.imgres_w = 120;
g_app_settings.imgres_h = 100;

while (1) {

if(id_car < 1000)
{
cnn_img_data_t img_data = stream_img(g_app_settings.imgres_w, g_app_settings.imgres_h,
g_app_settings.pixel_format,
g_app_settings.dma_channel);

save_stream_sd(img_data, id_c);
id_car ++;
sprintf(id_c, "%d", id_car);
MXC_Delay(5000000);
}
else
{
LED_Toggle(LED_GREEN);
MXC_Delay(500000);
}
}
}

   思路为直接设置图片尺寸,我所采集的图片尺寸固定为100*120,不使用串口通信确定拍摄照片的命令,直接设置拍照参数并且延时拍摄1000张照片,通过SD卡保存数据,再通过官方提供的batchconvert.py脚本将SD卡中的数据转化为PNG格式的照片,在这里需要注意的是我参考的为官方demo中的"ImgCapture",里面有两种保存图片的方式"imgres"和"capture","imgres"可以直接保存到SD卡中,但是图片必须为32的倍数,也就是W*H必须是32的倍数,我一开始计划拍摄100*120尺寸的图片(相比于官方ssd中的74*74更加适合我的水表数字表盘部分,刚刚好拍摄到数字部分并且上下部分没有过多的干扰),但是后面发现标注后的100*120尺寸的图片进行放缩可能对于实际表现效果不太好,又尝试重新制作74*74的数据集,但是发现不能正常储存到SD卡中,才发现必须要为32的倍数,而"capture"是通过"transmit_capture_uart"配合"console.py"直接保存在电脑上的,然后我又修改了“ImgCapture”和"console.py"拍摄74*74尺寸的照片直接保存在电脑上制作数据集,主要修改部分如图:

FgdeeOIGhYTbxrraQhJUJ25w_J2z

FnfowpUinE5cG6wnKFez9YL4QpXV

FmeuUCogbkdSILy7t7BH9FjMowvI

   在服务器端的ai8x-training-develop/data下创建MY_DATASETS目录,我编写了将图片按照2:8的比例划分的my_8_2.py脚本,将拍摄的照片分为测试集和训练集分别存放在对应的文件夹下。MY_DATASETS文件夹目录结构如下:

FksTiNIf4klLQA18ex2uKep41Eul

  • test:存放测试集图片和经过标注生成的对应的.xml文件
  • Labellmg:标注软件
  • processd:存放生成的train_info.csv test_info.csv以及由训练过程生成的对应.pkl文件
  • train:存放训练集图片和经过标注生成的对应的.xml文件
  • my_xml_to_csv.py:用于将.xml文件转化为训练所需要的.csv格式
  • my_8_2.py:将图片划分为2 :8的训练集和测试集

   my_8_2.py文件如下:

import os
import shutil
import random

def copy_images(source_folder, dest_folder1, dest_folder2, ratio=0.8):
# 获取源文件夹中的所有图片文件
image_files = [f for f in os.listdir(source_folder) if f.endswith(('.jpg', '.png', '.jpeg'))]

# 计算分配给第一个目标文件夹的图片数量
num_images_dest1 = int(len(image_files) * ratio)

# 随机选择图片并复制到两个目标文件夹
random.shuffle(image_files)

for i, file_name in enumerate(image_files):
source_path = os.path.join(source_folder, file_name)

if i < num_images_dest1:
dest_path = os.path.join(dest_folder1, file_name)
else:
dest_path = os.path.join(dest_folder2, file_name)

shutil.copyfile(source_path, dest_path)

if __name__ == "__main__":
# 设置源文件夹和目标文件夹
source_folder = "C:\\Users\\pyl\\python\\MY_DATASETS\\my_test"
dest_folder1 = "C:\\Users\\pyl\\python\\MY_DATASETS\\train"
dest_folder2 = "C:\\Users\\pyl\\python\\MY_DATASETS\\test"

# 复制图片
copy_images(source_folder, dest_folder1, dest_folder2, ratio=0.8)


      然后使用labelImg对于PNG图片进行标注,采用VOC格式,再将经过漫长标注过程生成的.xml文件通过my_xml_to_csv.py转化为训练所需格式。 
import os
import glob
import pandas as pd
import xml.etree.ElementTree as ET

def xml_to_csv(path):
xml_list = []

for xml_file in glob.glob(path + '/*.xml'):
tree = ET.parse(xml_file)
root = tree.getroot()

img_name = root.find('filename').text
img_width = int(root.find('size')[0].text)
img_height = int(root.find('size')[1].text)

labels = []
x0_list, y0_list, x1_list, y1_list = [], [], [], [],

for member in root.findall('object'):
label = member.find('name').text #对应标签
labels.append(label)
x0_list.append((member[4][0].text)) #对应x0
y0_list.append((member[4][1].text)) #对应y0
x1_list.append((member[4][2].text)) #对应x1
y1_list.append((member[4][3].text)) #对应y1

num_of_boxes = len(labels)
label_str = f"[{', '.join(labels)}]"
x0_str = f"[{', '.join(map(lambda x: '{:.1f}'.format(float(x)), x0_list))}]"
y0_str = f"[{', '.join(map(lambda y: '{:.1f}'.format(float(y)), y0_list))}]"
x1_str = f"[{', '.join(map(lambda x: '{:.1f}'.format(float(x)), x1_list))}]"
y1_str = f"[{', '.join(map(lambda y: '{:.1f}'.format(float(y)), y1_list))}]"

x0_list = [round(float(x), 1) for x in x0_list]
x1_list = [round(float(x), 1) for x in x1_list]
y0_list = [round(float(y), 1) for y in y0_list]
y1_list = [round(float(y), 1) for y in y1_list]

width_str = f"[{', '.join(map(str, [abs(x0 - x1) for x0, x1 in zip(x0_list, x1_list)]))}]"
height_str = f"[{', '.join(map(str, [abs(y0 - y1) for y0, y1 in zip(y0_list, y1_list)]))}]"

xml_list.append([img_name, label_str, width_str, height_str, x0_str, y0_str, x1_str, y1_str, num_of_boxes, img_width, img_height])

column_names = ['img_name', 'label', 'width', 'height', 'x0', 'y0', 'x1', 'y1', 'num_of_boxes', 'img_width', 'img_height']
xml_df = pd.DataFrame(xml_list, columns=column_names)


xml_df['bb_x0'] = xml_df['x0'].apply(lambda x: min(eval(x)))
xml_df['bb_y0'] = xml_df['y0'].apply(lambda y: min(eval(y)))
xml_df['bb_x1'] = xml_df['x1'].apply(lambda x: max(eval(x)))
xml_df['bb_y1'] = xml_df['y1'].apply(lambda y: max(eval(y)))

return xml_df

def main():
for folder in ['train_1', 'test_1']:
image_path = os.path.join(os.getcwd(), ('./' + folder))
xml_df = xml_to_csv(image_path)
xml_df.to_csv(('processed/'+folder+'_info.csv'), index=None)
print('Successfully converted xml to csv.')

main()

FmXv5c0onXUWcf4W2lXclhlUbEf4

  • img_name:图片名称
  • label:对应的标签
  • width和height:图片对应标注框的宽度和高度
  • x0、y0、x1、y1:图片对应标注框的左上角坐标和右下角坐标
  • num_of_boxes:标签的数量
  • img_width和img_height:图片的尺寸
  • bb_x0、bb_y0、bb_x1和bb_y1:全部标注框中最左上角和最右下角

模型搭建和训练:

   我所使用的训练指令为:

python train.py --deterministic --print-freq 200 --epochs 100 --optimizer Adam --lr 0.001 --wd 0 --model my_ssd_test_15 --use-bias --momentum 0.9 --weight-decay 5e-4 --dataset MY_DATASETS_15 --device MAX78000 --obj-detection --obj-detection-params parameters/obj_detection_params_svhn.yaml --batch-size 16 --qat-policy policies/qat_policy_svhn.yaml --validation-split 0

      事实上训练所需要的最重要的文件为model和dataset,也就是 my_ssd_test_15和MY_DATASETS_15,需要放到ai8x-training-develop\models和ai8x-training-develop\datasets下,其他的参数如parameters/obj_detection_params_svhn.yaml 和 --qat-policy policies/qat_policy_svhn.yaml为设置压缩和学习速率计划和定义 QAT 的策略,这些参数可以在开发后期进行调整,我直接使用了官方train_svhn_tinierssd.sh中的参数设置。

      模型上由于dataset中已经放缩为74*74,所以我直接使用了ai85net-tinierssd.py,其中需要了解的是def create_prior_boxes(aspect_ratios=default_aspect_ratios, device='cpu')返回的prior_boxes数量,生成了多少个预测框。其计算方法如下:

FqyLbcp4QCFK-jNWBcxS0veSNKWJ

       官方只使用了TinySSD论文中的Fire8、Fire9、Fire10和Conv12-2进行Detections,MAX78000只能够返回CNN输出的locs、classes_scores和基础卷积层,并不能自己返回预测框和预测值,这意味着我们需要自己使用C代码实softmax归一化、nms非极大值抑制等操作,最后输出预测框和预测值,不过官方demo中提供了post_process.h/.c解析此类CNN输出的方法,我也是在其中修改去达到效果的,事实上post_process.h/.c中卸载CNN输出结果使用了很多个极大的数组去保存类别分数,softmax输出的类别分数等,SSD、YOLO系列目标检测需要生成大量的预测框,最直接的影响就是导致出现超出ram的现象,一下子干到20k导致线程的栈直接不够使用了,或者是编译器通过了可以下载但是出现栈动态TFT显示异常,虽然官方的demo没有问题,但是我尝试移植到FreeRTOS任务中上再添加需要高频率中断的其他功能是直接不能运行的,可能需要通过信号量等去同步避免资源互斥,我认为官方之所以没有使用TinySSD论文中的Fire4、Fire8、Fire9、Fire10、Conv12-2和Conv13-2可能也是考虑了RAM限制的影响,我参考了其他demo并没有发现更好的卸载CNN输出的方法。

   Fv5bBLQip2iFroByuBR0K5CVC-VXFnIXira7jrwa_Zw0kXc02_6QNcR1

    使用官方的模型结构mAP率在我的数据集上可以达到0.995575,再此之后我尝试了其他模型结构,如只使用Fire9、Fire10/Fire9、Conv12-2//Fire8、Conv12-2等结构去尝试降低预测框生成的数量,使用Fire9、Conv12-2的mAP仅仅只有0.495左右,使用Fire8、Conv12-2可达 0.985146,但是其他数字识别效果和预测框个数稍稍低于使用官方的模型结构,事实上它是矛盾的,如果想要更好的识别效果只能增加预测框的数量,为了减少C代码中占用RAM大小,就只能减少预测框的数量,只使用Fire8、Conv12-2d model和两种模型检测结果如下:

from math import sqrt

import torch
import torch.nn.functional as F
from torch import nn

import ai8x
import utils.object_detection_utils as obj_detect_utils


class TinySSDBase(nn.Module):

def __init__(self, **kwargs):
super().__init__()

# Standard convolutional layers
self.fire1 = ai8x.FusedConv2dBNReLU(3, 32, 3, padding=1, **kwargs)
self.fire2 = ai8x.FusedConv2dBNReLU(32, 32, 3, padding=1, **kwargs)

self.fire3 = ai8x.FusedMaxPoolConv2dBNReLU(32, 64, 3, padding=1, **kwargs)
self.fire4 = ai8x.FusedConv2dBNReLU(64, 64, 3, padding=1, **kwargs)

self.fire5 = ai8x.FusedMaxPoolConv2dBNReLU(64, 64, 3, padding=1,
pool_size=3, **kwargs)
self.fire6 = ai8x.FusedConv2dBNReLU(64, 64, 3, padding=1, **kwargs)
self.fire7 = ai8x.FusedConv2dBNReLU(64, 128, 3, padding=1, **kwargs)
self.fire8 = ai8x.FusedConv2dBNReLU(128, 32, 3, padding=1, **kwargs)

self.fire9 = ai8x.FusedMaxPoolConv2dBNReLU(32, 32, 3, padding=1,
**kwargs)

self.fire10 = ai8x.FusedMaxPoolConv2dBNReLU(32, 32, 3, padding=1,
pool_size=3, **kwargs)

def forward(self, image):

out = self.fire1(image) # (N, 32, 50, 60)
out = self.fire2(out) # (N, 32, 50, 60)

out = self.fire3(out) # (N, 64, 50, 60)
fire4_feats = self.fire4(out) # (N, 64, 50, 60)

out = self.fire5(fire4_feats) # (N, 64, 25, 30)
out = self.fire6(out) # (N, 64, 25, 30)
out = self.fire7(out) # (N, 128, 25, 30)
fire8_feats = self.fire8(out) # (N, 32, 25, 30)

fire9_feats = self.fire9(fire8_feats) # (N, 32, 12, 15)

fire10_feats = self.fire10(fire9_feats) # (N, 32, 6, 7)

return fire4_feats, fire8_feats, fire9_feats, fire10_feats


class AuxiliaryConvolutions(nn.Module):

def __init__(self, **kwargs):
super().__init__()

# Auxiliary/additional convolutions on top of the VGG base
self.conv12_1 = ai8x.FusedConv2dBNReLU(32, 16, 3, padding=1, **kwargs) # (N, 16, 6, 7)
self.conv12_2 = ai8x.FusedMaxPoolConv2dBNReLU(16, 16, 3, padding=1, **kwargs) # (N, 16, 3, 3)

self.init_conv2d()

def init_conv2d(self):

for c in self.children():
if isinstance(c, nn.Conv2d):
nn.init.xavier_uniform_(c.weight)
if c.bias:
nn.init.constant_(c.bias, 0.)

def forward(self, fire10_feats):

out = self.conv12_1(fire10_feats)
conv12_2_feats = self.conv12_2(out)

return conv12_2_feats


class PredictionConvolutions(nn.Module):

def __init__(self, n_classes, **kwargs):

super().__init__()

self.n_classes = n_classes

n_boxes = {'fire8': 4,
# 'fire9': 4,
# 'fire10': 2,
'conv12_2': 4}

# 4 prior-boxes implies we use 4 different aspect ratios, etc.
self.loc_fire8 = ai8x.FusedConv2dBN(32, n_boxes['fire8'] * 4, kernel_size=3, padding=1, #产生 n_boxes['fire8'] * 4 个输出通道
**kwargs)

self.loc_conv12_2 = ai8x.FusedConv2dBN(16, n_boxes['conv12_2'] * 4, kernel_size=3,
padding=1, **kwargs)

# Class prediction convolutions (predict classes in localization boxes)
self.cl_fire8 = ai8x.FusedConv2dBN(32, n_boxes['fire8'] * n_classes, kernel_size=3,
padding=1, **kwargs)

self.cl_conv12_2 = ai8x.FusedConv2dBN(16, n_boxes['conv12_2'] * n_classes, kernel_size=3,
padding=1, **kwargs)

# Initialize convolutions' parameters
self.init_conv2d()

def init_conv2d(self):

for c in self.children():
if isinstance(c, nn.Conv2d):
nn.init.xavier_uniform_(c.weight)
if c.bias:
nn.init.constant_(c.bias, 0.)

def forward(self, fire4_feats, fire8_feats, conv12_2_feats):

batch_size = fire4_feats.size(0)


l_fire8 = self.loc_fire8(fire8_feats)
l_fire8 = l_fire8.permute(0, 2, 3, 1).contiguous()
l_fire8 = l_fire8.view(batch_size, -1, 4)


l_conv12_2 = self.loc_conv12_2(conv12_2_feats)
l_conv12_2 = l_conv12_2.permute(0, 2, 3, 1).contiguous()
l_conv12_2 = l_conv12_2.view(batch_size, -1, 4)


c_fire8 = self.cl_fire8(fire8_feats)
c_fire8 = c_fire8.permute(0, 2, 3, 1).contiguous()
c_fire8 = c_fire8.view(batch_size, -1, self.n_classes)


c_conv12_2 = self.cl_conv12_2(conv12_2_feats)
c_conv12_2 = c_conv12_2.permute(0, 2, 3, 1).contiguous()
c_conv12_2 = c_conv12_2.view(batch_size, -1, self.n_classes)

# Concatenate in this specific order (i.e. must match the order of the prior-boxes)

locs = torch.cat([ l_fire8, l_conv12_2], dim=1)
classes_scores = torch.cat([ c_fire8, c_conv12_2],
dim=1)


# print("类别分数的长度" , len(classes_scores))
# print("类别分数的维度" , classes_scores.shape)
return (locs, classes_scores)

class MY_TinierSSD(nn.Module):

default_aspect_ratios = (
(0.95, 0.6, 0.4, 0.25),
(0.95, 0.6, 0.4, 0.25),
)

def __init__(self, num_classes,
num_channels=3,
dimensions=(74, 74),
aspect_ratios=default_aspect_ratios,
device='cpu',
**kwargs):
super().__init__()

# print("类别数量", num_classes)

self.n_classes = num_classes

self.base = TinySSDBase(**kwargs)
self.aux_convs = AuxiliaryConvolutions(**kwargs)
self.pred_convs = PredictionConvolutions(self.n_classes, **kwargs)


self.device = device
self.priors_cxcy = self.__class__.create_prior_boxes(aspect_ratios=aspect_ratios,
device=self.device)


def forward(self, image):

# print("模型图片输入维度", image.shape)

fire4_feats, fire8_feats, fire9_feats, fire10_feats = self.base(image)

conv12_2_feats = self.aux_convs(fire10_feats)

locs, classes_scores = self.pred_convs(fire4_feats, fire8_feats, conv12_2_feats)

return locs, classes_scores


def create_prior_boxes(aspect_ratios=default_aspect_ratios, device='cpu'):

fmap_dims = {
'fire8': 18,
'conv12_2': 2}

fmaps = list(fmap_dims.keys())

obj_scales = {
'fire8': 0.15,
'conv12_2': 0.26}

if len(aspect_ratios) != len(fmaps):
raise ValueError(f'aspect_ratios list should have length {len(fmaps)}')

if True in (len(aspect_ratios_list) !=
len(MY_TinierSSD.default_aspect_ratios[0])
for aspect_ratios_list in aspect_ratios):
raise ValueError(f'Each aspect_ratios list should have length \
{len(MY_TinierSSD.default_aspect_ratios[0])}')

aspect_ratios = {
'fire8': aspect_ratios[0],
'conv12_2': aspect_ratios[1]}

prior_boxes = []

for k, fmap in enumerate(fmaps):
for i in range(fmap_dims[fmap]):
for j in range(fmap_dims[fmap]):
cx = (j + 0.5) / fmap_dims[fmap]
cy = (i + 0.5) / fmap_dims[fmap]

for ratio in aspect_ratios[fmap]:
prior_boxes.append([cx, cy, obj_scales[fmap] * sqrt(ratio),
obj_scales[fmap] / sqrt(ratio)])


if ratio == 1.:
try:
additional_scale = sqrt(obj_scales[fmap] *
obj_scales[fmaps[k + 1]])

except IndexError:
additional_scale = 1.
prior_boxes.append([cx, cy, additional_scale, additional_scale])


prior_boxes = torch.FloatTensor(prior_boxes).to(device)
prior_boxes.clamp_(0, 1)

print("预测框数量", len(prior_boxes))
return prior_boxes


def detect_objects(self, predicted_locs, predicted_scores, min_score, max_overlap, top_k):

batch_size = predicted_locs.size(0)
n_priors = self.priors_cxcy.size(0)
predicted_scores = F.softmax(predicted_scores, dim=2)

# Lists to store final predicted boxes, labels, and scores for all images
all_images_boxes = []
all_images_labels = []
all_images_scores = []


assert n_priors == predicted_locs.size(1) == predicted_scores.size(1)

for i in range(batch_size):
# Decode object coordinates from the form we regressed predicted boxes to
decoded_locs = obj_detect_utils.cxcy_to_xy(
obj_detect_utils.gcxgcy_to_cxcy(predicted_locs[i], self.priors_cxcy))

# Lists to store boxes and scores for this image
image_boxes = []
image_labels = []
image_scores = []

# Check for each class
for c in range(1, self.n_classes):
# Keep only predicted boxes and scores where scores for this class are above the
# minimum score
class_scores = predicted_scores[i][:, c]
score_above_min_score = class_scores > min_score
n_above_min_score = score_above_min_score.sum().item()
if n_above_min_score == 0:
continue
class_scores = class_scores[score_above_min_score]
class_decoded_locs = decoded_locs[score_above_min_score] # (n_qualified, 4)

# Sort predicted boxes and scores by scores
class_scores, sort_ind = class_scores.sort(dim=0, descending=True)
# (n_qualified), (n_min_score)
class_decoded_locs = class_decoded_locs[sort_ind] # (n_min_score, 4)

# Find the overlap between predicted boxes
overlap = obj_detect_utils.find_jaccard_overlap(class_decoded_locs,
class_decoded_locs)
# (n_qualified, n_min_score)

# Non-Maximum Suppression (NMS)

# A torch.bool tensor to keep track of which predicted boxes to suppress
# True implies suppress, False implies don't suppress
suppress = torch.zeros((n_above_min_score), dtype=torch.bool).to(self.device)
# (n_qualified)

# Consider each box in order of decreasing scores
for box in range(class_decoded_locs.size(0)):
# If this box is already marked for suppression
if suppress[box]:
continue

# Suppress boxes whose overlaps (with this box) are greater than maximum
# overlap
# Find such boxes and update suppress indices
suppress = torch.logical_or(suppress, overlap[box] > max_overlap)
# The max operation retains previously suppressed boxes, like an 'OR' operation

# Don't suppress this box, even though it has an overlap of 1 with itself
suppress[box] = False

# Store only unsuppressed boxes for this class
image_boxes.append(class_decoded_locs[~suppress])
image_labels.append(
torch.LongTensor((~suppress).sum().item() * [c]).to(self.device))
image_scores.append(class_scores[~suppress])

# If no object in any class is found, store a placeholder for 'background'
if len(image_boxes) == 0:
image_boxes.append(torch.FloatTensor([[0., 0., 1., 1.]]).to(self.device))
image_labels.append(torch.LongTensor([0]).to(self.device))
image_scores.append(torch.FloatTensor([0.]).to(self.device))

# Concatenate into single tensors
image_boxes = torch.cat(image_boxes, dim=0) # (n_objects, 4)
image_labels = torch.cat(image_labels, dim=0) # (n_objects)
image_scores = torch.cat(image_scores, dim=0) # (n_objects)
n_objects = image_scores.size(0)

# Keep only the top k objects
if n_objects > top_k:
image_scores, sort_ind = image_scores.sort(dim=0, descending=True)
image_scores = image_scores[:top_k] # (top_k)
image_boxes = image_boxes[sort_ind][:top_k] # (top_k, 4)
image_labels = image_labels[sort_ind][:top_k] # (top_k)

# Append to lists that store predicted boxes and scores for all images
all_images_boxes.append(image_boxes)
all_images_labels.append(image_labels)
all_images_scores.append(image_scores)

return all_images_boxes, all_images_labels, all_images_scores # lists of length batch_size


def my_ssd_test_28(pretrained=False, **kwargs):
"""
Constructs a Tinier SSD model
"""
assert not pretrained
return MY_TinierSSD(aspect_ratios=MY_TinierSSD.default_aspect_ratios, **kwargs)


models = [
{
'name': 'my_ssd_test_28',
'min_input': 1,
'dim': 2,
}
]

FvJ2lhjo6fQ-XJUOQoj-8enWer2QFvEsoWNREcI2_DKZdGWwB6pc0NHS

参考链接:


Data Loader设计

      也就是对应着 MY_DATASETS_15 的设计,PyTorch中dataset类用来处理单个训练样本,比如如何读取训练样本、标签、对训练样本进行变形等等,dataloader是对于多个训练样本而言的,通过dataloader将单个训练样本变成为mini-batch。官方在train.py中帮我们实现了PyTorch中dataloader的实现,我们只需要注重PyTorch中dataset的实现以及dataloader加载函数实现。

FoohSKVS2CvpQ_vy9ZiThtkL9JKZFicvnXo2XpiS9_s82HFjX8OHPk8_

   Data Loader设计对于模型训练至关重要,用于读取模型训练所需要的数据集以及标注等相关信息,对于数据进行预处理等。

FouLT3PRuUaNaVLC1EDm8HTzQPd_

   Data Loader设计至少应该包含以下三个部分:

  • Dataset class definition:PyTorch中自定义数据集加载,编写__len__,返回数据集的大小,编写__getiten__,基于索引返回一个训练样本以及其对应标签,编写__init__,根据实际所需内容编写,一般有数据集目录、输入数据维度等等。
  • Data loader function:dataloader加载函数,通过load_train和load_test区分载入数据为测试集还是训练集,并且转换数据范围,将输入数据转换为Tensor。有大致固定格式。

FslU43DY7C19qA96NcYhhjpUgdu0

   返回两个数据集,用于训练和测试,主要需要实现的为自定义的my_datasets()怎么获取数据和数据的标注信息。

  • Datasets dictionary:描述数据集并指向数据加载函数的数据结构。基本格式如下:

         1:name:数据集名称(唯一),标识作用,用于在train.py指向数据集

         2:input:输入数据维度

         3:output:输出的字符串或者数字,即分类、识别的结果

         4:loader:指向数据集的数据加载函数

Fsq8heZh4XW7scJun3pdo1-hE5pk

   我重写了datasets,主要修改为def __gen_datasets(self)和def __create_info_df_csv(self)处,使用my_xml_to_csv.py直接生成所需格式的csv文件,对原始image 3*100*120进行放缩为3*74*74,并且对box坐标进行相应的变换以保证一致性,再生成pkl文件,在目标检测任务中pytorch默认的collate_fn不能满足我们的需求,我们需要自己重新定义collate_fn函数,在def collate_fn(batch)指定dataloader每迭代处一个batch的数据是什么样的数据格式,因为我们写了collate_fn,所以我们需要在datasets中添加'collate': MY_DATASETS.collate_fn,以保证可以在训练中正确执行。

   设计datasets最重要的就是其返回的__len__,__getitem__返回的及其维度是否于模型所定义的相一致。

FsPHslhMAmD1fEwhrNPu3ULSfyVN

import ast
import errno
import os
import pickle
import random
import sys

import numpy as np
import torch
from torch.utils.data import Dataset
from torchvision import transforms

import h5py
import pandas as pd
from PIL import Image
import matplotlib.pyplot as plt

import ai8x

class MY_DATASETS(Dataset):
def __init__(self, root_dir, d_type, transform=None, resize_size=(74, 74)):

if d_type not in ('test', 'train'):
raise ValueError("d_type can only be set to 'test' or 'train'")

if resize_size[0] != 74:
raise ValueError('Input size error')

if resize_size[1] != 74:
raise ValueError('Input size error')

self.root_dir = root_dir
self.d_type = d_type
self.transform = transform
self.resize_size = resize_size
self.info_df = pd.DataFrame()

self.img_list = []
self.boxes_list = []
self.lbls_list = []

self.processed_folder = os.path.join(root_dir, self.__class__.__name__,'processed')
self.__makedir_exist_ok(self.processed_folder)

res_string = str(self.resize_size[0]) + 'x' + str(self.resize_size[1])

train_pkl_file_path = os.path.join(self.processed_folder, 'train_' + res_string + '_fold_' + '.pkl')
test_pkl_file_path = os.path.join(self.processed_folder, 'test_' + res_string + '_fold_' + '.pkl')

if self.d_type == 'train':
self.pkl_file = train_pkl_file_path
self.info_df_csv_file = os.path.join(self.processed_folder, 'train_info.csv')
elif self.d_type == 'test':
self.pkl_file = test_pkl_file_path
self.info_df_csv_file = os.path.join(self.processed_folder, 'test_info.csv')
else:
print(f'Unknown data type: {self.d_type}')
return

self.__create_info_df_csv()
self.__create_pkl_file()

def __create_info_df_csv(self):

if os.path.exists(self.info_df_csv_file):
self.info_df = pd.read_csv(self.info_df_csv_file)

for column in self.info_df.columns:
if column in ['label', 'x0', 'x1', 'y0', 'y1']:
self.info_df[column] = \
self.info_df[column].apply(ast.literal_eval)
else:
print("worry")

def __create_pkl_file(self):
if os.path.exists(self.pkl_file):
(self.img_list, self.boxes_list, self.lbls_list) = \
pickle.load(open(self.pkl_file, 'rb'))

return
self.__gen_datasets()

def __gen_datasets(self):

for _, row in self.info_df.iterrows():
image = Image.open(os.path.join(self.root_dir, self.__class__.__name__, self.d_type,
row['img_name']))

self.img_list.append(image)

boxes = []
for b in range(len(row['x0'])):

x0_new = row['x0'][b]
y0_new = row['y0'][b]
x1_new = row['x1'][b]
y1_new = row['y1'][b]

boxes.append([x0_new, y0_new, x1_new, y1_new])

self.boxes_list.append(boxes)

lbls = row['label']
self.lbls_list.append(lbls)

pickle.dump((self.img_list, self.boxes_list, self.lbls_list), open(self.pkl_file, 'wb'))

def __len__(self):

return len(self.img_list)


def __getitem__(self, index):

# index = 0
if torch.is_tensor(index):
index = index.tolist()

transform = transforms.Compose([
transforms.ToTensor(),
])

img = self.img_list[index]
boxes = self.boxes_list[index]
lbls = self.lbls_list[index]

img = self.__normalize_image(img).astype(np.float32)

if self.transform is not None:
img = self.transform(img)

# Normalize boxes:
boxes = [[box_coord / self.resize_size[0] for box_coord in box] for box in boxes]
boxes = torch.as_tensor(boxes, dtype=torch.float32)
labels = torch.as_tensor(lbls, dtype=torch.int64)

# print("getitem的图片", img.shape)
return img, (boxes, labels)

def __makedir_exist_ok(self, dirpath):
try:
os.makedirs(dirpath)
except OSError as e:
if e.errno == errno.EEXIST:
pass
else:
raise

def __normalize_image(self, image):
image = np.array(image)
return image / 256

def collate_fn(batch):

images = []
boxes_and_labels = []

for b in batch:
images.append(b[0])
boxes_and_labels.append(b[1])

images = torch.stack(images, dim=0)

return images, boxes_and_labels

def get_my_datasets(data, load_train=True, load_test=True, resize_size=(74, 74)):

(data_dir, args) = data

if load_train:
train_transform = transforms.Compose([
transforms.ToTensor(),
ai8x.normalize(args=args) #if args is not None else transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

train_dataset = MY_DATASETS(root_dir=data_dir, d_type='train',
transform=train_transform, resize_size=resize_size)
else:
train_dataset = None

if load_test:
test_transform = transforms.Compose([
transforms.ToTensor(),
ai8x.normalize(args=args) #if args is not None else transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

test_dataset = MY_DATASETS(root_dir=data_dir, d_type='test',
transform=test_transform, resize_size=resize_size)
else:
test_dataset = None

return train_dataset, test_dataset

def get_my_datasets_ls(data, load_train=True, load_test=True):

return get_my_datasets(data, load_train, load_test, resize_size=(74, 74))

datasets = [
{
'name': 'MY_DATASETS_28',
'input': (3, 74, 74),
'output': (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
'loader': get_my_datasets_ls,
'collate': MY_DATASETS.collate_fn
}
]

参考链接:


量化与评估模型:

   量化与评估模型并没有需要特别注意的地方,只需要参考ai8x-synthesis-develop\scripts和ai8x-training-develop\scripts中的脚本,并且注意量化后的mAP率是否下降的比较厉害。

   我所使用的指令分别为:

python quantize.py trained/my_test_ssd_28_1.pth.tar trained/my_test_ssd_28_1-q.pth.tar --device MAX78000 -v
python train.py --deterministic --print-freq 200 --batch-size 16 --model my_ssd_test_28 --use-bias --dataset MY_DATASETS_28 --device MAX78000 --obj-detection --obj-detection-params parameters/obj_detection_params_svhn.yaml --qat-policy policies/qat_policy_svhn.yaml --evaluate -8 --exp-load-weights-from ../ai8x-synthesis/trained/my_test_ssd_28_1-q.pth.tar --validation-split 0

生成C代码:

   我所使用的指令为:

python ai8xize.py --test-dir demos --prefix my_ssd_test_28 --checkpoint-file trained/my_test_ssd_28_1-q.pth.tar --config-file networks/my_ssd_test_28.yaml --sample-input tests/my_ssd_test_28.npy --fifo --device MAX78000 --timer 0 --display-checkpoint --verbose –overwrite

    其中最重要的为 trained/my_ssd_test_15-q.pth.tar 、networks/my_ssd_test_15.yaml 和 tests/my_ssd_test_15.npy。my_ssd_test_15.npy的生成比较简单,仅需要参考https://github.com/MaximIntegratedAI/ai8x-synthesis#generating-a-random-sample-input并且注意size中的H与W,和输出地址。

import os
import numpy as np

a = np.random.randint(-128, 127, size=(3, 74, 74), dtype=np.int64)
np.save(os.path.join('tests', 'my_ssd_test_28'), a, allow_pickle=False, fix_imports=False)

YAML设计:

   YAML文件是使用ai8xize.py生成C代码必不可缺的一部分,非常重要,YAML文件必须与训练所使用的模型相匹配。

   YAML类似于JSON,编写模型对应的YAML文件需要注意以下几点:

      1:大小写敏感,需要区分大小写

      2:使用缩进表示层级关系,不允许使用Tab,只能使用空格,并且使用空格间隔

      3:缩进的空格数不重要,相同层级的元素左对齐即可

全局配置:

   arch:  与 dataset:  是必须的,分别指定了训练所使用的模型与数据集名称,关键字后跟一个空格填写models中对应的'name'和训练所使用的数据集名称。

   接下来就是网络结构中各层的描述,以ai85nascifarnet模型对应的cifar10-nas.yaml文件为例。

每层配置:

  • out_offset:  写入输出数据的数据内存实例内的相对偏移量。用于构造Data Memory Ping-Pong,对于数据维度相对较小的简单网络,第一层在0x0000位置数据载入,在0x4000位置输出,第二层在0x4000位置数据载入,在0x0000位置输出,形成Data Memory Ping-Pong,0x0000与0x4000交替,以此类推,这样做的目的是避免覆盖两个连续层的输入输出,形成连续性。
  • processors:  指定哪些处理器将处理输入数据。处理器映射必须与输入通道数和输入数据格式匹配,一共有64个处理器,对应0xffffffffffffffff,也就是16个f对应64个处理器,一个f对应4个处理器,输入数据的通道数与其对应。

         如self.conv1_3 = ai8x.FusedConv2dBNReLU(32, 64, 3, stride=1, padding=1, bias=bias, batchnorm='NoAffine', **kwargs)对应的为0x00000000ffffffff

         如输入通道数超过64,则要多次执行,如输入通道数为100,则需要执行两次,52是4的下一个倍数,所以对应为0x000fffffffffffff

  • operation:  此层的主操作,如conv2d、conv1d、Linear等
  • kernel_size:  卷积核大小,硬件固定只能 conv2d为 3x3 / 1x1 , conv1d为 1 / 9 ,ConvTranspose2d为 3x3
  • pad:  原图周围需要填充的格子行(列)数,无填充的话卷积到边缘会直接忽略该边缘,设置卷积的填充
  • activate:  激活函数 ReLU / Abs / None
  • max_pool:  池化大小  MaxPool[H, W]中的 H  W 可以不同,范围为1~16

         在第一层中需要指定 data_format: 的格式为 HWC 还是 CHW(如果不填写则默认为HWC,官方推荐小于90*91时使用HWC),如果为 HWC格式则processors:  按照上述填写,如果为CHW格式则processors:  第一层需要处理器连接到不同的权重内存实例,data_format定义了如何将输入数据写入数据存储器,但它和模型的训练方式没有关系,也就是说不管你模型训练过程中datasets中 'input' :为(3, 100, 120)还是(3, 120, 100)。

         如:输入通道数为3,HWC为0x0000000000000007,CHW为0x0000000000000111。

FkoAt6I4Ge6rg2Fn9keql3cEuYrP

FsIE_OqzYNd9RqU2wYBG1_OVqqlh

  • output_width:  指定输出,8 / 32 ,仅在最后一层中使用,若使用则要使用 wide=True进行训练,并且不使用激活函数
  • flatten:  将2D 输入数据应转换为 1D 数据,对应Linear,在MAX78000/MAX78002中,C*H*W不超过16384

FlZykNE8Dd1sxIm91Vy9ZtFcblyy

    YAML文件用于描述神经网络结构,以硬件为中心的方式描述模型结构,它直接反映了CNN输出如何卸载,如图:

FlP57wIuUahc1UkSysT94dDzgBB9

      在YAML文件的编写中最容易出现错误的为“out_offset”与“in_offset”,这两个字段决定了从哪里输入数据存储器(Data Memory)、从哪里输出。因为一组四个处理器共享一个数据存储器(Data Memory),官方建议使用4、16、64的倍数以充分利用硬件。“out_offset”与“in_offset”错误最容易出现以下几种问题:

      1:类似于ERROR: Layer 7 (backbone_conv8): HWC (4 channels/word) 8-bit 50x60 output (size 12000) with output offset 0x5dc0 and expansion 1x exceeds data memory instance size of 32768

Fpgh_NtgSPUDQ1F0s_cYL6kaw02F

      这种问题出现的原因是定义的“out_offset”+ 50*60*4 > 一个数据存储器(Data Memory)的大小(MAX78000为0x8000,MAX78002为0x14000),所以我们需要减小定义的“out_offset”。其中 *4是因为每一个数据存储器包含32位4通道,并且与下一个地址相差4字节,如假设处理器1为0x4000,则处理器2为0x4004,以此类推。

      2:类似于 Processor 0: Layer 6 output for CHW=0,0,0 is overwriting input at offset 0x00401000 that was created by layer 5, CHW=0,17,4.

FmqTewOJZdsm66HIHNxh3G2HGpSU

   这种问题是因为 这一层的输入覆盖了上一层的输出,假设Layer 5的“out_offset”为0x2000,输出为12*10,那么Layer 6的"out_offset"必须设置为一个大于  0x2000+4*12*10 = 0x21E0的地址,避免造成覆盖。

   3:类似于 WARNING: Layer 5: All bias values are zero. Ignoring the input.

Fpgq75wQJkXDc743db7A4PTRD4ia

   这种报错需要检查所使用trained/的是否为量化后的,还有一种情况是数据集太少,模型的每一层不能正确训练,需要尝试增加数据集或者降低训练时候的学习率 --lr 0.001,以保证模型每一层能够充分训练。

   至此整个AI开发跑完,生成了相应的C代码,其中我们只需要 cnn.c cnn.h weight.h softmax.c(分类任务需要),事实上这些指令中其他参数在开发初期并不重要,我们只需要掌握上述所说的文件编写,就可以快速上手完成整个开发流程,其他参数只需要根据自己开发的类型去参考官方相应类型的参数即可,这些其他参数并不是重点,如我在量化直接参考quantize_svhn_tinierssd.sh,并没有去尝试post-training quantization这种量化方式,上述文件的编写、数据集制作和AI部署后的实际表现,才是需要开发前期着重注意的,其他参数可以在后期优化调试的时候着重观察其对于AI部署表现的影响。

   参考链接:


结构与PCB设计:

   我在网络上购买了一个水表,拆除镜片等不需要的部件,刮除商标等有影响的文字,使用N20减速电机驱动数字表盘。

FgAuADAj-Se4Fio3a3eNqi4GZdiXFsS0hZszKTMu9gWyv7p5tZCJplrf

   将MAX78000FTHR上的摄像头对准水表上的数字表盘部分,确定大概位置,预留出MAX78000FTHR的Micro USB连接器的连接处,预留出TFT固定孔,使用螺纹与水表外壳固定,打印出3D模型。

FmsfkFqwWC0jk8P-74G0Cpu6q_MwFplyPjU28UpxlYkP0ahSsTjD5Ow-

   PCB设计的主体是移远EC800M-CN 4G模块,用于与云端进行互联,与云端互联意味着你可以有更多操作,如OTA升级、微信小程序上控制水表水阀开关、修改水价等等。MAX78000FTHR的LPUART与EC800M-CN相连,MAX78000FTHR与TFT屏相连接,剩余未使用的引脚全部引出用于尝试备用,详细Kicad工程于附件处。

Fm1bKa9RjJVphT7xOk-IGYgzrYQv

   最终的实物图:

Fkmi_fbLDnHjb1suYpFIhSZ3C-15


C代码开发:

   我的整个工程基于官方提供的空白Vscode工程模板(MaximSDK\Tools\VSCode-Maxim\Inject),我将其复制出来重新命名为New_Project,我在此空白模板上编写了Led、Button和Uart等基础驱动,编写TFT、SD卡、EC800M和摄像的应用层。按照官方文档移植了FreeRTOS,摄像头获取图片数据,TFT显示摄像头获取的图片,EC800M将CNN推理的结果上传到云端,SD卡存储CNN推理的结果。SD卡主要使用App_Sd_Write()将CNN推理的结果写入到test.csv文件中,主要使用了SDHC库提供的FatFS文件系统的API。

FuQ-s4vaTNet8MZ99uyua1ERvJpj

int App_Sd_Write(char *date_num)
{

UINT wrote = 0;
uint32_t fil_size;
/* 挂载文件系统 */
if (!mounted)
mount();
/* 打开文件夹*/
sd_err = f_opendir(&sd_dir, "/"); /* 如果不带参数,则从当前目录开始 */
if (sd_err != FR_OK)
{
f_mount(NULL, "", 0);
return sd_err;
}
/* 打开test.csv */
sd_err = f_open(&sd_file, "test.csv", FA_OPEN_ALWAYS | FA_WRITE);
if (sd_err != FR_OK)
{
printf("Error opening file: %s\n", FF_ERRORS[sd_err]);
f_mount(NULL, "", 0);
return sd_err;
}
else
{
fil_size = f_size(&sd_file);
sd_err = f_lseek(&sd_file, fil_size);
/* 向test.csv中写入数据 */
sd_err = f_write(&sd_file, date_num, strlen(date_num), &wrote);
}
/* 关闭文件 数据才真正的写入到 SD 卡 */
if ((sd_err = f_close(&sd_file)) != FR_OK)
{
printf("Error closing file: %s\n", FF_ERRORS[sd_err]);
return sd_err;
}
/* 关闭文件夹 */
f_closedir(&sd_dir);
/* 卸载文件系统 f_mount可以上电后仅调用一次 */
/* f_mount(NULL, "", 0); */
umount();
Bsp_Uart_Init();

return MAX_OK;
}

   在嵌入式开发过程中,可能会出一些需要注意的部分。

1:在进行嵌入式开发前一定要先将settings.json中的"board"字段定义为"FTHR_RevA",如果没有修改则会在编译链接过程中调用到其他.c中的函数,如MXC_TFT_Init(),在"board"= "FTHR_RevA"时会调用tft_ili9341.c中的MXC_TFT_Init(),如果没有修改则可能调用到官方TFT其他驱动.c文件中的MXC_TFT_Init(),所以一开始就要将其修改。

Flz3FCselColyXAnCZnRlEqmemkr

FpnOJtmB_4m8WHv8T2SvTI_B0RGD

2:如果需要使用官方提供的各种库,如FreeRTOS、SDHC、LVGL,需要在project.mk中进行使能,并且在settings.josn中自行添加相关的路径,官方提供了哪些库和具体的使能关键字可以在MaximSDK\Libraries\ libs.mk中查看,如果需要添加自己所编写的.c .h文件则只需要在工程中包含并且在project.mk中添加路径。

FpeZmsc8VvladslqsKsUZ5O78_Gl

Fj1u5c4M1w2FvfIDMoUoDEQD_LMa

   了解了上述几点就可以进行传统的嵌入式开发了,我在开发过程中主要参考https://analog-devices-msdk.github.io/msdk/USERGUIDE/https://analog-devices-msdk.github.io/msdk/Libraries/PeriphDrivers/Documentation/MAX78000/中的API说明,更多的细节可以和操作需要查看MSDK用户指南。

   关于Ai在嵌入式端的开发我们只需要关注两个方面,即"数据的注入和预处理"与"卸载推理结果和后处理"。

1:数据注入和数据预处理

    数据预处理即规范化输入数据,需要将传感器的数据处理为模型输入所需要的数据类型,如摄像头需要从[0,255]转化到[-128,127]。

FsZXsenyNV4b05kmyFpGzzXj3p-i

   数据注入按照生成C代码时是否使用 --fifo 区分。

不使用—fifo时,生成C代码时会自动生成” load_input”函数,它大概的格式如图,它使用memcpy32函数将input_0中的数据每一次复制32bit的数据到0x50400000,这个地址为CNN数据存储器(cnn data memory)的起始地址。

FgPhVVQDZ0bgCcXYw46v6-F4NMRU

   在更复杂的情况下,如"digit-detection-demo"中,需要自己重新修改"load_input",需要转化相机数据格式,并将数据加载到地址中,此处地址为0x50402000,对应着YAML文件第一层"in_offset"的偏移量。

FhHBmG7vho_zPq7oXliQ7doDPAcXFsAWWDtohvyBADVxK_ZjTS5tLjUN

使用 -- fifo,在大多数情况下官方建议使用fifo加载数据,它与YAML文件无关,fifo的地址独立于CNN数据存储器之外,0x50000004与50000008事实上为CNN_FIFO_STAT与CNN_FIFO_WR0,在一些特殊情况下,如模型中以100*120输入大于90*91,则必须使用fifo加载数据,并且在YAML文件中定义streaming。

FgZVgepwkyfVHw0Jss3A6ItrdHcp

FjsbPQylErM1h2rRAb2mV4aARln4

   2:卸载推理结果和结果后处理

         分类任务在生成C代码时会自动生成卸载结果并且通过softmax输出概率比,但是目标检测任务中CNN卸载会生成大量数据,并且需要对于这些结果进行softmax归一化、nms非极大值抑制等操作,最后输出预测框和预测值。“digit-detection-demo”中提供了两个卸载函数“get_prior_locs”与”get_prior_cls”。

FmqriVCuzSIU7PExJhEeSLOaMSnAFq_fAFPbgWK_Utljf_RCMqZnM392

   上图为我使用上文所说的仅使用Fire9、Conv12-2d所生成C代码中的”cnn_unload”函数,0x50c13510~0x50c137e0(4*12*15 = 2d0),接下来的4个通道数据位于0x50c1b510(0x50c13510 + 0x8000)~ 0x50c1b7e0,但是接下来的4个通道数据位于0x51003510位于下一个Data memory(0x51000000~0x5101FFFF),此时指针为 0x50c13510+0x8000+0x8000 = 0x50c23510处,所以需要手动调整地址也就是当0x50c23510时加0x003e0000,使得指针指向0x51003510,以此类推,这样就能够正确卸载推理结果了,然后再对推理结果进行后处理,目标检测任务的后处理过程复杂,可以参考“digit-detection-demo”中的"get_prior_locs"和"get_prior_cls"中的"prior_locs"  "prior_cls"以数组为中心,主要操作有对"prior_cls"进行softmax,计算预测框的重叠度,进行非极大值抑制等操作。

Fn4slTZPkOtgnLlUTwp7abXsWMHwFlC_3-e3hS6NHl_2lVfAUBSa0JPx

一些值得参考项目的链接:


总结:

   至此我想你应该了解我做目标检测任务的整个流程,为了能够使用自己制作的数据集,无论是Ai开发中需要编写的文件还是C代码我都做了较大改动,可以对比官方来参考。整篇文章我主要以MAX78000为主体,风格更加倾向于教程,可能遇到的问题、注意点、官方为什么这样写以及解决办法,并且默认有了相关基础,如何环境搭建、如何使用vscode开发这类并没有说明,掌握了整个流程可以完成各类目标检测,如车牌识别、人数统计之类,希望此篇文章能够帮助你完成你的目标检测任务。文章中淡化了我设计中的其他部分,如PCB设计、嵌入式软件部分,这些淡化的部分如果你有兴趣可以参考我所提供的附件,附件工程我已注释掉SD卡与EC800M-CN部分可以直接下载观察效果,在附件仓库链接https://github.com/PYL4869/MAX78000_Release_V0中我提供了可以帮助你复现我项目的更多内容,相比于分类任务目标检测难度较大,你可能需要更多的时间去学习相关的基础知识。整个项目的效果功能展示如Ai、云端互联和存储数据可以观看演示视频。如果需要更好的效果需要增加数据集并且进行微调,自己制作数据集费时费力,公开数据集不一定有并且满足你的要求,大部分情况下都是需要自己制作数据集,制作数据集是一个很煎熬很痛苦的过程。文章中难免有些纰漏之处请多多包涵。

   最后感谢电子森林与ADI举办此次活动以及提供的技术支持。

知识产权归属说明

  • 本次竞赛结束后参赛者项目开源在电子森林,网友可以参考学习
  • 本次竞赛提交的项目及视频归属原作者、ADI、电子森林三方共同拥有,ADI及电子森林拥有发布这些项目和视频的权利
  • 关于大赛规则最终解释权归主办方所有
物料清单
附件下载
My_Project_74_fifo_my_model_test.rar
工程文件
Project_19.rar
Kicad工程
3D_Model.STL
3D外壳文件
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号