【opencv-ANNs】简单自动驾驶树莓派小车
關(guān)鍵詞:#樹莓派小車 #opencv #神經(jīng)網(wǎng)絡(luò) #自動駕駛 #python3
概要:設(shè)計一個可以自動駕駛的小車,能實時分析攝像頭采集到的數(shù)據(jù),完成左右轉(zhuǎn)向的判斷和執(zhí)行,使其可以保持在車道上。
原理簡述:先通過數(shù)據(jù)采集的python腳本,手動操控小車的同時用車載攝像頭采集神經(jīng)網(wǎng)絡(luò)所需要的圖像數(shù)據(jù)。每次手動按鍵操控都將自動拍攝一張圖片,拍攝到的圖片在經(jīng)過壓縮、裁剪、濾波、灰度處理、大津二值化之后,通過numpy模塊將圖像像素矩陣重塑成一維數(shù)組,并打上左轉(zhuǎn)、右轉(zhuǎn)或是直行的標簽,最后打包生成一個numpy的npz文件來保存一次運行的全部數(shù)據(jù)。在多次運行收集到一定量的數(shù)據(jù)之后,將數(shù)據(jù)傳到電腦上進行神經(jīng)網(wǎng)絡(luò)的訓練。訓練的結(jié)果以xml文件的形式傳回樹莓派,供自動駕駛腳本讀取。自動駕駛的過程中主要參照xml文件中的結(jié)果來進行判斷。
文章目錄
- 1 總體流程設(shè)計
- 2 OPENCV在上位機(macOS)的環(huán)境配置
- 3 測試:基于pygame的keybDrive.py腳本——“遙控車”
- 4 測試:圖像獲取——啟動攝像頭
- 5 小車跑道設(shè)計
- 6 小車攝像頭固定
- 7 數(shù)據(jù)采集
- 8 模型訓練
- 9 裝載模型——自動駕駛
- 10 copyright&源代碼
1 總體流程設(shè)計
1.分別在樹莓派和本地配置好python3.7和opencv的環(huán)境
2.測試在本地通過pygame模塊用鍵盤遠程控制小車移動
3.測試樹莓派端的攝像頭圖像獲取
4.在樹莓派端通過python腳本(myCollectData.py)實現(xiàn)神經(jīng)網(wǎng)絡(luò)的數(shù)據(jù)采集
5.將樹莓派端采集的npz向量數(shù)據(jù)傳回本地,通過python腳本(train.py)進行神經(jīng)網(wǎng)絡(luò)模型訓練,生成xml結(jié)果
6.將訓練結(jié)果xml傳回到樹莓派,通過python腳本(LoadData.py)讀取訓練結(jié)果,實現(xiàn)在樹莓派的實時路線判斷和自動駕駛
2 OPENCV在上位機(macOS)的環(huán)境配置
用的是anaconda+opencv+pycharm。
這個部分網(wǎng)上的教程是很多的,在這里就不貼出了。
為了之后的程序使用,不要忘記添加numpy和pygame包。
3 測試:基于pygame的keybDrive.py腳本——“遙控車”
實現(xiàn)在電腦上按下“wasd”、“qezc”或者“上下左右”可以控制小車的移動。按鍵和移動方式的具體對應(yīng)關(guān)系如下:
| w/up | 前進 | t_up(100,100) |
| a/left | 左轉(zhuǎn) | t_left(100,100) |
| s/down | 倒退 | t_down(100,100) |
| d/right | 右轉(zhuǎn) | t_right(100,100) |
| q | 前進-左前 | t_up(50,100) |
| e | 前進-右前 | t_up(100,50) |
| z | 后退-左后 | t_down(50,100) |
| c | 后退-右后 | t_down(100,50) |
在代碼中的體現(xiàn)如下:
import pygame …… try:while True:for event in pygame.event.get():if event.type == KEYDOWN:keys = pygame.key.get_pressed()if keys[pygame.K_w]:t_up(100,100)print("forward")elif keys[pygame.K_s]:t_down(100,100)print("back")elif keys[pygame.K_q]:t_up(50,100)print("left")elif keys[pygame.K_e]:t_up(100,50)print("right")elif keys[pygame.K_z]:t_down(50,100)print("left back")elif keys[pygame.K_c]:t_down(100,50)print("right back")elif keys[pygame.K_a]:t_left(100,100)print("turn left")elif keys[pygame.K_d]:t_right(100,100)print("turn right")breakelif event.type == KEYUP:t_stop(0)print("stop") except KeyboardInterrupt:GPIO.cleanup()需要注意的是,keybDrive.py中的控制方法和后面myCollectData.py和LoadData.py的控制方法是相同的,但是其中的參數(shù)不同。keybDrive.py僅作為測試代碼使用。
4 測試:圖像獲取——啟動攝像頭
目的是測試一下小車的攝像頭是否正常工作,端口是否正確。以下為測試代碼的一部分:
import cv2 …… # get a frame ret, frame = cap.read() # show a frame cv2.imshow("capture", frame)如果正常運行的話屏幕上會出現(xiàn)一個名叫“capture”的窗口,里面顯示當前攝像頭正在拍攝的畫面。如果拍攝正常的話代表攝像頭工作正常,同時說明了opencv模塊安裝正確,沒有環(huán)境上的問題。
在驗證小車鍵盤操作無誤和攝像頭工作正常之后即可開始下一步工作。
5 小車跑道設(shè)計
此次跑道的設(shè)計主要考慮以下因素:
(1) 首先,由于之后要由攝像頭實時判斷前進路線,且硬件設(shè)備能力有限,此次的跑道設(shè)計需要足夠?qū)?#xff0c;但同時要保持寬度的一致性,且轉(zhuǎn)彎弧度不要太大,給小車一定的容錯空間。
(2) 至少需要由左轉(zhuǎn)、右轉(zhuǎn)和直行道。
(3) 由于做的是基于圖像識別的神經(jīng)網(wǎng)絡(luò)模型,所以可以出現(xiàn)一些車道線之外的東西,例如一個十字路口,來模擬現(xiàn)實交通情況,同時增加難度,作為本次簡單自動駕駛設(shè)計的吸睛點,以體現(xiàn)神經(jīng)網(wǎng)絡(luò)的優(yōu)越性。
(4) 該跑道應(yīng)該是一個環(huán)線,這樣之后在收集訓練數(shù)據(jù)的時候可以減少人為干預(yù),可以通過鍵盤遙控來讓小車一直保持在軌道上,減少體力勞動。
如圖所示,本次跑道設(shè)計由兩根車道線組成,不僅有左轉(zhuǎn)道、右轉(zhuǎn)道、直行道、還有一個十字路口。路口使用了重復(fù)的小短線來模擬現(xiàn)實的斑馬線。小車需要按照圖中所標注的方向前進,且在經(jīng)過路口時需要保持直行。
6 小車攝像頭固定
小車在行駛過程中攝像頭需要保持穩(wěn)定,盡量不能有震動、晃動等會影響結(jié)果判斷的因素出現(xiàn),同時攝像頭最好能夠看清前方的兩根車道線,所以需要攝像頭架在一個比較高的位置。
用紙板差不多搭了一個,哈哈哈:D
7 數(shù)據(jù)采集
運行數(shù)據(jù)采集的python腳本——myCollectData.py,我將手動操控小車,同時用車載攝像頭采集神經(jīng)網(wǎng)絡(luò)所需要的圖像數(shù)據(jù)。
樣本采集的思路為按下某個按鍵時,同時記錄當前畫面,并對其添加該按鍵的標簽,比如按下“左轉(zhuǎn)“鍵時,記錄下當前畫面,并加入一個”左轉(zhuǎn)“的標簽。其他方向同理,這里只用三個標簽,分別是:”前進“、”左轉(zhuǎn)“、”右轉(zhuǎn)“。而為了減少畫面中的干擾,對畫面進行一系列處理,處理過程為:壓縮、裁剪、濾波、灰度處理、大津二值化,最終使用二值化后的圖像作為樣本。
接下來從各個部分對數(shù)據(jù)采集的過程進行講解。
在之前的基礎(chǔ)上,小車已經(jīng)可以通過鍵盤遙控行駛,現(xiàn)在需要調(diào)用攝像頭采集前方跑道的圖像,并遙控小車采集數(shù)據(jù),需要在之前的keybDrive.py上添加新的功能。
圖像處理就要用到OpenCV來作為工具,那么第一步先調(diào)用攝像頭,因為樹莓派性能做實時處理還有些難度,所以將采集的圖像像素進行壓縮,具體代碼如下:
import cv2 self.cap = cv2.VideoCapture(1) # 調(diào)用video1 self.cap.set(3,320) # 像素寬度 self.cap.set(4,240) # 像素高度攝像頭拍攝的效果大概是這樣(糊了是意外):
這樣,就會將攝像頭采集的圖像設(shè)置為320*240像素。而調(diào)用過后還需要讀取攝像頭,所以使用函數(shù):ret, cam = self.cap.read()
這個cam變量就可以看作攝像頭,每次使用攝像頭只需要調(diào)用cam就可以。在實際的操作中,攝像頭中的畫面基本只有下半部分是地面,上半部分沒有什么用,所以只選取下半部分作為我們感興趣的區(qū)域,這樣既減少硬件的運算,又減少非地面部分的干擾。這里就更加簡單了,只需要非常短的代碼:roi = cam[120:240, :] # 120:240
將直接取得的畫面經(jīng)過一些列處理,包括濾波、灰度和大津二值化等,得到最后的圖像處理結(jié)果如下:
這里調(diào)用的變量是環(huán)環(huán)相扣的,先調(diào)用cam獲得圖像,再對cam進行一系列的濾波,灰度,二值化的處理。
圖像的本質(zhì)是一個大號像素矩陣,其中每個像素的灰度顏色在0~255變化(二值化后只有0和255了),這里就要用到Python中的numpy模塊了,numpy模塊是專門用于矩陣運算的數(shù)學運算模塊,使用numpy可以非常方便的將圖像像素矩陣重塑成一維數(shù)組,方便存儲。
以下為采集程序的大概思路:
這個程序有一個缺點就是不能實時存儲并釋放內(nèi)存,每次占用內(nèi)存超過300M以后就開始運行拖慢,大概是存入240個數(shù)據(jù),或者是運行五分鐘左右的時候。所以這時候需要一個停止程序的按鍵,將采集的數(shù)據(jù)從內(nèi)存存入硬盤,重新運行采集程序。在本次程序設(shè)計中,按下“p”鍵,數(shù)據(jù)采集就會停止,并儲存一次npz數(shù)據(jù)。
不算上之前沒有用的數(shù)據(jù),我一共采集了3500張數(shù)據(jù)用于訓練最終的xml文件。然而總共大概拍了10000張左右,這個過程很枯燥,不過最后看到小車自己可以跑的時候很欣慰,這枯燥的時間并沒有白費。
8 模型訓練
這部分為在上位機運行的部分,所以需要將上一步采集來的數(shù)據(jù)拷貝到電腦中。我在這里是用scp來傳文件的,個人感覺還是很便利的。
具體只要將數(shù)據(jù)拷貝過來之后在電腦上運行train.py腳本,完了就把訓練出來的xml文件拷貝回小車上跑跑看LoadData.py,如果效果不好則要重新添加采集數(shù)據(jù),重新訓練,重新再試一遍。
直接使用OpenCV的機器學習模塊非常方便,核心的訓練部分只需要7行代碼。這里就大概講講每句代碼實現(xiàn)的功能。
在這七行代碼中,第一句就需要創(chuàng)建一個神經(jīng)網(wǎng)絡(luò):
# create MLP model = cv2.ml.ANN_MLP_create()創(chuàng)建完畢后需要對這個網(wǎng)絡(luò)的結(jié)構(gòu)以及一些參數(shù)進行設(shè)置:
# Create ANN layer model.setLayerSizes(np.int32([38400,64,3])) #將網(wǎng)絡(luò)結(jié)構(gòu)設(shè)置為38400個輸入層,64個隱層,3個輸出層。理論上隱層數(shù)量越多,訓練時間就越長,結(jié)果就越好,還可以加入多層隱層。我分別使用16、32、64個隱層都訓練過,64層的準確率基本是最高的,大約90%左右,就是需要一點時間,3500個數(shù)據(jù)大概十分鐘左右。
接下來需要設(shè)置激活函數(shù)和訓練停止條件,代碼如下:
model.setActivationFunction(cv2.ml.ANN_MLP_SIGMOID_SYM) model.setTermCriteria(( cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, 500, 0.001))停止條件分為兩個,分別是計算條件次數(shù),和計算精度,0.001就相當于精度達到了0.1%。
接下來這三行理解為訓練方法和學習速率,最后一條的作用是為了防止陷入局部最小值的函數(shù),后兩條默認為0.1。
# BP Method model.setTrainMethod(cv2.ml.ANN_MLP_BACKPROP) # speed of learning model.setBackpropWeightScale(0.01) # don't be part of lowest model.setBackpropMomentumScale(0.1)以上這7行代碼為核心的負責訓練程序的代碼,這些設(shè)定完成過后,還需要一步來開始訓練,代碼如下:
num_iter = model.train(np.float32(train), cv2.ml.ROW_SAMPLE, np.float32(train_labels))還需要數(shù)據(jù)讀取、創(chuàng)建訓練集和測試集、計算準確率和保存生成的模型。讀取數(shù)據(jù)需要對采集來的數(shù)據(jù)加載到內(nèi)存中。3500個數(shù)據(jù)大概有1g左右。代碼如下:
# load training data image_array = np.zeros((1, 38400)) label_array = np.zeros((1, 3), 'float') training_data = glob.glob('./training_data/*.npz') #在本工程目錄中找到training_data目錄并讀取里面的所有npz文件# if no data, exit if not training_data:print("No training data in directory, exit")sys.exit()for single_npz in training_data:with np.load(single_npz) as data:train_temp = data['train']train_labels_temp = data['train_labels']image_array = np.vstack((image_array, train_temp))label_array = np.vstack((label_array, train_labels_temp))X = image_array[1:, :] y = label_array[1:, :]print('Image array shape: ', X.shape) print('Label array shape: ', y.shape)讀取完成后需要對其進行一個分割,因為不光要有訓練集,還需要有測試集來測試我們的模型準確率,這個也有現(xiàn)成的模塊,分割數(shù)據(jù)集代碼如下:
from sklearn.model_selection import train_test_split # train test split, 8:2 train, test, train_labels, test_labels = train_test_split(X, y, test_size=0.2)測試部分代碼如下:
# train data ret_0, resp_0 = model.predict(train) prediction_0 = resp_0.argmax(-1) true_labels_0 = train_labels.argmax(-1)train_rate = np.mean(prediction_0 == true_labels_0) print('Train accuracy: ', "{0:.2f}%".format(train_rate * 100))# test data ret_1, resp_1 = model.predict(test) prediction_1 = resp_1.argmax(-1) true_labels_1 = test_labels.argmax(-1)test_rate = np.mean(prediction_1 == true_labels_1) print('Test accuracy: ', "{0:.2f}%".format(test_rate * 100))最終保存模型:
# save model model.save('mlp_xml/mlp.xml')下圖為訓練出結(jié)果的截圖:
9 裝載模型——自動駕駛
每次訓練的累的快不行的時候,最能振奮人心的就是將訓練好的結(jié)果裝載上去的時刻!
說得簡單點,與數(shù)據(jù)采集類似,裝載模型只是將操作手交給計算機,由計算機根據(jù)圖像來做出相應(yīng)決策。
在LoadData.py腳本中,首先我們需要將keybDrive.py里面的電機接口和方向控制的方法貼過來。這段代碼由于太長就先不貼了,請見我的github鏈接。
接下來讀取模型,將之前訓練好的xml文件加載到程序中:
class NeuralNetwork(object):def __init__(self):self.annmodel = cv2.ml.ANN_MLP_load('mlp_xml/mlp.xml')def predict(self, samples):ret, resp = self.annmodel.predict(samples)return resp.argmax(-1) # find max現(xiàn)在模型有了,如何控制方向的方法也有了,接下來該打開我們的攝像頭,讓“機器”看到前方的道路。同樣,初始化神經(jīng)網(wǎng)絡(luò)和攝像頭參數(shù):
def __init__(self):self.model = NeuralNetwork()print('load ANN model.')#self.obj_detection = ObjectDetection()self.car = Carctrl()print('----------------Caminit completed-----------------')self.handle()def handle(self):self.cap = cv2.VideoCapture(1)self.cap.set(3, 320)self.cap.set(4, 240)獲取圖像并處理:
#獲取圖像并處理: try:while True:ret, cam = self.cap.read()gray = cv2.cvtColor(cam, cv2.COLOR_RGB2GRAY)ret, th3 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)roi = th3[120:240, :]設(shè)置一個模型變量,供控制程序調(diào)用。這一步可以加入一句cv2.imshow('cam',cam) 或者cv2.imshow('cam',roi)就可以觀察小車實際獲取圖像的情況,調(diào)試的時候也能知道是什么情況導致的小車發(fā)出錯誤指令。
image_array = roi.reshape(1, 38400).astype(np.float32) prediction = self.model.predict(image_array) # cv2.imshow('roi',roi) cv2.imshow('cam', cam)設(shè)計一個程序終止方法:
if cv2.waitKey(1) & 0xFF == ord('l'):break else:self.car.self_driving(prediction)最終,模仿手動操作的方法,將方向的控制權(quán)交給計算機:
def self_driving(self, prediction):if prediction == 0:self.t_up(10, 10)print("Forward")elif prediction == 1:self.t_up(5, 10)print("Left")elif prediction == 2:self.t_up(10, 5)print("Right")else:self.t_stop(0)經(jīng)多多次反復(fù)訓練/實驗后,自動駕駛運行測試成功!連續(xù)跑了四圈!好耶!
10 copyright&源代碼
本文已經(jīng)過知乎原作者“丶冬沐”授權(quán)改寫,僅用于學習交流。
下方github鏈接是本人親自實踐用的代碼,和原作者的有所不同。
原文傳送門
代碼(Edited by tomatoz)-github
總結(jié)
以上是生活随笔為你收集整理的【opencv-ANNs】简单自动驾驶树莓派小车的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 5分钟带你了解音视频开发
- 下一篇: Saleae