本帖最后由 沧海笑1122 于 2020-1-13 21:45 编辑
【M5Train 视觉识别轨道小火车头】
【故事】 玩具轨道电动小火车历史悠久,是孩子们常见喜爱的玩具。米兔小火车轨道兼容宜家的轨道,孩子们百玩不厌。但是,毕竟电动小火车头只有一个车速,小火车头拖动着车厢匀速前进,时间长了,难免枯燥。如何增进趣味呢? 本项目一共训练了七个交通标志。我尝试将其用于米兔轨道小火车,用M5stickC作为执行元件,构成了一个基于视觉识别的,好玩的轨道小火车。
我们先一起看看视频吧,在视频中,小火车头拖着一节车厢,从始发站出发,经过了限速、注意火车、解除限速、亮灯、鸣笛以及停车等七个动作。小火车头变身可以识别交通标志的AI小火车,根据交通标志完成了变速、闪灯、各种音效鸣笛以及起步停车等动作。为儿童玩具增加了更多的趣味。 【硬件】 编号 | | | | | | 配置标称1200MAH的1.2V可充电电池以及管理模块,一套单电机及齿轮传动系统 | 来自闲鱼,这是一个兼容宜家、米兔的可充电电动火车头 | | | M5stickV/K210芯片,具有视觉识别、模型自主训练等功能 | | | | M5stickC/esp32,接收来自m5stcikV的识别结果并且转换为相应动作,发送至电机驱动板 | | | | | | | | 锂电池充电控制板、5V/750MA充电器以及14500电池构成。 | 外置的锂电池充电装置,代替了原有小火车内部的1.2V电池以及充电管理模块,14500电池(850MAH/3.7V)具有良好的动力性能,而且可以与L298N模块匹配 | | | | |
【软件】
【制作过程】 - 第一步:购买并且改装小火车头
-
在闲鱼上购买了一只黑红相间的漂亮扎实的小火车头,当时是看中了它具有充电以及车灯功能。到货拆解后,发现其设计比较紧凑合理,做工精良,使用内三角螺丝紧固。小火车头的动力系统包括一节标称1200MAH的1.2V可充电电池+单电机以及齿轮系统+电池充电管理模块。 但是发现存在三个问题:一是动力不足,1200mah的容量几乎可以肯定是虚标;二是电池1.2V的输出电压无法与L298N电机驱动板匹配,也就是难以完成调速、停车、正反转等功能。三是前车灯非常黯淡,可以肯定串接了一个高阻。使用M5sitckC测试后,输出亮度难以满意。 所以我们需要对小火车头进行改装。 (1)改装其动力系统,原有的1.2V电池以及充电管理模块忍痛去除,代之以850MAH/3.7V可充电电池14500,这颗电池的尺寸和原电池一致,所以很容易装配到原位置,我用热熔胶进行了较为严实的固定,将电池的充电系统外置,不再安装在火车内部,把腾出来的位置用于安装L298N电机驱动板。 (2)在车厢顶部开孔(M3)用于安装M5sitckV。 (3)将内部空间里面的原有拨动开关拆除,将内部的塑料结构用电磨进行拆除,空间用于电机驱动板以及布置铜芯导线。 (4)增设一节车厢,用于承载M5sitckC。这样,把小火车的动力部分、电机驱动部分以及视觉识别元件(M5sitckV)放在火车头,而电源开关以及执行元件(M5sitckC)装在一节单独的车厢,两者通过磁性连接件以及排线连接。
代码设计部分其实比较简单,我们一起来看看吧。 (1)M5sitckV部分 一是训练模型 使用M5sitckV的训练程序,将七个交通标志各拍摄100张以上的照片(合计700+照片),发送至http://v-training.m5stack.com/,如果你的训练是符合要求的,计算结果将收敛,这样大约在半小时左右,你会收到一份带有下载链接的邮件。下载后,你将得到一份自主训练的模型库,一个boot.py的demo代码,不要小看这个demo,我们会很容易移植到主程序中。注意:训练时尽可能将模型放入取景框且尝试不同的光线条件。 二是设计代码 这部分主要是将识别的结果发送至M5sitckC,所以uart的发送编程是主要内容,另外,由于M5sitckV带有喇叭,所以我们这个项目的火车音效就靠M5sitckV实现,前文说到的火车头车灯不亮,而M5sitckV本身就带有一个非常亮的全彩LED,这个亮灯的任务也交给M5sitckV吧。
[mw_shl_code=python,true]import audio
import gc
import image
import lcd
import sensor
import sys
import time
import uos
import os
import KPU as kpu
from fpioa_manager import *
from Maix import I2S, GPIO
from machine import I2C
from board import board_info
from pmu import axp192
pmu = axp192()
pmu.enablePMICSleepMode(True)
fm.register(board_info.SPK_SD, fm.fpioa.GPIO0)
spk_sd=GPIO(GPIO.GPIO0, GPIO.OUT)
spk_sd.value(1)
fm.register(board_info.SPK_DIN,fm.fpioa.I2S0_OUT_D1)
fm.register(board_info.SPK_BCLK,fm.fpioa.I2S0_SCLK)
fm.register(board_info.SPK_LRCLK,fm.fpioa.I2S0_WS)
wav_dev = I2S(I2S.DEVICE_0)
from machine import UART
fm.register(board_info.CONNEXT_B,fm.fpioa.UART1_TX)
fm.register(board_info.CONNEXT_A,fm.fpioa.UART1_RX)
uart_A = UART(UART.UART1, 115200, 8, None, 1, timeout=1000, read_buf_len=4096)
fm.register(board_info.LED_W, fm.fpioa.GPIO3)
led_w = GPIO(GPIO.GPIO3, GPIO.OUT)
led_w.value(1)
passlable=''
global v_state
v_state='no'
lcd.init()
lcd.rotation(2)
try:
img = image.Image("/sd/startup.jpg")
lcd.display(img)
except:
lcd.draw_string(lcd.width()//2-100,lcd.height()//2-4, "Error: Cannot find start.jpg", lcd.WHITE, lcd.RED)
task = kpu.load("/sd/3f758421b4b1db32_mbnet10_quant.kmodel")
labels=["1","2","3","4","5","6","7"]
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((224, 224))
sensor.run(1)
lcd.clear()
def play_sound(filename):
try:
player = audio.Audio(path = filename)
player.volume(30)
wav_info = player.play_process(wav_dev)
wav_dev.channel_config(wav_dev.CHANNEL_1, I2S.TRANSMITTER,resolution = I2S.RESOLUTION_16_BIT, align_mode = I2S.STANDARD_MODE)
wav_dev.set_sample_rate(wav_info[1])
spk_sd.value(1)
while True:
ret = player.play()
if ret == None:
break
elif ret==0:
break
player.finish()
spk_sd.value(0)
except:
pass
while(True):
img = sensor.snapshot()
fmap = kpu.forward(task, img)
plist=fmap[:]
pmax=max(plist)
max_index=plist.index(pmax)
a = lcd.display(img)
if pmax>0.99:
lcd.draw_string(40, 60, "Accu:%.2f Type:%s"%(pmax, labels[max_index].strip()))
passlable=str(labels[max_index])
if passlable==v_state:
pass
else:
v_state=passlable
uart_A.write(passlable)
if v_state=='1':
led_w.value(0)
time.sleep_ms(2000)
led_w.value(1)
elif v_state=='2':
play_sound("/sd/whistle3.wav")
time.sleep_ms(200)
elif v_state=='3':
play_sound("/sd/train.wav")
time.sleep_ms(200)
else:
pass
else:
pass
a = kpu.deinit(task)
uart_A.deinit()
[/mw_shl_code]
(1)M5sitckC部分 M5sitckC的代码比较简单,从uart接收到识别特征字以后,通过一系列判断语句,将动作行为发送至L298N即可。 (a)在设计M5sitckC的代码时,我用UIFLOW作为UI设计以及框架搭建,然后将生成的代码用thonny进行调试,玩家看到这里可能会问两个问题:一是为什么不是用uiflow继续调试呢?Uiflow毕竟有一定局限性,而thonny在调试esp32时可以得到非常全面的调试和错误信息;二是为什么调试M5sitckC时,不用vscode+m5插件这种方式呢?因为M5sitckC的屏幕很小,在vscode+m5插件调试时,得不到具体的出错信息。所以根据我的体会,在调试M5sitckC时,比较适合我的办法就是UIFLOW+thonny. (b)在M5sitckC的屏幕上,我设计了三个参数,一是电池的电压(C的续航能力较弱,实时显示电池电压非常重要,否则调试中会走很多弯路),二是显示从v获取的识别特征码,三是现实C的动作行为(如stop/go/......)便于与V联调时,对识别情况的把握。
[mw_shl_code=python,true]#2020-01-03
#C侧的火车控制程序v0.11
#
from m5stack import *
from m5ui import *
from uiflow import *
import machine
import time
#UI
setScreenColor(0x111111)
title0 = M5Title(title="Train0108", x=3 , fgcolor=0xFFFFFF, bgcolor=0x0000FF)
label0 = M5TextBox(31, 45, "Ready", lcd.FONT_Default,0xFFFFFF, rotate=0) #显示火车状态
label1 = M5TextBox(33, 89, "Inbox", lcd.FONT_Default,0xFFFFFF, rotate=0) #显示接收到的指令情况
circle0 = M5Circle(17, 52, 3, 0xFFFFFF, 0xFFFFFF)
circle1 = M5Circle(17, 97, 3, 0xFFFFFF, 0xFFFFFF)
#lcd setup
axp.setLDO2Volt(2.7)
title0.setTitle(str(axp.getBatVoltage()))
#setup
#===uart
uart = None
uart = machine.UART(1, tx=32, rx=33)
uart.init(115200, bits=8, parity=None, stop=1)
#===GPI0 SETUP
#pin1 = machine.Pin(5, mode=machine.Pin.OUT, pull=machine.Pin.PULL_UP)
pin0 = machine.Pin(0, mode=machine.Pin.OUT, pull=machine.Pin.PULL_UP)
PWM1 = machine.PWM(26, freq=10000, duty=0, timer=0)
PWM1.resume()
wait(1)
#===Train Action
def go(): #normal
pin0.value(0)
PWM1.duty(60) #speed=60
label0.setText('Go')
label1.setText('no')
def light(): #light of train
#light on m5stickV
label0.setText('Light')
label1.setText('1')
pin0.value(0)
PWM1.duty(50) #speed=50
def train(): #Train comming
# Train sound play on m5stickV
label0.setText('Train')
label1.setText('2')
pin0.value(1)
PWM1.duty(100) #IN1=1 IN2=1 停车3S后正常速度,相当于等待火车通过
time.sleep_ms(3000)
pin0.value(0)
PWM1.duty(50) #3S为speed=50
def whistle(): #whistle
# whistle play on m5stickV
label0.setText('Whistle')
label1.setText('3')
pin0.value(0)
PWM1.duty(50) #speed=50
def limit(): #train limit 5KM/h
pin0.value(0)
PWM1.duty(45) #speed=45
label0.setText('Limit')
label1.setText('4')
def nolimit(): #train no limit 5KM/h
pin0.value(0)
PWM1.duty(75) #speed=75
label0.setText('NoLmt')
label1.setText('5')
time.sleep_ms(2000)
pin0.value(0)
PWM1.duty(50) #2S后降速为speed=50
def stop():#train stop
pin0.value(1)
PWM1.duty(100) #IN1=1 IN2=1
label0.setText('Stop')
label1.setText('6')
def greenlight(): #train Go
pin0.value(0)
PWM1.duty(50) # speed=50
label0.setText('GLight')
label1.setText('7')
#=====Loop
while True:
title0.setTitle(str(axp.getBatVoltage()))
if uart.any():
bin_data = uart.readline(1)
decode_bin_data=bin_data.decode()
wait_ms(200)
if decode_bin_data=='1': #如果识别到1开灯
light()
time.sleep_ms(200)
elif decode_bin_data=='2':#如果识别到2===火车通过
train()
time.sleep_ms(200)
elif decode_bin_data=='3':#如果识别到3===鸣笛
whistle()
time.sleep_ms(200)
elif decode_bin_data=='4':#如果识别到4===限速
limit()
time.sleep_ms(200)
elif decode_bin_data=='5':#如果识别到5===解除限速
nolimit()
time.sleep_ms(200)
elif decode_bin_data=='6':#如果识别到6===停车
stop()
time.sleep_ms(200)
elif decode_bin_data=='7':#如果识别到7===绿灯
greenlight()
time.sleep_ms(200)
else: #其他数据正常前进即可
go()
else:
pass
[/mw_shl_code]
第三步:L298N电机驱动板调试 L298N电机驱动板的控制真值表: 联调的接线很简单,见下图:
在联调时,我用了一个小技巧,将一个测试用的电机,其输入线端焊接了两个杜邦线的母头,而L298N的输出输入的铜芯导线端部,都焊接了排针并且进行了热缩处理。这样非常方便地讲L298N与电池、电机以及M5sitckC连接起来。而在正式部署时,只需要将铜芯导线截断至合适长度即可。这样的小技巧可以大大缩短调试时间,并且增加调试的可靠性。L298N的调试目的就是测试各个控制行为以及根据电机实际情况得到PWM的参数(这个参数在装配小火车头电机后,还会进行一些修正)。 第四步:组装M5sitckV+M5sitckC以及小火车系统并且联调 将M5sitckV+M5sitckC以及小火车系统安装好,其中M5sitckC用扎带固定在车厢上,M5sitckV用M3螺丝以及M5提供的专用L型支架固定在火车头的车厢顶部。火车头的电机与调试后的L298N连接起来。 联调的过程比较简单,注意把火车头翻过来,车轮朝上,这样就不会在调试中,面对不停动弹的火车头手忙脚乱。然后把七个标志一一由V识别,观察C上面的接收情况。 注意:如果您是第一次调试M5sitckV,您需要增加一个步骤,就是M5sitckV与PC的串口助手要先行进行调试,确保M5sitckV识别到的特征码能够准确地传送至uart,我因为在M5sitckV模拟小超市中已经进行过类似调试,积累了很好的经验,所以不需要此步骤。 第五步:组装轨道以及路测 接下来就是比较有趣的部分了,也是亲子活动时间,和孩子一起组装一个环形轨道,然后把七个交通标志布置在你希望出现的地方,就可以开始路测了,路测时注意几个问题: 一是M5sitckC/V需要有足够充电,否则因电压不稳的重启会让你的路测不连续。 二是M5sitckV的摄像头角度需要在路测时不断调整。 三是电机的PWM参数需要在路测中不断进行微调,因为每个人小火车头的电机参数都会略有不同,轨道的弯度也需要进行一些调整,有一些过急的弯道可能会造成脱轨,需要进行一些修正。 这个调试过程总体是很有趣的,看着小火车头拖动着M5sitckC的车厢,在环形轨道上做出种种识别动作,欢声笑语使得前面的辛苦工作,都显得那么物有所值。
【鸣谢】 - 感谢m5stack.com以及arduino.cn社区,提供这样好的硬件以及交流平台。
- 感谢社区多位师兄给我的帮助和鼓励,如“滚筒洗衣机”师兄(抱歉您的ID的确如此)对动力系统的指导,笑笑以及jimmy、小华师兄对我的指导和鼓励。
- 感谢我的孩子能够欣赏我这个小小的项目,并且和我一起测试。
这个小项目抛砖引玉,希望更多玩家做出更有趣的尝试。 新春将至,春天的脚步更近了,祝福各位师兄新春大吉,诸事顺意。 沧海抱拳。
由于论坛附件size限制,请下载四个附件后,进行文件名修改,然后解压,我分享了code\model\wav。 原文件名 | 更改为 | upload_m5train00.zip | upload_m5train.zip | upload_m5train01.zip | upload_m5train.z01 | upload_m5train02.zip | upload_m5train.z02 | upload_m5train03.zip | upload_m5train.z03 |
|