├── main.py └── readme.md /main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import glob 4 | import os 5 | import pickle 6 | import matplotlib.pyplot as plt 7 | import pylab 8 | import time 9 | import imageio 10 | from scipy.misc import imread, imresize, imsave 11 | 12 | out_examples = 0 13 | MOV_AVG_LENGTH = 5 14 | 15 | 16 | def main(): 17 | 18 | # ------------------------ Camera Calibration ------------------------ 19 | # As calibration may take some time, save calibration data into pickle file to speed up testing 20 | if not os.path.exists('calibration.p'): 21 | # Read all jpg files from calibration image folder 22 | images = glob.glob('camera_cal/*.jpg') 23 | 24 | with open('calibration.p', mode='wb') as f: 25 | ret, mtx, dist, rvecs, tvecs = calibrate_camera(images, nx=9, ny=6) 26 | pickle.dump([ret, mtx, dist, rvecs, tvecs], f) 27 | f.close() 28 | else: 29 | with open('calibration.p', mode='rb') as f: 30 | ret, mtx, dist, rvecs, tvecs = pickle.load(f) 31 | f.close() 32 | 33 | if out_examples: 34 | # output undistorted image to output_image 35 | to_calibrate = imread('camera_cal/calibration3.jpg') 36 | imsave('output_images/calibration3_calibrated.jpg', cv2.undistort(to_calibrate, mtx, dist, None, mtx)) 37 | 38 | vid = imageio.get_reader('project_video.mp4', 'ffmpeg') 39 | 40 | for i, img in enumerate(vid): 41 | 42 | t_dist0 = time.time() 43 | t_fps0 = t_dist0 44 | img = cv2.undistort(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), mtx, dist, None, mtx) 45 | t_dist = time.time() - t_dist0 46 | 47 | 48 | # --------------------------- Binary Thresholding ---------------------------- 49 | # 50 | # if out_examples: 51 | # test_images = glob.glob('test_images/*.jpg') 52 | # plt.figure(figsize=(14, 10)) 53 | # for i, img in enumerate(test_images): 54 | # img_b = image_binary(cv2.undistort(cv2.imread(img), mtx, dist, None, mtx)) 55 | # plt.subplot(3, 3, i + 1) 56 | # plt.axis('off') 57 | # plt.title('%s' % str(img)) 58 | # plt.imshow(img_b, cmap='gray') 59 | # plt.show() 60 | 61 | t_bin0 = time.time() 62 | img_b = image_binary(img) 63 | t_bin = time.time() - t_bin0 64 | 65 | # ---------------------------- Perspective Transform -------------------------- 66 | 67 | t_warp0 = time.time() 68 | #src = [585, 457], [700, 457], [1110, img_b.shape[0]], [220, img_b.shape[0]] 69 | 70 | line_dst_offset = 200 71 | src = [595, 452], \ 72 | [685, 452], \ 73 | [1110, img_b.shape[0]], \ 74 | [220, img_b.shape[0]] 75 | 76 | dst = [src[3][0] + line_dst_offset, 0], \ 77 | [src[2][0] - line_dst_offset, 0], \ 78 | [src[2][0] - line_dst_offset, src[2][1]], \ 79 | [src[3][0] + line_dst_offset, src[3][1]] 80 | 81 | img_w = warp(img_b, src, dst) 82 | t_warp = time.time() - t_warp0 83 | 84 | if out_examples: 85 | # Count from mid frame beyond 86 | histogram = np.sum(img_w[int(img_w.shape[0] / 2):, :], axis=0) 87 | plt.plot(histogram) 88 | plt.savefig('histogram.jpg') 89 | plt.close() 90 | 91 | plt.figure(figsize=(21, 15)) 92 | for i, img in enumerate([img, img_b, img_w, imread('histogram.jpg')]): 93 | plt.subplot(2, 2, i + 1) 94 | plt.imshow(img, cmap='gray') 95 | if i == 3: 96 | plt.axis('off') 97 | plt.show() 98 | 99 | 100 | t_fit0 = time.time() 101 | try: 102 | left_fit, right_fit = fit_from_lines(left_fit, right_fit, img_w) 103 | 104 | mov_avg_left = np.append(mov_avg_left,np.array([left_fit]), axis=0) 105 | mov_avg_right = np.append(mov_avg_right,np.array([right_fit]), axis=0) 106 | 107 | except: 108 | left_fit, right_fit = sliding_windown(img_w) 109 | 110 | mov_avg_left = np.array([left_fit]) 111 | mov_avg_right = np.array([right_fit]) 112 | 113 | left_fit = np.array([np.mean(mov_avg_left[::-1][:,0][0:MOV_AVG_LENGTH]), 114 | np.mean(mov_avg_left[::-1][:,1][0:MOV_AVG_LENGTH]), 115 | np.mean(mov_avg_left[::-1][:,2][0:MOV_AVG_LENGTH])]) 116 | right_fit = np.array([np.mean(mov_avg_right[::-1][:,0][0:MOV_AVG_LENGTH]), 117 | np.mean(mov_avg_right[::-1][:,1][0:MOV_AVG_LENGTH]), 118 | np.mean(mov_avg_right[::-1][:,2][0:MOV_AVG_LENGTH])]) 119 | 120 | if mov_avg_left.shape[0] > 1000: 121 | mov_avg_left = mov_avg_left[0:MOV_AVG_LENGTH] 122 | if mov_avg_right.shape[0] > 1000: 123 | mov_avg_right = mov_avg_right[0:MOV_AVG_LENGTH] 124 | 125 | 126 | t_fit = time.time() - t_fit0 127 | 128 | t_draw0 = time.time() 129 | final = draw_lines(img, img_w, left_fit, right_fit, perspective=[src,dst]) 130 | t_draw = time.time() - t_draw0 131 | 132 | # print('fps: %d' % int((1./(t1-t0)))) 133 | print('undist: %f [ms] | bin: %f [ms]| warp: %f [ms]| fit: %f [ms]| draw: %f [ms] | fps %f' 134 | % (t_dist * 1000, t_bin * 1000, t_warp * 1000, t_fit * 1000, t_draw * 1000, 1./(time.time() - t_fps0))) 135 | cv2.imshow('final', final) 136 | 137 | if cv2.waitKey(1) & 0xFF == ord('q'): 138 | break 139 | 140 | 141 | def calibrate_camera(image_files, nx, ny): 142 | objpoints = [] 143 | imgpoints = [] 144 | 145 | objp = np.zeros(shape=(nx * ny, 3), dtype=np.float32) 146 | objp[:, :2] = np.mgrid[0:nx, 0:ny].T.reshape(-1, 2) 147 | 148 | for i in image_files: 149 | img = cv2.imread(i) 150 | if img.shape[0] != 720: 151 | img = cv2.resize(img,(1280, 720)) 152 | cv2.imshow('image',img) 153 | 154 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 155 | ret, corners = cv2.findChessboardCorners(gray, (nx, ny)) 156 | 157 | if ret: 158 | print("Calibrated!") 159 | imgpoints.append(corners) 160 | objpoints.append(objp) 161 | 162 | return cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) 163 | 164 | 165 | def image_binary(img, sobel_kernel=7, mag_thresh=(3, 255), s_thresh=(170, 255)): 166 | # --------------------------- Binary Thresholding ---------------------------- 167 | # Binary Thresholding is an intermediate step to improve lane line perception 168 | # it includes image transformation to gray scale to apply sobel transform and 169 | # binary slicing to output 0,1 type images according to pre-defined threshold. 170 | # 171 | # Also it's performed RGB to HSV transformation to get S information which in- 172 | # tensifies lane line detection. 173 | # 174 | # The output is a binary image combined with best of both S transform and mag- 175 | # nitude thresholding. 176 | 177 | hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS) 178 | gray = hls[:, :, 1] 179 | s_channel = hls[:, :, 2] 180 | 181 | 182 | # Binary matrixes creation 183 | sobel_binary = np.zeros(shape=gray.shape, dtype=bool) 184 | s_binary = sobel_binary 185 | combined_binary = s_binary.astype(np.float32) 186 | 187 | # Sobel Transform 188 | sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel) 189 | sobely = 0 #cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel) 190 | 191 | sobel_abs = np.abs(sobelx**2 + sobely**2) 192 | sobel_abs = np.uint8(255 * sobel_abs / np.max(sobel_abs)) 193 | 194 | sobel_binary[(sobel_abs > mag_thresh[0]) & (sobel_abs <= mag_thresh[1])] = 1 195 | 196 | # Threshold color channel 197 | s_binary[(s_channel >= s_thresh[0]) & (s_channel <= s_thresh[1])] = 1 198 | 199 | # Combine the two binary thresholds 200 | 201 | combined_binary[(s_binary == 1) | (sobel_binary == 1)] = 1 202 | combined_binary = np.uint8(255 * combined_binary / np.max(combined_binary)) 203 | 204 | #plt.imshow(combined_binary, cmap='gray') 205 | #plt.show() 206 | 207 | # ---------------- MASKED IMAGE -------------------- 208 | offset = 100 209 | mask_polyg = np.array([[(0 + offset, img.shape[0]), 210 | (img.shape[1] / 2.5, img.shape[0] / 1.65), 211 | (img.shape[1] / 1.8, img.shape[0] / 1.65), 212 | (img.shape[1], img.shape[0])]], 213 | dtype=np.int) 214 | 215 | # mask_polyg = np.concatenate((mask_polyg, mask_polyg, mask_polyg)) 216 | 217 | # Next we'll create a masked edges image using cv2.fillPoly() 218 | mask_img = np.zeros_like(combined_binary) 219 | ignore_mask_color = 255 220 | 221 | # This time we are defining a four sided polygon to mask 222 | # Applying polygon 223 | cv2.fillPoly(mask_img, mask_polyg, ignore_mask_color) 224 | masked_edges = cv2.bitwise_and(combined_binary, mask_img) 225 | 226 | return masked_edges 227 | 228 | 229 | def warp(img, src, dst): 230 | 231 | src = np.float32([src]) 232 | dst = np.float32([dst]) 233 | 234 | return cv2.warpPerspective(img, cv2.getPerspectiveTransform(src, dst), 235 | dsize=img.shape[0:2][::-1], flags=cv2.INTER_LINEAR) 236 | 237 | 238 | def sliding_windown(img_w): 239 | 240 | histogram = np.sum(img_w[int(img_w.shape[0] / 2):, :], axis=0) 241 | # Create an output image to draw on and visualize the result 242 | out_img = np.dstack((img_w, img_w, img_w)) * 255 243 | # Find the peak of the left and right halves of the histogram 244 | # These will be the starting point for the left and right lines 245 | midpoint = np.int(histogram.shape[0] / 2) 246 | leftx_base = np.argmax(histogram[:midpoint]) 247 | rightx_base = np.argmax(histogram[midpoint:]) + midpoint 248 | 249 | # Choose the number of sliding windows 250 | nwindows = 9 251 | # Set height of windows 252 | window_height = np.int(img_w.shape[0] / nwindows) 253 | # Identify the x and y positions of all nonzero pixels in the image 254 | nonzero = img_w.nonzero() 255 | nonzeroy = np.array(nonzero[0]) 256 | nonzerox = np.array(nonzero[1]) 257 | # Current positions to be updated for each window 258 | leftx_current = leftx_base 259 | rightx_current = rightx_base 260 | # Set the width of the windows +/- margin 261 | margin = 100 262 | # Set minimum number of pixels found to recenter window 263 | minpix = 50 264 | # Create empty lists to receive left and right lane pixel indices 265 | left_lane_inds = [] 266 | right_lane_inds = [] 267 | 268 | # Step through the windows one by one 269 | for window in range(nwindows): 270 | # Identify window boundaries in x and y (and right and left) 271 | win_y_low = img_w.shape[0] - (window + 1) * window_height 272 | win_y_high = img_w.shape[0] - window * window_height 273 | win_xleft_low = leftx_current - margin 274 | win_xleft_high = leftx_current + margin 275 | win_xright_low = rightx_current - margin 276 | win_xright_high = rightx_current + margin 277 | # Draw the windows on the visualization image 278 | cv2.rectangle(out_img, (win_xleft_low, win_y_low), (win_xleft_high, win_y_high), (0, 255, 0), 2) 279 | cv2.rectangle(out_img, (win_xright_low, win_y_low), (win_xright_high, win_y_high), (0, 255, 0), 2) 280 | # Identify the nonzero pixels in x and y within the window 281 | good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xleft_low) & ( 282 | nonzerox < win_xleft_high)).nonzero()[0] 283 | good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xright_low) & ( 284 | nonzerox < win_xright_high)).nonzero()[0] 285 | # Append these indices to the lists 286 | left_lane_inds.append(good_left_inds) 287 | right_lane_inds.append(good_right_inds) 288 | # If you found > minpix pixels, recenter next window on their mean position 289 | if len(good_left_inds) > minpix: 290 | leftx_current = np.int(np.mean(nonzerox[good_left_inds])) 291 | if len(good_right_inds) > minpix: 292 | rightx_current = np.int(np.mean(nonzerox[good_right_inds])) 293 | 294 | # Concatenate the arrays of indices 295 | left_lane_inds = np.concatenate(left_lane_inds) 296 | right_lane_inds = np.concatenate(right_lane_inds) 297 | 298 | # Extract left and right line pixel positions 299 | leftx = nonzerox[left_lane_inds] 300 | lefty = nonzeroy[left_lane_inds] 301 | rightx = nonzerox[right_lane_inds] 302 | righty = nonzeroy[right_lane_inds] 303 | 304 | # Fit a second order polynomial to each 305 | left_fit = np.polyfit(lefty, leftx, 2) 306 | right_fit = np.polyfit(righty, rightx, 2) 307 | 308 | # Generate x and y values for plotting 309 | # ploty = np.linspace(0, img_w.shape[0] - 1, img_w.shape[0]) 310 | # left_fitx = left_fit[0] * ploty ** 2 + left_fit[1] * ploty + left_fit[2] 311 | # right_fitx = right_fit[0] * ploty ** 2 + right_fit[1] * ploty + right_fit[2] 312 | # 313 | # out_img[nonzeroy[left_lane_inds], nonzerox[left_lane_inds]] = [255, 0, 0] 314 | # out_img[nonzeroy[right_lane_inds], nonzerox[right_lane_inds]] = [0, 0, 255] 315 | # plt.imshow(out_img) 316 | # plt.plot(left_fitx, ploty, color='yellow') 317 | # plt.plot(right_fitx, ploty, color='yellow') 318 | # plt.xlim(0, 1280) 319 | # plt.ylim(720, 0) 320 | 321 | return left_fit, right_fit 322 | 323 | 324 | def fit_from_lines(left_fit, right_fit, img_w): 325 | # Assume you now have a new warped binary image 326 | # from the next frame of video (also called "binary_warped") 327 | # It's now much easier to find line pixels! 328 | nonzero = img_w.nonzero() 329 | nonzeroy = np.array(nonzero[0]) 330 | nonzerox = np.array(nonzero[1]) 331 | margin = 100 332 | left_lane_inds = ((nonzerox > (left_fit[0] * (nonzeroy ** 2) + left_fit[1] * nonzeroy + left_fit[2] - margin)) & ( 333 | nonzerox < (left_fit[0] * (nonzeroy ** 2) + left_fit[1] * nonzeroy + left_fit[2] + margin))) 334 | right_lane_inds = ( 335 | (nonzerox > (right_fit[0] * (nonzeroy ** 2) + right_fit[1] * nonzeroy + right_fit[2] - margin)) & ( 336 | nonzerox < (right_fit[0] * (nonzeroy ** 2) + right_fit[1] * nonzeroy + right_fit[2] + margin))) 337 | 338 | # Again, extract left and right line pixel positions 339 | leftx = nonzerox[left_lane_inds] 340 | lefty = nonzeroy[left_lane_inds] 341 | rightx = nonzerox[right_lane_inds] 342 | righty = nonzeroy[right_lane_inds] 343 | # Fit a second order polynomial to each 344 | left_fit = np.polyfit(lefty, leftx, 2) 345 | right_fit = np.polyfit(righty, rightx, 2) 346 | 347 | return left_fit, right_fit 348 | 349 | 350 | def draw_lines(img, img_w, left_fit, right_fit, perspective): 351 | # Create an image to draw the lines on 352 | warp_zero = np.zeros_like(img_w).astype(np.uint8) 353 | color_warp = np.dstack((warp_zero, warp_zero, warp_zero)) 354 | #color_warp_center = np.dstack((warp_zero, warp_zero, warp_zero)) 355 | 356 | ploty = np.linspace(0, img.shape[0] - 1, img.shape[0]) 357 | 358 | left_fitx = left_fit[0] * ploty ** 2 + left_fit[1] * ploty + left_fit[2] 359 | right_fitx = right_fit[0] * ploty ** 2 + right_fit[1] * ploty + right_fit[2] 360 | 361 | # Recast the x and y points into usable format for cv2.fillPoly() 362 | pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))]) 363 | pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))]) 364 | pts = np.hstack((pts_left, pts_right)) 365 | 366 | # Draw the lane onto the warped blank image 367 | #cv2.fillPoly(color_warp_center, np.int_([pts]), (0, 255, 0)) 368 | cv2.fillPoly(color_warp, np.int_([pts]), (0, 255, 0)) 369 | 370 | # Warp the blank back to original image space using inverse perspective matrix (Minv) 371 | newwarp = warp(color_warp, perspective[1], perspective[0]) 372 | # Combine the result with the original image 373 | result = cv2.addWeighted(img, 1, newwarp, 0.2, 0) 374 | 375 | color_warp_lines = np.dstack((warp_zero, warp_zero, warp_zero)) 376 | cv2.polylines(color_warp_lines, np.int_([pts_right]), isClosed=False, color=(255, 255, 0), thickness=25) 377 | cv2.polylines(color_warp_lines, np.int_([pts_left]), isClosed=False, color=(0, 0, 255), thickness=25) 378 | newwarp_lines = warp(color_warp_lines, perspective[1], perspective[0]) 379 | 380 | result = cv2.addWeighted(result, 1, newwarp_lines, 1, 0) 381 | 382 | # ----- Radius Calculation ------ # 383 | 384 | img_height = img.shape[0] 385 | y_eval = img_height 386 | 387 | ym_per_pix = 30 / 720. # meters per pixel in y dimension 388 | xm_per_pix = 3.7 / 700 # meters per pixel in x dimension 389 | 390 | ploty = np.linspace(0, img_height - 1, img_height) 391 | # Fit new polynomials to x,y in world space 392 | left_fit_cr = np.polyfit(ploty * ym_per_pix, left_fitx * xm_per_pix, 2) 393 | right_fit_cr = np.polyfit(ploty * ym_per_pix, right_fitx * xm_per_pix, 2) 394 | 395 | # Calculate the new radii of curvature 396 | left_curverad = ((1 + (2 * left_fit_cr[0] * y_eval * ym_per_pix + left_fit_cr[1]) ** 2) ** 1.5) / np.absolute( 397 | 2 * left_fit_cr[0]) 398 | 399 | right_curverad = ( 400 | (1 + (2 * right_fit_cr[0] * y_eval * ym_per_pix + right_fit_cr[1]) ** 2) ** 1.5) / np.absolute( 401 | 2 * right_fit_cr[0]) 402 | 403 | radius = round((float(left_curverad) + float(right_curverad))/2.,2) 404 | 405 | # ----- Off Center Calculation ------ # 406 | 407 | lane_width = (right_fit[2] - left_fit[2]) * xm_per_pix 408 | center = (right_fit[2] - left_fit[2]) / 2 409 | off_left = (center - left_fit[2]) * xm_per_pix 410 | off_right = -(right_fit[2] - center) * xm_per_pix 411 | off_center = round((center - img.shape[0] / 2.) * xm_per_pix,2) 412 | 413 | # --- Print text on screen ------ # 414 | #if radius < 5000.0: 415 | text = "radius = %s [m]\noffcenter = %s [m]" % (str(radius), str(off_center)) 416 | #text = "radius = -- [m]\noffcenter = %s [m]" % (str(off_center)) 417 | 418 | for i, line in enumerate(text.split('\n')): 419 | i = 50 + 20 * i 420 | cv2.putText(result, line, (0,i), cv2.FONT_HERSHEY_DUPLEX, 0.5,(255,255,255),1,cv2.LINE_AA) 421 | return result 422 | 423 | 424 | 425 | if __name__ == '__main__': 426 | main() 427 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Advanced Lane Finding 2 | 3 | ## Goals 4 | 5 | * Compute the camera calibration matrix and distortion coefficients given a set of chessboard images. 6 | * Apply a distortion correction to raw images. 7 | * Use color transforms, gradients, etc., to create a thresholded binary image. 8 | * Apply a perspective transform to rectify binary image ("birds-eye view"). 9 | * Detect lane pixels and fit to find the lane boundary. 10 | * Determine the curvature of the lane and vehicle position with respect to center. 11 | * Warp the detected lane boundaries back onto the original image. 12 | * Output visual display of the lane boundaries and numerical estimation 13 | of lane curvature and vehicle position. 14 | 15 | | Raw Input | Final Output | 16 | | ------------- | ------------- | 17 | |  |  | 18 | 19 | 20 | ### Camera Calibration 21 | 22 | This is a fundamental step of the project, since without calibration, the image analisys may fall into uncorrect results. 23 | 24 | Camera calibration is performed by opencv in two steps: 25 | 26 | 1. Find Chess Board Corners 27 | * This function retutns radial and tangential transform parameters 28 | * [OpenCV Chess Board Corners](http://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html#drawchessboardcorners) 29 | 2. Camera Calibration 30 | * Calibrate Camera function takes as arguments the output of Chess Board Corners Function, plus object points values to return the Calibration Function. 31 | * [OpenCV Calibrate Camera](http://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html#calibratecamera) 32 | 33 | **Undistorted Image Example:** 34 | 35 | | Before | After | 36 | | ------------- | ------------- | 37 | |  |  | 38 | 39 | ***Find in the code: main.py:44*** 40 | 41 |