基于MAX78000FTHR的汽车Logo识别
该项目使用了undefined,实现了undefined的设计,它的主要功能为:undefined。
标签
嵌入式系统
显示
开发板
MAX78000应用设计大赛
子牧r
更新2024-01-09
北方民族大学
306

一、项目介绍

不知道你的身边有没有这样的人,对车感兴趣,但又不是很懂车,在路上遇到不认识的车时会很好奇,但是又嫌搜起来太麻烦,不会刻意地去搜索,可再次见到时,还是会好奇地想知道是什么车。

笔者正是这样的,在遇到不认识的车的时候,会好奇这是什么牌子的车,但又嫌手机搜起来太麻烦。所以我就在想如果手头能有一个手持设备,只需要简单的拍摄一下汽车的Logo就能获得汽车的品牌信息,那该多方便呀。因此本项目就基于MAX78000FTHR开发板,做了这么一个汽车Logo识别的手持设备。

本项目基于MAX78000FTHR实现对汽车Logo的识别,实现功能如下:

1、通过按键控制本设备拍摄图片并通过屏幕进行显示

2、在确认拍摄到想要的汽车Logo后,通过按键控制控制是否识别Logo信息

3、通过本项目设计的模型对图片进行识别

4、在屏幕上显示识别到的汽车品牌信息

 

本人所作工作如下:

1、基于ADI官方文件设计模型并完成模型的训练、量化、评估和部署

2、基于MAX78000芯片完成软件设计,实现本项目功能

3、基于MAX78000FTHR开发板设计扩展板,完成本项目的硬件设计

 

本项目使用板卡信息如下:

  1. 核心芯片:MAX78000微控制器
  2. 电池电源管理:MAX20303 PMIC
  3. 规格:0.9in x 2.6in、双排连接器,兼容Adafruit Feather Wing外设扩展板
  4. 音频处理:多关键词识别、声音分类、消噪声
  5. 面部识别
  6. 目标检测和分类
  7. 时间序列数据处理:心率/健康信号分析、多传感器分析、预测性维护
  8. 集成外设:
    • RGB指示LED
    • 用户按钮
    • CMOS VGA图像传感器(OVM769)
    • 低功耗、立体声音频编解码器(MAX9867)
    • SPH0645LM4H-B数字麦克风
    • SWD调试器
    • 虚拟UART控制台
    • 10引脚Cortex调试接头,用于RISC-V协处理器

board-pc.png

图1-1 MAX78000FTHR

 

本项目实现过程如下:   (1-3部分内容放到进度中说明,这里仅显示基本信息)

(1)根据官方文档在windows上配置开发环境,使用VScode实现MAX78000的程序开发

(2)学习使用MAX78000芯片,通过官方例程以及各种开源资料,实现MAX78000芯片的串口打印(Hello world例程)、GPIO控制(GPIO例程)、屏幕显示(TFT_Demo例程)和摄像头使用并拍照保存到SD卡中(ImgCapture例程)

(3)配置windows模型训练开发环境,根据MAX78000的AI手册,设计模型并完成模型的训练、量化和评估,实现车辆Logo的识别,并生成相应的文件,导入VScode工程中,通过板卡实现车辆Logo的识别

(4)绘制板卡的扩展板,扩展板功能包括显示屏模块、按键控制模块、电源管理模块和电池电压检测模块

(5)编写最后的代码,实现整体功能

 

二、项目设计思路

使用MAX78000FTHR开发板进行汽车Logo的分类识别,首先需要通过各种例程学习使用MAX78000款芯片,在此基础上学习深度学习相关知识(这是由于本人之前并没有接触过深度学习相关内容),并根据学习官方发布的ai8x-training和ai8x-synthesis工程学习MAX78000的AI相关知识,并根据官方的模型设计适合于汽车Logo识别的模型,最后根据模型的训练-部署流程将模型部署到MAX78000FTHR开发板上,以实现汽车Logo的分类识别。

在硬件上,可以通过开发板+屏幕来实现人机交互,以便于捕获图像进行识别,但因为本次项目要做的是手持设备,所以我们需要设计一个扩展板,将开发板与屏幕连接起来变成一个整体。除了屏幕以外,因为开发板上的按键比较迷你,无法实现方便的按键操作,所以还需要基于GPIO扩展出一些按键。此外,为了能够将该设备随身携带,而不是通过线缆进行供电,该扩展板还需要连接电池进行供电并进行电源管理。

在浏览第一届比赛的优秀作品时,我发现不少人都提到了一个问题,就是在模型训练中使用的数据集是从网络上找到的,训练时的识别率很不错,但在实际识别时却因为拍摄效果原因,识别效果并不是很好。因此如果直接使用MAX78000FTHR开发板拍摄的图片进行训练的话,识别效果应该能有一定的改善。所以、本项目基于扩展板设计拍照程序,以方便的使用MAX78000FTHR开发板拍摄图像。

FvGD9i7SEZzTNBLf3JjGlZcJpIHn

图1-2 设计框图

 

三、搜集素材的思路

1、关于MAX78000FTHR可以从ADI官网查询开发板及其芯片的相关信息,网址如下:

开发板:MAX78000FTHR Evaluation Board | Analog Devices

芯片:MAX78000 Datasheet and Product Info | Analog Devices

2、关于本次比赛可以制作的项目,可以参考电子森林官方发布的信息,链接如下:

MAX78000板卡可做项目:MAX78000板卡项目汇总 - 电子森林 (eetree.cn)

第一届比赛项目汇总:MAX78000FTHR-快速实现超低功耗、人工智能 (AI) 方案 - 电子森林 (eetree.cn)

3、关于板卡模型的信息,可以参考github上官方发布的工程,链接如下:

量化工程链接:https://github.com/MaximIntegratedAI/ai8x-synthesis

训练工程链接:https://github.com/MaximIntegratedAI/ai8x-training

当配置工程和环境出现问题时,也可以查看我在进度3.1中写的教程,里面记录了一下我在配置工程和环境时遇到的问题和针对这些问题的解决办法,估计会有一定的帮助

4、使用MAX78000FTHR进行开发,需要使用集成开发工具,如Eclipse或者VScoede,关于如何使用这些软件进行开发,可以参考官方发布的教程文档,链接如下:

MSDK用户指南:MSDK User Guide - Analog Devices MSDK Documentation (analog-devices-msdk.github.io)

(关于MSDK的安装也可以参看这个链接中Installation的内容,不过因为服务器不在国内,使用这种方式进行安装可能会比较困难,我还是推荐直接解压MSDK的压缩包,希望以后情况能有所改善)

5、关于模型的设计也可以参考以下链接中的内容,来自微信公众号: AI研修 潜水摸大鱼   针对Maxim78000Evaluation Kit开发板的AI开发内容,这对我学习MAX78000FTHR的模型开发设计真的提供了很大帮助,链接如下:

MAX78000 AI开发实战:#maxim78000 (qq.com)

 

四、预训练实现过程及关键代码说明

4.1 数据集

4.1.1 数据集来源

本项目中使用的数据集来源共有三部分,每部分有5种汽车Logo,共计15种,下面分别对三部分来源进行说明。

第一部分是使用MAX78000FTHR+本项目中制作的扩展板拍摄实际汽车Logo获取到的图片,汽车品牌分别是宝马,比亚迪,大众,广汽和马自达。

第二部分是拍摄电脑屏幕上的汽车Logo图片(非实际汽车)获取到的图片,如图4-1所示,汽车品牌分别是保时捷,奔驰,本田,雷克萨斯和五菱。

FrY3Sr17o1EjsvxFeXlAx4BLvLfP

图4-1 拍摄电脑屏幕上的汽车Logo图片

      

第三部分是来自于开源的汽车Logo数据集:GitHub - GeneralBlockchain/vehicle-logos-dataset: A computer vision dataset of vehicle logo segmentation masks. There are 34 logos each with 16 images and masks.,本项目使用该数据集列表中的前5种,分别是三菱,欧宝,西雅特,铃木和丰田,如图4-2所示。

FkVoGkh6TD9KofCSFlQ97KbTZI26

图4-2 开源的汽车Logo数据集

 

在使用开源数据集的图片进行训练之前,需要将该数据集内的图片逆时针旋转90度后再进行训练,之所以这么做是因为本项目中的扩展板屏幕拍摄方向与MAX78000FTHR开发板的正拍摄方向相互垂直导致的,如图4-3所示。

FmepPhYzl-Chh18D4d9A9xmVOtal

图4-3 方向示意图

