拖更了n久的备赛日记终于来啦,最近实现了关于K210图像识别并将所需数据(即目标类别,目标在图像中的加权坐标)其中,加权坐标指K210识别到的目标并框出的框的宽和高与框左上顶点的坐标加权,希望以此来判断目标所处的位置并方便后续进行诸如寻迹,目标跟随等任务。其中涉及包括YOLO网络的训练,上位机K210进行目标检测并利用串口对数据进行发出。下位机STM32则要接收到K210传出的数据,对数据进行解码,存入对应数组便于后续引用,在此次实验中再利用串口二将接收数据传给PC端利用串口助手进行数据显示。
我们一步一步来,从K210的YOLO网络训练开始讲起,K210中常用的YOLO网络训练包括利用spieed公司的线上训练平台,本地训练中除了深度学习大佬依旧可以利用之前常用的训练方法进行训练外,身为初学者,没有那么多的经历也没有能力完成深度学习环境的搭建与代码的编写,可以选择大佬开发好的MX-YOLO进行训练。本次实验就是采用MX-YOLO进行本地训练(跑例程的mask检测),开发的模型,在本次日记中不做过多赘述,后续会和相关训练方法同期更新出。在模型运行中遇到了诸如内存不足的问题(感觉是运行内存不足),在重刷了spieed的最小固件后得以解决。也建议大家在使用K210跑深度学习模型的时候刷最小固件来执行程序,这样能容纳稍微大一点的模型。一般能支持2兆到3兆左右(模型存至sd卡)。
模型导入进行目标识别后,我们需要启动K210的串口将数据发出,要注意K210的串口不能单独发出数字,所以此处选择定义了一个一个元组参数来统一发送数据。为了让STM32更好的接收数据,我们定义了一个简单的通信协议。长这样
帧头1:0xfe
帧头2: 0xfd
数据1:classid
数据2:cx
数据3:cy
帧尾:0xff
其中之所以选择0xfe 0xfd 0xff作为帧头,是因为我们的捕捉的图像大小为224*224,皆小于0xfd,不会出现加权后数据大于0xff从而影响通信的情况。
下面就是我们K210端的全部代码啦,烧即用,只需要修改对应模型以及对应的anchor文件和lable文件即可。(注意我选用的spieed的固件包,选用canmv或者其它固件包大概率会无法使用喔)
#此代码中0为未带口罩,1为戴口罩
import sensor
import image
import lcd
import KPU as kpu
from machine import UART
from fpioa_manager import fm
import ustruct
lcd.init()
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((224, 224))
sensor.set_vflip(1)#设置摄像头后置即所见即所得
sensor.run(1)
#初始化串口,pin6为串口1RX口,pin7为串口1TX口
fm.register(6,fm.fpioa.UART1_RX)
fm.register(7,fm.fpioa.UART1_TX)
def sending_data(cc,cx,cy):
global uart;
data = ustruct.pack("<bbhhhb", #格式为俩个字符俩个短整型(2字节)
0xfe,#帧头1
0xfd,#帧头2
int(cc), #数据1为传入类型 #四字节
int(cx), #数据2为框与顶点坐标的加权的长#四字节
int(cy), #数据3为框与顶点坐标的加权的高#四字节
0xff)
uart = UART(UART.UART1, 115200, 8, 0, 0, timeout=1000, read_buf_len=4096)
uart.write(data); #必须要传入一个字节数组
task = kpu.load("/sd/mask.kmodel")
info_list = kpu.netinfo(task)
f=open("mask.anchors.txt","r")
anchor_txt=f.read()
L=[]
for i in anchor_txt.split(","):
L.append(float(i))
anchor=tuple(L)
f.close()
a = kpu.init_yolo2(task, 0.6, 0.3, 5, anchor)
f=open("mask.lable.txt","r")
labels_txt=f.read()
labels = labels_txt.split(",")
f.close()
while(True):
img = sensor.snapshot()
code = kpu.run_yolo2(task, img)
if code:#识别到对应目标
for i in code:
#print(i.w())
obj_x=(i.x()+i.w())/2
obj_y=(i.y()+i.h())/2#数据加权平均计算x,y
sending_data(i.classid(),obj_x,obj_y)#发送数据
if i.classid():#检测目标为mask,仅适用于二分类问题运用此行
a=img.draw_rectangle(i.rect(),(0,255,0),2)
a = lcd.display(img)
for i in code:
lcd.draw_string(i.x()+45, i.y()-20, labels[i.classid()]+" "+'%.2f'%i.value(), lcd.WHITE,lcd.GREEN)
else:#检测目标为un_mask
a=img.draw_rectangle(i.rect(),(255,0,0),2)
a = lcd.display(img)
for i in code:
lcd.draw_string(i.x()+45, i.y()-20, labels[i.classid()]+" "+'%.2f'%i.value(), lcd.WHITE,lcd.RED)
else:#没有要检测的目标出现
a = lcd.display(img)
a = kpu.deinit(task)
K210的数据会按ASCII形式发出(其实无所谓,对应都有十六进制形式),这里要留意,一个整形是四字节,也就是比如我classid为01,但实际发出的数据是 00 01
然后就是stm32的部分了,为了方便移植和开发,本程序中选用hal库开发,对于较底层的解读后续更新。
目前用到的所有初始化都是最基础的cubemx初始化串口的步骤,并无特殊操作,要注意需要使能串口一的中断用于接收中断。同时将串口模式设置为asynchronous(异步)。下面简单介绍一下具体配置过程。
首先我们需要配置系统的RCC,将高速时钟配置成Crystal/Ceramic Resonator:外部无源晶振(陶瓷晶振)
编辑时钟树 配置对应串口
这里只展示了配置串口1,串口2同串口1一致,注意串口1要打开中断,即NVIC那块enable
中断优先级可以不动,让系统自动按中断号跑就行。
初始化就是这样,我们就可以生成代码啦。
将我们这里需要的一些定义代码写在此处,注意一定一定要把代码写在user code里,这样重新配置文件的时候不会被覆盖掉。这里有如此多的hello world主要是为了测试程序是否正常执行。可以进行删除。
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
uint8_t aRxBuffer; //接收中断缓冲
uint8_t Uart1_RxBuffer[9] = {0}; //接收缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
uint8_t Uart1_RxFlag = 0;
uint8_t hello[]={"hello,world\r\n"};
uint8_t hello1[]={"hello1,world\r\n"};
uint8_t hello2[]={"hello2,world\r\n"};
uint8_t hello3[]={"hello3,world\r\n"};
uint8_t hello4[]={"hello4,world\r\n"};
uint8_t hello5[]={"hello5,world\r\n"};
uint8_t hello6[]={"hello6,world\r\n"};
uint8_t error3[]={"please check the end of the frame\r\n"};
uint8_t error2[]={"please check the end of the header1\r\n"};
uint8_t error1[]={"please check the end of the header1\r\n"};
uint8_t classid;
uint8_t obj_x;
uint8_t obj_y;
uint8_t jieguo[3]={0};
/* USER CODE END PTD */
下面是main函数中的部分,要记得在main函数中除了生成的初始化函数,我们还要开启对应的接收中断。
HAL_UART_Transmit(&huart2, hello,sizeof(hello) , 1000);
HAL_UART_Receive_IT(&huart1,Uart1_RxBuffer, 9);
这里将串口1接收的数据直接存储进数组Uart1_RxBuffer中要注意这里其实需要的是一个地址,但是我们知道数组的第一位可以代表他整个数组的地址,所以这里只需要这么写,9代表接收数据的长度为9个字节(参考上述的通信协议,总共是九个字节)
串口接收满9个字节的数据后会引起中断,本程序目前while循环中无任何代码,如果有需要也可以将数据解析任务放置在main函数的while循环中。
引起串口中断后会执行串口中断服务函数,在服务函数中调用串口中断回调函数
此处串口中断回调函数代码如下:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
//HAL_UART_Transmit(&huart2, hello, sizeof(hello),0xFFFF);
HAL_UART_Transmit(&huart2, hello1, sizeof(hello1),0xFFFF);
if(Uart1_RxBuffer[0]==0XFE)//判断帧头1
{
HAL_UART_Transmit(&huart2, hello2, sizeof(hello2),0xFFFF);
if(Uart1_RxBuffer[1]==0XFD)//判断帧头2
{
HAL_UART_Transmit(&huart2, hello3, sizeof(hello2),0xFFFF);
jieguo[0]=Uart1_RxBuffer[3];
jieguo[1]=Uart1_RxBuffer[5];
jieguo[2]=Uart1_RxBuffer[7];
if(Uart1_RxBuffer[8]==0xFF) //判断帧尾
{
HAL_UART_Transmit(&huart2, hello4, sizeof(hello4),0xFFFF);
}
else
{
HAL_UART_Transmit(&huart2, error3, sizeof(error3), 1000);
}
}
else
{
HAL_UART_Transmit(&huart2, error2, sizeof(error2), 1000);
}
}
else
{
HAL_UART_Transmit(&huart2, error1, sizeof(error1), 1000);
}
HAL_UART_Transmit(&huart2, jieguo, sizeof(jieguo), 1000);
memset(jieguo,0x00,sizeof(jieguo)); //清空数组
memset(Uart1_RxBuffer,0x00,sizeof(Uart1_RxBuffer)); //清空数组
HAL_UART_Transmit(&huart2, Uart1_RxBuffer, sizeof(Uart1_RxBuffer), 1000);
HAL_UART_Receive_IT(&huart1,Uart1_RxBuffer, 9); //再开启接收中断
HAL_UART_Transmit(&huart2, hello5, sizeof(hello5), 1000);
}
此处要注意必须用memset清除数组,如果不用memset清除数组,则串口接收数据无法更新(我也不懂,明明进中断了但是不会覆盖第一次的数据结果。有懂的大佬麻烦给我讲一下),同时一定要在中断回调函数中再次使能接收中断。
进行完上述所有,就可正常接收K210传输的数据并将接收数据通过串口2利用USB转TTL传输回PC机串口助手显示。