使用keras构建CNN神经网络智能(智障)小车开发

2021-07-30

整个项目简述

智障小车,带了个智能的智字,证明其还是有点智能的,不过智能不高,只能做一点比较简单的事情。为什么想要做这个项目呢?早些时候在youtu上看到有主播用python制作了GTA5自动驾驶的教程,于是受到了启发,再加上我想入门一下机器学习,所以选择做一个智障小车。

项目所需硬件:

1.树莓派

用于作为小车的中枢控制大脑。可用其他单片机代替,树莓派在本项目中只作为一个控制指令的接收以及将指令输出到电路板即可,淘宝有售。

树莓派示意图,来源树莓派官网

2.小车电路板、电机、轮子等。

可以直接通过淘宝购买树莓派小车套装即可,也可以把零件购买好了,然后自行组装

3.电脑一台

用于代码编写、训练神经网络

4.广角摄像头一个

不使用鱼眼摄像头,用鱼眼摄像头的话需要进行图像矫正,略显麻烦。
为什么不使用普通的摄像头呢? 广角摄像头的视角更大,放在车头就可以看到地上的跑道。普通摄像头需要调整摄像头对准地面才能看到跑道

项目用到的软件:

1.python

作为项目的主要开发语言,如何安装,请看:windows 安装sublime text3 部署python开发环境也可以直接看python+VSCode组建开发环境 。本项目中所需要用到的包会随着文章的推进列出来,不在这里一一细表。
对于mac上怎么安装python,由于mac是自带python的,但版本可能过低。我建议看下面的第二点,通过Anaconda来安装新的python环境

2.Anaconda.安装简易使用教程

请看Anaconda安装&配置
我之前使用过Virtualenv作为虚拟环境,但一用了Anaconda后,只能惊叹这个乃神器,一方面它可以很方便去管理各种python的环境、依赖包。另一方面,它可以帮你把某个包的依赖环境都装好。例如你要安装keras-gpu或者tensor flow-gpu来利用gpu加速训练时,官网教程是需要你自行安装cuda(这里还有版本兼容的问题,非常麻烦),但是通过Anaconda上安装的话,可以自动帮你安装好对应的cuda。所以建议使用安装Anaconda.

3.VSCode

代码编写的工具,可以使用任何你觉得熟手的工具,单拎出来,是因为有朋友问我可不可以用sublime text,但我觉得VSCode更好用

项目最终的效果展示:

在电脑端输出当前小车的画面,并根据画面给出预测动作(前后左右)

电脑展示图 小车根据电脑给出的指令进行运动

小车运动图
初版的小车需要优化的地方有很多,本次分享是基于初版代码分享

整个分享的文章架构,也是我在做这个项目时的思路和步骤

1.树莓派小车的控制
2.在电脑上控制树莓派小车
3.获取摄像头画面
4.处理图像,神经网络简单讲解
5.获取、存储训练图像
6.构建神经网络
7.训练神经网络,并根据训练结果调参
8.使用神经网络
9.后续如何更进一步做的更好?

希望这个项目系列分享可以给到大家帮助,下面开始本项目的第一部分
接下来就不多说废话了,教程开始

一、组装树莓派小车

这个在购买树莓派或者小车配件的时候,商家就会给你一些视频的安装教程,我这里就不多讲了,真的有需要的。。可私信留言。组装并且配置成可以通过远程登录即可
我个人认为windows的远程登录已经足够使用,不需要安装其他插件
如果不知道如何烧录树莓派的系统,可以百度一下,资料相当多
推荐大家烧录带界面以及基本软件的,怎么知道是不是带界面和基本软件的呢?很简单,选择文件大小最大的那个就可以了 红色框框这个即可

二、编写脚本进行控制

我们通过使用python的GPIO包来对电路板的进行信号的输出,一般来说,树莓派上面会预先安装好。    
#可以用import这个包来是否有安装
import RPi.GPIO as GPIO
#如果没有则用pip来安装就好
pip install RPi.GPIO
为什么不在电脑上进行开发,而需要在树莓派上?因为windows上不支持GPIO的硬件,安装不了这个包,所以无法在windows上进行调试,只能在树莓派上直接进行开发调试

