Это четвертый проект инженера по беспилотным автомобилям nanodegree. В этом проекте мы будем использовать передовые методы и OpenCV для поиска полос движения.
Расширенный проект поиска полосы движения
Цели / шаги этого проекта следующие:
- Вычислите калибровочную матрицу камеры и коэффициенты искажения для набора изображений шахматной доски.
- Примените коррекцию искажения к необработанным изображениям.
- Используйте преобразования цвета, градиенты и т. Д., Чтобы создать двоичное изображение с пороговыми значениями.
- Примените перспективное преобразование, чтобы исправить двоичное изображение («вид с высоты птичьего полета»).
- Обнаружение пикселей полосы и подгонка, чтобы найти границу полосы.
- Определите кривизну полосы движения и положение автомобиля относительно центра.
- Деформируйте обнаруженные границы полосы движения обратно на исходное изображение.
- Вывод визуального отображения границ полосы движения и числовой оценки кривизны полосы движения и положения автомобиля.
В мой проект входят следующие файлы:
- project.ipynb, содержащий весь код.
- project_video_output.mp4, который показывает рабочий пример.
- writeup.md/README.md подводит итоги.
Этот проект состоит из 2 частей:
- Определите пошагово обработку изображения.
- Определите полный конвейер для обработки видео.
Я разбил первую часть проекта на 7 шагов:
- Калибровка камеры
- Коррекция искажений
- Перспективная трансформация
- Порог цвета / градиента
- Обнаружение полос движения
- измерение кривизны и положения кабины
- Рисование линий полос движения на исходном изображении
Конвейер для изображения
1. Калибровка камеры
На этом этапе я использовал изображения шахматной доски из папки camera_cal. Сначала я попробовал найти углы шахматной доски для всех изображений,
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0) objp = np.zeros((6*9,3), np.float32) objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2) # Arrays to store object points and image points from all the images. objpoints = [] # 3d points in real world space imgpoints = [] # 2d points in image plane. # Make a list of calibration images images = glob.glob('./camera_cal/calibration*.jpg') calibration_output_images = [] # Step through the list and search for chessboard corners for i, fname in enumerate(images): img = cv2.imread(fname) gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) # Find the chessboard corners ret, corners = cv2.findChessboardCorners(gray, (9,6),None) # If found, add object points, image points if ret == True: objpoints.append(objp) imgpoints.append(corners) # Draw and display the corners img = cv2.drawChessboardCorners(img, (9,6), corners, ret) cv2.imwrite( "./output_images/calibration_output%d.jpg" % i, img ) calibration_output_images.append(img) axs = plot_images(10, 2, (20, 50), calibration_output_images)
что приводит к:
Я начинаю с подготовки «точек объекта», которые будут координатами (x, y, z) углов шахматной доски в мире. Здесь я предполагаю, что шахматная доска закреплена на плоскости (x, y) в точке z = 0, так что точки объекта одинаковы для каждого калибровочного изображения. Таким образом, objp
- это просто реплицированный массив координат, и к objpoints
будет добавляться его копия каждый раз, когда я успешно обнаруживаю все углы шахматной доски на тестовом изображении. К imgpoints
будет добавлено положение в пикселях (x, y) каждого из углов в плоскости изображения при каждом успешном обнаружении шахматной доски.
Для большинства изображений углы были найдены, и в результате я получил objpoints
, который будет координатами (x, y, z) углов шахматной доски в мире. Здесь я предполагаю, что шахматная доска закреплена на плоскости (x, y) в точке z = 0, так что точки объекта одинаковы для каждого калибровочного изображения. Таким образом, objp
- это просто реплицированный массив координат, и к objpoints
будет добавляться его копия каждый раз, когда я успешно обнаруживаю все углы шахматной доски на тестовом изображении. К imgpoints
будет добавлено положение в пикселях (x, y) каждого из углов в плоскости изображения при каждом успешном обнаружении шахматной доски.
2. Коррекция искажений
На этом этапе я использовал одно из изображений шахматной доски, чтобы проверить исправление искажений с помощью этого кода:
test_image = cv2.imread('./camera_cal/calibration1.jpg') axs_titles = ['Original', 'Undistorted'] ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, test_image.shape[0:2], None, None) undist = cv2.undistort(test_image, mtx, dist, None, mtx) axs = plot_images(1, 2, (20, 50), [test_image, undist], axs_titles)
что приводит к:
На предыдущем изображении мы видим, что после применения коррекции искажения шахматная доска фактически не искажалась.
Следующее, что нужно сделать, это протестировать это на изображениях с камеры на машине, едущей по шоссе.
test_images = glob.glob('./test_images/*.jpg') undistorted_images=[] original_images=[] output_test_images=[] axs_titles=[] for i, fname in enumerate(test_images): img = cv2.imread(fname) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) output_test_images.append(img) original_images.append(img) axs_titles.append('Original %d' %int(i+1)) undist = cv2.undistort(img, mtx, dist, None, mtx) undistorted_images.append(undist) output_test_images.append(undist) axs_titles.append('Undistorted %d' %int(i+1)) axs = plot_images(8, 2, (20, 50), output_test_images, axs_titles=axs_titles, title_fontsize=30)
этот код использует mtx, dist, None, mtx
, который мы получаем от cv2.calibrateCamera
. Вот результаты:
Сначала вы не увидите никакой разницы. Но вы увидите разницу в капоте машины.
3. Трансформация перспективы
После того, как изображения не искажены, мы применяем перспективное преобразование, чтобы увидеть более точное представление линий полос движения.
Во-первых, нам нужно определить область, которую мы хотим преобразовать. Итак, с помощью оси я определил 4 точки:
axs_titles = [] for i, image in enumerate(undistorted_images): axs_titles.append('Test %d' %int(i+1)) axs = plot_images(4, 2, (20, 50), undistorted_images, axis=True, axs_titles=axs_titles, title_fontsize=30) x = [580.0, 740.0, 1100.0, 270.0, 580.0] y = [460.0, 460.0, 670.0, 670.0, 460.0] for ax in axs: ax.plot(x, y, color='#ff1010', alpha=0.5, linewidth=3, solid_capstyle='round', zorder=2)
Теперь мы видим область, которая будет использоваться для трансформации перспективы.
Затем я определил warp_image
метод для использования в будущем:
def warp_image(img): height, width = img.shape[0:2] src = np.float32( [ [580.0, 460.0], [740.0, 460.0], [1100.0, 670.0], [270.0, 670.0], ] ) dst = np.float32( [ [200.0, 0], [width - 200.0, 0], [width - 200.0, height], [200.0, height], ] ) M = cv2.getPerspectiveTransform(src, dst) Minv = cv2.getPerspectiveTransform(dst, src) warped = cv2.warpPerspective(img, M, (width, height), flags=cv2.INTER_LINEAR) return warped, M, Minv
Это привело к появлению следующих точек отправления и назначения:
Затем я применил к тестовым изображениям:
output_test_images = [] warped_images = [] axs_titles = [] Minvs = [] for i, undist in enumerate(undistorted_images): output_test_images.append(undist) axs_titles.append('Undistorted %d' %int(i+1)) warped_image, M, Minv = warp_image(undist) Minvs.append(Minv) warped_images.append(warped_image) output_test_images.append(warped_image) axs_titles.append('Warped %d' %int(i+1)) axs = plot_images(8, 2, (20, 50), output_test_images, axs_titles=axs_titles, title_fontsize=30)
В итоге я получил такой результат:
Теперь вы можете увидеть реальную кривизну и расстояние между каждой полосой движения, и это сфокусирует алгоритм на поиске только тех полос, которые нам интересны.
4. Порог цвета / градиента
На этом этапе я решил проверить разные цветовые пространства (RGB и HLS). Итак, я разделил изображение на три канала:
output_test_images = [] axs_titles = [] warped_image = warped_images[5] # RGB R = warped_image[:,:,0] G = warped_image[:,:,1] B = warped_image[:,:,2] thresh = (180, 255) axs_titles.append('R Channel') output_test_images.append(R) binary = np.zeros_like(R) axs_titles.append('G Channel') output_test_images.append(G) binary = np.zeros_like(G) axs_titles.append('B Channel') output_test_images.append(B) binary = np.zeros_like(B) axs = plot_images(1, 3, (20, 50), output_test_images, axs_titles=axs_titles, title_fontsize=15, height_limit=undistorted_images[0].shape[0], width_limit=undistorted_images[0].shape[1], cmap='gray') # HLS output_test_images = [] axs_titles = [] warped_image = warped_images[5] hls_image = cv2.cvtColor(warped_image, cv2.COLOR_RGB2HLS) H = hls_image[:,:,0] L = hls_image[:,:,1] S = hls_image[:,:,2] axs_titles.append('H Channel') output_test_images.append(H) axs_titles.append('L Channel') output_test_images.append(L) axs_titles.append('S Channel') output_test_images.append(S) axs = plot_images(1, 3, (20, 50), output_test_images, axs_titles=axs_titles, title_fontsize=15, height_limit=undistorted_images[0].shape[0], width_limit=undistorted_images[0].shape[1], cmap='gray')
С помощью этих фрагментов кода мы можем видеть изображение в трех разделенных каналах (R, G, B) или (H, L, S):
В этом случае мы видим, что RGB более понятен, но после тестирования с видео (2-я часть) каналы L и S работают лучше, чем RGB.
Итак, после нескольких тестов я решил использовать комбинацию каналов (L и S) и пороговых значений Sobel, Magnitude и Direction.
def abs_sobel_thresh(img, orient='x', thresh=(0, 255), sobel_kernel=3): thresh_min, thresh_max = thresh s_thresh = (120, 255) l_thresh = (30, 255) hls_image = cv2.cvtColor(img, cv2.COLOR_RGB2HLS) H = hls_image[:,:,0] L = hls_image[:,:,1] S = hls_image[:,:,2] if orient == 'x': sobel = cv2.Sobel(L, cv2.CV_64F, 1, 0, ksize=sobel_kernel) else: sobel = cv2.Sobel(L, cv2.CV_64F, 0, 1, ksize=sobel_kernel) abs_sobel = np.absolute(sobel) scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel)) l_sobel = np.zeros_like(scaled_sobel) l_sobel[(scaled_sobel >= thresh_min) & (scaled_sobel <= thresh_max)] = 1 s_binary = np.zeros_like(S) s_binary[(S >= s_thresh[0]) & (S <= s_thresh[1])] = 1 l_binary = np.zeros_like(L) l_binary[(L >= l_thresh[0]) & (L <= l_thresh[1])] = 1 binary_output = np.zeros_like(l_sobel) binary_output[((l_binary == 1) & (s_binary == 1) | (l_sobel==1))] = 1 return binary_output def mag_thresh(img, sobel_kernel=3, thresh=(0,255)): img = cv2.cvtColor(img, cv2.COLOR_RGB2HLS)[:,:,2] sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=sobel_kernel) sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=sobel_kernel) gradmag = np.sqrt(sobelx**2 + sobely**2) scale_factor = np.max(gradmag)/255 gradmag = (gradmag/scale_factor).astype(np.uint8) binary_output = np.zeros_like(gradmag) binary_output[(gradmag >= thresh[0]) & (gradmag <= thresh[1])] = 1 return binary_output def dir_threshold(img, sobel_kernel=3, thresh=(0, np.pi/2)): img = cv2.cvtColor(img, cv2.COLOR_RGB2HLS)[:,:,2] sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=sobel_kernel) sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=sobel_kernel) abs_sobelx = np.absolute(sobelx) abs_sobely = np.absolute(sobely) dirs = np.arctan2(abs_sobely, abs_sobelx) binary_output = np.zeros_like(dirs) binary_output[(dirs >= thresh[0]) & (dirs <= thresh[1])] = 1 return binary_output def combined_thresh(img, sobel_kernel=3, abs_thresh=(15,255), _mag_thresh=(15,255), dir_thresh=(0, np.pi/2)): gradx = abs_sobel_thresh(img, orient='x', sobel_kernel=sobel_kernel, thresh=abs_thresh) mag_binary = mag_thresh(img, sobel_kernel=sobel_kernel, thresh=_mag_thresh) dir_binary = dir_threshold(img, sobel_kernel=sobel_kernel, thresh=dir_thresh) combined = np.zeros_like(dir_binary) combined[((gradx == 1) ) | ((mag_binary == 1) & (dir_binary == 1))] = 1 return combined
Как мы видим, в abs_sobel_thresh
методе я применил порог Собела к каналу L, но я также использую двоичный L и двоичный код S. Наконец я их объединил
binary_output[((l_binary == 1) & (s_binary == 1) | (l_sobel==1))] = 1
В mag_thresh
и dir_threshold
я использовал только S канал.
Наконец, я определил combined_thresh
, где я объединил разные пороги.
Тестируя с тем же предыдущим изображением, мы получаем следующее двоичное изображение:
combined = combined_thresh(warped_images[5], sobel_kernel=3, abs_thresh=(20,255), _mag_thresh=(20,255), dir_thresh=(0, np.pi/2)) plt.imshow(combined, cmap='gray')
5. Обнаружение полос движения
Теперь, когда у нас есть двоичное изображение, мы можем использовать тонкие линии. Но сначала мы увидим гистограмму того, как ведут себя данные.
histogram = np.sum(combined[combined.shape[0]//2:,:], axis=0) plt.plot(histogram)
И получаем:
Мы можем видеть, что около 200 пикселей и 1000 пикселей четко обозначены линиями полосы движения.
Следующее, что нужно сделать, это найти линии с помощью скользящих окон и подобрать многочлен 2-го порядка.
def sliding_window_polyfit(img): # Assuming you have created a warped binary image called "binary_warped" # Take a histogram of the bottom half of the image histogram = np.sum(img[img.shape[0]//2:,:], axis=0) # Create an output image to draw on and visualize the result out_img = np.uint8(np.dstack((img, img, img))*255) # Find the peak of the left and right halves of the histogram # These will be the starting point for the left and right lines midpoint = np.int(histogram.shape[0]//2) leftx_base = np.argmax(histogram[:midpoint]) rightx_base = np.argmax(histogram[midpoint:]) + midpoint # Choose the number of sliding windows nwindows = 9 # Set height of windows window_height = np.int(img.shape[0]//nwindows) # Identify the x and y positions of all nonzero pixels in the image nonzero = img.nonzero() nonzeroy = np.array(nonzero[0]) nonzerox = np.array(nonzero[1]) # Current positions to be updated for each window leftx_current = leftx_base rightx_current = rightx_base # Set the width of the windows +/- margin margin = 80 # Set minimum number of pixels found to recenter window minpix = 50 # Create empty lists to receive left and right lane pixel indices left_lane_inds = [] right_lane_inds = [] # Step through the windows one by one for window in range(nwindows): # Identify window boundaries in x and y (and right and left) win_y_low = img.shape[0] - (window+1)*window_height win_y_high = img.shape[0] - window*window_height win_xleft_low = leftx_current - margin win_xleft_high = leftx_current + margin win_xright_low = rightx_current - margin win_xright_high = rightx_current + margin # Draw the windows on the visualization image cv2.rectangle(out_img,(win_xleft_low,win_y_low),(win_xleft_high,win_y_high),(0,255,0), 2) cv2.rectangle(out_img,(win_xright_low,win_y_low),(win_xright_high,win_y_high),(0,255,0), 2) # Identify the nonzero pixels in x and y within the window good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0] good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0] # Append these indices to the lists left_lane_inds.append(good_left_inds) right_lane_inds.append(good_right_inds) # If you found > minpix pixels, recenter next window on their mean position if len(good_left_inds) > minpix: leftx_current = np.int(np.mean(nonzerox[good_left_inds])) if len(good_right_inds) > minpix: rightx_current = np.int(np.mean(nonzerox[good_right_inds])) # Concatenate the arrays of indices left_lane_inds = np.concatenate(left_lane_inds) right_lane_inds = np.concatenate(right_lane_inds) # Extract left and right line pixel positions leftx = nonzerox[left_lane_inds] lefty = nonzeroy[left_lane_inds] rightx = nonzerox[right_lane_inds] righty = nonzeroy[right_lane_inds] # Fit a second order polynomial to each left_fit = np.polyfit(lefty, leftx, 2) right_fit = np.polyfit(righty, rightx, 2) return left_fit, right_fit, left_lane_inds, right_lane_inds, out_img def visualize_polyfit(img, out_img, left_fit, right_fit, left_lane_inds, right_lane_inds): ploty = np.linspace(0, img.shape[0]-1, img.shape[0]) left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2] right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2] nonzero = img.nonzero() nonzeroy = np.array(nonzero[0]) nonzerox = np.array(nonzero[1]) out_img[nonzeroy[left_lane_inds], nonzerox[left_lane_inds]] = [255, 0, 0] out_img[nonzeroy[right_lane_inds], nonzerox[right_lane_inds]] = [0, 0, 255] plt.figure(figsize = (10,5)) plt.imshow(out_img) plt.plot(left_fitx, ploty, color='yellow') plt.plot(right_fitx, ploty, color='yellow') plt.xlim(0, 1280) plt.ylim(720, 0) left_fit, right_fit, left_lane_inds, right_lane_inds, out_img = sliding_window_polyfit(combined) visualize_polyfit(combined, out_img, left_fit, right_fit, left_lane_inds, right_lane_inds)
Как вы можете видеть на изображении, я решил использовать 9 окон с полем 80 пикселей, которые работают очень хорошо.
Как только мы узнаем, где находятся линии, мы можем пропустить скользящие окна, протестировав то же изображение:
def skip_windows_step(binary_warped, left_fit, right_fit): nonzero = binary_warped.nonzero() nonzeroy = np.array(nonzero[0]) nonzerox = np.array(nonzero[1]) margin = 80 left_lane_inds = ((nonzerox > (left_fit[0]*(nonzeroy**2) + left_fit[1]*nonzeroy + left_fit[2] - margin)) & (nonzerox < (left_fit[0]*(nonzeroy**2) + left_fit[1]*nonzeroy + left_fit[2] + margin))) right_lane_inds = ((nonzerox > (right_fit[0]*(nonzeroy**2) + right_fit[1]*nonzeroy + right_fit[2] - margin)) & (nonzerox < (right_fit[0]*(nonzeroy**2) + right_fit[1]*nonzeroy + right_fit[2] + margin))) # Again, extract left and right line pixel positions leftx = nonzerox[left_lane_inds] lefty = nonzeroy[left_lane_inds] rightx = nonzerox[right_lane_inds] righty = nonzeroy[right_lane_inds] # Fit a second order polynomial to each left_fit = np.polyfit(lefty, leftx, 2) right_fit = np.polyfit(righty, rightx, 2) return out_img, left_fit, right_fit, left_lane_inds, right_lane_inds def visualize_skip_window_step(binary_warped, out_img, left_fitx, right_fitx, left_lane_inds, right_lane_inds): margin = 80 # Generate x and y values for plotting ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0]) left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2] right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2] # Create an image to draw on and an image to show the selection window out_img = np.uint8(np.dstack((binary_warped, binary_warped, binary_warped))*255) window_img = np.zeros_like(out_img) # Color in left and right line pixels nonzero = binary_warped.nonzero() nonzeroy = np.array(nonzero[0]) nonzerox = np.array(nonzero[1]) out_img[nonzeroy[left_lane_inds], nonzerox[left_lane_inds]] = [255, 0, 0] out_img[nonzeroy[right_lane_inds], nonzerox[right_lane_inds]] = [0, 0, 255] # Generate a polygon to illustrate the search window area # And recast the x and y points into usable format for cv2.fillPoly() left_line_window1 = np.array([np.transpose(np.vstack([left_fitx-margin, ploty]))]) left_line_window2 = np.array([np.flipud(np.transpose(np.vstack([left_fitx+margin, ploty])))]) left_line_pts = np.hstack((left_line_window1, left_line_window2)) right_line_window1 = np.array([np.transpose(np.vstack([right_fitx-margin, ploty]))]) right_line_window2 = np.array([np.flipud(np.transpose(np.vstack([right_fitx+margin, ploty])))]) right_line_pts = np.hstack((right_line_window1, right_line_window2)) # Draw the lane onto the warped blank image cv2.fillPoly(window_img, np.int_([left_line_pts]), (0,255, 0)) cv2.fillPoly(window_img, np.int_([right_line_pts]), (0,255, 0)) result = cv2.addWeighted(out_img, 1, window_img, 0.3, 0) plt.figure(figsize = (10,5)) plt.imshow(result) plt.plot(left_fitx, ploty, color='yellow') plt.plot(right_fitx, ploty, color='yellow') plt.xlim(0, 1280) plt.ylim(720, 0) out_img, left_fit2, right_fit2, left_lane_inds2, right_lane_inds2 = skip_windows_step(combined, left_fit, right_fit) visualize_skip_window_step(combined, out_img, left_fit2, right_fit2, left_lane_inds2, right_lane_inds2)
Используя данные из скользящих окон, я просто искал поля вокруг предыдущей позиции строки.
6. Измерение кривизны и положения автомобиля.
Я определил два метода расчета кривизны и положения автомобиля, которые покажут нам, как далеко автомобиль находится от центра дороги:
def measure_curvature(binary_warped, left_lane_inds, right_lane_inds): # Define conversions in x and y from pixels space to meters ym_per_pix = 30/720 # meters per pixel in y dimension xm_per_pix = 3.7/700 # meters per pixel in x dimension ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0]) y_eval = np.max(ploty) nonzero = binary_warped.nonzero() nonzeroy = np.array(nonzero[0]) nonzerox = np.array(nonzero[1]) leftx = nonzerox[left_lane_inds] lefty = nonzeroy[left_lane_inds] rightx = nonzerox[right_lane_inds] righty = nonzeroy[right_lane_inds] # Fit new polynomials to x,y in world space left_fit_cr = np.polyfit(lefty*ym_per_pix, leftx*xm_per_pix, 2) right_fit_cr = np.polyfit(righty*ym_per_pix, rightx*xm_per_pix, 2) # Calculate the new radii of curvature left_curverad = ((1 + (2*left_fit_cr[0]*y_eval*ym_per_pix + left_fit_cr[1])**2)**1.5) / np.absolute(2*left_fit_cr[0]) right_curverad = ((1 + (2*right_fit_cr[0]*y_eval*ym_per_pix + right_fit_cr[1])**2)**1.5) / np.absolute(2*right_fit_cr[0]) return left_curverad, right_curverad def measure_car_pos(binary_warped, left_fit, right_fit): xm_per_pix = 3.7/700 car_position = binary_warped.shape[1]/2 height = binary_warped.shape[0] left_fit_x = left_fit[0]*height**2 + left_fit[1]*height + left_fit[2] right_fit_x = right_fit[0]*height**2 + right_fit[1]*height + right_fit[2] lane_center_position = (right_fit_x + left_fit_x) /2 center_dist = (car_position - lane_center_position) * xm_per_pix return center_dist left_curverad, right_curverad = measure_curvature(combined, left_lane_inds2, right_lane_inds2) print(left_curverad, 'm', right_curverad, 'm')car_pos = measure_car_pos(combined, left_fit2, right_fit2)
print(car_pos, 'm')
В случае тестового изображения:
- Кривизна вправо: 1613,87430742 м
- Кривизна слева: 724.003885913 м
- Положение кабины: 0.170246440697 м
Мы можем заметить, что правая кривизна и левая кривизна - это разные числа, потому что на предыдущих изображениях мы видели, что левая и правая линии не полностью параллельны (в данном случае). Во второй части мы используем среднее значение.
7. Рисование линий на исходном изображении
И наконец! Рисуем результаты на исходном изображении:
def draw_lines(original_img, binary_img, left_fit, right_fit, Minv): ploty = np.linspace(0, binary_img.shape[0]-1, binary_img.shape[0]) # Create an image to draw the lines on warp_zero = np.zeros_like(binary_img).astype(np.uint8) color_warp = np.dstack((warp_zero, warp_zero, warp_zero)) left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2] right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2] # Recast the x and y points into usable format for cv2.fillPoly() pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))]) pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))]) pts = np.hstack((pts_left, pts_right)) # Draw the lane onto the warped blank image cv2.fillPoly(color_warp, np.int_([pts]), (0,255, 0)) # Warp the blank back to original image space using inverse perspective matrix (Minv) newwarp = cv2.warpPerspective(color_warp, Minv, (original_img.shape[1], original_img.shape[0])) # Combine the result with the original image result = cv2.addWeighted(original_img, 1, newwarp, 0.3, 0) return result new_img = draw_lines(original_images[5], combined, left_fit, right_fit, Minvs[5]) plt.imshow(new_img)
И, конечно же, я также нарисовал кривизну и данные о положении машины:
def draw_curvature_data(img, curv_rad, car_pos): font = cv2.FONT_HERSHEY_DUPLEX text = 'Curve radius: ' + '{:02.2f}'.format(curv_rad/1000) + 'Km' cv2.putText(img, text, (30,70), font, 1.5, (0,255,0), 2, cv2.LINE_AA) text = 'Car pos. from center: ' + '{:02.3f}'.format(car_pos) + 'm' cv2.putText(img, text, (30,120), font, 1.5, (0,255,0), 2, cv2.LINE_AA) return img final_img = draw_curvature_data(new_img, (left_curverad + right_curverad)/2, car_pos) plt.imshow(final_img)
Конвейер для видео
Пришло время протестировать предыдущий конвейер на видео.
Я определил класс Line
для хранения данных строк вдоль видео.
class Line(): def __init__(self): # was the line detected in the last iteration? self.detected = False #polynomial coefficients averaged over the last n iterations self.best_fit = None #polynomial coefficients for the most recent fit self.current_fit = [] #difference in fit coefficients between last and new fits self.diffs = np.array([0,0,0], dtype='float') def add_best_fit(self, lane_fit, lane_inds): if lane_fit is not None: if self.best_fit is not None: self.diffs = abs(lane_fit - self.best_fit) if (self.diffs[0] > 0.001 or self.diffs[1] > 1.0 or self.diffs[2] > 100.) and len(self.current_fit) > 0: self.detected = False else: self.detected = True self.px_count = np.count_nonzero(lane_inds) self.current_fit.append(lane_fit) if len(self.current_fit) > 5: self.current_fit = self.current_fit[len(self.current_fit)-5:] self.best_fit = lane_fit else: self.detected = False if len(self.current_fit) > 0: self.current_fit = self.current_fit[:len(self.current_fit)-1] self.best_fit = np.average(self.current_fit, axis=0)
add_best_fit
используется для обновления данных внутри класса. В каждом кадре, если линия была найдена, мы проверяем, что она близка к предыдущему лучшему совпадению, который является средним из предыдущих 5 совпадений.
Если обнаруженная линия не является подходящей мерой, мы удаляем ее, а если она близка к другим, мы добавляем ее в массив current_fit
, который затем будет использоваться для определения нового best_fit
.
Если обнаруженная строка является первой (1-й кадр), мы просто добавили как best_fit
Затем я создал метод process_img
, который будет обрабатывать каждый кадр видео.
def process_img(img): original_img = np.copy(img) undist = cv2.undistort(original_img, mtx, dist, None, mtx) warped_image, M, Minv = warp_image(undist) binary_img = combined_thresh(warped_image) if not left_line.detected or not right_line.detected: left_fit, right_fit, left_lane_inds, right_lane_inds, out_img = sliding_window_polyfit(binary_img) else: out_img, left_fit, right_fit, left_lane_inds, right_lane_inds = skip_windows_step(binary_img, left_line.best_fit, right_line.best_fit) left_line.add_best_fit(left_fit, left_lane_inds) right_line.add_best_fit(right_fit, right_lane_inds) if left_line.best_fit is not None and right_line.best_fit is not None: new_img = draw_lines(original_img, binary_img, left_line.best_fit, right_line.best_fit, Minv) left_curverad, right_curverad = measure_curvature(binary_img, left_lane_inds, right_lane_inds) car_pos = measure_car_pos(binary_img, left_line.best_fit, right_line.best_fit) final_img = draw_curvature_data(new_img, (left_curverad + right_curverad)/2, car_pos) else: new_img = original_img return new_img
В этом методе в основном я проверяю, был ли найден best_fit
. Если нет, используйте sliding_window_polyfit
. Но, если это правда, используйте skip_windows_step
. Используя эти данные, он обновляет left_line
и right_line
методом add_best_fit
.
Наконец, он рисует только линии, кривизну и данные о положении автомобиля, если есть best_fit
, и возвращает изображение.
Я использовал предыдущий код с project_video.mp4
следующим образом:
left_line = Line() right_line = Line()
video_output1 = 'project_video_output.mp4' video_input1 = VideoFileClip('project_video.mp4') processed_video = video_input1.fl_image(process_img) %time processed_video.write_videofile(video_output1, audio=False)
Вывод
Сравнивая этот проект с первым Проектом« Поиск линий переулка , это намного лучший способ выполнить работу. Однако, когда я протестировал свой код с помощью видеороликов о задачах, я увидел следующие проблемы:
- Сильные изменения цвета (линии, параллельные линиям полос движения) внутри пространства полос полос движения являются проблемой, потому что она определяет их как линии полос движения. Чтобы исправить это, я думаю, что я мог бы добавить условие, которое разрешает только строки, разделенные ~ 300 пикселей и не меньше.
- Работа только с цветовым пространством HLS может быть проблемой в некоторых случаях, я думаю, что было бы лучше объединить его с другим цветовым пространством, таким как RGB или LAB (я читал, что это может работать лучше).
- Также было бы неплохо добавить условие, которое допускает только почти параллельные линии, и попытаться улучшить трансформацию восприятия.
- Для действительно крутых поворотов действительно сложно найти полосу движения. Возможным решением может быть использование переменной области для преобразования перспективы, которая изменяется в зависимости от предыдущего расчета кривизны.
Как всегда, мне это очень понравилось, и я уже хочу посмотреть, что будет дальше.