我们正常握持时,本设备的方向是y方向,而MAX78000FTHR开发板拍摄图片的正方向是x方向,所以正常握持时,拍摄并送到模型里的图片与正常图片相比像是逆时针旋转了90度一样。所以我们在使用开源数据集训练模型时,需要将使用的图片逆时针旋转90。但对于使用MAX78000FTHR+本项目中制作的扩展板拍摄的图片,则不需要处理。

 

4.1.2 数据集生成

在本项目中引用的开源数据集中的图片,每种汽车Logo各有16张,所以我们自己拍摄的图片也是每种使用16张,由此得到本项目中的原始图片,共计15x16=240张。

为保证识别准确度并增加训练集数量,本项目中所有原始图片直接作为测试集,训练集则是参考猫狗识别例程,先在原始图片的基础上,对其先进行中心裁剪和模糊等操作,这样就得到了第二张图片,操作代码如下:

def augment_blur(orig_img):
"""
Augment with center crop and bluring
"""
train_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.CenterCrop((220, 220)),
transforms.GaussianBlur(kernel_size=(5, 5), sigma=(0.1, 5))
])
return train_transform(orig_img)

然后再对第二张图片进行随机抖动,仿射,亮度和模糊等操作,将该操作执行3次,就又得到3张图片,操作代码如下:

def augment_affine_jitter_blur(orig_img):
"""
Augment with multiple transformations
"""
train_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.RandomAffine(degrees=10, translate=(0.05, 0.05), shear=5),
transforms.RandomPerspective(distortion_scale=0.3, p=0.2),
transforms.CenterCrop((180, 180)),
transforms.ColorJitter(brightness=.7),
transforms.GaussianBlur(kernel_size=(5, 5), sigma=(0.1, 5)),
transforms.RandomHorizontalFlip(),
])
return train_transform(orig_img)

由此我们能够得到测试集240张,训练集为240x5=1200张,测试集和训练集的比例为1:5。

确认好数据集后,开始加载和规范化数据集,这里我们将图片大小重定义为128x128,并将图片格式转化为tensor格式,以上两种操作可使用transforms包中的Compose进行组合,代码如下:

transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
ai8x.normalize(args=args)
])

最后使用torchvision包中的datasets包中的ImageFolder读取并处理图片,代码如下:

test_dataset = torchvision.datasets.ImageFolder(root,transform)

其中root是数据集目录,transform即为上一部分代码中提到的规范化数据集操作函数。加载和规范化数据集这部分操作,对于测试和训练数据集是相同的。完整的数据集生成代码如下(见附件ai8x-training/datasets/vehicle_logo.py):

import errno
import os
import shutil
import sys

import torch
import torchvision
from torchvision import transforms

from PIL import Image

import ai8x

torch.manual_seed(0)


def augment_affine_jitter_blur(orig_img):
"""
Augment with multiple transformations
"""
train_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.RandomAffine(degrees=10, translate=(0.05, 0.05), shear=5),
transforms.RandomPerspective(distortion_scale=0.3, p=0.2),
transforms.CenterCrop((180, 180)),
transforms.ColorJitter(brightness=.7),
transforms.GaussianBlur(kernel_size=(5, 5), sigma=(0.1, 5)),
transforms.RandomHorizontalFlip(),
])
return train_transform(orig_img)


def augment_blur(orig_img):
"""
Augment with center crop and bluring
"""
train_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.CenterCrop((220, 220)),
transforms.GaussianBlur(kernel_size=(5, 5), sigma=(0.1, 5))
])
return train_transform(orig_img)


def vehicle_logo_get_datasets(data, load_train=True, load_test=True, aug=3):
"""
Load vehicle_logo dataset
"""
(data_dir, args) = data
path = data_dir
dataset_path = os.path.join(path, "vehicle_logo")
is_dir = os.path.isdir(dataset_path)
if not is_dir:
sys.exit("Dataset not found!")
else:
processed_dataset_path = os.path.join(dataset_path, "augmented")

if os.path.isdir(processed_dataset_path):
print("augmented folder exits. Remove if you want to regenerate")

train_path = os.path.join(dataset_path, "train")
test_path = os.path.join(dataset_path, "test")
processed_train_path = os.path.join(processed_dataset_path, "train")
processed_test_path = os.path.join(processed_dataset_path, "test")
if not os.path.isdir(processed_dataset_path):
os.makedirs(processed_dataset_path, exist_ok=True)
os.makedirs(processed_test_path, exist_ok=True)
os.makedirs(processed_train_path, exist_ok=True)

# create label folders
for d in os.listdir(test_path):
mk = os.path.join(processed_test_path, d)
try:
os.mkdir(mk)
except OSError as e:
if e.errno == errno.EEXIST:
print(f'{mk} already exists!')
else:
raise
for d in os.listdir(train_path):
mk = os.path.join(processed_train_path, d)
try:
os.mkdir(mk)
except OSError as e:
if e.errno == errno.EEXIST:
print(f'{mk} already exists!')
else:
raise

# copy test folder files
test_cnt = 0
for (dirpath, _, filenames) in os.walk(test_path):
print(f'copying {dirpath} -> {processed_test_path}')
for filename in filenames:
if filename.endswith('.png'):
relsourcepath = os.path.relpath(dirpath, test_path)
destpath = os.path.join(processed_test_path, relsourcepath)

destfile = os.path.join(destpath, filename)
shutil.copyfile(os.path.join(dirpath, filename), destfile)
test_cnt += 1

# copy and augment train folder files
train_cnt = 0
for (dirpath, _, filenames) in os.walk(train_path):
print(f'copying and augmenting {dirpath} -> {processed_train_path}')
for filename in filenames:
if filename.endswith('.png'):
relsourcepath = os.path.relpath(dirpath, train_path)
destpath = os.path.join(processed_train_path, relsourcepath)
srcfile = os.path.join(dirpath, filename)
destfile = os.path.join(destpath, filename)

# original file
shutil.copyfile(srcfile, destfile)
train_cnt += 1

orig_img = Image.open(srcfile)

# crop center & blur only
aug_img = augment_blur(orig_img)
augfile = destfile[:-4] + '_ab' + str(0) + '.png'
aug_img.save(augfile)
train_cnt += 1

# random jitter, affine, brightness & blur
for i in range(aug):
aug_img = augment_affine_jitter_blur(orig_img)
augfile = destfile[:-4] + '_aj' + str(i) + '.png'
aug_img.save(augfile)
train_cnt += 1
print(f'Augmented dataset: {test_cnt} test, {train_cnt} train samples')

# Loading and normalizing train dataset
if load_train:
train_transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
ai8x.normalize(args=args)
])

train_dataset = torchvision.datasets.ImageFolder(root=processed_train_path,
transform=train_transform)
else:
train_dataset = None

# Loading and normalizing test dataset
if load_test:
test_transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
ai8x.normalize(args=args)
])

test_dataset = torchvision.datasets.ImageFolder(root=processed_test_path,
transform=test_transform)

if args.truncate_testset:
test_dataset.data = test_dataset.data[:1]
else:
test_dataset = None

return train_dataset, test_dataset



datasets = [
{
'name': 'vehicle_logo',
'input': (3, 128, 128),
'output': ('Benz', 'BWM', 'BYD', 'GAC', 'Honda', 'Lexus', 'Mazda', "Mitsubishi", "Opel", "Porsche", "Seat", "Suzuki", "Toyota", "VW", "WuLing"),
'loader': vehicle_logo_get_datasets,
},
]

4.1.3 数据集图片放置

本项目中的数据集图片需要放置到指定位置后,再调用4.1.2中的数据集生成代码来生成用来训练模型的输入数据。

首先将汽车Logo图片按品牌放置到对应文件夹中,每个品牌一个文件夹,共计15个文件夹(注意文件夹名字用英文),并将上一级文件夹名改为train,如图4-4所示。

FpXa-Wt9XRuKOYVQqE7odzPaGUaN

图4-4 数据集放置文件夹

该图中文件夹的顺序即为4.1.2中完整的数据集生成代码最后,“output"的顺序,如图4-5所示。如果汽车种类发生改变,则需要根据文件夹顺序修改“output"的内容。

FmqpLJ1jmCQA_2axjH07a3jmlvPv

图4-5 dataset-output

最后直接复制train文件夹到同级文件夹中,并修改为test。

 至此数据集图片放置完成,等待训练模型时调用即可。

 

4.2 模型训练及部署

模型训练及部署部分包括三部分,模型设计,模型训练和模型部署。

4.2.1 模型设计

