Advanced Car Lane Detection === 由於之前做的[簡易版車道偵測](https://hackmd.io/@yoch/ByVdEVZP_)容易不准,且現在市面上所兜售的車款幾乎都可以搭配車道維持系統或是車道偏移警示系統...等,所以藉此機會來加強車道偵測的效能,甚至能達到偵測汽車是否有偏移的問題。當然這網路上有很多人做過,本次也算是藉由這次的 project 來自我學習~ ### 架構 ```flow st=>start: Camera Calibration e=>end: Draw window on Overhead Image & Draw Lane on undistort Image op=>operation: Read Image op2=>operation: Undistort & Overhead op3=>operation: Score pixel Image op4=>operation: Window Sliding & Window Update op5=>operation: Fit Lane st->op->op2->op3->op4->op5 op5->e ``` ### 方法 1. **Camera calibtaion**: 那為什麼要做相機校正呢? 因為拍出來的影像經常都會有形變(distortion)的問題,常見的形變有桶狀形變(barrel distortion)與枕狀形變(pincushion distortion),因此需要把變形後的影像給拉回矩形。 ![](https://i.imgur.com/nJYONoI.png) ![](https://i.imgur.com/BL1GIyT.png) 左邊原始影像、右邊校正後影像,看起來有無校正好像都沒差齁~ 依自己要求可以選擇要不要做校正這個步驟 ![](https://i.imgur.com/OQCefpK.jpg) 以下是校正的 Class,主要 method 為 **calibrate**,一開始初始化 class 的時候就會先進行相機校正,那得到的 matrix 就能應用在後續要讀進來的影像上,而我們也在 class 內先把 **overhead** 與 **inverse overhead** 變數先建置起來,後續轉換也會用到。 ```python= class DashboardCamear: """ 相機校正 """ def __init__(self, chessboard_img_files, chessboard_size, lane_shape): example_img = cv2.imread(chessboard_img_files[0]) self.img_size = example_img.shape[:2] self.img_height = self.img_size[0] self.img_width = self.img_size[1] self.camera_matrix, self.dist_coeffs = self.calibrate(chessboard_img_files, chessboard_size) ## 校正 top_left, top_right, bottom_left, bottom_right = lane_shape source = np.float32([top_left, top_right, bottom_right, bottom_left]) destination = np.float32([(bottom_left[0], 0), (bottom_right[0], 0), (bottom_right[0], self.img_height - 1), (bottom_left[0], self.img_height - 1)]) self.overhead_transform = cv2.getPerspectiveTransform(source, destination) self.inverse_overhead_transform = cv2.getPerspectiveTransform(destination, source) def calibrate(self, chessboard_img_files, chessboard_size): """ 影像校正 """ chess_rows, chess_cols = chessboard_size corners = np.zeros((chess_cols*chess_rows, 3), np.float32) corners[:,:2] = np.mgrid[0:chess_cols, 0:chess_rows].T.reshape(-1,2) objpoints = [] # 3d point in real world space imgpoints = [] # 2d points in image plane. for frame in chessboard_img_files: img = cv2.imread(frame) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, chess_corners = cv2.findChessboardCorners(gray, (chess_rows, chess_cols), None) if ret == True: objpoints.append(corners) imgpoints.append(chess_corners) sucess, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, self.img_size, None, None) if not sucess: raise Exception('Camera calibration unsucessful') return camera_matrix, dist_coeffs def undistort(self, image): return cv2.undistort(image, self.camera_matrix, self.dist_coeffs, None, self.camera_matrix) def warp_to_overhead(self, image): return cv2.warpPerspective(image, self.overhead_transform, dsize=(self.img_width, self.img_height)) def warp_to_dashboard(self, image): return cv2.warpPerspective(image, self.inverse_overhead_transform, dsize=(self.img_width, self.img_height)) ``` 2. **Over Head** 這邊透過影像幾何變換將選定區域的影像變為俯視圖,當然可以去看看[OpenCV - Geometric Transformations of Images](https://docs.opencv.org/master/dd/d52/tutorial_js_geometric_transformations.html)有很多範例可以嘗試。 那下圖就是將需要的地方變成俯視圖,藉由上面的 overhead 方法就能實現。 `self.overhead_img = self.camera.warp_to_overhead(self.undistort_img)` ![](https://i.imgur.com/HdCZAcV.jpg =500x) 3. **Score Pixel Image** 這部分就是將影像轉為不同色彩空間,以下為 LAB、HSV、HSL 各取不同通道而產生的圖,雖有些通道效果不好但相加就能有互補的效果 ![](https://i.imgur.com/XN20yP0.png) ```python= def score_pixel(self, img): settings = [{'name': 'lab_b', 'cspace': 'LAB', 'channel': 2, 'clipLimit': 2.0, 'threshold': 150}, {'name': 'value', 'cspace': 'HSV', 'channel': 2, 'clipLimit': 6.0, 'threshold': 220}, {'name': 'lightness', 'cspace': 'HLS', 'channel': 1, 'clipLimit': 2.0, 'threshold': 210}] scores = np.zeros((self.height, self.width)) for params in settings: # color_t = getattr(cv2, 'COLOR_BGR2{}'.format(params['cspace'])) color_t = eval('cv2.COLOR_BGR2{}'.format(params['cspace'])) gray = cv2.cvtColor(img, color_t)[:, :, params['channel']] # Normalize regions of the image using CLAHE clahe = cv2.createCLAHE(params['clipLimit'], tileGridSize=(8, 8)) norm_img = clahe.apply(gray) ret, binary = cv2.threshold(norm_img, params['threshold'], 1, cv2.THRESH_BINARY) scores += binary return cv2.normalize(scores, None, 0, 255, cv2.NORM_MINMAX) ``` 4. **Window Slide & Window Update** 這次藉由 window 來抓取車道線的位置,首先分為左邊車道線與右邊車道線,將影像剖半我這邊是先將左邊 window 初始位置設在 1/4 寬度,而右邊 window 則是設在 3/4 位置,如下圖 ![](https://i.imgur.com/U72FXHH.jpg) 那有了初始的 window 就能藉由這些 window 去判斷該區域內是否有數值,再進行 window 位置更新。那一開始的方法是偵測 window 範圍內有無數值,沒有就把 window 剔除掉就導致下面狀況,右邊的車道線是一段一段的所以就很容易 window 被砍掉後就不見了... ```python= def update(self, pixel_scores, x_search_range): """ window 位置更新 """ x_search_range = (max(0, int(x_search_range[0])), min(int(x_search_range[1]), self.img_w)) x_offset = x_search_range[0] search_region = pixel_scores[self.y_begin:self.y_end, x_offset:x_search_range[1]] column_scores = gaussian_filter(np.sum(search_region, axis=0), sigma=5) if max(column_scores) > 0: ## 判斷該區域是否有影像 self.detected = True x_measure = np.argmax(column_scores) + x_offset ## window 量測後的 x 位置 ## 估測 window x 位置 self.filter.update(x_measure) ## kalman filter update self.x_position = self.filter.get_position() else: self.detected = False self.drop = True ``` left ![](https://i.imgur.com/NoKCMly.gif =500x) right ![](https://i.imgur.com/JBrZMdG.gif =500x) 上面是程式邏輯有寫錯導致的,在第 17 行加入 drop = Flase 就不會導致整張影像的 window 都被移除掉 ```python= def update(self, pixel_scores, x_search_range): """ window 位置更新 """ x_search_range = (max(0, int(x_search_range[0])), min(int(x_search_range[1]), self.img_w)) x_offset = x_search_range[0] search_region = pixel_scores[self.y_begin:self.y_end, x_offset:x_search_range[1]] column_scores = gaussian_filter(np.sum(search_region, axis=0), sigma=5) # debug_show(column_scores, 'curve') if max(column_scores) > 0: ## 判斷該區域是否有影像 self.detected = True x_measure = np.argmax(column_scores) + x_offset ## window 量測後的 x 位置 # self.calculate_x_begin(column_scores, x_offset) ## 估測 window x 位置 self.filter.update(x_measure) ## kalman filter update self.x_position = self.filter.get_position() self.drop = False else: self.detected = False self.drop = True ``` left & right ![](https://i.imgur.com/nIAdCfC.gif =500x) Update 部分採用 Kalman Filter,這邊很推薦 [Kalman and Bayesian Filters in Python](https://github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python),裡面用到的 library 也是這作者寫的,內有詳細的教學程式碼,超佛心~~ 這裡就簡單的介紹卡爾曼濾波器: 通過對物體位置的觀察序列(可能有偏差)預測出物體的位置坐標及速度,其主要應用在 tracking 方面,這邊就採用上面 github 作者的圖如下。可以看到在這些位移的資料點位,它能去估計出整體位移的曲線,當然在現實生活中一定有很多的雜訊會干擾 sensor 感應出來的資訊,所以在雜訊干擾的情況下該如何得出一個最佳的路線就可以採用卡爾曼濾波器。 ![](https://i.imgur.com/iQcTsM7.png) >https://github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/blob/master/08-Designing-Kalman-Filters.ipynb 5. **Fit Lane** 將上面得到的 window 位置應用多項式去擬合曲線,要用多少項隨意,本次採用: $$y=ax^2+bx+c$$ 擬合出的曲線如下,從圖中可看見右邊因為車道線一段一段的,容易導致偵測抖動 ![](https://i.imgur.com/ZmSdu4n.gif =500x) ### Demo ![](https://imgur.com/I93XjgY.gif =500x) ## 問題/缺陷 計算量有點龐大,我的 mac pro 跑到發燙哩,用 real time 去偵測的話會發現影片的 fps 有下降的趨勢,而網路上的幾乎都是整個 video 下去偵測後再製作成新的 video 所以看起來速度很正常。 ## 參考 >Advanced Lane Detection:https://github.com/georgesung/advanced_lane_detection >Highway Lane Tracker:https://github.com/peter-moran/highway-lane-tracker