Создание автомобилей с автопилотом является одной из самых динамично развивающихся отраслей в современном мире. Одним из лидеров данного направления является компания Tesla, являющаяся одновременно и лидером в производстве электромобилей. В нашей стране созданием автопилота для автомобилей занимается компания Яндекс. Современные машины с автономным вождением представляют собой достаточно сложные устройства, обладающие интеллектуальной системой управления, многочисленными датчиками, исполнительными механизмами, сложными алгоритмами, системами машинного обучения и мощными процессорами.
Обнаружение линии является одной из важнейших задач, стоящих перед автопилотами автомобилей, поскольку данная функция позволяет удерживать автомобиль между двумя линиями разметки на дороге. В предыдущей статье на нашем сайте мы рассмотрели первую часть создания машины с автономным обнаружением линии на Raspberry Pi и OpenCV, в которой мы рассмотрели подготовку аппаратной части проекта, установку необходимого программного обеспечения и протестировали двигатели машины. В этой же статье мы рассмотрим продолжение данного проекта и напишем код программы для обнаружения линии с использованием библиотеки OpenCV, откалибруем двигатели для автономного вождения вдоль линии и будем управлять автомобилем чтобы он двигался вдоль линии.
Подготовка к реализации проекта
Для обнаружения линий и обработки изображений в нашем проекте мы будем использовать библиотеку OpenCV. Поэтому убедитесь в том, что библиотека OpenCV установлена на вашу плату Raspberry Pi. Также подайте питание на плату с помощью адаптера 2A и подключите ее к монитору чтобы упростить процесс отладки проекта.
Перед реализацией данного проекта рекомендуем прочитать основы работы с библиотекой OpenCV в плате Raspberry Pi. Также можете посмотреть все проекты на нашем сайте, в которых использовалась библиотека OpenCV.
Написание программы для Raspberry Pi для автономного обнаружения линии
В нашем проекте мы будем использовать следующие основные шаги для автономного обнаружения линии:
1. Перспективное преобразование (Perspective Transformation). На первом шаге нам необходимо получить перспективный (необходимый нам) вид кадра. Этот шаг позволит нам преобразовать изображение (видео) к виду, с помощью которого мы будем лучше понимать его суть. Для получения перспективного вида изображения нам необходимо определить точки на нем, в которых мы будем собирать информацию, и точки выходного изображения, внутри которых мы будем показывать наше изображение.
2. Пороговая классификация изображения (Image Thresholding) и определение его краев (Canny Edge Detection). Пороговая классификация изображения – это достаточно популярная технология сегментации, используемая для выделения интересующего нас фрагмента изображения с фона изображения. В нашем случае мы будем использовать ее для обнаружения линий на разметке. А технология Canny Edge detection будет использоваться для определения краев изображения.
3. Трансформация линии (Hough Line Transform). Данная технология используется для обнаружения любой формы (форма может быть представлена в математическом виде) на изображении или видео. В нашем проекте мы будем использовать данную технологию для обнаружения линий на дороге. Но перед тем как использовать данную технологию желательно произвести обнаружение краев изображения.
Рассмотрим содержание данных шагов более подробно.
Перспективная трансформация (Perspective Transformation)
На этом шаге нам необходимо получить перспективный вид нашей дороги. Для этого нам сначала необходимо выделить интересующий нас кадр из видео потока. Исходное изображение нашей дороги показано на следующем рисунке.
Теперь, как указывалось ранее, для получения перспективного вида изображения нам необходимо в этом исходном изображении определить точки, в которых мы будем собирать информацию, и точки выходного кадра, в пределах которых мы хотим показывать наше изображение. Мы будем сохранять эти точки в массивах NumPy и затем передавать их в функцию перспективной трансформации (Perspective Transformation).
1 2 3 4 5 |
width, height = 320,240 pts1 = [[0,240], [320,240], [290,30], [30,30]] pts2 = [[0, height], [width, height], [width,0], [0,0]] target = np.float32(pts1) destination = np.float32(pts2) |
После определения точек мы будем получать перспективную трансформацию из двух наборов точек и интегрировать ее в исходное изображение с помощью функций cv2.getPerspectiveTransform() и cv2.warpPerspective(). Далее представлен синтаксис этих функций.
1 |
cv2.getPerspectiveTransform(src, dst) |
где: src – координаты области, перспективный вид которой вы хотите получить;
dst – координаты выходного кадра, внутри которого вы хотите показывать изображение.
1 |
cv2.warpPerspective(src, dst, dsize) |
где: src – исходное изображение;
dst – выходное изображение, которое имеет такой же тип, как и исходное изображение;
dsize – размер выходного изображения.
1 2 3 |
matrix = cv2.getPerspectiveTransform(target, destination) result = cv2.warpPerspective(frame, matrix, (width, height)) cv2.imshow('Result', result) |
Пороговая классификация изображения (Image Thresholding) и определение его краев (Canny Edge Detection)
Перед тем как приступать к данному шагу мы преобразуем наш кадр в черно-белое изображение с оттенками серого, то есть наложим на него соответствующий фильтр (Gray Scale filter). Преобразование цветного изображения в серое – это стандартная процедура, применяемая в алгоритмах обработки изображений. Это значительно ускоряет все последующие процессы обработки изображения поскольку нам уже будет не нужно обрабатывать информацию о цвете. Преобразование цветного изображения в серое мы будем осуществлять с помощью функции:
1 |
gray = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY) |
Пороговая классификация изображения (Image Thresholding) используется для выделения основного интересующего нас объекта с фона изображения. Мы будем производить ее с помощью функции:
1 |
cv2.inRange(src, lowerb, upperb) |
где: src – массив входного изображения (оно должно быть "серым" (Grayscale));
lowerb – нижняя граница области;
upperb – верхняя граница области.
В нашем случае:
1 |
threshold = cv2.inRange(gray, 80, 200) # THRESHOLD IMAGE OF GRAY IMAGE |
На следующем шаге мы будем производить обнаружение краев. Существует достаточно много способов сделать это, самый простой и популярный способ – это использование метода canny edge из библиотеки OpenCV. В нашем случае он реализуется с помощью функции:
1 |
edges = cv2.Canny(gray, 1, 100, apertureSize=3) |
А синтаксис в общем виде этой функции выглядит следующим образом - cv2.Canny(src, thresholdValue 1, thresholdValue 2), где Threshold Value 1 и Threshold Value 2 – это минимальная и максимальная значения границы, а src – это входное изображение.
Трансформация линии (Hough Line Transform)
После осуществления двух предыдущих шагов на этом шаге мы будем использовать технологию трансформации линии (Hough Line Transform) чтобы обнаружить линии на дороге. Начнем этот метод мы с определения центра кадра используя точки перспективного изображения, которые мы определили ранее.
1 2 3 4 |
firstSquareCenters1 = findCenter((pts2[1][0], pts2[1][1]), (pts2[2][0], pts2[2][1])) firstSquareCenters2 = findCenter((pts2[3][0], pts2[3][1]), (pts2[0][0], pts2[0][1])) cv2.line(result, firstSquareCenters1, firstSquareCenters2, (0, 255, 0), 1) mainFrameCenter = findCenter(firstSquareCenters1,firstSquareCenters2) |
После нахождения центра кадра мы будем использовать метод Hough Line Transform чтобы обнаружить линии на дороге. Синтаксис данной функции выглядит следующим образом:
1 |
cv2.HoughLines (image, lines, rho, theta, threshold) |
где: image – входное изображение, должно быть бинарным изображением, поэтому перед этим к нему нужно применить процедуру определения краев;
lines – вектор с параметрами (r, Φ) линий;
rho – разрешение параметра r в пикселах;
theta – разрешение параметра Φ в радианах;
threshold – минимальное число пересечений для обнаружения линии.
В нашем случае мы будем использовать эту функцию со следующими параметрами:
1 |
lines = cv2.HoughLinesP(mergedImage,1,np.pi/180,10,minLineLength=120,maxLineGap=250) |
После обнаружения линий следующим шагом будем нахождение центра обоих линий используя координаты, которые мы получили из функции hough line transform. После определения центров линий мы будем использовать функции left.append и right.append чтобы произвести необходимые настройки местоположения линий.
1 2 3 4 5 6 7 8 9 10 11 12 |
for line in lines: x1,y1,x2,y2 = line[0] if 0<=x1 <=width and 0<= x2 <=width : center = findCenter((x1,y1),(x2,y2)) if center[0] < (width//2): center1 = center left.append((x1, y1)) left.append((x2,y2)) else: center2 = center right.append((x1, y1)) right.append((x2,y2)) |
Движение машины
Теперь, когда мы определили линии и центры обоих линий, нам необходимо управлять машиной таким образом, чтобы она оставалась внутри этих линий. В идеале мы должны стараться удержать ее в центре между линиями. То есть если координаты центра кадра находятся посередине между линиями, мы должны двигать машину прямо, без ее отклонения вправо или влево. Если координаты центра отклоняются вправо, мы должны двигать машину влево, и наоборот.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if maincenter <= 6 and maincenter > -6: mot.frontmiddle() speed = 25 elif maincenter > 6 and frame_counter%10 ==0: mot.frontleft() speed = 25 print("Right") elif(frame_counter%10 ==0): print("Forward") mot.forward(speed) elif maincenter < -6 and frame_counter%10 ==0: mot.frontright() speed = 25 print("left") |
Тестирование работы проекта
Когда наша машина научилась обнаруживать линии и находить центральную точку между линиями, мы можем поместить ее на сконструированный трек и приступить к тестированию проекта.
Машина достаточно уверенно едет прямо и замедляется на поворотах.
Но машине приходится "тяжеловато" когда она встречает крутой поворот. Это происходит из-за того, что на крутых поворотах одна из линий уходит за границы кадра и поэтому мы уже не можем найти центр между двумя линиями. Также автор проекта (ссылка на оригинал приведена в конце статьи) обнаружил, что машина иногда "рыскает" вправо или влево между линиями иногда выходит за границы линии. Это вызвано различными углами поворота, вычисляемыми для текущего и последующего кадра.
Но автор проекта продолжает над ним работу и постарается найти решения, улучшающие обнаружение машиной линии. И как только он опубликует эти новые решения на сайте-источнике мы постараемся как можно быстрее перевести их и для нашего сайта.
Исходный код программы на Python
Код файла houghline.py
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 |
import cv2 import numpy as np import motors as mot def findCenter(p1,p2): center = ((p1[0] + p2[0]) // 2, (p1[1] + p2[1]) // 2) return center def minmax_centerPoints(tergetList,pos): if len(tergetList) > 0: maximum = max(tergetList, key = lambda i: i[pos]) minimum = min(tergetList, key = lambda i: i[pos]) return [maximum,minimum] else: return None global count def detectedlane1(imageFrame): center1= 0 center2 = 0 width,height = 320,240 pts1 = [[0,240],[320,240],[290,30],[30,30]] pts2 = [[0, height], [width, height], [width,0], [0,0]] target = np.float32(pts1) destination = np.float32(pts2) # Apply Perspective Transform Algorithm matrix = cv2.getPerspectiveTransform(target, destination) result = cv2.warpPerspective(frame, matrix, (width,height)) cv2.imshow('Result', result) # cv2.line(imageFrame, (pts1[0][0],pts1[0][1]), (pts1[1][0],pts1[1][1]), (0, 255, 0), 1) #cv2.line(imageFrame, (pts1[1][0],pts1[1][1]), (pts1[2][0],pts1[2][1]), (0, 255, 0), 1) #cv2.line(imageFrame, (pts1[2][0],pts1[2][1]), (pts1[3][0],pts1[3][1]), (0, 255, 0), 1) #cv2.line(imageFrame, (pts1[3][0], pts1[3][1]), (pts1[0][0], pts1[0][1]), (0, 255, 0), 1) # cv2.imshow('Main Image Window', imageFrame) gray = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY) threshold = cv2.inRange(gray, 80, 200) # THRESHOLD IMAGE OF GRAY IMAGE edges = cv2.Canny(gray, 1, 100, apertureSize=3) mergedImage = cv2.add(threshold,edges) #cv2.line(result, (pts2[0][0], pts2[0][1]), (pts2[1][0], pts2[1][1]), (0, 255, 0), 2) #cv2.line(result, (pts2[1][0], pts2[1][1]), (pts2[2][0], pts2[2][1]), (0, 255, 0), 2) #cv2.line(result, (pts2[2][0], pts2[2][1]), (pts2[3][0], pts2[3][1]), (0, 255, 0), 2) #cv2.line(result, (pts2[3][0], pts2[3][1]), (pts2[0][0], pts2[0][1]), (0, 255, 0), 2) firstSquareCenters1 = findCenter((pts2[1][0], pts2[1][1]), (pts2[2][0], pts2[2][1])) firstSquareCenters2 = findCenter((pts2[3][0], pts2[3][1]), (pts2[0][0], pts2[0][1])) # print("Centers:", firstSquareCenters1,firstSquareCenters2) #cv2.circle (frame, (firstSquareCenters1,firstSquareCenters1),5,(0,0,255),cv2.FILLED) cv2.line(result, firstSquareCenters1, firstSquareCenters2, (0, 255, 0), 1) mainFrameCenter = findCenter(firstSquareCenters1,firstSquareCenters2) lines = cv2.HoughLinesP(mergedImage,1,np.pi/180,10,minLineLength=120,maxLineGap=250) centerPoints = [] left = [] right = [] if lines is not None: for line in lines: x1,y1,x2,y2 = line[0] if 0<=x1 <=width and 0<= x2 <=width : center = findCenter((x1,y1),(x2,y2)) if center[0] < (width//2): center1 = center left.append((x1, y1)) left.append((x2,y2)) else: center2 = center right.append((x1, y1)) right.append((x2,y2)) if center1 !=0 and center2 !=0: centroid1 = findCenter(center1,center2) centerPoints.append(centroid1) centers = minmax_centerPoints(centerPoints,1) laneCenters = 0 mainCenterPosition = 0 if centers is not None: laneframeCenter = findCenter(centers[0],centers[1]) #print(mainFrameCenter,laneframeCenter) mainCenterPosition = mainFrameCenter[0] - laneframeCenter[0] cv2.line(result, centers[0], centers[1], [0, 255, 0], 2) laneCenters = centers # print(centers) return [laneCenters,result,mainCenterPosition] frame_counter = 0 if __name__ == '__main__': cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH,320) # set the width to 320 p cap.set(cv2.CAP_PROP_FRAME_HEIGHT,240) # set the height to 240 p count = 0 speed = 0 maincenter = 0 while(cap.isOpened()): frame_counter = frame_counter+1 print(frame_counter) ret, frame = cap.read() if ret == True: # Display the resulting frame laneimage1 = detectedlane1(frame) if laneimage1 is not None: maincenter = laneimage1[2] cv2.putText(laneimage1[1],"Pos="+str(maincenter),(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0)) cv2.imshow('FinalWindow',laneimage1[1]) #print("Position-> "+str(maincenter)) else: cv2.imshow('FinalWindow', frame) resizeWindow('FinalWindow',570, 480) if maincenter <= 6 and maincenter > -6: mot.frontmiddle() speed = 25 elif maincenter > 6 and frame_counter%10 ==0: mot.frontleft() speed = 25 print("Right") elif(frame_counter%10 ==0): print("Forward") mot.forward(speed) elif maincenter < -6 and frame_counter%10 ==0: mot.frontright() speed = 25 print("left") #mot.forward(speed) # cv2.imshow('Frame', frame) key = cv2.waitKey(1) if key == 27: break cap.release() cv2.destroyAllWindows() mot.stop() |
Код файла motors.py
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 |
#MOTOR CLASS import RPi.GPIO as GPIO from time import sleep in1 = 4 in2 = 17 in3 = 18 in4 = 27 en = 22 en2 = 23 temp1 = 1 GPIO.setmode(GPIO.BCM) GPIO.setup(in1, GPIO.OUT) GPIO.setup(in2, GPIO.OUT) GPIO.setup(in3, GPIO.OUT) GPIO.setup(in4, GPIO.OUT) GPIO.setup(en, GPIO.OUT) GPIO.setup(en2, GPIO.OUT) GPIO.output(in1, GPIO.LOW) GPIO.output(in2, GPIO.LOW) GPIO.output(in3, GPIO.LOW) GPIO.output(in4, GPIO.LOW) p1 = GPIO.PWM(en, 1000) p1.stop() p2 = GPIO.PWM(en2, 1000) p2.stop() print("r-run s-stop f-forward b-backward l-low m-medium h-high dm-frontmiddle dr-frontright dl-frontleft e-exit") def frontmiddle(): GPIO.output(in3, GPIO.LOW) GPIO.output(in4, GPIO.LOW) p2.stop() def frontright(): GPIO.output(in3, GPIO.LOW) GPIO.output(in4, GPIO.HIGH) p2.start(100) def frontleft(): GPIO.output(in3, GPIO.HIGH) GPIO.output(in4, GPIO.LOW) p2.start(100) def forward(speed=50,time=0): GPIO.output(in1, GPIO.HIGH) GPIO.output(in2, GPIO.LOW) p1.start(speed) sleep(time) def backward(speed=50,time=0): p1.start(speed) GPIO.output(in1, GPIO.LOW) GPIO.output(in2, GPIO.HIGH) frontmiddle() sleep(time) def stop(): GPIO.output(in1, GPIO.LOW) GPIO.output(in2, GPIO.LOW) GPIO.output(in3, GPIO.LOW) GPIO.output(in4, GPIO.LOW) p1.stop() p2.stop() def fright(speed=50,time=0): forward(speed) frontright() sleep(time) def fleft(speed=50,time=0): forward(speed) frontleft() sleep(time) def bright(speed=50,time=0): backward(speed) frontright() sleep(time) def bleft(speed=50,time=0): backward(speed) frontleft() sleep(time) if __name__ == '__main__': stop() |