最开始时,本项目模型使用的是猫狗识别模型,当时仅是为了学习模型训练流程,两分类能够进行很好的识别,后来经测试发现,针对本项目的15分类,该模型也能够得到很好的识别效果,所以本项目最终模型通过对猫狗识别模型进行简单修改得到,并命名为ai85vlnet。

在本项目的汽车logo识别模型中,存在有6个卷积层,4个最大池化层和一个线性层,除第一层卷积层和最后一层卷积层外,都是一层卷积层后面连接一层最大池化层。由于本项目进行的是15分类,所以最后的线性层的输出为15。

注意,在本项目中使用的ai8x库是由美信官方封装的库,里面的卷积层函数除了实现卷积功能外,还包含有归一化操作和激活层,函数名为:FusedConv2dReLU,而当卷积层和最大池化层相连时,则可直接使用名为FusedMaxPoolConv2dReLU的函数。

汽车Logo分类模型框架如图4-6所示。

FuyUo4nC4UO-wH1tNCuJorxN1tGp

图4-6 汽车Logo分类模型框架

模型代码如下:

"""
Classification of 15 car logos network for AI85
"""
from torch import nn

import ai8x


class AI85VehicleLogoNet(nn.Module):
"""
Define CNN model for image classification.
"""
def __init__(self, num_classes=15, num_channels=3, dimensions=(128, 128),
fc_inputs=16, bias=False, **kwargs):
super().__init__()

# AI85 Limits
assert dimensions[0] == dimensions[1] # Only square supported

self.conv1 = ai8x.FusedConv2dReLU(num_channels, 16, 3,
padding=1, bias=bias, **kwargs)
# padding=1 -> no change in dimensions -> 16x128x128

# ai8x.FusedMaxPoolConv2dReLU 的输入参数 in_channels, out_channels, kernel_size
self.conv2 = ai8x.FusedMaxPoolConv2dReLU(16, 32, 3, pool_size=2, pool_stride=2,
padding=1, bias=bias, **kwargs)
# pooling, padding=1 -> 32x64x64

self.conv3 = ai8x.FusedMaxPoolConv2dReLU(32, 64, 3, pool_size=2, pool_stride=2,
padding=1, bias=bias, **kwargs)
# pooling, padding 0 -> 64x32x32

self.conv4 = ai8x.FusedMaxPoolConv2dReLU(64, 32, 3, pool_size=2, pool_stride=2,
padding=1, bias=bias, **kwargs)
# pooling, padding 0 -> 32x16x16

self.conv5 = ai8x.FusedMaxPoolConv2dReLU(32, 32, 3, pool_size=2, pool_stride=2,
padding=1, bias=bias, **kwargs)
# pooling, padding 0 -> 32x8x8

self.conv6 = ai8x.FusedConv2dReLU(32, fc_inputs, 3, padding=1, bias=bias, **kwargs)
# padding 0 -> fc_inputsx8x8

self.fc = ai8x.Linear(fc_inputs*8*8, num_classes, bias=True, wide=True, **kwargs)

for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

def forward(self, x): # pylint: disable=arguments-differ
"""Forward prop"""
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
x = self.conv6(x)
x = x.view(x.size(0), -1)
x = self.fc(x)

return x


def ai85vlnet(pretrained=False, **kwargs):
"""
Constructs a AI85CatsDogsNet model.
"""
assert not pretrained
return AI85VehicleLogoNet(**kwargs)


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

 

4.2.2 模型训练

模型的训练流程分为以下步骤:

模型训练->模型量化->模型评估->模型转换

下面分这四步进行说明。

(1)模型训练

打开ai8x-training工程中的train.py文件,该训练文件需要输入参数,本项目训练参数如下所示:

--epochs 120 --optimizer Adam --lr 0.001 --wd 0 --deterministic --compress policies/schedule-vehicle-logos.yaml --model ai85vlnet --dataset vehicle_logo --confusion --param-hist --embedding --device MAX78000

各参数含义如下:

--epochs 训练轮数,本项目一共训练120轮

--optimizer 优化器,本项目使用Adam

--lr 学习率,本项目为0.001

--wd 权重衰退,本项目为0

--deterministic 确保可重复生产的确定性执行

--compress 用于修剪模型的配置文件

--model 用于训练的模型

--dataset 数据集

--confusion 混淆矩阵

--param-hist 将参数张量直方图记录到文件中,启用该功能会占用大量磁盘空间

--embedding 嵌入层

--device 训练的目标设备

train.py文件还可以设置更多的输入参数,可直接将输入参数设置为-h,然后运行train.py文件,就可以输出所有可输入参数的详细信息。

将本项目的输入参数复制到train.py的运行参数中去,如图4-7所示

FvIZQy07hXkasf9W2GhEMbqrNzaD

图4-7 train.py的运行参数设置

 

然后点击运行train.py文件,它会自动加载我们刚输入的参数,并开始训练。

训练完成后,会在logs文件夹内生成下一步需要的量化需要的文件,如图4-8所示。

Fkhiq5G4kGLcQdg-SFjRWlrPe_W9

图4-8 train.py生成的文件

 

其中.log文件是模型训练的日志,记录了模型训练过程中的详细内容,其中最后一轮训练的内容如下所示:

2023-12-08 20:50:49,250 - ==> Best [Top1: 98.333   Top5: 100.000   Sparsity:0.00   Params: 71088 on epoch: 119]
2023-12-08 20:50:49,250 - Saving checkpoint to: logs\2023.12.08-193101\qat_checkpoint.pth.tar
2023-12-08 20:50:49,267 - --- test ---------------------
2023-12-08 20:50:49,267 - 240 samples (256 per mini-batch)
2023-12-08 20:51:07,387 - Test: [ 1/ 1] Loss 0.004003 Top1 100.000000 Top5 100.000000
2023-12-08 20:51:07,909 - ==> Top1: 100.000 Top5: 100.000 Loss: 0.004

2023-12-08 20:51:07,909 - ==> Confusion:
[[16 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 16 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 16 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 16 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 16 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 16 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 16 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 16 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 16 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 16 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 16 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 16 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 16 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 16 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16]]

可以看到,在完成最后一轮的训练后,对测试集中的原始数据可以达到100%的识别率。

 

(2)模型量化

将上一步生成的best.pth.tar文件复制到ai8x-synthesis工程下的trained文件夹下并重命名为vehicle_logo.pth.tar。

打开quantize.py文件,并为该文件配置运行参数,如图4-9所示

运行参数为:

trained/vehicle_logo.pth.tar trained/vehicle_logo-q.pth.tar --device MAX78000 -v

FoyvlTXuXDUxWiYc-ciHkrhSx_le

图4-9 quantize.py的运行参数设置

 

其中第一个参数是待量化文件路径,第二个参数是量化后生成文件的存储路径,均可根据需要自定义修改,最后一个参数的目标设备。

配置完成后运行quantize.py文件,运行完成后在trained文件中生成的vehicle_logo-q.pth.tar即为量化后的模型文件。

(3)模型评估

在得到量化后的模型文件之后,我们可以切换回ai8x-training工程进行模型的评估。

模型评估使用的文件和模型训练使用的是同一个文件,只不过是运行参数不同,这里我们对train.py的运行参数进行修改即可进行模型的评估,评估输入参数如下:

--model ai85vlnet --dataset vehicle_logo --confusion --evaluate --exp-load-weights-from ../ai8x-synthesis/trained/vehicle_logo-q.pth.tar -8 --device MAX78000 

运行train.py文件,运行结果如图4-10:

FglIRurDJ-AG9mKMOaMv5Sf60uRt

图4-10 模型评估结果

 

我们可以发现,量化后的模型的识别效果相较于量化前有所降低,这是因为模型的量化过程是将模型的参数由浮点型数据转变为整型,会对模型的识别能力有所影响。但量化后模型的识别效果仍然很不错,能够满足本项目的使用。

(4)模型转换

模型转换是将训练好的的模型转换为可以烧录进单片机内的工程文件,以便于我们方便的部署模型。

再次切换到量化工程中,这次我们需要使用到ai8xize.py文件,同样,我们需要给该文件配置运行参数来完成模型的转换,该文件的运行参数如下:

--verbose --test-dir demos --prefix the-project-of-vehivle-logo --checkpoint-file trained/vehicle_logo-q.pth.tar --config-file networks/vehicle_logo-hwc.yaml --sample-input tests/sample_vehicle_logo.npy --fifo --device MAX78000 --softmax

各参数含义如下:

--verbose 详细输出(默认值:false)

--test-dir 生成的单片机工程的存放路径

--prefix 生成的单片机工程的名称

--checkpoint-file 包含量化权重的检查点文件

--config-file 包含层配置的YAML配置文件

