一、项目介绍
本设计将实现一个趣味答题系统。系统会自动出题,用户看到题目之后通过手势回答题目,通过摄像头采集手势图片,然后经过CNN计算识别出是什么手势,回答结果会在屏幕上进行显示。
本项目采用实时计算,随时采集随时计算,并通过TFTLCD显示出手势类型、答题情况。
二、项目设计思路
项目所需的外设功能:
摄像头(板载):用于捕获手势,通过 Camera_IF 工程范例进行调试,调试初期,关闭ENABLE_TFT宏定义,使用工程自带的 pc_utility 脚本,通过串口接收摄像头捕获的图像并在PC上进行显示;
串口(板载):用于输出调试信息;
显示屏(外购):用于实时显示摄像头捕获的图像信息,采购的是 DFRobot 2.8" 320x240 TFT电阻触摸显示屏,驱动芯片为 ILI9341,通过 TFT_Demo 工程范例进行调试测试;
整体流程如下:
1、寻找素材,通过网上查找,自己拍摄等方式收集两种手势与空白时的照片,单种数量越多越好。
2、训练模型,搭建训练环境,由于该板卡仅支持在Windows中进行AI模型部署,模型训练和量化还需在Linux环境下进行。因此我们需要搭建Linux环境,选择使用WSL(Windows Subsystem for Linux),它是适用于 Linux 的 Windows 子系统,修改学习率,多次尝试取最优值。
3、量化模型,量化后,测试模型准确率。
4、转换代码,部署板卡,修改编译脚本,部署测试训练图片在板卡上的情况。
5、编写摄像头、屏幕等驱动,使用SPI驱动LCD显示屏,编写汉字显示代码,驱动摄像头采集图像,并显示到LCD上。
6、调节识别情况,通过摄像头采集图像,看一下识别效果,根据识别情况去调节判断阈值。
7、编写代码,编写题目显示,题目判断逻辑,并测试实际情况。
三、搜集素材的思路
1、数据集图片
(1). HaGRID数据集
手势识别(HGR)系统的大型图像数据集HaGRID(HAnd Gesture Recognition Image Dataset)。您可以将其用于图像分类或图像检测任务。提出的数据集允许构建HGR系统,该系统可用于视频会议服务(Zoom,Skype,Discord,Jazz等),家庭自动化系统,汽车行业等。
HaGRID大小为716GB,数据集包含552,992张FullHD(1920×1080)RGB图像,分为18类手势。此外,如果框架中有第二个空闲的手,则某些图像具有类。这个额外的类包含 123,589 个样本。数据分为训练92%和测试8%的受试者,其中509,323张图像用于训练,43,669张图像用于测试。
下载地址:GitHub - hukenovs/hagrid: HAnd Gesture Recognition Image Dataset
(2). ASL Alphabet数据集
该数据集是美国手语字母图像的集合,分为 29 个文件夹,代表各个类别。
训练数据集包含 87,000张200x200 像素的图像。有29个类,其中26个用于字母AZ,3 个类用于SPACE、DELETE和NOTHING。这3个类在实时应用和分类中非常有用。测试数据集仅包含 29 张图像,以鼓励使用真实世界的测试图像。
2、自拍摄图片
由于训练集都是在使用比较好的摄像头进行采集,而MAX78000板卡上板载的摄像头拍摄的效果跟像素并不好,只使用数据集的数据实际部署到板卡上有可能出现“水土不服”的问题,因此我们需要使用MAX78000板载的摄像头采集一些图像用于补充数据集。美信提供了相关例程,参考官方提供的例程做一个拍摄照片并将拍摄好的素材保存到SD卡中。
四、预训练实现过程
训练脚本
#!/bin/sh
python3 train.py --epochs 100 --optimizer Adam --lr 0.00030 --batch-size 256 --deterministic --compress schedule-okno.yaml --model ai85aslnet --dataset OkNo --confusion --device MAX78000 --use-bias "$@"
训练过程(最后一次训练与验证)
2023-01-12 01:24:16,907 - Training epoch: 8100 samples (256 per mini-batch)
2023-01-12 01:24:43,706 - Epoch: [99][ 10/ 32] Overall Loss 0.241984 Objective Loss 0.241984 LR 0.000300 Time 2.679598
2023-01-12 01:25:07,537 - Epoch: [99][ 20/ 32] Overall Loss 0.241805 Objective Loss 0.241805 LR 0.000300 Time 2.531213
2023-01-12 01:25:30,176 - Epoch: [99][ 30/ 32] Overall Loss 0.241677 Objective Loss 0.241677 LR 0.000300 Time 2.442066
2023-01-12 01:25:33,737 - Epoch: [99][ 32/ 32] Overall Loss 0.241651 Objective Loss 0.241651 Top1 100.000000 LR 0.000300 Time 2.400650
2023-01-12 01:25:33,980 - --- validate (epoch=99)-----------
2023-01-12 01:25:33,986 - 900 samples (256 per mini-batch)
2023-01-12 01:25:39,475 - Epoch: [99][ 4/ 4] Loss 0.249281 Top1 99.666667
2023-01-12 01:25:39,681 - ==> Top1: 99.667 Loss: 0.249
2023-01-12 01:25:39,695 - ==> Confusion:
[[283 0 3]
[ 0 307 0]
[ 0 0 307]]
2023-01-12 01:25:39,707 - ==> Best [Top1: 100.000 Sparsity:0.00 Params: 59568 on epoch: 98]
2023-01-12 01:25:39,708 - Saving checkpoint to: logs/2023.01.11-230830/qat_checkpoint.pth.tar
2023-01-12 01:25:39,719 - --- test ---------------------
2023-01-12 01:25:39,720 - 3 samples (256 per mini-batch)
2023-01-12 01:25:40,819 - Test: [ 1/ 1] Loss 0.241214 Top1 100.000000
2023-01-12 01:25:41,024 - ==> Top1: 100.000 Loss: 0.241
2023-01-12 01:25:41,025 - ==> Confusion:
[[1 0 0]
[0 1 0]
[0 0 1]]
量化模型
生成代码
五、实现结果展示
界面布局
识别为空
识别为NO
识别为OK
回答错误
六、主要代码
主函数
int main(void)
{
int i, dma_channel;
int ret = 0;
int result[CNN_NUM_OUTPUTS] = {0};
// Wait for PMIC 1.8V to become available, about 180ms after power up.
MXC_Delay(200000);
/* Enable camera power */
Camera_Power(POWER_ON);
MXC_ICC_Enable(MXC_ICC0); // Enable cache
// Switch to 100 MHz clock
MXC_SYS_Clock_Select(MXC_SYS_CLOCK_IPO);
SystemCoreClockUpdate();
MXC_Delay(SEC(2)); // Let debugger interrupt if needed
cnn_enable(MXC_S_GCR_PCLKDIV_CNNCLKSEL_PCLK, MXC_S_GCR_PCLKDIV_CNNCLKDIV_DIV1);
mxc_gpio_cfg_t gpio_out;
gpio_out.port = MXC_GPIO2;
gpio_out.mask = MXC_GPIO_PIN_5;
gpio_out.pad = MXC_GPIO_PAD_NONE;
gpio_out.func = MXC_GPIO_FUNC_OUT;
MXC_GPIO_Config(&gpio_out);
MXC_GPIO_OutSet(gpio_out.port, gpio_out.mask);
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_SetRotation(ROTATE_270);
MXC_TFT_ShowImage(0, 0, image_bitmap_1);
MXC_TFT_SetForeGroundColor(WHITE); // set chars to white
MXC_Delay(1000000);
MXC_DMA_Init();
dma_channel = MXC_DMA_AcquireChannel();
camera_init(CAMERA_FREQ);
ret = camera_setup(IMAGE_SIZE_X, IMAGE_SIZE_Y, PIXFORMAT_RGB888, FIFO_THREE_BYTE, USE_DMA,
dma_channel);
if (ret != STATUS_OK) {
printf("Error returned from setting up camera. Error %d\n", ret);
return -1;
}
MXC_TFT_SetPalette(image_bitmap_2);
MXC_TFT_SetBackGroundColor(4);
memset(buff, ' ', TFT_BUFF_SIZE);
uint8_t question_num = 0, answer = 0, user_answer = 0, past_user_answer = 0, state = 0;
while (1)
{
capture_camera_img();
process_camera_img(input_0_camera, input_1_camera, input_2_camera);
convert_img_unsigned_to_signed(input_0_camera, input_1_camera, input_2_camera);
cnn_init(); // Bring state machine into consistent state
cnn_load_weights(); // Load kernels
cnn_load_bias();
cnn_configure(); // Configure state machine
cnn_start(); // Start CNN processing
load_input(); // Load data input via FIFO
MXC_TMR_SW_Start(MXC_TMR0);
while (cnn_time == 0)
__WFI(); // Wait for CNN
softmax_layer();
for (i = 0; i < CNN_NUM_OUTPUTS; i++)
{
result[i] = ((1000 * ml_softmax[i] + 0x4000) >> 15) / 10;
}
if (result[0] > 60) //适应实际修改
{
//No
TFT_Print(buff, 180, 55, font_2, sprintf(buff, "No "));
user_answer = 2;
}
else if (result[1] > 60)
{
//Ok
TFT_Print(buff, 180, 55, font_2, sprintf(buff, "Ok "));
user_answer = 1;
}
else if (result[2] > 60)
{
//空
TFT_Print(buff, 180, 55, font_2, sprintf(buff, "Empty "));
user_answer = 0;
}
else
{
TFT_Print(buff, 180, 55, font_2, sprintf(buff, "Unknown"));
user_answer = 0;
}
if(state == 0)
{
MXC_TFT_WritePixel(0, 0, 319, 40, 0);
MXC_TFT_WritePixel(60, 140, 80, 16, 0);
answer = display_question(question_num);
question_num ++;
state = 1;
}
else if(user_answer != 0)
{
if(past_user_answer == user_answer) state ++;
else state = 1;
if(state == 5)
{
if(user_answer == answer) TFT_printf_str(60, 140, buff, sprintf(buff, "回答正确!"));
else TFT_printf_str(60, 140, buff, sprintf(buff, "回答错误!"));
}
past_user_answer = user_answer;
}
if(user_answer == 0 && state >= 5 && question_num < 10)
{
state = 0;
}
TFT_Print(buff, 180, 160, font_2, sprintf(buff, "No:%d ",result[0]));
TFT_Print(buff, 180, 180, font_2, sprintf(buff, "Ok:%d ",result[1]));
TFT_Print(buff, 180, 200, font_2, sprintf(buff, "Unknown:%d ",result[2]));
convert_img_signed_to_unsigned(input_0_camera, input_1_camera, input_2_camera);
lcd_show_sampledata(input_0_camera, input_1_camera, input_2_camera, 180, 85, 1024);
}
return 0;
}
字符串显示(支持汉字显示与汉字、ASCII字符混合显示)
void TFT_printf_str(uint8_t x, uint8_t y, char *str, uint8_t lenght)
{
uint8_t i = 0, j = 0, work = 0, num = 0;
for(num = 0; num < lenght;)
{
if(str[num] > 127)
{
for(i=0;i<sizeof(word_bank_16X16_indexes);)
{
if(str[num] == word_bank_16X16_indexes[i] && str[num + 1] == word_bank_16X16_indexes[i+1])
{
break;
}
i+=2;
}
work = i/2;
for(i = 0; i < 16; i++)
{
char temp = word_bank_16X16[i + work*32];
for(j = 0; j < 8; j++)
{
if(temp & 0x01)
MXC_TFT_WritePixel(x+i + num*8, y+j, 0, 0, 0xFFFF);
temp = temp >> 1;
}
}
for(i = 0; i < 16; i++)
{
char temp = word_bank_16X16[16 + i + work*32];
for(j = 0; j < 8; j++)
{
if(temp & 0x01)
MXC_TFT_WritePixel(x+i + num*8, y+8+j, 0, 0, 0xFFFF);
temp = temp >> 1;
}
}
num += 2;
}
else
{
TFT_Print(&str[num], x + num*8, y, font_1, 1);
num++;
}
}
}
摄像头图像显示
void lcd_show_sampledata(uint32_t* data0, uint32_t* data1, uint32_t* data2, int xcord, int ycord,
int length)
{
int i,j,x,y,r,g,b;
int scale = 1.2;
uint32_t color;
uint8_t* ptr0;
uint8_t* ptr1;
uint8_t* ptr2;
x = 0;
y = 0;
for (i = 0; i < length; i++)
{
ptr0 = (uint8_t*)&data0[i];
ptr1 = (uint8_t*)&data1[i];
ptr2 = (uint8_t*)&data2[i];
for (j = 0; j < 4; j++)
{
r = ptr0[j];
g = ptr1[j];
b = ptr2[j];
color = RGB(r, g, b); // convert to RGB565
MXC_TFT_WritePixel(xcord * scale + x * scale, ycord * scale + y * scale, scale, scale, color);
x += 1;
if (x >= (IMAGE_SIZE_X))
{
x = 0;
y += 1;
if ((y + 6) >= (IMAGE_SIZE_Y))
return;
}
}
}
}
七、问题与下一步计划
加入更多的手势识别内容,支持选择题的功能。
加入联网功能,支持在线获取题库。
优化训练模型,在极端情况下也能识别。
八、后记
很开心能参加这一次比赛,群里也有很多大佬在讨论,也给予了我许多的帮助,但是不得不说的是,咕咕咕太严重了,而且,公司事比我想象中的还要多!!!直到活动快结束才加把劲赶进度,实现的东西也比较简单,对此不是特别满意。希望自己再下一次的活动中,能快一点,不再咕咕咕。