实验采用树莓派作为控制系统的核心部件,操控L298N电机、摄像头、超声波传感器来进行小车的自动化行驶。小车通过摄像头获取寻迹线位置,经过数字图像处理、PID调节得到与小车自身位置的行驶偏差,经过程序判断改变行驶方向,实现自动驾驶。整体代码思路见流程图


       软件代码基于python编写,实现了摄像头寻迹功能。使用了BOARD编号规范。主要流程是首先打开摄像头捕获图像,将图像转化为灰度图,再将灰度图像二值化;通过计算黑色赛道像素中心点的位置,得到其与小车中心的偏移量;再根据偏移量计算PID调整参数,从而调整脉冲的占空比,通过左右四个车轮的前进、后退控制小车的运行速度与方向。
       在代码主体部分,首先利用函数cv.VideoCapture(0)开启摄像头,开启while(1)循环读入摄像头捕获的图像。在我们得到3维图像数据之后,需要进行图像处理。我们主要思想就是要通过阈值化把赛道与环境区分开来。但是由于收集到的数据是一个三维数组,计算量较大,为了降低计算量,需要将RGB彩图转换为灰度图,这里用到了cv.cvtColor()函数,输入参数分别是待处理图像和转换的类型。类型采用BGR2GRAY(表示从RGB图像转化成灰度图)。输出参数表示灰度图。灰度化的原理就是将RGB三个通道的数值大致以3:6:1的权重加权,得到灰度值,此时的RGB三个值相同,就可以将三维数组降成一维,大大降低了计算量。之后再用cv.threshold()函数实现二值化。函数的最后一个参数cv.THRESH_BINARY表示分割方式。其具体实现思路是判断灰度图的像素点的像素值是否大于阈值50,大于即赋255(表示白色),小于赋0(标识黑色)。从而将0-255的颜色范围简化为二值0与255,减小了检测黑色像素点的难度。下图为初始图像、灰度化处理图像、二值化处理图像的对比。
       我们选取图像第400行像素值来确定方向,找到黑色像素群的中心像素点位置(即为赛道中心位置),计算它与摄像图捕获图像中心点的偏移量,根据偏移量操纵小车的行进方向。当偏移较小时,我们让两侧轮子仍然直行;当偏移量较大时,我们采取将需要偏转的方向反方向侧轮子前进获取较大的转向速度。并且加入了PID调节,让小车偏转过程更加平稳。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import RPi.GPIO as GPIO
import cv2 as cv
import numpy as np

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD) # BCM引脚规则


class MyCar(object):
def __init__(self):
moter_lf_num = 12 # 控制左轮向前速度电机编号 lf:left forward
moter_lb_num = 16 # 控制左轮向后速度电机编号 lb:left backward
moter_rf_num = 18 # 控制右轮向前速度电机编号 rf:right forward
moter_rb_num = 22 # 控制右轮向后速度电机编号 rb:right backward
pwm = 500 # 给定PWM的频率

GPIO.setup(moter_lf_num, GPIO.OUT) # 设定端口为输出端
GPIO.setup(moter_lb_num, GPIO.OUT)
GPIO.setup(moter_rf_num, GPIO.OUT)
GPIO.setup(moter_rb_num, GPIO.OUT)

self.moter_lf = GPIO.PWM(moter_lf_num, pwm) # 传入PWM频率参数
self.moter_lb = GPIO.PWM(moter_lb_num, pwm)
self.moter_rf = GPIO.PWM(moter_rf_num, pwm)
self.moter_rb = GPIO.PWM(moter_rb_num, pwm)

self.moter_lf.start(0) # 启动电机
self.moter_lb.start(0)
self.moter_rf.start(0)
self.moter_rb.start(0)

def forward(self, speed):
self.moter_lf.ChangeDutyCycle(speed) # 前进:左右轮均给前向速度
self.moter_lb.ChangeDutyCycle(0)
self.moter_rf.ChangeDutyCycle(speed)
self.moter_rb.ChangeDutyCycle(0)

def backward(self, speed):
self.moter_lf.ChangeDutyCycle(0) # 后退:左右轮均给后向速度
self.moter_lb.ChangeDutyCycle(speed)
self.moter_rf.ChangeDutyCycle(0)
self.moter_rb.ChangeDutyCycle(speed)

def left(self, speed):
self.moter_lf.ChangeDutyCycle(0) # 左转:右轮给前向速度,左轮不动
self.moter_lb.ChangeDutyCycle(0)
self.moter_rf.ChangeDutyCycle(speed)
self.moter_rb.ChangeDutyCycle(0)