--sample-input 示例数据输入文件名(默认:tests/sample_dataset.npy')

--fifo 使用fifo来加载流数据(默认值:false)

--device 目标设备

--softmax 增加软件softmax功能(默认为false)

其中,我们要准备三个文件,第一个是--checkpoint-file-包含量化权重的检查点文件,即我们在模型量化中已经获得的vehicle_logo-q.pth.tar文件。

第二个文件是--config-file-包含层配置的YAML配置文件,该文件用于描述模型,该文件的编写可参考这个链接:MaximAI_Documentation/Guides/YAML Quickstart.md at master · MaximIntegratedAI/MaximAI_Documentation · GitHub

针对本项目模型的YAML配置文件如下:

---
# HWC (big data) configuration for Vehicle-Logo classification

arch: ai85vlnet
dataset: vehicle_logo

# Define layer parameters in order of the layer sequence
layers:
- pad: 1 # conv1_1 0
activate: ReLU
out_offset: 0x0000
processors: 0x0000000000000007
data_format: HWC
operation: Conv2d
streaming: true
- max_pool: 2 # conv2 1
pool_stride: 2
pad: 1
activate: ReLU
out_offset: 0x0500
processors: 0x0000ffff00000000
operation: Conv2d
streaming: true
- max_pool: 2 # conv3 2
pool_stride: 2
pad: 1
activate: ReLU
out_offset: 0x1500
processors: 0x00000000ffffffff
operation: Conv2d
streaming: true
- max_pool: 2 # conv4 3
pool_stride: 2
pad: 1
activate: ReLU
out_offset: 0x2000
processors: 0xffffffffffffffff
operation: Conv2d
streaming: true
- max_pool: 2 # conv5 4
pool_stride: 2
pad: 1
activate: ReLU
out_offset: 0x3000
processors: 0x00000000ffffffff
operation: Conv2d
streaming: true
- pad: 1 # conv6 5
activate: ReLU
out_offset: 0x3500
processors: 0xffffffff00000000
operation: Conv2d
- op: mlp # Linear 6
flatten: true
out_offset: 0x4000
output_width: 32
processors: 0x000000000000ffff
activate: None

第三个文件是--sample-input-示例数据输入文件名,这个是用于生成一个模拟的输入,以用于测试,可使用ai8x-synthesis/tests路径下的convert_sample.py文件通过已有图片生成,也可以手机用ai8x-synthesis/tests路径下的make_sample.py文件随机生成,本项目是使用make_sample.py文件随机生成的,但对于本项目需要保证输入文件的格式为(3, 128, 128),将make_sample.py如图4-11修改。

FpRrEkpcwJBwjgO-GlpD4LTdKsBe

图4-11 make_sample.py的修改

 

将ai8xize的运行参数输入并运行,运行后会在量化工程下的demos文件夹中生成我们需要的单片机工程文件,如图4-12所示:

Fijyp-ly93lO4JovogJ7P4o7-ED_

图4-12 生成的单片机工程

 

这个工程文件中就存在着本项目最终使用的汽车Logo分类模型文件,我们将模型文件取出,移植到猫狗识别例程中去,可以简单地检测一下模型的识别效果,关于移植过程在进度3.3节中有详细的说明,这里就不再赘述,仅展示识别效果,如图4-13所示:

Fg9ZXZDd2Hitcl47nGii36EGx6ol

图4-13 模型的实际识别效果

由图可知,本项目的汽车logo分类模型对这15种汽车logo都能够进行良好的识别。

 

五、MAX78000FTHR扩展板

在本项目中设计有MAX78000FTHR开发板的扩展板,最初目的是用来做图片采集,并将采集到的图片用作模型的数据集。但在完成设计后发现,该扩展板用来做汽车Logo的识别也完全没有问题,而且也是出于为了节省成本的目的,最终仅使用一个扩展板来完成本项目。

5.1 硬件结构

扩展板硬件结构图如图5-1所示。

FiA1NnReCNg7vB7VhuU82ywHnluI

图5-1 扩展板硬件结构图

 

5.2 原理图说明

5.2.1 电源管理

本项目使用TP5400进行电源管理。当外部电源(TYPE-C)供电时,TP5400能够给3.7V锂电池充电并输出5V电压,当外部电源未供电时,TP5400能够将3.7V电压升压至5V,从而实现充电升压二合一,原理图如图5-2所示。

FoSNm8MR7L4_VEgaYsmVjMFv4bU8

图5-2 电源管理原理图

 

5.2.2 屏幕电路

屏幕部分使用的是一块2.4寸屏幕,驱动是ILI9341,也可使用驱动为ST7789的屏幕,引脚通用,通信接口为SPI接口。该屏幕可直接使用官方针对MAX78000提供的屏幕驱动,原理图如图5-3所示。

FreVMnhs7kUEWJWuwZ3W-AdXzd1R

图5-3 屏幕电路原理图

 

5.2.3 按键电路

之所以设计按键电路部分,是因为MAX78000FTHR上的按键太过迷你,如果放到手持设备上不便于操作,所以在本扩展板上额外设计三个功能按键和一个复位按键以实现按键操作,原理图如图5-4所示。

Fq7IHzsEdrHrt4P2jxUm1duAg9u5

图5-4 按键电路原理图

 

5.2.4 电压检测电路

在本项目中需要使用到一块3.7V锂电池,而为了设备的稳定运行,对电池电压的实时检测是有必要的。在MAX78000FTHR板卡上有ADC检测引脚,而且精度可以满足本项目的需要,所以本项目使用ADC引脚进行检测即可。

通过测试相关例程发现ADC引脚能够检测的最大电压为1.2V,但本项目使用的锂电池最大电压能够达到4.2V,所以本项目将电池电压进行1/4分压,使锂电池最大电压降低到1.2V以下后再与ADC连接,原理图如图5-5所示。

FmREnXDmz5D54_oY0a3DL8eal91Q

图5-5 电池检测原理图

 

另外,本项目还设计有一个电源开关,当开关连通时,电池被接入电路,本设备开始工作,当开关断开时,电池与电路断开连接,设备关闭。而需要注意的是上图中的BAT_IN网络标号(用于电压检测)应该放置在电源开关的后端,即与整体电路相连的一端。之所以不直接与电池相连,是因为电压检测的阻抗虽然大,但仍然会产生电流,造成电池电压的损耗,而使用电源开关进行控制就可以避免这个问题。开关原理图如图5-6所示。

Fhe1DV0Mq9UEp5Ih2o7k1Eq75XvO

图5-6 电源开关原理图

 

5.2.5 主控电路

MAX78000FTHR与其他部分连接包括5V输入,3.3V输出,ADC检测端口,LCD通信端口和电源控制端口,原理图如图5-7所示。

Fm25xNHF6N4aVlRzZEsNYOxbuaWM

图5-7 主控电路原理图

 

以上就是本项目MAX78000扩展板的全部内容,至此我们已经得到了用于识别汽车Logo的模型文件和能够支持项目实现的扩展板,下面将对本项目的最终工程进行说明。

 

六、工程关键代码说明

本项目共有两个MAX78000的工程,第一个工程是汽车logo识别的工程,第二个工程是用于采集图片的工程,下面分别对这两个工程进行说明。

6.1 汽车logo识别工程

本工程用于汽车logo的识别,程序框图如图6-1所示。

FqtTE8CJ_z3AujgEW-nnG1PpVzMM

图6-1 汽车logo识别工程的程序框图

 

本项目在此工程的主要工作量在两个方面,一个是将驱动屏幕的硬件SPI改为软件SPI,另一个是汉字的显示。当然模型的相关内容也是该工程的重要内容,但在上文中已详细说明,这里就不再赘述。

注:以下屏幕函数的移植参考屏幕卖家中景园电子提供的例程。

6.1.1 软件SPI驱动屏幕

在说明代码之前,先说明一下为什么要使用软件SPI驱动屏幕。这是因为我发现在MAX78000开发板烧录进带同时使用模型和屏幕(硬件SPI驱动)的程序的情况下,初次烧录时屏幕还能够正常运行,但断电再重新上电后,屏幕就白屏了,无法正常显示,而且除屏幕外的其他功能基本都能正常使用,这种情况时有发生。而如果仅烧录带有屏幕的程序,断电后再重新上电,屏幕是能够正常显示的。

而本项目在正常使用中,肯定是需要经常执行断电上电操作的,这样硬件SPI驱动屏幕在本项目中是无法使用的。而软件SPI是通过改变GPIO引脚的高低电平来模拟SPI的时序,从而实现SPI通信,理论上只要GPIO的电平翻转不被干扰,软件SPI就能够正常实现,就不会出现屏幕白屏的情况。所以为了提高设备运行的稳定性,本项目最终决定使用软件SPI驱动屏幕。

本项目使用的屏的驱动芯片是ili9341,在美信官方提供的库中,有针对这个驱动封装好的文件,分别是tft_ili9341.c和tft_ili9341.h文件,里面的函数可以理解为两类,分别是ili9341应用函数和SPI驱动函数,结构如图6-2所示。

FtukndhqqPICy-yEzU-g-CMQUUf0

图6-2 函数结构图

 

如果要实现软件SPI,只需要改动其中的SPI驱动函数,而对应ili9341应用函数,直接调用即可。但因为tft_ili9341.c和tft_ili9341.h是系统内的库文件,直接修改会干扰其他程序的使用,所以我将这两个文件复制出来进行修改,并重新命名为lcd_ili9341.c和lcd_ili9341.h

(1)SPI通信模拟

void LCD_Writ_Bus(u8 dat)
{
u8 i;
LCD_CS_Clr();
for (i = 0; i < 8; i++)
{
LCD_SCLK_Clr();
if (dat & 0x80)
{
LCD_MOSI_Set();
}
else
{
LCD_MOSI_Clr();
}
LCD_SCLK_Set();
dat <<= 1;
}
LCD_CS_Set();
}

(2)写命令与写数据

static void write_command(u8 dat)
{
LCD_DC_Clr(); // 写命令
LCD_Writ_Bus(dat);
LCD_DC_Set(); // 写数据
}


static void write_data(u8 dat)
{
LCD_Writ_Bus(dat);
}

(3)GPIO控制

使用到的GPIO引脚的初始化函数

void LCD_GPIO_Init(void)
{
sck_ctrl.port = LCD_SCK;
sck_ctrl.mask = LCD_SCK_PIN;
sck_ctrl.pad = MXC_GPIO_PAD_PULL_UP;
sck_ctrl.func = MXC_GPIO_FUNC_OUT;
sck_ctrl.vssel = MXC_GPIO_VSSEL_VDDIOH;
MXC_GPIO_Config(&sck_ctrl);


mosi_ctrl.port = LCD_MOSI;
mosi_ctrl.mask = LCD_MOSI_PIN;
mosi_ctrl.pad = MXC_GPIO_PAD_PULL_UP;
mosi_ctrl.func = MXC_GPIO_FUNC_OUT;
mosi_ctrl.vssel = MXC_GPIO_VSSEL_VDDIOH;
MXC_GPIO_Config(&mosi_ctrl);


reset_ctrl.port = LCD_RST;
reset_ctrl.mask = LCD_RST_PIN;
reset_ctrl.pad = MXC_GPIO_PAD_PULL_UP;
reset_ctrl.func = MXC_GPIO_FUNC_OUT;
reset_ctrl.vssel = MXC_GPIO_VSSEL_VDDIOH;
MXC_GPIO_Config(&reset_ctrl);


dc_ctrl.port = LCD_DC;
dc_ctrl.mask = LCD_DC_PIN;
dc_ctrl.pad = MXC_GPIO_PAD_PULL_UP;
dc_ctrl.func = MXC_GPIO_FUNC_OUT;
dc_ctrl.vssel = MXC_GPIO_VSSEL_VDDIOH;
MXC_GPIO_Config(&dc_ctrl);


cs_ctrl.port = LCD_CS;
cs_ctrl.mask = LCD_CS_PIN;
cs_ctrl.pad = MXC_GPIO_PAD_PULL_UP;
cs_ctrl.func = MXC_GPIO_FUNC_OUT;
cs_ctrl.vssel = MXC_GPIO_VSSEL_VDDIOH;
MXC_GPIO_Config(&cs_ctrl);


bl_ctrl.port = LCD_BLK;
bl_ctrl.mask = LCD_BLK_PIN;
bl_ctrl.pad = MXC_GPIO_PAD_PULL_UP;
bl_ctrl.func = MXC_GPIO_FUNC_OUT;
bl_ctrl.vssel = MXC_GPIO_VSSEL_VDDIOH;
MXC_GPIO_Config(&bl_ctrl);


MXC_GPIO_OutSet(mosi_ctrl.port, mosi_ctrl.mask);
MXC_GPIO_OutSet(reset_ctrl.port, reset_ctrl.mask);
MXC_GPIO_OutSet(dc_ctrl.port, dc_ctrl.mask);
MXC_GPIO_OutSet(cs_ctrl.port, cs_ctrl.mask);
MXC_GPIO_OutSet(bl_ctrl.port, bl_ctrl.mask);
}

为方便翻转电平,设置翻转电平的宏定义

#define LCD_SCLK_Clr() MXC_GPIO_OutClr(LCD_SCK, LCD_SCK_PIN) // SCL=SCLK
#define LCD_SCLK_Set() MXC_GPIO_OutSet(LCD_SCK, LCD_SCK_PIN)


#define LCD_MOSI_Clr() MXC_GPIO_OutClr(LCD_MOSI, LCD_MOSI_PIN) // SDA=MOSI
#define LCD_MOSI_Set() MXC_GPIO_OutSet(LCD_MOSI, LCD_MOSI_PIN)


#define LCD_RES_Clr() MXC_GPIO_OutClr(LCD_RST, LCD_RST_PIN) // RES
#define LCD_RES_Set() MXC_GPIO_OutSet(LCD_RST, LCD_RST_PIN)


#define LCD_DC_Clr() MXC_GPIO_OutClr(LCD_DC, LCD_DC_PIN) // DC
#define LCD_DC_Set() MXC_GPIO_OutSet(LCD_DC, LCD_DC_PIN)


#define LCD_CS_Clr() MXC_GPIO_OutClr(LCD_CS, LCD_CS_PIN) // CS
#define LCD_CS_Set() MXC_GPIO_OutSet(LCD_CS, LCD_CS_PIN)


#define LCD_BLK_Clr() MXC_GPIO_OutClr(LCD_BLK, LCD_BLK_PIN) // BLK
#define LCD_BLK_Set() MXC_GPIO_OutSet(LCD_BLK, LCD_BLK_PIN)

(4)其他内容

以上内容是针对软件SPI对驱动函数的修改,ili9341应用函数基本不用修改,但为防止lcd_ili9341.c和lcd_ili9341.h的函数名与tft_ili9341.c和tft_ili9341.h中的冲突,所以需要对应用函数名进行修改,我的修改逻辑为将函数名中的tft都修改为lcd,函数比较多,就不一一展示了。还有一些细节上的修改,包括但不限于lcd_displayInit,MXC_LCD_Init和MXC_LCD_SetRotation函数,具体可以到附件project\Vehicle_logo_15_Chinese\lcd\lcd_ili9341.c查看。

 

6.1.2 汉字显示

(1)汉字显示函数

函数如下所示

void LCD_ShowChinese(u16 x, u16 y, u8 *s, u16 fc, u16 bc, u8 sizey, u8 mode)
{
while (*s != 0)
{
if (sizey == 12)
LCD_ShowChinese12x12(x, y, s, fc, bc, sizey, mode);
else if (sizey == 16)
LCD_ShowChinese16x16(x, y, s, fc, bc, sizey, mode);
else if (sizey == 24)
LCD_ShowChinese24x24(x, y, s, fc, bc, sizey, mode);
else if (sizey == 32)
LCD_ShowChinese32x32(x, y, s, fc, bc, sizey, mode);
else
return;
s += 3;
x += sizey;
}
}

包含四种文字尺寸,分别是12x12,16x16,24x24和32x32,这里仅展示16x16尺寸的函数,如下所示:

void LCD_ShowChinese16x16(u16 x, u16 y, u8 *s, u16 fc, u16 bc, u8 sizey, u8 mode)
{
u8 i, j, m = 0;
u16 k;
u16 HZnum; // 汉字数目
u16 TypefaceNum; // 一个字符所占字节大小
u16 x0 = x;
TypefaceNum = (sizey / 8 + ((sizey % 8) ? 1 : 0)) * sizey;
HZnum = sizeof(tfont16) / sizeof(typFNT_GB16); // 统计汉字数目
for (k = 0; k < HZnum; k++)
{
if ((tfont16[k].Index[0] == *(s)) && (tfont16[k].Index[1] == *(s + 1)) && (tfont16[k].Index[2] == *(s + 2)))
{
LCD_Address_Set(x, y, x + sizey - 1, y + sizey - 1);
for (i = 0; i < TypefaceNum; i++)
{
for (j = 0; j < 8; j++)
{
if (!mode) // 非叠加方式
{
if (tfont16[k].Msk[i] & (0x01 << j))
{
write_data(fc >> 8);
write_data(fc);
}
else
{
write_data(bc >> 8);
write_data(bc);
}
m++;
if (m % sizey == 0)
{
m = 0;
break;
}
}
else // 叠加方式
{
if (tfont16[k].Msk[i] & (0x01 << j))
LCD_DrawPoint(x, y, fc); // 画一个点
x++;
if ((x - x0) == sizey)
{
x = x0;
y++;
break;
}
}
}
}
}
continue; // 查找到对应点阵字库立即退出,防止多个汉字重复取模带来影响
}
}

本项目仅使用16x16尺寸汉字,所以仅针对16x16尺寸制作了字库,共包含54个汉字,如下所示(仅展示部分)。其他尺寸汉字仅提供“中景园电子”五个字作为实例。

typedef struct
{
unsigned char Index[3];
unsigned char Msk[72];
} typFNT_GB24;


const typFNT_GB24 tfont24[] = {


"汽", 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x18, 0x00, 0x10, 0x08, 0x00, 0x30, 0x08, 0x10, 0x20,
0xFC, 0x3F, 0x20, 0x04, 0x00, 0x80, 0x02, 0x04, 0x82, 0xFE, 0x0F, 0x4C, 0x01, 0x00, 0xC8, 0x00,
0x00, 0x40, 0xFF, 0x03, 0x20, 0x00, 0x01, 0x20, 0x00, 0x01, 0x10, 0x00, 0x01, 0x10, 0x00, 0x03,
0x18, 0x00, 0x02, 0x1C, 0x00, 0x22, 0x18, 0x00, 0x22, 0x08, 0x00, 0x24, 0x08, 0x00, 0x2C, 0x08,
0x00, 0x38, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, /*"汽",0*/
"车", 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x02, 0x08, 0xF8, 0xFF, 0x1F, 0x00,
0x01, 0x00, 0x80, 0x09, 0x00, 0x80, 0x18, 0x00, 0xC0, 0x18, 0x00, 0x40, 0x18, 0x00, 0x60, 0x18,
0x04, 0xF0, 0xFF, 0x0F, 0x20, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x20,
0xFE, 0xFF, 0x3F, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00,
0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, /*"车",1*/
"识", 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x20, 0xF8, 0x1F, 0x60, 0x08, 0x18, 0x40,
0x08, 0x18, 0x00, 0x08, 0x18, 0x00, 0x08, 0x18, 0x00, 0x08, 0x18, 0x7E, 0x08, 0x18, 0x20, 0x08,
0x18, 0x20, 0x08, 0x18, 0x20, 0xF8, 0x1F, 0x20, 0x08, 0x18, 0x20, 0x08, 0x08, 0x20, 0x00, 0x00,
0x20, 0x62, 0x02, 0x20, 0x31, 0x0C, 0xA0, 0x30, 0x18, 0x60, 0x18, 0x30, 0x60, 0x0C, 0x30, 0x00,
0x02, 0x20, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, /*"识",2*/
"别", 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xF0, 0x1F, 0x10, 0x10, 0x08, 0x10, 0x10,
0x08, 0x10, 0x10, 0x88, 0x11, 0x10, 0x88, 0x11, 0xF0, 0x8F, 0x11, 0x50, 0x88, 0x11, 0x80, 0x81,
0x11, 0x80, 0x80, 0x11, 0x80, 0x80, 0x11, 0xFC, 0xBF, 0x11, 0x80, 0x90, 0x11, 0xC0, 0x90, 0x11,
0x40, 0x90, 0x11, 0x40, 0x98, 0x10, 0x60, 0x18, 0x10, 0x20, 0x18, 0x10, 0x10, 0x08, 0x10, 0x0C,
0x0E, 0x1E, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, /*"别",3*/

 6.1.3 摄像头画面显示

将摄像头画面显示到屏幕上可以参考猫狗识别例程,先采集摄像头拍摄的画面,然后将画面显示到屏幕上,但由于本项目屏幕显示使用的是软件SPI,刷新率较慢,如果边采集边显示,会造成摄像头的流缓冲区溢出。所以本项目选择边采集边存储,等到一张画面采集完成后再显示到屏幕上。

而在程序中因为要将图片送进模型,已经将摄像头采集到的画面存进了cnn缓冲区中,所以这里无需再次进行存储操作,直接使用要送进模型的图片数据即可。但存储在cnn缓冲区中的数据,是被处理过后的数据,我们在使用时,需要进行一定的解码操作。

(1)将图片数据存储进cnn缓冲区

        for (int k = 0; k < 4 * w; k += 4)
{
// data format: 0x00bbggrr
r = data[k];
g = data[k + 1];
b = data[k + 2];
// skip k+3


// change the range from [0,255] to [-128,127] and store in buffer for CNN
input_0[cnt++] = ((b << 16) | (g << 8) | r) ^ 0x00808080;


#ifdef BOARD_EVKIT_V1
j -= 2; // mirror on display
#else
j += 2;
#endif
}

(2)显示图片到屏幕上

#ifdef TFT_ENABLE
int cnt_for_s = 0;
for (int row = 0; row < h; row++)
{
j = 0;
for (int k = 0; k < 4 * w; k += 4)
{
// data format: 0x00bbggrr
r = (input_0[cnt_for_s] ^ 0x00808080) & 0xFF;
g = ((input_0[cnt_for_s] ^ 0x00808080) >> 8) & 0xFF;
b = ((input_0[cnt_for_s] ^ 0x00808080) >> 16) & 0xFF;
cnt_for_s++;


// convert to RGB656 for display
rgb = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3);
data565[j] = (rgb >> 8) & 0xFF;
data565[j + 1] = rgb & 0xFF;
j += 2;
}
MXC_LCD_ShowImageCameraRGB565(96 + h - row, 0, data565, 1, h);
}
#endif

(3)capture_process_camera完整函数

void capture_process_camera(void)
{
uint8_t *raw;
uint32_t imgLen;
uint32_t w, h;


int cnt = 0;


uint8_t r, g, b;
uint16_t rgb;
int j = 0;


uint8_t *data = NULL;
stream_stat_t *stat;


camera_start_capture_image();


// Get the details of the image from the camera driver.
camera_get_image(&raw, &imgLen, &w, &h);


// Get image line by line
for (int row = 0; row < h; row++)
{
// Wait until camera streaming buffer is full
while ((data = get_camera_stream_buffer()) == NULL)
{
if (camera_is_image_rcv())
{
break;
}
}


// LED_Toggle(LED2);
#ifdef BOARD_EVKIT_V1
j = IMAGE_SIZE_X * 2 - 2; // mirror on display
#else
j = 0;
#endif
for (int k = 0; k < 4 * w; k += 4)
{
// data format: 0x00bbggrr
r = data[k];
g = data[k + 1];
b = data[k + 2];
// skip k+3


// change the range from [0,255] to [-128,127] and store in buffer for CNN
input_0[cnt++] = ((b << 16) | (g << 8) | r) ^ 0x00808080;


#ifdef BOARD_EVKIT_V1
j -= 2; // mirror on display
#else
j += 2;
#endif
}


// LED_Toggle(LED2);
// Release stream buffer
release_camera_stream_buffer();
}


// camera_sleep(1);
stat = get_camera_stream_statistic();


if (stat->overflow_count > 0)
{
printf("OVERFLOW DISP = %d\n", stat->overflow_count);
LED_On(LED2); // Turn on red LED if overflow detected
// while (1)
// {
// }
}


#ifdef TFT_ENABLE
int cnt_for_s = 0;
for (int row = 0; row < h; row++)
{
j = 0;
for (int k = 0; k < 4 * w; k += 4)
{
// data format: 0x00bbggrr
r = (input_0[cnt_for_s] ^ 0x00808080) & 0xFF;
g = ((input_0[cnt_for_s] ^ 0x00808080) >> 8) & 0xFF;
b = ((input_0[cnt_for_s] ^ 0x00808080) >> 16) & 0xFF;
cnt_for_s++;


// convert to RGB656 for display
rgb = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3);
data565[j] = (rgb >> 8) & 0xFF;
data565[j + 1] = rgb & 0xFF;
j += 2;
}
MXC_LCD_ShowImageCameraRGB565(96 + h - row, 0, data565, 1, h);
}
#endif
}