三、根据引脚对照表进行编写。

安装好小车以及接好电路后,开始编码
淘宝卖家的教学视频一般会告诉你他都接到什么引脚,你参照视频里的就好了。拓展板上有对应的电机芯片,而不同商家的电机芯片(控制并输出电路给电机的芯片,树莓派自带的是没有的)绑定的引脚口是不一样的,所以需要咨询下卖家
下面的代码没有编写缓慢加速以及缓慢减速的过程,而我买的电机比较差,只要停止输出信号就会秒停(其实也可以通过编程的方式实现,不过一开始的时候比较懒XD)    
import RPi.GPIO as GPIO
import time

class Move:
    def __init__(self):
       
        self.LForward = 22 # 左侧前轮的引脚编号
        self.RForward = 25 # 右侧前轮的引脚编号

        self.LBack = 27  # 左侧后轮的引脚编号
        self.RBack = 24  # 左侧后轮的引脚编号
       
        #这里可能有人不明白为什么轮子要4个引脚,
        #而这个pwm控制只要两个引脚。
        #因为一个电机芯片可以控制两个轮子。
        #而控制输出成pwm信号的是由芯片来控制,
        #所以只需要两个端口即可
        self.LMotorCore = 18  # 左侧PWM控制的引脚编号
        self.RMotorCore = 23  # 右侧PWM控制的引脚编号

        self.moveTime = 0.055 # 设定每次触发运动时的持续运动时间
        self.speed = 100  # 设定运动速度
        self.setup() # 初始化所有引脚


    # 小车在启动前需要先对引脚初始化,设定引脚的状态
    def setup(self):
        # print ('car setup')
        GPIO.setwarnings(False) # 屏蔽waring,没有这句的话,会一直有waring出来导致程序无法执行下去
        GPIO.setmode(GPIO.BCM) # 引脚编号的模式,有BCM以及BOARD两种模式,通过参照引脚编码表来选择模式
        GPIO.setup(self.LForward, GPIO.OUT) # 将对应的引脚设置为输出模式,也就是让这个引脚是负责输出信号
        GPIO.setup(self.RForward, GPIO.OUT)
        GPIO.setup(self.LBack, GPIO.OUT)
        GPIO.setup(self.RBack, GPIO.OUT)

        GPIO.setup(self.LMotorCore, GPIO.OUT)
        GPIO.setup(self.RMotorCore, GPIO.OUT)
        self.LMotor= GPIO.PWM(self.LMotorCore,100) # PWM我理解为是一个信号强度,通过控制这个强度大小,我们可以控制小车的速度,初始化为100
        self.RMotor = GPIO.PWM(self.RMotorCore,100)
        self.LMotor.start(0) #设定初始化的时候为0
        self.RMotor.start(0)

    # 小车前进
    def goFoward(self):
        # print ('car goFoward')
        self.LMotor.ChangeDutyCycle(self.speed)# 通过调节PWM的强度,来改变的速度
        self.RMotor.ChangeDutyCycle(self.speed)
        GPIO.output(self.LForward, True) # 对对应的引脚输出信号,使电机转动
        GPIO.output(self.RForward, True)
        GPIO.output(self.LBack, False) # 不对对应的引脚输出信号,电机不转动
        GPIO.output(self.RBack, False)
        time.sleep(self.moveTime) # 运动持续的时间
    #小车后退
    def goDown(self):
        # print ('car goDown')
        self.LMotor.ChangeDutyCycle(self.speed)
        self.RMotor.ChangeDutyCycle(self.speed)
        GPIO.output(self.LForward, False)
        GPIO.output(self.RForward, False)
        GPIO.output(self.LBack, True)
        GPIO.output(self.RBack, True)
        time.sleep(self.moveTime)

    #另外一种右转的方式
    #def goRight(self):
        # print ('car goRight')
        # self.LMotor.ChangeDutyCycle(self.speed)
        # self.RMotor.ChangeDutyCycle(self.speed)
        # GPIO.output(self.LForward, True)
        # GPIO.output(self.RForward, False)
        # GPIO.output(self.LBack, False)
        # GPIO.output(self.RBack, True)
        # time.sleep(self.moveTime)

    #小车右转
    def goRight(self):
        # print ('car goRight')
        self.LMotor.ChangeDutyCycle(self.speed)
        self.RMotor.ChangeDutyCycle(self.speed)
        GPIO.output(self.LForward, True)
        GPIO.output(self.RForward, False)
        GPIO.output(self.LBack, False)
        GPIO.output(self.RBack, False)
        time.sleep(self.moveTime)

  #另外一种小车左转的方式
    #def goLeft(self):
        # print ('car goLeft')
        # self.LMotor.ChangeDutyCycle(self.speed)
        # self.RMotor.ChangeDutyCycle(self.speed)
        # GPIO.output(self.LForward, False)
        # GPIO.output(self.RForward, True)
        # GPIO.output(self.LBack, True)
        # GPIO.output(self.RBack, False)
        # time.sleep(self.moveTime)

     #小车左转
    def goLeft(self):
        # print ('car goLeft')
        self.LMotor.ChangeDutyCycle(self.speed)
        self.RMotor.ChangeDutyCycle(self.speed)
        GPIO.output(self.LForward, False)
        GPIO.output(self.RForward, True)
        GPIO.output(self.LBack, False)
        GPIO.output(self.RBack, False)
        time.sleep(self.moveTime)
       
    def Stop(self):
        # print ('car Stop')
        self.LMotor.ChangeDutyCycle(0)
        self.RMotor.ChangeDutyCycle(0)
        GPIO.output(self.LForward, False)
        GPIO.output(self.RForward, False)
        GPIO.output(self.LBack, False)
        GPIO.output(self.RBack, False)
        time.sleep(self.moveTime)

