使用Raspberry Pi制作循迹小车

  • 最近一直在做树莓派相关的东西,终极目标是用它做一个使用cnn卷积神经网络算法的人工智能小车。这个坑很大需要好多时间来填,万里长征第一步就是训练数据的采集。在这篇文章里介绍了通过Python和CV2视觉库实现自动驾驶。

## 硬件的准备 - Raspberry Pi 3(树莓派) 操作系统ubuntu 16.04 - 电机驱动电路 - 电池与稳压电路 - 舵机 - 车模 ### 电机/舵机控制方案 - 说明一下,因为去年参加了飞思卡尔比赛,所以车模直接使用了飞思卡尔比赛的C车,电机是由NXP公司的MKL25单片机控制的。且给的是固定速度,也就是说由Raspberry Pi控制转向,单片机控制速度,两者之间并没有耦合关系,是完全独立的系统。
整车 因为Pi貌似只能提供软件PWM,也就是说PWM波的频率越高,CPU使用越多。示波器实测频率极限大约为5KHz,电机要求频率为10KHz,所以不使用Pi来控制电机。
> Each pin activated in softPWM mode uses approximately 0.5% of the CPU.

  • 舵机由树莓派提供100Hz的PWM波控制。

供电方案

所有的设备由7.2V电池提供电力,树莓派需降压到5V,舵机需降压到5.7V。
tips:Pi的开机电流比较大,实测AMS11175.0稳压芯片提供的5V电压无法保证Pi正常开机,所以需要大电流稳压芯片 稳压电路与电机驱动

图像采集方案

车上搭载了支持UVC协议的数字摄像头,具体的型号我也记不清了。在某宝上搜索UVC摄像头即可。摄像头与Raspberry Pi之间使用USB连接,我们使用的摄像头640*480分辨率,但是实测帧数较低,可能是Pi本身性能有限,代码拍照间隔大约为0.1s。

代码实现

粗糙但可靠的方案

  • 思路
    摄像头采集图像流,使用Python定时器的功能每0.1s处理一次图像。赛道为白色,赛道外部为蓝色,通过图像的灰度差来判断赛道边界。取图像的中线,按640*480的分辨率取第320行。设置一个灰度阈值,从320行的第240个像素点左右找灰度跳变处并记录坐标,左右边界坐标相加取平均与240作差即可获得与赛道中线的偏差。
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import threading
import numpy as np
import cv2
import cv2.cv as cv
import RPi.GPIO as GPIO
#读视频流
cap=cv2.VideoCapture(0)
i = 0
LEdge = 0
REdge = 0
lineErr = 0
#GPIO-PWM初始化,设定频率为100Hz
GPIO.setmode(GPIO.BOARD)
GPIO.setup(12,GPIO.OUT)
p = GPIO.PWM(12,100)
p.start(0)

获取图像函数部分,由于需要为神经网络系统提供训练数据,所以把采集到的图片按顺序存放在img文件夹下。

1
2
3
4
5
6
def fetch_img():
global i,fn,im
fn = 'img/'+str(i)+'.jpg'
ret,im=cap.read()
cv2.imwrite(fn, im)
i = i + 1

处理图像函数部分,每隔0.1s执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def process_img():
t = threading.Timer(0.1,process_img)
t.start()
global LEdge,REdge,lineErr
fetch_img()
#获取灰度图像
gray=cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
for j in range(1,640):
if gray[240][j] < 80: #设置阈值为80
gray[240][j] = 0
else:
gray[240][j] = 1
for k in range(320,640,1):
if gray[240][k] == 0:
REdge = k
break
for k in range(320,0,-1):
if gray[240][k] == 0:
LEdge = k
break
lineErr = (REdge+LEdge)/2-320
print lineErr

主函数部分,控制舵机方向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def main():
global t,lineErr
process_img()
t = threading.Timer(1, process_img)
t.start()
try:
while 1:
dc = 8 - lineErr/40
#简单的比例控制,舵机PWM波占空比动态范围为6%-10%,8%时转向角度为0
if dc > 10:
dc = 10
if dc < 6:
dc = 6
p.ChangeDutyCycle(dc)
except KeyboardInterrupt:
pass
p.stop()
GPIO.cleanup()
if __name__ == "__main__":
main()

精致但不可靠的方案

霍夫线(HoughLine)变换

霍夫线变换是一种用来寻找直线的方法,在opencv中相关函数为HoughLinesHoughLinesP #### 代码实现 既然霍夫线变换可以寻找图片中的直线,那么我们可以利用这种算法提取赛道边缘并得出边缘线和车身前进方向的夹角,以此判断前进方向与赛道中线的偏差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import cv2.cv as cv
import math
im=cv.LoadImage('img/213.jpg', cv.CV_LOAD_IMAGE_GRAYSCALE)
pi = math.pi
x = 0
dst = cv.CreateImage(cv.GetSize(im), 8, 1)
cv.Canny(im, dst, 200, 200)
cv.Threshold(dst, dst, 100, 255, cv.CV_THRESH_BINARY)
color_dst_standard = cv.CreateImage(cv.GetSize(im), 8, 3)
cv.CvtColor(im, color_dst_standard, cv.CV_GRAY2BGR)#Create output image in RGB to put red lines
lines = cv.HoughLines2(dst, cv.CreateMemStorage(0), cv.CV_HOUGH_STANDARD, 1, pi/180, 90, 80, 200)
old = lines[0][1]*180/pi
for (rho, theta) in lines[:100]:
err = theta*180/pi - old
if abs(err) > 3:
x = x + theta*180/pi
old = theta*180/pi
a = math.cos(theta)
b = math.sin(theta)
x0 = a * rho
y0 = b * rho
pt1 = (cv.Round(x0 + 1000*(-b)), cv.Round(y0 + 1000*(a)))
pt2 = (cv.Round(x0 - 1000*(-b)), cv.Round(y0 - 1000*(a)))
cv.Line(color_dst_standard, pt1, pt2, cv.CV_RGB(255, 0, 0), 2, 4)
print x
cv.ShowImage("Hough Standard", color_dst_standard)
cv.WaitKey(0)

这里载入了一张已经拍摄好的赛道图片,提取直线并绘图,运行结果如下:

screenshot 可以清楚地看到赛道的两边被描绘出来,由theta值计算出角度。

局限性

screenshot 尽管HoughLines函数提供了阈值,最长线,最短线的设置,想获得准确的路线信息还是很困难。测试场地的干扰实在太多,很难消除周边环境的影响,最终在测试了各种情况后,放弃了这种方法。
另外霍夫线变换对直线的提取对于树莓派的CPU来说实在是太吃力了,增加了系统的控制周期,导致系统的动态性能很差。