6.1.4 电量显示

程序如下所示。

#ifdef ADC_Charge
MXC_ADC_StartConversion(ADC_CHANNEL);
MXC_ADC_GetData(&adc_val);
adc_val = adc_val * 4 * 120 / 1024;
printf("%d: %d.%dV\n\n", ADC_CHANNEL, adc_val / 100, adc_val % 100);


if ((adc_val - 310) > 90)
TFT_Print(buff, 250, 0, font, snprintf(buff, sizeof(buff), "C:100%%"));
else if ((adc_val - 310) > 75)
TFT_Print(buff, 250, 0, font, snprintf(buff, sizeof(buff), "C:75%%"));
else if ((adc_val - 310) > 50)
TFT_Print(buff, 250, 0, font, snprintf(buff, sizeof(buff), "C:50%%"));
else if ((adc_val - 310) > 25)
TFT_Print(buff, 250, 0, font, snprintf(buff, sizeof(buff), "C:25%%"));
else if ((adc_val - 310) > 10)
TFT_Print(buff, 250, 0, font, snprintf(buff, sizeof(buff), "C:10%%"));
#endif

6.2 图片采集工程

在上一节中我们提到使用软件SPI是因为硬件SPI和模型一起使用时有可能会导致屏幕显示异常,但仅使用硬件SPI的话,屏幕是能正常显示的。而在图片采集工程中,不需要使用模型,所以在图片采集工程中,使用硬件SPI来与屏幕进行通信。

