尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
作者 | Adam Geitgey
譯者 | 風車雲馬
整理 | Jane
出品 | AI科技大本營(ID:rgznai100)
【導語】今天這篇文章的選題非常貼近生活。營長生活在北京,深知開車出門最怕的就是堵車和找不到停車位。記得冬至那個周末,幾個小夥伴滑雪回來找了一家餃子館吃餃子,結果七拐八拐,好不容易才找到一個停車位。看到這篇技術文章,馬上就想要學習一下,分享給大家,希望有助於解決大家這個痛點問題,春節出行沒準就可以用得上了。
作者通過相機結合深度學習算法,基於 Python 語言建立一個高精度的停車位的通知系統,每當有新停車位時就會發簡訊提醒我。聽起來好像很複雜,真的方便實用嗎?但實際上所使用的工具都是現成的,只要將這些工具進行有機的組合,就可以快速、簡便的做到。
下面我們就開始學習整個工程流程:
分解問題
解決一個複雜的問題,首先第一步是要把問題分解成幾個簡單子任務。然後,針對每個子任務,運用機器學習中不同的方法來分別解決每個問題。最後把這些子任務貫穿起來,形成整套解決方案。
下面是檢測開放停車位的流程圖:
輸入:普通錄影頭采集的視頻流
有了輸入數據後,接下來我們需要知道圖像的哪一部分是停車位,而且停車位是沒有人使用的。
第一步:檢測視頻幀中所有可能的停車位。
第二步:檢測每一幀視頻中的所有車輛。可以跟蹤每輛車從一幀到另一幀的運動。
第三步:確定目前有哪些停車位被占用,哪些沒有被占用。這需要結合第一步和第二步的結果。
最後一步:當停車位變成可用時,系統發出通知。
其實可以使用多種不同的方法來完成這些步驟。不同的方法將具有不同的優勢和缺點。接下來具體來看:
一、探測停車位
相機視圖如下圖所示:
需要掃描圖像,並得到有效停車區域,如下面黃色標識出來的位置:
一個懶辦法是程序寫死每個停車場的位置,而不是自動檢測停車場。但是如果移動錄影機,探測不同街道上的停車位,就必須再次手動定位停車位置。這樣看來這個方法一點都不好,還是要採用自動檢測停車位的方法。
其中一個想法是識別停車計時器並假設每個計時器旁邊都有一個停車位:
但是這種方法也有一些問題。首先,並不是每個停車位有一個停車計時器,而且我們最想先找到免費停車位。第二,知道停車計時器的位置還不能告訴我們確切的停車位置點。
另一個想法是搭建一個目標檢測模型,找到在馬路上畫的停車位記號,就像下圖標識出來的:
這種方法也有兩個難點。首先,從遠處看,停車位線的標誌很小,很難看到,增加了識別難度;其次,道路上還有各種交通標記線,比如車道線、人行道斑馬線,這也給識別增加了難度。
或許還可以換個思維方式,回避一些技術挑戰。重新思考停車位到底是什麼呢?無非是一個車需要停放一定時間的位置。所以我們可能根本不需要檢測停車位,只要檢測出長時間不動的車,並假設它們的位置就是停車位。
所以,如果我們能檢測出汽車,並找出哪些車在視頻幀之間沒有移動,就可以推斷停車位的位置。
二、在圖像中檢測汽車
在視頻中檢測車輛是一個經典的目標檢測問題。有很多機器學習方法可以做到。下面列出了一些最常見的目標檢測算法:
1、通過 HOG(梯度方向直方圖)目標檢測器檢測出所有的車。這種非深度學習方法運行起來相對較快,但它無法處理汽車在不同方向上的旋轉問題。
2、通過 CNN(卷積神經網路)目標檢測器檢測所有的車。這種方法是準確的,但是效率比較低,因為同一張圖像必須掃描多次,以檢測到所有的汽車。雖然它可以很容易地對不同旋轉方向的汽車定向,但是比 HOG 方法需要更多的訓練數據。
3、使用新的深度學習方法,如 Mask R-CNN,Faster R-CNN 或者 YOLO 算法,兼容準確性和運行效率,大大加快了檢測過程。一旦有大量的訓練數據,在 GPU 上運行也很快。
通常來說,我們希望選擇最簡單可行的算法和最少的訓練數據,而不是一定要用那些流行的新的算法。基於目前這種特殊場景下,Mask R- CNN 是一個不錯的選擇。
Mask R-CNN 架構就是在整個圖像中檢測對象,不使用滑動窗口的方式,所以運行速度很快。有了 GPU 處理器,我們能夠每秒處理多幀的高分辨率視頻,從中檢測到車輛。
Mask R-CNN 為我們提供了很多檢測到的信息。大多數目標檢測算法只返回每個對象的邊框。但是 Mask R-CNN 不僅會給我們每個對象的位置,也會給出一個對象的輪廓,像這樣:
為了訓練 Mask R-CNN 模型,我們需要很多這類檢測物體的圖片。可以花幾天的時間出去拍攝照片,不過已經存在一些汽車圖像的公共數據集。有一個很流行的數據集叫做COCO(Common Objects In Context的縮寫),它里面已經有超過 12000 張汽車的圖片。下面就是一個 COCO 數據集中的圖像:
這些數據可以很好的訓練 Mask R-CNN 模型,而且已經有很多人使用過 COCO數據集,並分享了訓練的結果。所以我們可以直接使用一些訓練好的模型,在本項目中使用 Matterport 的開源模型。
不僅能識別車輛,還能識別到交通燈和人。有趣的是,它把其中一棵樹識別成「potted plant」。對於圖像中檢測到的每個對象,我們從 MaskR-CNN 模型得出以下 4 點:
(1)不同對象的類別,COCO 模型可以識別出 80 種不同的物體,比如小轎車和卡車。
(2)目標識別的置信度,數字越大,說明模型識別對象的精準度越高。
(3)圖像中物體的邊界框,給定了 X/Y 像素的位置。
(4)位圖「mask」說明了邊框內哪些像素是對象的一部分,哪些不是。使用「mask」數據,我們也可以算出物體的輪廓。
下面是 Python 代碼,使用 Matterport 的 Mask R-CNN 的訓練模型和 OpenCV 來檢測汽車邊框:
1import os 2import numpy as np 3import cv2 4import mrcnn.config 5import mrcnn.utils 6from mrcnn.model import MaskRCNN 7from pathlib import Path 8 9 10# Configuration that will be used by the Mask-RCNN library 11class MaskRCNNConfig(mrcnn.config.Config): 12 NAME = "coco_pretrained_model_config" 13 IMAGES_PER_GPU = 1 14 GPU_COUNT = 1 15 NUM_CLASSES = 1 + 80 # COCO dataset has 80 classes + one background class 16 DETECTION_MIN_CONFIDENCE = 0.6 17 18 19# Filter a list of Mask R-CNN detection results to get only the detected cars / trucks 20def get_car_boxes(boxes, class_ids): 21 car_boxes = [] 22 23 for i, box in enumerate(boxes): 24 # If the detected object isn't a car / truck, skip it 25 if class_ids[i] in [3, 8, 6]: 26 car_boxes.append(box) 27 28 return np.array(car_boxes) 29 30 31# Root directory of the project 32ROOT_DIR = Path(".") 33 34# Directory to save logs and trained model 35MODEL_DIR = os.path.join(ROOT_DIR, "logs") 36 37# Local path to trained weights file 38COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5") 39 40# Download COCO trained weights from Releases if needed 41if not os.path.exists(COCO_MODEL_PATH): 42 mrcnn.utils.download_trained_weights(COCO_MODEL_PATH) 43 44# Directory of images to run detection on 45IMAGE_DIR = os.path.join(ROOT_DIR, "images") 46 47# Video file or camera to process - set this to 0 to use your webcam instead of a video file 48VIDEO_SOURCE = "test_images/parking.mp4" 49 50# Create a Mask-RCNN model in inference mode 51model = MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=MaskRCNNConfig()) 52 53# Load pre-trained model 54model.load_weights(COCO_MODEL_PATH, by_name=True) 55 56# Location of parking spaces 57parked_car_boxes = None 58 59# Load the video file we want to run detection on 60video_capture = cv2.VideoCapture(VIDEO_SOURCE) 61 62# Loop over each frame of video 63while video_capture.isOpened(): 64 success, frame = video_capture.read() 65 if not success: 66 break 67 68 # Convert the image from BGR color (which OpenCV uses) to RGB color 69 rgb_image = frame[:, :, ::-1] 70 71 # Run the image through the Mask R-CNN model to get results. 72 results = model.detect([rgb_image], verbose=0) 73 74 # Mask R-CNN assumes we are running detection on multiple images. 75 # We only passed in one image to detect, so only grab the first result. 76 r = results[0] 77 78 # The r variable will now have the results of detection: 79 # - r['rois'] are the bounding box of each detected object 80 # - r['class_ids'] are the class id (type) of each detected object 81 # - r['scores'] are the confidence scores for each detection 82 # - r['masks'] are the object masks for each detected object (which gives you the object outline) 83 84 # Filter the results to only grab the car / truck bounding boxes 85 car_boxes = get_car_boxes(r['rois'], r['class_ids']) 86 87 print("Cars found in frame of video:") 88 89 # Draw each box on the frame 90 for box in car_boxes: 91 print("Car: ", box) 92 93 y1, x1, y2, x2 = box 94 95 # Draw the box 96 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 1) 97 98 # Show the frame of video on the screen 99 cv2.imshow('Video', frame) 100 101 # Hit 'q' to quit 102 if cv2.waitKey(1) & 0xFF == ord('q'): 103 break 104 105# Clean up everything when finished 106video_capture.release() 107cv2.destroyAllWindows()
運行該腳本後,將會看到在圖像中識別到的汽車和邊框:
同時會得到檢測的每輛車的像素坐標:
這樣我們已經成功地在圖像中檢測到了汽車。接下來到了下一個步驟。
三、探測空車位
知道圖像中每輛車的像素位置後,通過觀察連續多幀視頻,可以很容易地算出哪幀里汽車沒有移動。但我們如何檢測到汽車何時離開停車位?經觀察,圖像中汽車的邊框部分有所重疊:
如果假設每個邊界框代表一個停車場空間,這個區域即使有車開走了,但是仍可能被另外汽車部分占據。因此我們需要一種方法來測量重疊,檢查出「大部分為空」的框。我們使用的度量方法稱為 Intersection Over Union(IoU)。通過計算兩個物體重疊的像素量,然後除以兩個物體所覆蓋的像素:
有了這個值,接下來就可以很容易確定一輛車是否在停車位。如果 IoU 測量值低,比如 0.15,表示汽車並沒有占據大部分的停車位空間。但是如果測量值很高,比如 0.6,就表示汽車占據了大部分的停車位,因此可以確定停車位已被占用。
IoU 是計算機視覺中常用的一種測量方法,提供了現成的代碼。Matterport 的Mask R-CNN 庫可以直接調用這個函數 mrcnn.utils.compute_overlaps()。假設我們有一個表示停車位邊界框的列表,要檢識別到的車輛是否在這些邊界內框很簡單,只需添加一兩行代碼:
1 # Filter the results to only grab the car / truck bounding boxes 2 car_boxes = get_car_boxes(r['rois'], r['class_ids']) 3 4 # See how much cars overlap with the known parking spaces 5 overlaps = mrcnn.utils.compute_overlaps(car_boxes, parking_areas) 6 7 print(overlaps)
結果顯示為:
在二維數組中,每一行表示一個停車位邊界框。同樣的,每一列表示停車場被汽車所覆蓋的程度。1.0 分意味著汽車完全占據了,而 0.02 這樣的低分數,意味著有重疊區域,但不會占據大部分空間。
要找到無人使用的停車位,只需要計算出這個數組。如果所有的數都是 0 或者很小,也就表示空間沒有被占用,因此一定是空停車位。
盡管 Mask R-CNN 非常精確,但目標檢測並不能做到完美。有時也會在一段視頻中漏掉一兩輛車。所以在定位到一個空車位時,還應該檢測在一段時間內都是空的,比如 5或10幀連續視頻。這也可以避免視頻本身出現故障而造成誤檢。一旦看到幾個連續視頻中都有空車位,馬上發送提醒通知!
四、發送消息
最後一步是發送 SMS 提醒消息。利用 Twilio 通過 Python 發送 SMS 消息非常簡單,基本上幾行代碼就可以做到。當然,Twilio 只是這個項目中用到的方法,你也可以用其他方式做到。
要使用 Twilio,先要註冊一個試用帳戶,創建一個 Twilio 電話號碼並獲取您的帳戶憑證。然後,您需要安裝 Twilio Python 客戶端庫:
下面是發送 SMS 消息的 Python 代碼 (需用自己的帳戶信息替換這些值):
1from twilio.rest import Client 2 3# Twilio account details 4twilio_account_sid = 'Your Twilio SID here' 5twilio_auth_token = 'Your Twilio Auth Token here' 6twilio_source_phone_number = 'Your Twilio phone number here' 7 8# Create a Twilio client object instance 9client = Client(twilio_account_sid, twilio_auth_token) 10 11# Send an SMS 12message = client.messages.create( 13 body="This is my SMS message!", 14 from_=twilio_source_phone_number, 15 to="Destination phone number here" 16)
在添加 SMS 發送功能時要注意,不要連續發送已經識別過的空車位信息。可以用一個 flag 來跟蹤已經發過的簡訊,除非是設定一段時間後再次提醒或是檢測到新的空車位。
五、把所有流程串在一起
現在將每個步驟集成一個Python腳本。下面是完整代碼,要運行這段代碼,需要安裝Python 3.6+,Matterport 的 Mask R-CNN 和 OpenCV:
1import os 2import numpy as np 3import cv2 4import mrcnn.config 5import mrcnn.utils 6from mrcnn.model import MaskRCNN 7from pathlib import Path 8from twilio.rest import Client 9 10 11# Configuration that will be used by the Mask-RCNN library 12class MaskRCNNConfig(mrcnn.config.Config): 13 NAME = "coco_pretrained_model_config" 14 IMAGES_PER_GPU = 1 15 GPU_COUNT = 1 16 NUM_CLASSES = 1 + 80 # COCO dataset has 80 classes + one background class 17 DETECTION_MIN_CONFIDENCE = 0.6 18 19 20# Filter a list of Mask R-CNN detection results to get only the detected cars / trucks 21def get_car_boxes(boxes, class_ids): 22 car_boxes = [] 23 24 for i, box in enumerate(boxes): 25 # If the detected object isn't a car / truck, skip it 26 if class_ids[i] in [3, 8, 6]: 27 car_boxes.append(box) 28 29 return np.array(car_boxes) 30 31 32# Twilio config 33twilio_account_sid = 'YOUR_TWILIO_SID' 34twilio_auth_token = 'YOUR_TWILIO_AUTH_TOKEN' 35twilio_phone_number = 'YOUR_TWILIO_SOURCE_PHONE_NUMBER' 36destination_phone_number = 'THE_PHONE_NUMBER_TO_TEXT' 37client = Client(twilio_account_sid, twilio_auth_token) 38 39 40# Root directory of the project 41ROOT_DIR = Path(".") 42 43# Directory to save logs and trained model 44MODEL_DIR = os.path.join(ROOT_DIR, "logs") 45 46# Local path to trained weights file 47COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5") 48 49# Download COCO trained weights from Releases if needed 50if not os.path.exists(COCO_MODEL_PATH): 51 mrcnn.utils.download_trained_weights(COCO_MODEL_PATH) 52 53# Directory of images to run detection on 54IMAGE_DIR = os.path.join(ROOT_DIR, "images") 55 56# Video file or camera to process - set this to 0 to use your webcam instead of a video file 57VIDEO_SOURCE = "test_images/parking.mp4" 58 59# Create a Mask-RCNN model in inference mode 60model = MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=MaskRCNNConfig()) 61 62# Load pre-trained model 63model.load_weights(COCO_MODEL_PATH, by_name=True) 64 65# Location of parking spaces 66parked_car_boxes = None 67 68# Load the video file we want to run detection on 69video_capture = cv2.VideoCapture(VIDEO_SOURCE) 70 71# How many frames of video we've seen in a row with a parking space open 72free_space_frames = 0 73 74# Have we sent an SMS alert yet? 75sms_sent = False 76 77# Loop over each frame of video 78while video_capture.isOpened(): 79 success, frame = video_capture.read() 80 if not success: 81 break 82 83 # Convert the image from BGR color (which OpenCV uses) to RGB color 84 rgb_image = frame[:, :, ::-1] 85 86 # Run the image through the Mask R-CNN model to get results. 87 results = model.detect([rgb_image], verbose=0) 88 89 # Mask R-CNN assumes we are running detection on multiple images. 90 # We only passed in one image to detect, so only grab the first result. 91 r = results[0] 92 93 # The r variable will now have the results of detection: 94 # - r['rois'] are the bounding box of each detected object 95 # - r['class_ids'] are the class id (type) of each detected object 96 # - r['scores'] are the confidence scores for each detection 97 # - r['masks'] are the object masks for each detected object (which gives you the object outline) 98 99 if parked_car_boxes is None: 100 # This is the first frame of video - assume all the cars detected are in parking spaces. 101 # Save the location of each car as a parking space box and go to the next frame of video. 102 parked_car_boxes = get_car_boxes(r['rois'], r['class_ids']) 103 else: 104 # We already know where the parking spaces are. Check if any are currently unoccupied. 105 106 # Get where cars are currently located in the frame 107 car_boxes = get_car_boxes(r['rois'], r['class_ids']) 108 109 # See how much those cars overlap with the known parking spaces 110 overlaps = mrcnn.utils.compute_overlaps(parked_car_boxes, car_boxes) 111 112 # Assume no spaces are free until we find one that is free 113 free_space = False 114 115 # Loop through each known parking space box 116 for parking_area, overlap_areas in zip(parked_car_boxes, overlaps): 117 118 # For this parking space, find the max amount it was covered by any 119 # car that was detected in our image (doesn't really matter which car) 120 max_IoU_overlap = np.max(overlap_areas) 121 122 # Get the top-left and bottom-right coordinates of the parking area 123 y1, x1, y2, x2 = parking_area 124 125 # Check if the parking space is occupied by seeing if any car overlaps 126 # it by more than 0.15 using IoU 127 if max_IoU_overlap < 0.15: 128 # Parking space not occupied! Draw a green box around it 129 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 3) 130 # Flag that we have seen at least one open space 131 free_space = True 132 else: 133 # Parking space is still occupied - draw a red box around it 134 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 1) 135 136 # Write the IoU measurement inside the box 137 font = cv2.FONT_HERSHEY_DUPLEX 138 cv2.putText(frame, f"{max_IoU_overlap:0.2}", (x1 + 6, y2 - 6), font, 0.3, (255, 255, 255)) 139 140 # If at least one space was free, start counting frames 141 # This is so we don't alert based on one frame of a spot being open. 142 # This helps prevent the script triggered on one bad detection. 143 if free_space: 144 free_space_frames += 1 145 else: 146 # If no spots are free, reset the count 147 free_space_frames = 0 148 149 # If a space has been free for several frames, we are pretty sure it is really free! 150 if free_space_frames > 10: 151 # Write SPACE AVAILABLE!! at the top of the screen 152 font = cv2.FONT_HERSHEY_DUPLEX 153 cv2.putText(frame, f"SPACE AVAILABLE!", (10, 150), font, 3.0, (0, 255, 0), 2, cv2.FILLED) 154 155 # If we haven't sent an SMS yet, sent it! 156 if not sms_sent: 157 print("SENDING SMS!!!") 158 message = client.messages.create( 159 body="Parking space open - go go go!", 160 from_=twilio_phone_number, 161 to=destination_phone_number 162 ) 163 sms_sent = True 164 165 # Show the frame of video on the screen 166 cv2.imshow('Video', frame) 167 168 # Hit 'q' to quit 169 if cv2.waitKey(1) & 0xFF == ord('q'): 170 break 171 172# Clean up everything when finished 173video_capture.release() 174cv2.destroyAllWindows()
這段代碼寫的比較簡潔,做到了基本的功能。大家也可以試著修改代碼以適應不同的場景。僅僅更改一下模型參數,出現的效果就可能完全不同,在不同應用中盡情發揮想像力!
原文鏈接:https://medium.com/@ageitgey/snagging-parking-spaces-with-mask-r-cnn-and-python-955f2231c400
(本文為 AI科技大本營編譯文章,轉載請微信聯繫 1092722531。)
——————————————— 征稿 ————————————————