使用Python和Mask R-CNN自動尋找停車位,這是什麼神操作?
我居住在一個大城市。但是和在很多大城市一樣,找個停車位總沒那麼容易。車位很快就被搶佔一空,即使你有一個屬於自己的專用車位,朋友們順路來訪也很難,因為他們找不到車位。
我的解決方案就是將一個攝像頭伸出窗外,再用深度學習讓我的計算機在有車位空出來的時候給我發簡訊:
這聽起來也許很複雜,但是藉助深度學習構建一個實用的版本實際上又簡單又快。所有的工具都是可用的——你只要知道去哪兒找到工具並把它們組合在一起就行。
那麼,我們花點時間用 python 和深度學習構建一個高準確率的停車位通知系統吧!
問題分解
當我們面臨想要用機器學習解決的複雜問題時,第一步就是將問題分解成若干簡單問題。然後,使用詳細分解作為指導,我們可以從機器學習工具箱中使用不同的工具來解決這些小問題。通過將幾個不同的解決方案連結到一個流程中,我們就得到了能夠做一些複雜事情的系統。
下面是我分解的空車位檢測流程:
機器學習流程的輸入是來自一個伸出窗外的普通網路攝像頭的視訊流:
從攝像頭中擷取的示例視訊
我們將通過工作流程傳送每一幀視訊,一次一幀。
這個流程的第一步就是檢測一幀視訊中所有可能的停車位。顯然,在我們能夠檢測哪個是沒有被佔用的停車位之前,我們需要知道影象中的哪些部分是停車位。
第二步就是檢測每幀視訊中的所有車輛。這樣我們可以逐幀跟蹤每輛車的運動。
第三步就是確定哪些車位目前是被佔用的,哪些沒有。這需要結合前兩步的結果。
最後一步就是出現新車位時通知我。這需要基於視訊中兩幀之間車輛位置的變化。
這裡的每一步,我們都可以使用多種技術用很多種方式實現。構建這個流程並沒有唯一正確或者錯誤的方式,但不同的方法會有優劣之分。
第一步:檢測一幅影象中的停車位
攝像頭的視野是這樣的:
我們需要掃描這幅圖,然後返回一個有效停車區域列表,就像這樣:
這個城市街道上的有效停車位。
一種比較懶的方法就是手動把每個停車位的位置硬編碼到程式中,而不是自動檢測停車位。但是如果我們移動了攝像頭或者想要檢測另一條街道上的車位時,就必須再一次手動硬編碼車位的位置。這樣很麻煩,還是找一種自動檢測車位的方法吧。
一個思路是尋找停車計時器並假設每個計時器旁邊都有一個停車位:
檢測每幅影象中的停車計時器。
但這種方法有些複雜。首先,並非每個停車位都有停車計時器——實際上,我們最喜歡找的是無需付費的車位!其次,只知道停車計時器的位置並不能確切地告訴我們停車位的確切位置。它只是讓我們更加接近停車位罷了。
另一個思路就是構建目標檢測模型,讓它尋找道路上繪製的停車位標誌,就像這樣:
請注意這些黃色的小標誌——它們就是畫在道路上的每個車位的邊界。
但是這種方法也很令人痛苦。首先,我所在城市的停車位標誌線特別小,在這麼遠的距離很難看見,所以很難用計算機檢測到它們。其次,道路上有各種無關的線和標誌。很難區分哪些線是停車位標誌,哪些是車道分離線或者人行道線。
當你遇到似乎很困難的問題時,花幾分鐘時間想一想,你是否可以採用不同的方法來解決這個問題,避開一些技術性挑戰。到底什麼是停車位?停車位不就是車輛可以停放很長時間的地方嗎。所以,也許我們根本就沒必要去檢測停車位。為何不能僅僅檢測長時間沒有移動的車輛並且假設它們就停在停車位呢?
換句話說,有效停車位就是包含非移動車輛的地方。
這裡,每輛車的邊界框實際上就是一個停車位!如果我們能夠檢測靜態的車輛,就沒必要檢測停車位。
所以,如果我們能夠檢測車輛,並且可以判斷哪些車輛在視訊幀中是沒有移動的,那我們就能夠推測出停車位的位置。夠簡單了——讓我們來檢測車輛吧!
檢測影象中的車輛
檢測視訊幀裡的車輛就是目標檢測中的一道練習題。我們可以用很多機器學習方法來檢測影象中的目標。下面是我列出的幾種最常用的目標檢測演算法:
-
訓練一個 HOG(方向梯度直方圖) 目標檢測器,並用它滑過我們的影象以尋找所有的車輛。這個古老的非深度學習方法執行起來相當快,但是它並不能很好地處理向不同方向移動的車輛。
-
訓練一個 CNN(卷積神經網路) 目標檢測器,用它滑過我們的影象直到找到所有的車輛。這種方法很準確,但並不是很高效,因為我們必須多次掃描同一張影象來尋找所有的車輛。並且,雖然它可以輕易找到向不同方向移動的車輛,但它需要的訓練資料要比 HOG 目標檢測器多得多。
-
使用更新的深度學習方法,如Mask R-CNN、Faster R-CNN 或者YOLO。它們將靈活的設計和高效的技巧與 CNN 的準確性結合在了一起,能夠極大地加速檢測過程。只要我們有足夠多的資料來訓練模型,它能在 GPU 上執行地相對快一些。
通常情況下,我們希望選擇最簡單的方法來解決問題,使用最少的訓練資料,並不認為需要最新、最流行的演算法。但是在這個特殊的情況下,*Mask R-CNN*是一個比較合理的選擇,雖然它是一個比較新、比較流行的演算法。
Mask R-CNN架構在不使用滑動視窗的情況下以一種高效的計算方式在整幅影象中檢測目標。換句話說,它執行得相當快。在具有比較先進的 GPU 時,我們應該能夠以數幀每秒的速度檢測到高解析度視訊中的目標。所以它應該比較適合這個專案。
此外,Mask R-CNN給我們提供了很多關於每個檢測物件的資訊。絕大多數目標檢測演算法僅僅返回了每個物件的邊界框。但是Mask R-CNN並不會僅僅給我們提供每個物件的位置,它還會給出每個物件的輪廓 (掩模),就像這樣:
為了訓練Mask R-CNN,我們需要大量關於需要檢測的目標的影象。我們可以拍一些車輛的影象,然後將這些影象中的汽車標註出來,但是這可能會花費幾天的工作。幸運的是,汽車是很常見的檢測目標,很多人都想檢測,因此早就有了幾個公開的汽車資料集。
有一個很流行的資料集叫做 COCO,它裡面的影象都用目標掩膜標註過。在這個資料集中,已經有超過 12000 張汽車影象做好了輪廓標註。下面就是 COCO 資料集中的一張影象。
COCO 資料集中已標註輪廓的影象。
這個資料集非常適合用來訓練Mask R-CNN模型。
使用 COCO 資料集來構建目標檢測資料集是很常見的一件事情,所以好多人已經做過並且分享了他們的結果。因此,我們可以用一個訓練好的模型作為開始,而不用從頭去訓練自己的模型。針對這個專案,我們可以使用很棒的開源Mask R-CNN,它是由 Matterport 公司實現的,還提供了訓練好的模型。
地址:https://github.com/matterport/Mask_RCNN
旁註:你不必為訓練定製化的Mask R-CNN而擔心!標註資料是很耗時間的,但是並不困難。如果你想使用自己的資料完整地訓練Mask R-CNN模型,可以參考這本書:
https://www.machinelearningisfun.com/get-the-book
如果在我自己的相機影象上執行預訓練模型,以下是檢測結果:
經過 COCO 預設目標識別的影象——車輛、人、交通訊號燈和樹。
我們不僅識別了車輛,還識別出了交通訊號燈和人。而且比較滑稽的是,它將其中的一棵樹識別成了「盆栽植物」。
對於影象中被檢測到的每一個目標,我們從Mask R-CNN模型中得到了下面四個結果:
-
被檢測到的目標(作為整數)型別。預訓練的 COCO 模型知道如何檢測 80 種不同的常見目標,例如汽車和卡車。
-
目標檢測的置信得分。這個數字越大,越說明模型準確識別了目標。
-
中目標的邊界框,以 X/Y 畫素位置地形式給了出來。
-
點陣圖「掩模」,能夠分辨出邊界框裡哪些畫素是目標的一部分,哪些不是。有了掩模資料,我們也可以標註目標的輪廓。
下面是 python 程式碼,用於根據 Matterport』sMask R-CNN實現和 OpneCV 預訓練的模型來檢測汽車邊界框:
import os import numpy as np import cv2 import mrcnn.config import mrcnn.utils from mrcnn.model import MaskRCNN from pathlib import Path # Configuration that will be used by the Mask-RCNN library class MaskRCNNConfig(mrcnn.config.Config): NAME = "coco_pretrained_model_config" IMAGES_PER_GPU = 1 GPU_COUNT = 1 NUM_CLASSES = 1 + 80# COCO dataset has 80 classes + one background class DETECTION_MIN_CONFIDENCE = 0.6 # Filter a list of Mask R-CNN detection results to get only the detected cars / trucks def get_car_boxes(boxes, class_ids): car_boxes = [] for i, box in enumerate(boxes): # If the detected object isn't a car / truck, skip it if class_ids[i] in [3, 8, 6]: car_boxes.append(box) return np.array(car_boxes) # Root directory of the project ROOT_DIR = Path(".") # Directory to save logs and trained model MODEL_DIR = os.path.join(ROOT_DIR, "logs") # Local path to trained weights file COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5") # Download COCO trained weights from Releases if needed if not os.path.exists(COCO_MODEL_PATH): mrcnn.utils.download_trained_weights(COCO_MODEL_PATH) # Directory of images to run detection on IMAGE_DIR = os.path.join(ROOT_DIR, "images") # Video file or camera to process - set this to 0 to use your webcam instead of a video file VIDEO_SOURCE = "test_images/parking.mp4" # Create a Mask-RCNN model in inference mode model = MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=MaskRCNNConfig()) # Load pre-trained model model.load_weights(COCO_MODEL_PATH, by_name=True) # Location of parking spaces parked_car_boxes = None # Load the video file we want to run detection on video_capture = cv2.VideoCapture(VIDEO_SOURCE) # Loop over each frame of video while video_capture.isOpened(): success, frame = video_capture.read() if not success: break # Convert the image from BGR color (which <mark data-type="technologies" data-id="9c38872b-5720-41cc-84ed-debc155db10c">OpenCV</mark> uses) to RGB color rgb_image = frame[:, :, ::-1] # Run the image through the Mask R-CNN model to get results. results = model.detect([rgb_image], verbose=0) # Mask R-CNN assumes we are running detection on multiple images. # We only passed in one image to detect, so only grab the first result. r = results[0] # The r variable will now have the results of detection: # - r['rois'] are the bounding box of each detected object # - r['class_ids'] are the class id (type) of each detected object # - r['scores'] are the confidence scores for each detection # - r['masks'] are the object masks for each detected object (which gives you the object outline) # Filter the results to only grab the car / truck bounding boxes car_boxes = get_car_boxes(r['rois'], r['class_ids']) print("Cars found in frame of video:") # Draw each box on the frame for box in car_boxes: print("Car: ", box) y1, x1, y2, x2 = box # Draw the box cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 1) # Show the frame of video on the screen cv2.imshow('Video', frame) # Hit 'q' to quit if cv2.waitKey(1) & 0xFF == ord('q'): break # Clean up everything when finished video_capture.release() cv2.destroyAllWindows()
當你執行這段指令碼時,會在螢幕上得到一幅圖,每輛檢測到的汽車都有一個邊界框,像這樣:
每輛檢測到的汽車都有一個綠色的邊界框。
你還會看到被檢測到的汽車座標被列印在了控制檯上,就像這樣:
Cars found in frame of video: Car:[492 871 551 961] Car:[450 819 509 913] Car:[411 774 470 856]
經過以上這些步驟,我們已經成功地檢測到了影象中的汽車。
檢測空置的停車位
我們知道了每張影象中每輛車的畫素位置。通過檢視視訊中按順序出現的多幀,我們可以輕易知道哪些車沒有動,並且假設它們所在的位置就是車位。但是,當一輛車離開車位的時候,我們如何檢測得到呢?
問題在於我們影象中的邊界框是部分重疊的。
即使是在不同車位中的車輛,每輛車的邊界框都會有一小部分的重疊。
所以,如果我們假設每個邊界框代表一個車位,那麼,即使車位是空的,也有可能顯示為被部分佔用。我們需要一個方法來測量兩個物件的重疊度,以便檢查「大部分是空的」邊界框。
我們將要使用的測量方法為交併比(IoU)。IoU 通過兩個物件重疊的畫素數量除以兩個物件覆蓋的畫素數量計算得到。像這樣:
這將為我們提供汽車邊界框與停車位邊界框重疊的程度。有了這個,我們可以輕易確定汽車是否在停車位。如果 IoU 測量值很低,如 0.15,那意味著汽車並沒有真正佔用大部分停車位。但如果指標很高,如 0.6,這意味著汽車佔據了大部分停車位區域,因此我們可以確定該空間被佔用。
由於 IoU 是計算機視覺中常見的測量方法,因此你在使用的庫通常已經實現了它的計算。事實上,MatterportMask R-CNN庫將它作為一個名為 mrcnn.utils.compute_overlaps()的函式包含在內,因此我們可以直接使用該函式。
假設我們有一個表示影象中停車區域的邊界框列表,檢視檢測到的車輛是否在這些邊界框內就像新增一行或兩行程式碼一樣簡單:
# Filter the results to only grab the car / truck bounding boxes car_boxes = get_car_boxes(r['rois'], r['class_ids']) # See how much cars overlap with the known parking spaces overlaps = mrcnn.utils.compute_overlaps(car_boxes, parking_areas) print(overlaps)
結果是這樣子的:
[ [1.0.07040032 0.0.] [0.07040032 1.0.07673165 0.] [0.0.0.02332112 0.] ] In that 2D array, each
在這個二維陣列中,每一行代表一個停車位的邊界框。同樣,每一列代表著這個停車位被檢測到的汽車佔用了多少。1.0 分表示完全被佔用,較低的分,如 0.02 則表示這輛汽車接觸到了車位的空間,但是並沒有佔據大部分割槽域。
要尋找未被佔用的停車位,我們只需要檢查此陣列中的每一行。如果所有數字都為零或非常小,那意味著沒有任何東西佔據那個空間,它就是空著的!
但請記住,目標檢測並非總是與實時視訊完美配合。即使Mask R-CNN非常準確,偶爾也會在單幀視訊中錯過一兩輛車。因此,在將停車位標記為空閒之前,我們應該確保它在一段時間內保持空閒 - 可能是 5 或 10 個連續的視訊幀。這將防止系統僅僅因為目標檢測在一幀視訊上有短暫的停頓就錯誤地檢測到空閒的停車位。但是,只要我們看到至少有一個空閒停車位出現在連續幾幀視訊中,我們就可以傳送簡訊了!
傳送簡訊
這個專案的最後一步就是當檢測到一個空閒停車位出現在視訊的連續幾幀中時就傳送簡訊提醒。
使用 Twilio 從 Python 中傳送簡訊很簡單。Twilio 是一個很流行的 API,它可以讓你用任何程式語言只需幾行程式碼就可以傳送簡訊。當然,如果你更喜歡使用其它簡訊服務提供商,也可以。我和 Twilio 並沒有利益關係。它只是我想到的第一個工具而已。
要使用 Twilio,你需要註冊一個試用賬戶,建立兩個 Twilio 電話號碼,然後認證賬戶。然後,你需要安裝 Twilio Python 客戶端。
pip3 install twilio
安裝完成後,這是用 Python 傳送簡訊的完整程式碼(只需用你自己的帳戶詳細資訊替換這些值即可):
from twilio.rest import Client # Twilio account details twilio_account_sid = 'Your Twilio SID here' twilio_auth_token = 'Your Twilio Auth Token here' twilio_source_phone_number = 'Your Twilio phone number here' # Create a Twilio client object instance client = Client(twilio_account_sid, twilio_auth_token) # Send an SMS message = client.messages.create( body="This is my SMS message!", from_=twilio_source_phone_number, to="Destination phone number here" )
為了在指令碼中新增簡訊傳送功能,我們可以把這些程式碼丟進去。但需要注意的是,我們並不需要在每一個有空閒車位的新視訊幀中傳送簡訊。所以我們需要一個標誌來跟蹤是否已經發過簡訊了,這是為了保證不會在短期內再次傳送或者在新車位空出來之前再次傳送。
總結
將以上流程中的所有步驟整合在一起,構成一個獨立的 Python 指令碼,完整程式碼如下所示:
import os import numpy as np import cv2 import mrcnn.config import mrcnn.utils from mrcnn.model import MaskRCNN from pathlib import Path from twilio.rest import Client # Configuration that will be used by the Mask-RCNN library class MaskRCNNConfig(mrcnn.config.Config): NAME = "coco_pretrained_model_config" IMAGES_PER_GPU = 1 GPU_COUNT = 1 NUM_CLASSES = 1 + 80# COCO dataset has 80 classes + one background class DETECTION_MIN_CONFIDENCE = 0.6 # Filter a list of Mask R-CNN detection results to get only the detected cars / trucks def get_car_boxes(boxes, class_ids): car_boxes = [] for i, box in enumerate(boxes): # If the detected object isn't a car / truck, skip it if class_ids[i] in [3, 8, 6]: car_boxes.append(box) return np.array(car_boxes) # Twilio config twilio_account_sid = 'YOUR_TWILIO_SID' twilio_auth_token = 'YOUR_TWILIO_AUTH_TOKEN' twilio_phone_number = 'YOUR_TWILIO_SOURCE_PHONE_NUMBER' destination_phone_number = 'THE_PHONE_NUMBER_TO_TEXT' client = Client(twilio_account_sid, twilio_auth_token) # Root directory of the project ROOT_DIR = Path(".") # Directory to save logs and trained model MODEL_DIR = os.path.join(ROOT_DIR, "logs") # Local path to trained weights file COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5") # Download COCO trained weights from Releases if needed if not os.path.exists(COCO_MODEL_PATH): mrcnn.utils.download_trained_weights(COCO_MODEL_PATH) # Directory of images to run detection on IMAGE_DIR = os.path.join(ROOT_DIR, "images") # Video file or camera to process - set this to 0 to use your webcam instead of a video file VIDEO_SOURCE = "test_images/parking.mp4" # Create a Mask-RCNN model in inference mode model = MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=MaskRCNNConfig()) # Load pre-trained model model.load_weights(COCO_MODEL_PATH, by_name=True) # Location of parking spaces parked_car_boxes = None # Load the video file we want to run detection on video_capture = cv2.VideoCapture(VIDEO_SOURCE) # How many frames of video we've seen in a row with a parking space open free_space_frames = 0 # Have we sent an SMS alert yet? sms_sent = False # Loop over each frame of video while video_capture.isOpened(): success, frame = video_capture.read() if not success: break # Convert the image from BGR color (which <mark data-type="technologies" data-id="9c38872b-5720-41cc-84ed-debc155db10c">OpenCV</mark> uses) to RGB color rgb_image = frame[:, :, ::-1] # Run the image through the Mask R-CNN model to get results. results = model.detect([rgb_image], verbose=0) # Mask R-CNN assumes we are running detection on multiple images. # We only passed in one image to detect, so only grab the first result. r = results[0] # The r variable will now have the results of detection: # - r['rois'] are the bounding box of each detected object # - r['class_ids'] are the class id (type) of each detected object # - r['scores'] are the confidence scores for each detection # - r['masks'] are the object masks for each detected object (which gives you the object outline) if parked_car_boxes is None: # This is the first frame of video - assume all the cars detected are in parking spaces. # Save the location of each car as a parking space box and go to the next frame of video. parked_car_boxes = get_car_boxes(r['rois'], r['class_ids']) else: # We already know where the parking spaces are. Check if any are currently unoccupied. # Get where cars are currently located in the frame car_boxes = get_car_boxes(r['rois'], r['class_ids']) # See how much those cars overlap with the known parking spaces overlaps = mrcnn.utils.compute_overlaps(parked_car_boxes, car_boxes) # Assume no spaces are free until we find one that is free free_space = False # Loop through each known parking space box for parking_area, overlap_areas in zip(parked_car_boxes, overlaps): # For this parking space, find the max amount it was covered by any # car that was detected in our image (doesn't really matter which car) max_IoU_overlap = np.max(overlap_areas) # Get the top-left and bottom-right coordinates of the parking area y1, x1, y2, x2 = parking_area # Check if the parking space is occupied by seeing if any car overlaps # it by more than 0.15 using IoU if max_IoU_overlap < 0.15: # Parking space not occupied! Draw a green box around it cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 3) # Flag that we have seen at least one open space free_space = True else: # Parking space is still occupied - draw a red box around it cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 1) # Write the IoU measurement inside the box font = cv2.FONT_HERSHEY_DUPLEX cv2.putText(frame, f"{max_IoU_overlap:0.2}", (x1 + 6, y2 - 6), font, 0.3, (255, 255, 255)) # If at least one space was free, start counting frames # This is so we don't alert based on one frame of a spot being open. # This helps prevent the script triggered on one bad detection. if free_space: free_space_frames += 1 else: # If no spots are free, reset the count free_space_frames = 0 # If a space has been free for several frames, we are pretty sure it is really free! if free_space_frames > 10: # Write SPACE AVAILABLE!! at the top of the screen font = cv2.FONT_HERSHEY_DUPLEX cv2.putText(frame, f"SPACE AVAILABLE!", (10, 150), font, 3.0, (0, 255, 0), 2, cv2.FILLED) # If we haven't sent an SMS yet, sent it! if not sms_sent: print("SENDING SMS!!!") message = client.messages.create( body="Parking space open - go go go!", from_=twilio_phone_number, to=destination_phone_number ) sms_sent = True # Show the frame of video on the screen cv2.imshow('Video', frame) # Hit 'q' to quit if cv2.waitKey(1) & 0xFF == ord('q'): break # Clean up everything when finished video_capture.release() cv2.destroyAllWindows()
要執行這份程式碼,你首先需要安裝 python 3.6+,MatterportMask R-CNN以及OpenCV。
我特意保留了比較簡單的程式碼。例如,它只是假設第一幀視訊中出現的任何車輛都是停放的汽車。試用一下,看看你是否能夠提升它的可用性。
不必擔心為了在其它場景中使用而修改程式碼。僅僅改變模型尋找的目標 ID,你就能夠將這份程式碼完全轉換成另一個東西。例如,假設你在滑雪場工作。經過一些調整,你就可以將這份指令碼轉換為一個系統,它可以自動檢測滑雪板從斜坡上跳越,並創建出很酷的滑雪板跳越路線。或者如果你在野生動物保護區工作,你可以將這份程式碼轉換成一個統計野生斑馬數量的系統。唯一的限制只是你的想象力。祝你玩得開心!
參考原文:https://medium.com/@ageitgey/snagging-parking-spaces-with-mask-r-cnn-and-python-955f2231c400