该工程程序框图如图6-3所示。

FmG0nRfNeIeQ8FkvVSiQ34MnE83z

图6-3 图片采集工程的程序框图

 

6.2.1 获取图片

参考使用ImgCapture例程中的stream_img函数,该函数返回值可得到摄像头的捕捉到的画面,代码如下:

cnn_img_data_t stream_img(uint32_t w, uint32_t h, pixformat_t pixel_format, int dma_channel)
{
// Enable CNN accelerator memory.
// CNN clock: APB (50 MHz - PCLK) div 1
cnn_enable((0 << MXC_F_GCR_PCLKDIV_CNNCLKSEL_POS), MXC_S_GCR_PCLKDIV_CNNCLKDIV_DIV1);
cnn_init();


cnn_img_data_t img_data;


// Resolution check. This method only supports resolutions that are multiples of 32.
// Additionally, resolutions beyond 352x352 may result in image artifacts.
if ((w * h) % 32 != 0)
{
img_data.raw = NULL;
printf("Failed to stream! Image resolutions must be multiples of 32.\n");
return img_data;
}


// 1. Configure the camera. This is the same as the standard blocking capture, except
// the DMA mode is set to "STREAMING_DMA".
printf("Configuring camera\n");
fifomode_t fifo_mode = (pixel_format == PIXFORMAT_RGB888) ? FIFO_THREE_BYTE : FIFO_FOUR_BYTE;
int ret = camera_setup(w, // width
h, // height
pixel_format, // pixel format
fifo_mode, // FIFO mode
STREAMING_DMA, // Set streaming mode
dma_channel); // Allocate the DMA channel retrieved in initialization


// Error check the setup function.
if (ret != STATUS_OK)
{
printf("Failed to configure camera! Error %i\n", ret);
img_data.raw = NULL;
return img_data;
}


// 2. Retrieve image format and info.
img_data.pixel_format = camera_get_pixel_format(); // Retrieve the pixel format of the image
camera_get_image(NULL, &img_data.imglen, &img_data.w,
&img_data.h); // Retrieve info using driver function.
img_data.raw = (uint32_t *)
CNN_QUAD0_DSRAM_START; // Manually save the destination address at the first quadrant of CNN data SRAM


printf("Starting streaming capture...\n");
MXC_TMR_SW_Start(MXC_TMR0);


// 3. Start streaming
camera_start_capture_image();


uint8_t *data = NULL;
int buffer_size = camera_get_stream_buffer_size();
uint32_t *cnn_addr = img_data.raw; // Hard-coded to Quadrant 0 starting address


// 4. Process the incoming stream data.
while (!camera_is_image_rcv())
{
if ((data = get_camera_stream_buffer()) !=
NULL)
{ // The stream buffer will return 'NULL' until an image row is received.
// 5. Unload buffer
cnn_addr = write_bytes_to_cnn_sram(data, buffer_size, cnn_addr);
// 6. Release buffer in time for next row
release_camera_stream_buffer();
}
}


int elapsed_us = MXC_TMR_SW_Stop(MXC_TMR0);
printf("Done! (Took %i us)\n", elapsed_us);


// 7. Check for any overflow
stream_stat_t *stat = get_camera_stream_statistic();
printf("DMA transfer count = %d\n", stat->dma_transfer_count);
printf("OVERFLOW = %d\n", stat->overflow_count);


return img_data;
}

