先来一段视频演示
实物演示视频
项目目标:
(1)通过视觉检测和语音识别对垃圾进行识别,并完成分类:
(2)实现垃圾满载检测已经满载时,信息上报服务器或云平台;
(3)垃圾分类完成后,能够进行语音播报;
资源准备
sipeed的K210用于垃圾识别
STM32ZET6开发板作为主控(需要用到很多串口)
LD3320用于语音识别
JQ8900用于语音播报
ESP12F连接wifi(ESP8266都行)
步进电机驱动器和两个步进电机(舵机什么的都行,取决于机械结构)
12V以上电源
硬件接线
主控与K210芯片连接说明如下:
STM32 | K210 |
PA9 | 10 |
PA10 | 9 |
VCC | 5V |
GND | GND |
驱动与主控连接说明如下:
STM32 | 步进电机驱动1 | STM32 | 步进电机驱动2 |
PF1 | PUL- | PF3 | PUL- |
+5v | PUL+ | +5v | PUL+ |
PF2 | DIR+ | PF4 | DIR+ |
GND | DIR- | GND | DIR- |
电源及驱动连接说明
电源采用了4节18650电池并联提供14.4V的电压,用于给驱动42BYGH34-401A供电,驱动连接步进电机。
驱动与步进电机连接如下:
驱动 | 步进电机 |
VMOT | 12V电源+ |
GND | 12V电源- |
B2 | B- |
A2 | B+ |
A1 | A+ |
B1 | A- |
主控与语音播报模块连接说明如下:
STM32 | JQ8900 |
PA2 | 3 |
PA3 | 4 |
5V | 6 |
GND | 5 |
主控与语音识别模块连接说明如下:
STM32 | LD3320 |
PC10 | RXD |
PC11 | TXD |
5V | 5V |
GND | GND |
红外模块连接说明如下:
STM32 | 红外 |
GND | GND |
PC6 | OUT |
5V | 5V |
WIFI模块连接说明如下:
STM32 | ESP8266 |
PB10 | RXD |
PB11 | TXD |
5V | 5V |
GND | GND |
软件设计
首先进行需要收集垃圾检测的数据集,需要进行多角度的拍照,最好就是用K210进行拍照,不要使用手机取样。写个代码,按键按下就拍照,将图片保存在SD卡,拍照时把垃圾的背景弄成A4纸垫上,识别效果会好些。数据集准备好后就要进行数据集标注,像这样
标注完后进行训练,这里我勾选了随机模糊和数据均衡。
即便是同一个设置,每次的训练都有不同的效果。还有不是说上面的成功lv越高就效果越好,放在芯片中跑的时候不一定,光线的影响是很大的,有条件的话保证光线一致。
然后直接就导入模型到K210了,会自动生成一个的代码。给代码加上串口发送的功能,就能把数据发到STM32了。附上K210代码。
# generated by maixhub, tested on maixpy3 v0.4.8
# copy files to TF card and plug into board and power on
import sensor, image, lcd, time
import KPU as kpu
import gc, sys
from machine import UART
from fpioa_manager import fm
fm.register(10, fm.fpioa.UART1_TX, force=True)
fm.register(9, fm.fpioa.UART1_RX, force=True)
uart = UART(UART.UART1, 9600, 8, 0, 1, timeout=1000, read_buf_len=4096)
input_size = (224, 224)
labels = ['4', '2', '5', '1']
anchors = [2.62, 2.47, 1.5, 0.84, 2.09, 1.16, 1.5, 1.38, 1.13, 1.09]
def lcd_show_except(e):
import uio
err_str = uio.StringIO()
sys.print_exception(e, err_str)
err_str = err_str.getvalue()
img = image.Image(size=input_size)
img.draw_string(0, 10, err_str, scale=1, color=(0xff,0x00,0x00))
lcd.display(img)
def main(anchors, labels = None, model_addr="/sd/m.kmodel", sensor_window=input_size, lcd_rotation=0, sensor_hmirror=False, sensor_vflip=False):
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing(sensor_window)
sensor.set_hmirror(sensor_hmirror)
sensor.set_vflip(sensor_vflip)
sensor.run(1)
lcd.init(type=1)
lcd.rotation(lcd_rotation)
lcd.clear(lcd.WHITE)
if not labels:
with open('labels.txt','r') as f:
exec(f.read())
if not labels:
print("no labels.txt")
img = image.Image(size=(320, 240))
img.draw_string(90, 110, "no labels.txt", color=(255, 0, 0), scale=2)
lcd.display(img)
return 1
try:
img = image.Image("startup.jpg")
lcd.display(img)
except Exception:
img = image.Image(size=(320, 240))
img.draw_string(90, 110, "loading model...", color=(255, 255, 255), scale=2)
lcd.display(img)
try:
task = None
task = kpu.load(model_addr)
kpu.init_yolo2(task, 0.5, 0.3, 5, anchors) # threshold:[0,1], nms_value: [0, 1]
while(True):
img = sensor.snapshot()
t = time.ticks_ms()
objects = kpu.run_yolo2(task, img)
t = time.ticks_ms() - t
if objects:
for obj in objects:
pos = obj.rect()
img.draw_rectangle(pos)
img.draw_string(pos[0], pos[1], "%s : %.2f" %(labels[obj.classid()], obj.value()), scale=2, color=(255, 0, 0))
if obj.value()>0.5:
uart.write('%s\r\n'%(labels[obj.classid()]))#·¢±êÇ©
img.draw_string(0, 200, "t:%dms" %(t), scale=2, color=(255, 0, 0))
lcd.display(img)
except Exception as e:
raise e
finally:
if not task is None:
kpu.deinit(task)
if __name__ == "__main__":
try:
# main(anchors = anchors, labels=labels, model_addr=0x300000, lcd_rotation=0)
main(anchors = anchors, labels=labels, model_addr="/sd/model-30957.kmodel")
except Exception as e:
sys.print_exception(e)
lcd_show_except(e)
finally:
gc.collect()
stm32端的数据接收使用正点原子那套就行,K210发送的结尾是回车换行。视觉识别实现了,下面是语音识别,找到卖家给的代码,有一个函数,用来添加识别的语音。
/************************************************************************
功能描述: 向LD模块添加关键词
入口参数: none
返 回 值: flag:1->添加成功
其他说明: 用户修改.
1、根据如下格式添加拼音关键词,同时注意修改sRecog 和pCode 数组的长度
和对应变了k的循环置。拼音串和识别码是一一对应的。
2、开发者可以学习"语音识别芯片LD3320高阶秘籍.pdf"中
关于垃圾词语吸收错误的用法,来提供识别效果。
3、”xiao jie “ 为口令,故在每次识别时,必须先发一级口令“小捷”
**************************************************************************/
uint8 LD_AsrAddFixed()
{
uint8 k, flag;
uint8 nAsrAddLength;
#define DATE_A 50 /*数组二维数值*/
#define DATE_B 20 /*数组一维数值*/
uint8 code sRecog[DATE_A][DATE_B] =
{
"xiao jie",\
"dian chi",\
"ju zi",\
"zhi ban",\
"che lun",\
}; /*添加关键词,用户修改*/
uint8 code pCode[DATE_A] =
{
CODE_CMD, \
}; /*添加识别码,用户修改*/
flag = 1;
for (k=0; k<DATE_A; k++)
{
if(LD_Check_ASRBusyFlag_b2() == 0)
{
flag = 0;
break;
}
LD_WriteReg(0xc1, pCode[k] );
LD_WriteReg(0xc3, 0 );
LD_WriteReg(0x08, 0x04);
delay(1);
LD_WriteReg(0x08, 0x00);
delay(1);
for (nAsrAddLength=0; nAsrAddLength<DATE_B; nAsrAddLength++)
{
if (sRecog[k][nAsrAddLength] == 0)
break;
LD_WriteReg(0x5, sRecog[k][nAsrAddLength]);
}
LD_WriteReg(0xb9, nAsrAddLength);
LD_WriteReg(0xb2, 0xff);
LD_WriteReg(0x37, 0x04);
}
return flag;
}
我这只添加了4个,第一个“xiao jie”,是一级指令,就像小爱同学一样,先呼叫一级指令,才会进行识别。假如识别到语音为电池,就会通过串口发送1\r\n。收到“橘子”,就会发2\r\n。STM32通过接收的数字,就知道识别的结果。语音识别完成。
接下来,就该根据K210和语音模块收到的结果,将物品通过步进电机推动进行分类。接下来是STM32端的根据识别结果驱动步进电机运动的代码。
if (((USART_RX_STA & 0x8000) || (USART4_RX_STA & 0x8000))) //接收完成
{
if ((USART_RX_BUF[0] == 0x32) || (USART4_RX_BUF[0] == 0x33))
{
mode = 2;
MyUSART_SendArr(Play2, 6); //可回收垃圾
delay_ms(1863);
MyUSART_SendArr(play[sum[mode]], 6); //垃圾数
}
if (USART_RX_BUF[0] == 0x31 || (USART4_RX_BUF[0] == 0x34)) //车轮
{
mode = 4;
MyUSART_SendArr(Play4, 6);
delay_ms(1863);
MyUSART_SendArr(play[sum[mode]], 6);
}
if (USART_RX_BUF[0] == 0x34 || (USART4_RX_BUF[0] == 0x32)) //橘子
{
mode = 3;
MyUSART_SendArr(Play3, 6);
delay_ms(1863);
MyUSART_SendArr(play[sum[mode]], 6);
}
if (USART_RX_BUF[0] == 0x35 || (USART4_RX_BUF[0] == 0x31)) //电池
{
mode = 1;
MyUSART_SendArr(Play1, 6);
delay_ms(1863);
MyUSART_SendArr(play[sum[mode]], 6);
}
if ((mode == 1) || (mode == 2) || ((mode == 3) && (flags3 == 0)) || (mode == 4))
{
XYcontrol();//控制电机往返
sum[mode]++;
mode = 0;
}
LED0 = 1;
LED1 = 1;
USART_RX_STA = 0;
USART4_RX_STA = 0;
}
void motor(unsigned int motor1_dir, unsigned int motor1_step, unsigned int motor2_dir, unsigned int motor2_step)
{
unsigned int i;
switch(motor1_dir)
{
case 0 : GPIO_SetBits(Motor_GPIO,Motor1_DIR); break;
case 1 : GPIO_ResetBits(Motor_GPIO,Motor1_DIR); break;
default : break;
}
switch(motor2_dir)
{
case 0 : GPIO_SetBits(Motor_GPIO,Motor2_DIR); break;
case 1 : GPIO_ResetBits(Motor_GPIO,Motor2_DIR); break;
default : break;
}
for(i = 0;i < motor1_step || i < motor2_step; i++)
{
if(i<motor1_step)
{
GPIO_SetBits(Motor_GPIO,Motor1_STEP);
delay_us(motortime); //周期motortime*4
GPIO_ResetBits(Motor_GPIO,Motor1_STEP);
delay_us(motortime);
}
if(i<motor2_step)
{
GPIO_SetBits(Motor_GPIO,Motor2_STEP);
delay_us(motortime);
GPIO_ResetBits(Motor_GPIO,Motor2_STEP);
delay_us(motortime);
}
}
//delay_ms(2); //延时一会
}
void XYcontrol(void)
{
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);//关闭串口接收中断
//USART_ITConfig(USART3, USART_IT_RXNE, DISABLE);//关闭串口接收中断
if(mode==1)
motor(a[0],a[1],a[2],a[3]);
if(mode==2)
motor(b[0],b[1],b[2],b[3]);
if(mode==3)
motor(c[0],c[1],c[2],c[3]);
if(mode==4)
motor(d[0],d[1],d[2],d[3]);
if(mode!=0)
flag=1;
returninit();
}
void returninit(void)
{
unsigned char i;
if(flag)
{
delay_ms(30);
if(mode==1)
motor(0,a[1],0,a[3]);
if(mode==2)
motor(1,b[1],0,b[3]);
if(mode==3)
motor(1,c[1],1,c[3]);
if(mode==4)
motor(0,d[1],1,d[3]);
flag=0;
for(i=0;i<USART_REC_LEN;i++)
{
USART_RX_BUF[i]=0;
USART4_RX_BUF[i]=0;
} //清楚串口接收缓冲
MyUSART_SendArr(Play6,6);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接收中断
USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);//开启串口接收中断
}
}
这里说明一下,当时做这个项目的时候是大二,写代码很不规范,不过我现在也懒得改了。。。
mode变量是识别到的结果,flag3是满载检测标志位。MyUSART_SendArr()这个函数是用来和语音播报模块通信的,接下来介绍的就是JQ8900(不过现在已经很落后)
JQ8900支持文件系统,需要将提前生成的音频文件保存到其内部,然后通过串口去让它选择播报哪一个音频文件,使用串口发送的数据要符合他规定的协议。
u8 Play1[] = {0xAA, 0x07, 0x02, 0x00, 0x01, 0xB4};//有害垃圾
u8 Play2[] = {0xAA, 0x07, 0x02, 0x00, 0x02, 0xB5}; //可回收垃圾
u8 Play3[] = {0xAA, 0x07, 0x02, 0x00, 0x03, 0xB6}; //厨余垃圾
u8 Play4[] = {0xAA, 0x07, 0x02, 0x00, 0x04, 0xB7}; //其他垃圾
u8 Play5[] = {0xAA, 0x07, 0x02, 0x00, 0x05, 0xB8}; //垃圾已满
u8 Play6[] = {0xAA, 0x07, 0x02, 0x00, 0x0e, 0xC1}; //OK
u8 play[][6]={{0xAA, 0x07, 0x02, 0x00, 0x0d, 0xC0},{0xAA, 0x07, 0x02, 0x00, 0x06, 0xB9},{0xAA, 0x07, 0x02, 0x00, 0x07, 0xBA},
{0xAA, 0x07, 0x02, 0x00, 0x08, 0xBB},{0xAA, 0x07, 0x02, 0x00, 0x09, 0xBC},{0xAA, 0x07, 0x02, 0x00, 0x0a, 0xBD},{0xAA, 0x07, 0x02, 0x00, 0x0b, 0xBE},
{0xAA, 0x07, 0x02, 0x00, 0x0c, 0xBF}};//数字0~7
它的数据格式有一个求和校验,就拿有害垃圾的语音举例吧。最后一个0xB4是校验字节,他的低位是(A(也就是10)+7+2+0+1)%16=4,A+(A(也就是10)+7+2+0+1)/16=B,所以校验字节是0xB4。现在有的语音播报模块直接通过Printf发送文字就行了。
语音播报也完成,接下来就是满载检测和阿里云的信息上传了。
满载检测就不详细介绍了,就是垃圾箱顶端安装一个红外二极管,根据返回的电平信号判断。
阿里云的连接网上也有恒多资料,这里就不详细说明了。