查看: 1569|回复: 2

M5Train 视觉识别轨道小火车头

[复制链接]
  • TA的每日心情
    开心
    2019-7-17 13:13
  • 签到天数: 264 天

    [LV.8]以坛为家I

    发表于 2020-1-13 21:13 | 显示全部楼层 |阅读模式
    本帖最后由 沧海笑1122 于 2020-1-13 21:45 编辑

    M5Train 视觉识别轨道小火车头】

    【故事】
       front_s.jpg
         玩具轨道电动小火车历史悠久,是孩子们常见喜爱的玩具。米兔小火车轨道兼容宜家的轨道,孩子们百玩不厌。但是,毕竟电动小火车头只有一个车速,小火车头拖动着车厢匀速前进,时间长了,难免枯燥。如何增进趣味呢?
        M5stack(明栈科技)出品了基于K210的视觉识别core-M5stickV。我们利用http://v-training.m5stack.com/这个友好的训练服务,训练自己的模型并且上传至服务器,将得到返回的训练模型。这样就为小火车装上了一双能识别交通标志的聪明眼睛。
      本项目一共训练了七个交通标志。我尝试将其用于米兔轨道小火车,用M5stickC作为执行元件,构成了一个基于视觉识别的,好玩的轨道小火车。
    icon1.jpg

       我们先一起看看视频吧,在视频中,小火车头拖着一节车厢,从始发站出发,经过了限速、注意火车、解除限速、亮灯、鸣笛以及停车等七个动作。小火车头变身可以识别交通标志的AI小火车,根据交通标志完成了变速、闪灯、各种音效鸣笛以及起步停车等动作。为儿童玩具增加了更多的趣味。
    【硬件】
    编号
    内容
    型号或性能配置
    备注
    1
    电动火车头
    配置标称1200MAH1.2V可充电电池以及管理模块,一套单电机及齿轮传动系统
    来自闲鱼,这是一个兼容宜家、米兔的可充电电动火车头
    2
    视觉识别装置
    M5stickV/K210芯片,具有视觉识别、模型自主训练等功能
    鸣笛及火车音效、亮灯等通过M5stickV完成
    3
    执行装置
    M5stickC/esp32,接收来自m5stcikV的识别结果并且转换为相应动作,发送至电机驱动板
    驱动电机的相关动作,有C完成
    4
    电机驱动板
    L298N双路电机驱动板
    本项目火车头仅有一个电机,因此仅用一路;
    5
    小火车头动力部分改装
    锂电池充电控制板、5V/750MA充电器以及14500电池构成。
    外置的锂电池充电装置,代替了原有小火车内部的1.2V电池以及充电管理模块,14500电池(850MAH/3.7V)具有良好的动力性能,而且可以与L298N模块匹配
    6
    配件
    自保持开关、铜芯导线若干
    vc.jpg


    【软件】
    编号
    内容
    出品及版本
    备注
    1
    M5sitckV固件
    M5出品/V1022-beta
    2
    M5StickC固件
    M5出品/V1.4.3
    固件基于UIFLOW
    3
    UIFLOW开发环境
    M5出品/V1.4.3
    4
    Thonny IDE
    Thonny.org /v3.2.6
    优秀的micropython开发环境
    5
    Vscode+m5stack插件
    Microsoft出品
    优秀的通用代码开发环境




    【制作过程】
    • 第一步:购买并且改装小火车头
    • 火车头1_s.jpg
      在闲鱼上购买了一只黑红相间的漂亮扎实的小火车头,当时是看中了它具有充电以及车灯功能。到货拆解后,发现其设计比较紧凑合理,做工精良,使用内三角螺丝紧固。小火车头的动力系统包括一节标称1200MAH1.2V可充电电池+单电机以及齿轮系统+电池充电管理模块。
      但是发现存在三个问题:一是动力不足,1200mah的容量几乎可以肯定是虚标;二是电池1.2V的输出电压无法与L298N电机驱动板匹配,也就是难以完成调速、停车、正反转等功能。三是前车灯非常黯淡,可以肯定串接了一个高阻。使用M5sitckC测试后,输出亮度难以满意。
      所以我们需要对小火车头进行改装。
    (1)改装其动力系统,原有的1.2V电池以及充电管理模块忍痛去除,代之以850MAH/3.7V可充电电池14500,这颗电池的尺寸和原电池一致,所以很容易装配到原位置,我用热熔胶进行了较为严实的固定,将电池的充电系统外置,不再安装在火车内部,把腾出来的位置用于安装L298N电机驱动板。
    (2)在车厢顶部开孔(M3)用于安装M5sitckV
    (3)将内部空间里面的原有拨动开关拆除,将内部的塑料结构用电磨进行拆除,空间用于电机驱动板以及布置铜芯导线。
    (4)增设一节车厢,用于承载M5sitckC。这样,把小火车的动力部分、电机驱动部分以及视觉识别元件(M5sitckV)放在火车头,而电源开关以及执行元件(M5sitckC)装在一节单独的车厢,两者通过磁性连接件以及排线连接。
    old_s.jpg

    • 第二步:代码设计
      代码设计部分其实比较简单,我们一起来看看吧。
    (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联调时,对识别情况的把握。
    stop1s.jpg

    [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电机驱动板的控制真值表:
    l298n2.jpg
    联调的接线很简单,见下图:

    l298n_s.jpg

       在联调时,我用了一个小技巧,将一个测试用的电机,其输入线端焊接了两个杜邦线的母头,而L298N的输出输入的铜芯导线端部,都焊接了排针并且进行了热缩处理。这样非常方便地讲L298N与电池、电机以及M5sitckC连接起来。而在正式部署时,只需要将铜芯导线截断至合适长度即可。这样的小技巧可以大大缩短调试时间,并且增加调试的可靠性。L298N的调试目的就是测试各个控制行为以及根据电机实际情况得到PWM的参数(这个参数在装配小火车头电机后,还会进行一些修正)。
    第四步:组装M5sitckV+M5sitckC以及小火车系统并且联调
      将M5sitckV+M5sitckC以及小火车系统安装好,其中M5sitckC用扎带固定在车厢上,M5sitckVM3螺丝以及M5提供的专用L型支架固定在火车头的车厢顶部。火车头的电机与调试后的L298N连接起来。
      联调的过程比较简单,注意把火车头翻过来,车轮朝上,这样就不会在调试中,面对不停动弹的火车头手忙脚乱。然后把七个标志一一由V识别,观察C上面的接收情况。
      注意:如果您是第一次调试M5sitckV,您需要增加一个步骤,就是M5sitckVPC的串口助手要先行进行调试,确保M5sitckV识别到的特征码能够准确地传送至uart,我因为在M5sitckV模拟小超市中已经进行过类似调试,积累了很好的经验,所以不需要此步骤。
    charge1_s.jpg
    第五步:组装轨道以及路测
      接下来就是比较有趣的部分了,也是亲子活动时间,和孩子一起组装一个环形轨道,然后把七个交通标志布置在你希望出现的地方,就可以开始路测了,路测时注意几个问题:
    一是M5sitckC/V需要有足够充电,否则因电压不稳的重启会让你的路测不连续。
    二是M5sitckV的摄像头角度需要在路测时不断调整。
    三是电机的PWM参数需要在路测中不断进行微调,因为每个人小火车头的电机参数都会略有不同,轨道的弯度也需要进行一些调整,有一些过急的弯道可能会造成脱轨,需要进行一些修正。
    allmap_s_1.jpg
       这个调试过程总体是很有趣的,看着小火车头拖动着M5sitckC的车厢,在环形轨道上做出种种识别动作,欢声笑语使得前面的辛苦工作,都显得那么物有所值。

    【鸣谢】
    • 感谢m5stack.com以及arduino.cn社区,提供这样好的硬件以及交流平台。
    • 感谢社区多位师兄给我的帮助和鼓励,如“滚筒洗衣机”师兄(抱歉您的ID的确如此)对动力系统的指导,笑笑以及jimmy、小华师兄对我的指导和鼓励。
    • 感谢我的孩子能够欣赏我这个小小的项目,并且和我一起测试。
      这个小项目抛砖引玉,希望更多玩家做出更有趣的尝试。
      新春将至,春天的脚步更近了,祝福各位师兄新春大吉,诸事顺意。
      沧海抱拳。

    upload_m5train00.zip (363.28 KB, 下载次数: 4)
    last1_s.jpg

    upload_m5train.zip

    363.28 KB, 下载次数: 4

  • TA的每日心情
    开心
    2019-7-17 13:13
  • 签到天数: 264 天

    [LV.8]以坛为家I

     楼主| 发表于 2020-1-19 10:22 | 显示全部楼层
    【01-19】更正:
    这个小火车头里面的电机驱动板,并非L298N,实际是MX1508。特此更正。
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    热门推荐

    5分钟带你快速了解新一代开发板:M5STACK
    5分钟带你快速了解新一代
    一、什么是M5Stack M5Stack是一种模块化、可堆叠扩展的开发板,每个模块
    创客火首发无人机编队套装,开启不一样的人工智能教育
    创客火首发无人机编队套装
    2017年国务院发布《新一代人工智能发展规划》,提出要广泛开展人工智能科普活动,在中
    Bliker 技术群,让我这个客户感到极度失望!!
    Bliker 技术群,让我这个
    Bliker 技术群,让我这个客户感到极度失望!! 我用了一小段时间接入ARDUINO ,后来
    如何用小爱同学控制网红仿AWTRIX2.0
    如何用小爱同学控制网红仿
    如标题所见,本期将讲述如何用小爱同学控制仿AWTRIX2.0。 视频演示: http://www.b
    【原创】全球最小口袋3D打印机mini one直播教程贴
    【原创】全球最小口袋3D打
    最近闲得蛋疼,没事搞个掌上3D打印机,先放效果图吧。 搞了半天,终于能正常打印,
    Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   
    快速回复 返回顶部 返回列表