if __name__ == '__main__':
    car = Move()
    car.goDown()
    car.goFoward()
    car.goLeft()
    car.goRight()
    GPIO.cleanup()#释放所有引脚的资源
  代码里提供了两种转向方式,可以按需要选择自己适合的。 GPIO.cleanup()一定要带上并放在整个程序结束的时候,刚开始时,我有几次没有加这句来释放引脚的资源,导致更新程序再启动时,不能按照最新的程序进行,因为原有引脚已有上个程序的"记忆"。 四、如何进行键盘控制。 上文已经实现了小车运动的方向了,那接下来我们需要通过键盘控制小车运动 这里就要引入pynput这个包,这个包可以实现监听鼠标、键盘输入的功能,也可以模仿鼠标键盘的点击
#通过pip安装
pip install pynput
加载对应的包
from pynput.keyboard import Listener
class keyboardControlMove:
  def __init__(self, car):
      self.car = car
      with Listener(on_press = self.press) as listener:
              listener.join()
         
  def press(self, key):
        #之前试过直接用pynput自带的特殊key事件,发现在mac上、window上以及树莓派上
        #对于某些键是否为特殊key事件的判断会有所不同,所以我统一处理成字符串来判断
          key = str(key)
          if key == "Key.up":
              # print ('up')
              self.car.goFoward()
              self.car.Stop()
          elif key == "Key.down":
              self.car.goDown()
              self.car.Stop()
              # print ('down')
          elif key == "Key.left":
                self.car.goLeft()
                self.car.Stop()
                # print ('left')
          elif key == "Key.right":
                # print ('right')
                self.car.goRight()
                self.car.Stop()
          elif key =="Key.esc":
                self.car.stop()
                GPIO.cleanup()
                return False
先创建小车move的对象,随后把对象传入到键盘控制的类中
if __name__ == '__main__':
        car = Move()
        k = keyboardControlMove(car)
就可以通过键盘控制小车了 小小提醒,pynput是阻塞线程的,所以为了不阻塞主线程,他另开了一个线程。如果想让小车运动独立一个线程的运行而不是传入到键盘控制的类中,就需要通过队列来进行通信。 五、如何可以做的更好?尝试让运动模式变的更顺滑 上面的代码实现其实没有实现顺滑起速、降速的功能,所以运动起来会一卡卡的,可以利用代码的方式实现顺滑一点的起速降速功能。 六、接下来做什么? 在电脑(非树莓派)实现远程控制小车 That's All, Thank You For Reading To Be Continued