6.2.2 保存图片

参考使用ImgCapture例程中的save_stream_sd函数,将上一小节函数的返回值作为save_stream_sd函数输入,可将捕捉到的图片存储进SD卡中,在该函数中,存储图片的路径是固定的,可将路径改为一个全局变量,并在其他地方通过按键控制修改,即可实现存储路径的改变,该路径修改位置如图6-4所示,全局变量为path。

Fi0sp2-xakWp2m5M9CywEaVGtcT_

图6-4 路径修改示意图

 

save_stream_sd的全部代码如下。

void save_stream_sd(cnn_img_data_t img_data, char *file)
{
if (img_data.raw != NULL)
{ // Image data will be NULL if something went wrong during streaming
// If file is NULL, find the next available file to save to.
if (file == NULL)
{
int i = 0;


for (;;)
{
// We'll use the global sd_filename buffer for this and
// try to find /raw/imgN while incrementing N.
memset(sd_filename, '\0', sizeof(sd_filename));
snprintf(sd_filename, sizeof(sd_filename), "/%s/img%u", path, i++);
sd_err = f_stat(sd_filename, &sd_fno);
if (sd_err == FR_NO_FILE)
{
file = sd_filename; // Point 'file' to the available path string
break;
}
else if (sd_err != FR_OK)
{
printf("Error while searching for next available file: %s\n",
FR_ERRORS[sd_err]);
break;
}
}
}


sd_err = f_open(&sd_file, (const TCHAR *)file, FA_WRITE | FA_CREATE_NEW);


if (sd_err != FR_OK)
{
printf("Error opening file: %s\n", FR_ERRORS[sd_err]);
}
else
{
printf("Saving image to %s\n", file);


MXC_TMR_SW_Start(MXC_TMR0);


// Write image info as the first line of the file.
clear_serial_buffer();
snprintf(g_serial_buffer, SERIAL_BUFFER_SIZE,
"*IMG* %s %i %i %i\n", // Format img info into a new-line terminated string
img_data.pixel_format, img_data.imglen, img_data.w, img_data.h);


unsigned int wrote = 0;
sd_err = f_write(&sd_file, g_serial_buffer, strlen(g_serial_buffer), &wrote);
if (sd_err != FR_OK || wrote != strlen(g_serial_buffer))
{
printf("Failed to write header to file: %s\n", FR_ERRORS[sd_err]);
}
clear_serial_buffer();


// Similar to streaming over UART, a secondary buffer is needed to
// save the raw data to the SD card since the CNN data SRAM is non-contiguous.
// Raw image data is written row by row.
uint32_t *cnn_addr = img_data.raw;
uint8_t *buffer = (uint8_t *)malloc(img_data.w);
for (int i = 0; i < img_data.imglen; i += img_data.w)
{
cnn_addr = read_bytes_from_cnn_sram(buffer, img_data.w, cnn_addr);
sd_err = f_write(&sd_file, buffer, img_data.w, &wrote);


if (sd_err != FR_OK || wrote != img_data.w)
{
printf("Failed to image data to file: %s\n", FR_ERRORS[sd_err]);
}


// Print progress %
if (i % (img_data.w * 32) == 0)
{
printf("%.1f%%\n", ((float)i / img_data.imglen) * 100.0f);
}
}
free(buffer);
int elapsed = MXC_TMR_SW_Stop(MXC_TMR0);
printf("Finished (took %ius)\n", elapsed);
}
f_close(&sd_file);
}
}

6.2.3 按键检测

在扩展板说明部分,我们共设计有3个按键,在汽车logo识别工程中,我们仅使用了一个,而在图片采集工程中,我们使用三个按键。这里可以将按键状态检测封装为一个函数,提高检测效率。检测函数如下:

uint8_t Key_get()
{
if (MXC_GPIO_InGet(gpio_set.port, gpio_set.mask) == 0 | MXC_GPIO_InGet(gpio_down.port, gpio_down.mask) == 0 |
MXC_GPIO_InGet(gpio_up.port, gpio_up.mask) == 0)
{
MXC_Delay(10000);
if (MXC_GPIO_InGet(gpio_set.port, gpio_set.mask) == 0 | MXC_GPIO_InGet(gpio_down.port, gpio_down.mask) == 0 |
MXC_GPIO_InGet(gpio_up.port, gpio_up.mask) == 0)
{
if (MXC_GPIO_InGet(gpio_set.port, gpio_set.mask) == 0)
return key_set;
else if (MXC_GPIO_InGet(gpio_up.port, gpio_up.mask) == 0)
return key_up;
else if (MXC_GPIO_InGet(gpio_down.port, gpio_down.mask) == 0)
return key_down;
}
}
else
return 0;
}

 

七、实现结果显示

实现结果包括两部分,第一部分是汽车logo识别,第二部分是采集图片,下面分别进行说明。

7.1 汽车logo识别效果展示

(1)开机提示界面

FlkrZ5yiC8zdTRb1FHc8gZcxLh01

图7-1 开机提示界面

(2)预识别界面

FvtP66Suo-cUaj0qDvmMEMixuBb-

图7-2 预识别界面

(3)识别结果

Ftj0_Jcbx-yYJehacignWb8qiEri

图7-3 识别结果

(4)15种汽车logo识别

FsPb2spR57nSZz79bmy0-EEDWZil

图7-4 15种汽车logo识别

  (5)实际识别效果

本部分识别是在实际场景下针对汽车logo进行识别。我在学校里共找到这15种汽车品牌中的8种,分别是宝马,奔驰,比亚迪,大众,广汽,马自达,五菱和丰田,其中前7种的识别效果较好,如图7-5至11所示。

图7-5 实际宝马logo的识别


图7-6 实际奔驰logo的识别


图7-7 实际比亚迪logo的识别


图7-8 实际大众logo的识别


图7-9 实际广汽logo的识别


图7-10 实际马自达logo的识别


图7-11 实际五菱logo的识别

而丰田的识别效果较差(基本无法识别,大概是因为使用开源数据集的原因),这里就不做展示了。


7.2 扩展板采集图像效果展示

首先使用扩展板拍摄图像并保存在0:/car0目录下

FoRRkHZ_M2OIY8oYErFBEom23bf4

图7-12 拍摄图像并保存

然后将SD卡取下与电脑连接,并在电脑上使用打开Pycharm软件打开MAX78000的Imgcapture例程里的utils文件夹

Fg0GyBi-t7z0CJeaMjU_Vlf2kP1Q

图7-13 utils文件夹位置示意图

然后在Pycharm中给batchconvert.py文件添加输入参数:-o G:/car0 G:/car0,如图7-14所示,输入参数需根据自身电脑情况进行修改,-o G:/car0代表用于保存转换后的图像的输出目录,G:/car0代表包含要转换的原始图像的输入目录

FjTq74RK5FSx4ajOPC7PnwflIJgw

图7-14 batchconvert.py的运行参数

点击运行,程序会将拍摄的图片转化成.png文件

转化效果如图7-15所示

Fkb2jSgeZOhrDyCYbdhagMDOptK1

图7-15 图片转化效果

 

八、遇到的主要难题及解决办法

(1)断电再上电后屏幕无法正常显示问题

这个是本项目遇到的最大的难题,直接导致了这个移动手持设备无法外带使用。

解决办法:正如上文提到的那样,使用软件SPI与屏幕进行通信,顺利解决了这个问题。

 

(2)环境配置问题

作为一名深度学习方面的小白,在项目开始时,对于环境的配置可以说完全不明白,跟着官方文档对WSL2进行配置,也遇到了各种无法解决的问题。

解决方法:在我熟悉的windows环境下进行配置,但对于我来说仍然十分困难,幸好在哔哩哔哩最后找到了小土堆老师的超级详细的环境配置教程和深度学习教程,这不仅帮助我学会了环境的配置,还带我入门了深度学习,对后面的开发帮助很大。这两个视频链接如下:

windows下Pytorch入门深度学习环境安装与配置:最详细的 Windows 下 PyTorch 入门深度学习环境安装与配置 CPU GPU 版 | 土堆教程_哔哩哔哩_bilibili

Pytorch深度学习快速入门教程:PyTorch深度学习快速入门教程(绝对通俗易懂!)【小土堆】_哔哩哔哩_bilibili

 

(3)识别效果问题

本项目中识别分类包含15种汽车品牌,数据集来源有三部分,但也可理解为两部分,第一部分是使用板载摄像头拍摄的画面,包括10种汽车品牌,第二部分是来自于开源数据集,包括5种汽车品牌,但在实际识别中,第一部分的10种汽车品牌的识别效果非常好,识别效率可达90%以上,但对于第二部分的5种汽车品牌的识别效果并不好,识别效率甚至不到50%。我认为识别效果不佳有以下几点原因

      1)、本项目模型的输出图像为正方形,而开源数据集中的部分图片为长方形,经数据集的resize操作后,图像产生了形变。而这与实际识别时拍摄的图片的图像是不同的,会导致模型识别成其他汽车品牌。

      2)、开源数据集中的图片与实际识别时拍摄的图片差异较大,无法正常识别

解决方法:使用MAX78000FTHR板载摄像头实际拍摄的图片作为数据集,如果非要使用开源数据集中的图片或其他设备的图片,要保证图片的形状为正方形。

 

(4)中文的编码问题

在中景园电子提供的中文显示程序程序中使用的汉字编码是GB2312,在该编码中一个汉字占2个字节,而在VScode中,将编码自动改为了UTF-8,而在该编码中,一个汉字占3-4个编码(实测直接用3编码就可以),这就导致在汉字显示时会出现跳字显示的情况。

解决方法:根据UTF-8编码修改程序,首先修改LCD_ShowChinese函数中的参数,如图8-1所示。

FqqZbN4MUvi6vFXn_EDYEMsN_KMG

图8-1 LCD_ShowChinese函数的参数更改

该参数代表的含义是显示完一个汉字后切换下一个汉字时要增加的字节数,原函数因为使用的是GB2312编码,所以增加的是2,而UTF-8则需要修改为3。

然后更改具体的显示函数,更改参数如图8-2所示(以16x16显示函数为例)。

Fhi1fxFbw8hRsCGQ5o7iFr74mNCN

图8-2 LCD_ShowChinese16x16函数的参数修改

与上一个参数类似,也是编码格式导致的问题,按图中修改即可。

 

九、项目总结

本项目的整体设计和实现的功能在实际制作过程中是比较复杂的,但与以往项目的不同之处在于,本项目涉及到了嵌入式AI,不仅要设计硬件电路和嵌入式代码,还要设计基于深度学习的模型并实现部署。

我以前是没有学习过深度学习相关内容的,但在以前的嵌入式项目中,却有想过要在项目中添加深度学习,做嵌入式AI相关的内容,但因为硬件不支持或开发难度较大,就不了了之了。而看到专门针对嵌入式AI设计的MAX78000FTHR开发板,又让我产生了做嵌入式AI的想法,最后我与自己一拍即合,就制作了本项目的这些内容。

最后感谢硬禾学堂和ADI共同举办了本次大赛,让我能够有机会学习到嵌入式AI的相关知识以及项目开发的经验。同时也感谢看到这里的各位,谢谢大家!

 

十、参考资料

1、MAX78000FTHR-快速实现超低功耗、人工智能 (AI) 方案 - 电子森林 (eetree.cn)

2、https://github.com/MaximIntegratedAI/ai8x-synthesis

3、https://github.com/MaximIntegratedAI/ai8x-training

4、MSDK User Guide - Analog Devices MSDK Documentation (analog-devices-msdk.github.io)

5、#maxim78000 (qq.com)

6、MAX78000-AI宝可梦图鉴 - 电子森林 (eetree.cn)

7、MAX78000FTHR-边缘AI - 嘉立创EDA开源硬件平台 (oshwhub.com)

8、最详细的 Windows 下 PyTorch 入门深度学习环境安装与配置 CPU GPU 版 | 土堆教程_哔哩哔哩_bilibili

9、PyTorch深度学习快速入门教程(绝对通俗易懂!)【小土堆】_哔哩哔哩_bilibili

 

附件下载
BOM_MAX78000FTHR扩展板.xlsx
扩展板的bom表
project.7z
开发板工程文件集合,包括Vehicle_logo_15_Chinese-中文界面的汽车logo识别工程,Vehicle_logo_15_English英文界面的汽车logo识别工程,MAX78000_Camera-图片采集工程
ai8x-synthesis+ai8x-training网盘链接.txt
包含我使用的ai8x-synthesis工程和ai8x-training工程的全部内容,但因为文件较大,所以放到百度网盘中
团队介绍
个人项目
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号