def right(self, speed):
self.moter_lf.ChangeDutyCycle(speed) # 右转:左轮给前向速度,右轮不动
self.moter_lb.ChangeDutyCycle(0)
self.moter_rf.ChangeDutyCycle(0)
self.moter_rb.ChangeDutyCycle(0)

def brake(self):
self.moter_lf.ChangeDutyCycle(0) # 刹车:四轮均不动
self.moter_lb.ChangeDutyCycle(0)
self.moter_rf.ChangeDutyCycle(0)
self.moter_rb.ChangeDutyCycle(0)

def MotorRelease(self): # 电机停止转动
self.moter_lf.stop()
self.moter_lb.stop()
self.moter_rf.stop()
self.moter_rb.stop()

def sign(x):
if x>0:
return 1.0
else:
return -1.0


error = [0.0] * 3 # PID 初始数据
adjust = [0.0] * 3

kp = 1.2 # PID 参数配置
ki = 0.1
kd = 0.0

if __name__ == '__main__':
try:
car = MyCar() # 类对象实例化
V = 50 # 给定速度
cap = cv.VideoCapture(0) # cv.VideoCapture:获取当前摄像头的一帧 用cap保存这一帧数据

last_act = 0 # 记录上一次的转弯方向
LEFT = 1
RIGHT = 2
while (True): # 主循环
ret, frame = cap.read() # cap.read(): 把cap这一帧的数据读取出来,ret表示是否读取成功,frame表示这一帧的图像(用数组的形式存储 形状为480*640*3 其中480*640表示长和宽 3代表RGB三通道)
b, g, r = cv.split(frame) # 将三通道的frame图片 拆成RGB三个通道 即480*640*3的数组 拆成3个480*640的数组
blur = cv.blur(g, (3, 3)) # 选取g通道,进行高斯滤波(为什么选g通道? 实测效果最好)
retval, dst = cv.threshold(blur, 50, 255, cv.THRESH_BINARY) # 对图像进行二值化处理 低于50的像素认为是黑色(0) 否则认为是白色(255)
dilate = cv.dilate(dst, (5, 5)) # 膨胀操作,让黑点与领域内不连续的黑点相连
erode = cv.erode(dilate, (5, 5)) # 腐蚀操作,让膨胀的黑线边缘恢复原来大小

detect_arr = [400, 450, 300] # 定义三个检测区域 分别为400 450 300行
i = 0 # i表示选择哪一个检测区域 默认为第400行
miss = 0 # miss=1 丢失目标 miss=0 未丢失目标

color = erode[detect_arr[i]] # color记录了检测的那一行数据
black_cnt = np.sum(color == 0) # 记录color中有多少个黑色像素

while black_cnt == 0: # 若无黑色像素,说明该行检测不到黑线
i = i + 1 # 切换下一个检测区域
if i != 3:
color = erode[detect_arr[i]]
black_cnt = np.sum(color == 0)
else: # 三行全部都检测不到
i = 0
miss = 1

if miss == 1 and last_act == LEFT: # 检测不到 并且上一次为左转
print("undetect_left!")
# car.left(V)
car.left(V * 1.6) # 最大速度左转
break;
if miss == 1 and last_act == RIGHT: # 检测不到 并且上一次为右转
print("undetect_right!")
# car.right(V)
car.right(V * 1.6) # 最大速度右转
break;

if miss != 1: # 如果检测到了黑线
black_index = np.where(color == 0) # 找到所有黑色像素的下标,保存在black_index中
center = np.mean(black_index[0]) # 求均值 保存为center
# bias = abs(center - 320) # bias为center与目标中心的差值
direction = center - 320
# 更新PID误差
error[0] = error[1]
error[1] = error[2]
error[2] = direction

# 更新PID输出(增量式PID表达式)
adjust[0] = adjust[1]
adjust[1] = adjust[2]
adjust[2] = adjust[1] + kp * (error[2] - error[1]) + ki * error[2] + kd * (error[2] - 2 * error[1] + error[0])
if center <= 220: # 如果center偏左
print("left")
# car.left(V)
car.left(V + adjust[2] / 200)
# car.left(V * (max(1, bias // 200))) # 左转 速度的增益系数由差值确定 最小为1 最大1.6
last_act = LEFT # 记录上一次的转弯动作为左


elif center > 420:
print("right")
# car.right(V)
car.right(V + abs(adjust[2]) / 200)
# car.right(V * (max(1, bias // 200))) # 右转 速度的增益系数由差值确定 最小为1 最大1.6
last_act = RIGHT # 记录上一次的转弯动作为右


else:
print("forward")
car.forward(V) # 如果center在中心周围,则前进

except KeyboardInterrupt: # 用户用CTRL+C打断程序
print("Stopped by User")
car.brake() # 速度归0
car.MotorRelease() # 释放电机
GPIO.cleanup() # 端口断电