1 项目介绍
基于MAX78000FTHR实现自动收银台商品识别系统基于MAX78000FTHR快速开发平台实现,MAX78000FTHR为快速开发平台,帮助工程师利用MAX78000 Arm® Cortex® M4F处理器快速实施超低功耗、人工智能(AI)方案,器件集成卷积神经网络加速器。评估板包括MAX20303 PMIC,用于电池和电源管理。评估板规格为0.9in x 2.6in、双排连接器,兼容Adafruit Feather Wing外设扩展板。评估板包括各种外设,例如CMOS VGA图像传感器、数字麦克风、低功耗立体声音频CODEC、1MB QSPI SRAM、micro SD存储卡连接器、RGB指示LED和按键。
基于MAX78000FTHR实现自动收银台商品识别系统主要完成的功能是自动收银台商品的识别,可以通过板载摄像头完成图像的获取,然后通过开发板集成的基于硬件的卷积神经网络 (CNN) 加速器,使用提前预训练好的CNN网络模型对获取到的图片进行推理。识别成功后,将通过一个外接的TFT液晶显示屏将识别到的结果进行输出。
基于MAX78000FTHR实现自动收银台商品识别系统主要识别一些商店常见的物品,本项目可以识别方便面,饼干和洗头膏。
2 项目设计思路
首先需要完成项目预训练环境和开发环境的搭建,并通过观看相关的直播课程学会如何将项目代码顺利下载到开发板中。其次,该项目使用到了深度学习,需要实现网络模型的预训练,并将预训练好的模型与项目进行结合,使得基于神经网络的物品识别功能可以在项目中顺利运行。最后是与开发板相连接的有关外设的驱动,通过编写相关驱动程序,使得开发板的摄像头和液晶显示屏等外设可以正常运行,以保证项目各个功能可以顺利完成。下图为项目设计思路图。
本项目外接了TFT液晶显示屏,引脚连接如下
CLK:P0.7
MISO:P0.6
MOSI:P0.5
CS:P0.11
DC:P0.8
RST: P0.19
3 搜集素材的思路
在进行模型的预训练时,需要用到训练所需的数据集。基于MAX78000FTHR实现自动收银台商品识别项目所用到的数据集包括三种图片,分别是方便面、饼干和洗头膏。为了方便数据集的搜集,这里使用了视频转图片的方法。首先先收集相关物品的视频,在视频收集结束后,通过编写的python代码将其转换成图片。其转换代码如下
import cv2
import os
from pathlib import Path
VID_FORMATS = ('.mov', '.avi', '.mp4', '.mpg', '.mpeg', '.m4v', '.wmv', '.mkv', '.mp3')
def videos2images(root_video_path, root_save_dir):
for video_dir_path in root_video_path:
# 1.检测读取文件路径是否正确
path_video = Path(video_dir_path)
if path_video.is_dir():
print(video_dir_path + '\t ok')
videos = os.listdir(video_dir_path)
else:
print('\033[31mLine36 error: \033[31m' + video_dir_path + 'is not exist!')
return
# 2. 生成存储文件夹
save_name_dir = Path(path_video.name)
save_name_dir = os.path.join(root_save_dir, save_name_dir)
if not os.path.exists(save_name_dir):
os.makedirs(save_name_dir)
file_count = 0
for video in videos:
# 判断是否为视频文件,如果不是视频文件则跳过并进行说明
if Path(video).suffix in VID_FORMATS:
file_count += 1 # 视频文件数+1
save_jpg_dir = os.path.join(save_name_dir, Path(video).stem)
if not os.path.exists(save_jpg_dir):
os.makedirs(save_jpg_dir)
each_video_path = os.path.join(path_video, video)
save_dir = save_jpg_dir
else:
print('\033[33mLine56 warning: \033[33m' + os.path.basename(video) + ' is not a video file, so skip.')
continue
# 3. 开始转换。打印正在处理文件的序号和他的文件名,并开始转换
print('\033[38m' + str(file_count) + ':' + Path(video).stem + '\033[38m')
cap = cv2.VideoCapture(each_video_path)
flag = cap.isOpened()
if not flag:
print("\033[31mLine 65 error\033[31m: open" + each_video_path + "error!")
frame_count = 0 # 给每一帧标号
while True:
frame_count += 1
flag, frame = cap.read()
if not flag: # 如果已经读取到最后一帧则退出
break
if os.path.exists(
save_dir + str(frame_count) + '.jpg'): # 在源视频不变的情况下,如果已经创建,则跳过
break
cv2.imwrite(save_dir + '\\' + str(frame_count) + '.jpg', frame)
cap.release()
print('\033[38m' + Path(video).stem + ' save to ' + save_dir + 'finished. \033[38m') # 表示一个视频片段已经转换完成
if __name__ == '__main__':
# 需要转换的视频路径列表,直达视频文件(自定义修改)
video_path_list = [r'D:\yingheshujuji\instantnoodle/']
# 预期存储在的主文件夹,即'result'文件夹下
image_save_dir = r'D:\yingheshujuji\train'
path_save = Path(image_save_dir)
if not path_save.exists():
path_save.mkdir()
# 进行转换
videos2images(video_path_list, image_save_dir)
当完成转化后将获得的图片进行整理便形成我们所需的数据集。如下图所示
数据集分为训练集和测试集,分别有biscuits、instantnoodle和shampoo三个文件,里边的图片对应了三种商品的图片。
4 预训练实现过程及关键代码说明
想要完成预训练,首先是训练环境的搭建。本项目使用预训练环境的操作系统是Ubuntu Linux 22.04LTS,使用了GPU进行加速(使用CUDA11),使用到的深度学习框架为Python3.8.11版本对应的PyTorch 。首先是Ubuntu Linux 22.04LTS的安装,该操作系统可以通过微软的应用商店进行下载,下载完成后需要设置用户名和密码,设置完成后就完成该操作系统环境的搭建。
然后需要在该环境下安装Python的3.8.11版本,为了方便python版本的管理,这里我们使用到了pyenv,其安装过程可以参考网站http://t.csdnimg.cn/gtznJ 进行安装,安装完成后只需执行相关的命令便可快速切换python版本,下载好python3.8.11后通过执行pyenv versions便可查看python版本。
接下来,如果在训练时要使用GPU进行加速,还需要下载相应的CUDA,只需要去官网选择相应的版本,然后通过其给的代码安装下载即可。安装完成后便可以通过代码nvidia-sim进行cuda版本的查看。接下来需要将官方提供的两个文件,ai8x-synthesis和ai8x-training下载到linx系统环境中,这里使用到了GIT,通过它可以直接从目标网站上拉取代码,将文件拉取完成后便可以在目录中发现这两个文件。下载好两个文件后需要安装相应的模块,通过PIP进行模块的导入,导入完成后便可以通过python -m venv venv --prompt ai8x-training建立虚拟环境,通过source venv/bin/activate进入虚拟环境。到此进行预训练的环境已经搭建完成。在进行训练前,需要指定训练的模型,这里通过将猫狗识别模型的二输出改为三输出作为自己的模型进行训练。然后将自己准备的数据集放入到data文件中,将文件中图片的目录格式改为与官方模型训练集相同的目录,然后执行训练脚本文件即可开始进行预训练。
模型训练过程中需要读取数据集,首先将数据集分为测试集和训练集放置到目录ai8x-training/data/store下。 然后在ai8x-training/datasets下新建一个文件store.py其内容如下
import torch.nn as nn
import torchvision
from torchvision import transforms
import ai8x
def store_get_datasets(data, load_train=True, load_test=True):
(data_dir, args) = data
image_size = (64, 64) #图片大小为64X64
if load_train:
train_data_path = data_dir + '/store/train/'
train_transforms = transforms.Compose([
transforms.RandomHorizontalFlip(), #随机翻转,水平方向
transforms.Resize(image_size),
transforms.ToTensor(), #转化成Tensor
# transforms.Normalize(mean=[0.485,0.456,0.406],
# std=[0.229,0.224,0.225])]),
ai8x.normalize(args=args)
])
train_dataset = torchvision.datasets.ImageFolder(root = train_data_path,
transform = train_transforms)
else:
train_dataset = None
if load_test:
test_data_path = data_dir + '/store/test/'
test_transforms = transforms.Compose([
transforms.Resize(image_size),
transforms.ToTensor(),
# transforms.Normalize(mean=[0.485,0.456,0.406],
# std=[0.229,0.224,0.225])]) #标准化处理
ai8x.normalize(args=args)
])
test_dataset = torchvision.datasets.ImageFolder(root = test_data_path,
transform = test_transforms)
if args.truncate_testset:
test_dataset.data = test_dataset.data[:1]
else:
test_dataset = None
return train_dataset, test_dataset
datasets = [
{
'name': 'store',
'input': (3, 64, 64),
'output': ('biscuits', 'instantnoodle', 'shampoo'),
'loader': store_get_datasets,
},
]
进行模型训练使用到的模型为官方历程中的猫狗模型,因为猫狗模型是两输出,我们使用的模型要实现对方便面、饼干和洗头膏的识别,为三输出,故将其二输出改为三输出,及将代码第10行中的num_classes=2改为num_classes=3 其代码如下。
from torch import nn
import ai8x
class AI85CatsDogsNet(nn.Module):
"""
Define CNN model for image classification.
"""
def __init__(self, num_classes=3, num_channels=3, dimensions=(128, 128),
fc_inputs=16, bias=False, **kwargs):
super().__init__()
# AI85 Limits
assert dimensions[0] == dimensions[1] # Only square supported
# Keep track of image dimensions so one constructor works for all image sizes
dim = dimensions[0]
self.conv1 = ai8x.FusedConv2dReLU(num_channels, 16, 3,
padding=1, bias=bias, **kwargs)
# padding 1 -> no change in dimensions -> 16x128x128
pad = 2 if dim == 28 else 1
self.conv2 = ai8x.FusedMaxPoolConv2dReLU(16, 32, 3, pool_size=2, pool_stride=2,
padding=pad, bias=bias, **kwargs)
dim //= 2 # pooling, padding 0 -> 32x64x64
if pad == 2:
dim += 2 # padding 2 -> 32x32x32
self.conv3 = ai8x.FusedMaxPoolConv2dReLU(32, 64, 3, pool_size=2, pool_stride=2, padding=1,
bias=bias, **kwargs)
dim //= 2 # pooling, padding 0 -> 64x32x32
self.conv4 = ai8x.FusedMaxPoolConv2dReLU(64, 32, 3, pool_size=2, pool_stride=2, padding=1,
bias=bias, **kwargs)
dim //= 2 # pooling, padding 0 -> 32x16x16
self.conv5 = ai8x.FusedMaxPoolConv2dReLU(32, 32, 3, pool_size=2, pool_stride=2, padding=1,
bias=bias, **kwargs)
dim //= 2 # pooling, padding 0 -> 32x8x8
self.conv6 = ai8x.FusedConv2dReLU(32, fc_inputs, 3, padding=1, bias=bias, **kwargs)
self.fc = ai8x.Linear(fc_inputs*dim*dim, 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 ai85cdnet(pretrained=False, **kwargs):
"""
Constructs a AI85CatsDogsNet model.
"""
assert not pretrained
return AI85CatsDogsNet(**kwargs)
models = [
{
'name': 'ai85cdnet',
'min_input': 1,
'dim': 2,
},
]
更改完成后便可运行代码开始进行训练
python train.py --epochs 100 --optimizer Adam --lr 0.001 --wd 0.001 --batch-size 10 --deterministic --compress policies/schedule-store.yaml --model ai85cdnet --dataset store --param-hist --pr-curves --embedding --device MAX78000 "$@"
模型训练完成后需要将生成的模型文件转换为MAX78000可识别的c代码,通过使用对CNN网络模型的描述性yaml文件配合ai8x-synthesis项目下的ai8xize.py文件即可完成代码的转换。
为了使训练完成的模型可以在MAX78000板子上正常运行,还需使用到板载的摄像头模块和外接的液晶显示屏。
通过编写图片获取代码和液晶屏显示代码以及主函数即可完成项目的实现。
图片获取的主要代码如下
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;
}
}
j = 0;
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;
// convert to RGB565 for display
rgb = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3);
data565[j] = (rgb >> 8) & 0xFF;
data565[j + 1] = rgb & 0xFF;
j += 2;
}
屏幕显示图片的代码如下
void display_img(int x, int y)
{
int j, cnt = 0;
uint16_t rgb;
uint8_t r, g, b;
for (int row = DISP_SIZE_Y - 1; row >= 0; row--) {
j = 0;
for (int col = 0; col < DISP_SIZE_X; col++) {
// convert to RGB565 for display
b = thumb[cnt++];
g = thumb[cnt++];
r = thumb[cnt++];
rgb = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3);
data565[j] = (rgb >> 8) & 0xFF;
data565[j + 1] = rgb & 0xFF;
j += 2;
}
MXC_TFT_ShowImageCameraRGB565(x, y + row, data565, DISP_SIZE_X, 1);
}
}
完成图片获取和图片显示函数后,通过编写主函数,实现商品识别的功能
以下为主函数代码
int main(void)
{
int i;
int digs;
int ret = 0;
int result[NUM_OF_Goods]; // = {0};
int dma_channel;
int Goods_index[3]={2,1,0};
uint8_t detect = 0; // detect=1: detect mode, detect=0: browse mode
char buff[TFT_BUFF_SIZE];
// Wait for PMIC 1.8V to become available, about 180ms after power up.
MXC_Delay(200000);
/* Enable camera power */
Camera_Power(POWER_ON);
//MXC_Delay(300000);
/* Enable cache */
MXC_ICC_Enable(MXC_ICC0);
/* Switch to 100 MHz clock */
MXC_SYS_Clock_Select(MXC_SYS_CLOCK_IPO);
SystemCoreClockUpdate();
/* Enable peripheral, enable CNN interrupt, turn on CNN clock */
/* CNN clock: 50 MHz div 1 */
cnn_enable(MXC_S_GCR_PCLKDIV_CNNCLKSEL_PCLK, MXC_S_GCR_PCLKDIV_CNNCLKDIV_DIV1);
/* Configure P2.5, turn on the CNN Boost */
cnn_boost_enable(MXC_GPIO2, MXC_GPIO_PIN_5);
/* Bring CNN state machine into consistent state */
cnn_init();
/* Load CNN kernels */
cnn_load_weights();
/* Load CNN bias */
cnn_load_bias();
/* Configure CNN state machine */
cnn_configure();
#ifdef TFT_ENABLE
/* Initialize TFT display */
printf("Init LCD.\n");
#ifdef BOARD_EVKIT_V1
MXC_TFT_Init();
MXC_TFT_ClearScreen();
MXC_TFT_ShowImage(0, 0, image_bitmap_1);
#endif
#ifdef BOARD_FTHR_REVA
/* Initialize TFT display */
mxc_gpio_cfg_t tft_reset_pin = {MXC_GPIO0, MXC_GPIO_PIN_19, MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
mxc_gpio_cfg_t tft_blen_pin = {MXC_GPIO0, MXC_GPIO_PIN_9, MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
MXC_TFT_Init(MXC_SPI0, 1, &tft_reset_pin, &tft_blen_pin);
// MXC_TFT_Init(MXC_SPI0, 1, NULL, NULL);
MXC_TFT_SetRotation(ROTATE_270);
// MXC_TFT_ShowImage(0, 0, image_bitmap_1);
MXC_TFT_SetForeGroundColor(WHITE); // set chars to white
#endif
MXC_Delay(1000000);
#endif
// Initialize DMA for camera interface
MXC_DMA_Init();
dma_channel = MXC_DMA_AcquireChannel();
// Initialize camera.
printf("Init Camera.\n");
camera_init(CAMERA_FREQ);
ret = camera_setup(IMAGE_SIZE_X, IMAGE_SIZE_Y, PIXFORMAT_RGB888, FIFO_THREE_BYTE, STREAMING_DMA,
dma_channel);
camera_set_hmirror(0);
camera_set_vflip(0);
// camera_set_brightness(-2);
if (ret != STATUS_OK) {
printf("Error returned from setting up camera. Error %d\n", ret);
return -1;
}
camera_write_reg(0x11, 0x3); // set camera clock prescaller to prevent streaming overflow
#ifdef TFT_ENABLE
MXC_TFT_SetBackGroundColor(BLACK);
memset(buff, 32, TFT_BUFF_SIZE);
TFT_Print(buff, 55, 20, font_1, snprintf(buff, sizeof(buff), "MAX78000 store"));
TFT_Print(buff, 10, 180, font_2, snprintf(buff, sizeof(buff), "PRESS PB1(SW1) TO DETECT!"));
#endif
while (1)
{
if (PB_Get(0))
{
detect = 1;
break;
}
}
#ifdef TFT_ENABLE
MXC_TFT_ClearScreen();
#endif
// Enable CNN clock
MXC_SYS_ClockEnable(MXC_SYS_PERIPH_CLOCK_CNN);
while (1) {
if (detect)
{
#ifdef TFT_ENABLE
MXC_TFT_ClearScreen();
TFT_Print(buff, TEXT_X_START, TEXT_Y_START + 30, font_1,
snprintf(buff, sizeof(buff), "Press PB1(SW1) to"));
TFT_Print(buff, TEXT_X_START, TEXT_Y_START + 55, font_1,
snprintf(buff, sizeof(buff), "capture an image"));
#endif
LED_Off(LED1);
LED_Off(LED2);
while (!PB_Get(0))
capture_process_camera();
cnn_start();
cnn_load_input();
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; // SLEEPDEEP=0
while (cnn_time == 0) {
__WFI(); // Wait for CNN interrupt
}
// Unload CNN data
cnn_unload((uint32_t *)ml_data);
cnn_stop();
// Softmax
softmax_shift_q17p14_q15((q31_t *)ml_data, CNN_NUM_OUTPUTS, 4, ml_softmax);
printf("Time for CNN: %d us\n\n", cnn_time);
printf("Classification results:\n");
for (i = 0; i < NUM_OF_Goods; i++) {
digs = ml_softmax[class2num[i]] ;
result[i] = digs;
}
printf("\n");
#ifdef ASCII_ART
asciiart((uint8_t *)input_0);
#endif
}
#ifdef TFT_ENABLE
area_t area;
area.x = TEXT_X_START - 2;
area.y = 0;
area.w = 320 - TEXT_X_START;
area.h = 240;
MXC_TFT_ClearArea(&area, BLACK);
memset(buff, 32, TFT_BUFF_SIZE);
if (detect)
{
for (i = 0; i < 3; i++)
TFT_Print(buff, TEXT_X_START, TEXT_Y_START+20+20*i, font_2,
snprintf(buff, sizeof(buff), "%s (%d%%)", store_names[Goods_index[i]], (((result[i]+3*i+1)*100)/(result[Goods_index[0]]+result[Goods_index[1]]+result[Goods_index[2]]+12) ) ));
}
#endif
while (1)
{
if (PB_Get(0))
{
detect = 1;
break;
}
}
#ifdef TFT_ENABLE
MXC_TFT_ClearScreen();
#endif
MXC_Delay(500000);
}
return 0;
}
4 实现结果展示
下图为最后识别结果的暂时,当系统获取到图片后,便会对图片进行识别,三种商品的可能性都会显示在屏幕上,百分比最高的即为识别到的结果。
方便面识别结果
饼干识别结果
洗头膏识别结果
遇到的主要难题及解决办法
在项目进行的过程中,我遇到了许多的难题,如对深度学习的基础知识了解不够多,感到项目课程听起来较为费力,我通过网络学习与深度学习和卷积神经网络有关的知识,使得自己可以掌握该项目的原理。在进行训练环境搭建的时候,我也遇到了很多报错的地方,同时也有网络不好,下载速度较慢等情况,通过查询网络相关问题,在交流群众询问,最终完成该项目的实现。
未来的计划或建议
通过参加本次硬禾学堂组织的MAX78000活动,让我对深度学习有了更加深刻的了解,也学到了很多新的知识,积累了宝贵的经验。同时我也意识到我完成的该项目有许多不足之处,如模型训练的精度不够高,物品识别的类型较少等。在接下来的日子里,我也会通过不断修改模型参数和数据集来对模型进行训练,以达到更好的训练效果。