Я решил проблему с парковкой у дома с помощью машинного обучения

Рассказывает Адам Гайтги, разработчик из Калифорнии.

Я живу в прекрасном городе. Но как и в большинстве городов, поиски свободного места для парковки здесь всегда раздражают. Места быстро занимают, и даже если у вас есть закреплённое место, друзьям просто так к вам не заехать, ведь им придётся искать, где бы оставить машину.

Поэтому я решил разместить у окна камеру и с помощью глубокого обучения создать систему, дающую знать, когда у дороги появляется свободное место.

Можно подумать, что это сложно, но на деле всё довольно быстро и просто — надо лишь знать, где искать подходящие инструменты и как их объединить. Начнём!

Разбиваем проблему

Когда перед нами стоит сложная задача, которую мы хотим решить при помощи машинного обучения, первым делом нужно разбить её на несколько задач поменьше, а затем, словно доставая различные инструменты из ящика, решить каждую из них. Соединив всё в цепочку, мы получим систему, которой по зубам нечто сложное.

Процесс обнаружения свободных парковочных мест я разбил на следующие этапы:

Вход (изображение с камеры) → Распознавание мест для парковки → Распознавание автомобилей → Распознавание свободных мест → Выход (SMS об освободившемся месте)
Вход (изображение с камеры) → Распознавание мест для парковки → Распознавание автомобилей → Распознавание свободных мест → Выход (SMS об освободившемся месте)

Входные данные для машинного обучения даёт обычная веб-камера, смотрящая на улицу:

Каждый кадр мы пропустим по цепочке выше, один за раз.

  • Первый шаг — найти в кадре все возможные места для парковки. Прежде чем определить, свободно ли место, нужно выяснить, где оно вообще.
  • Второй шаг — найти в кадре все машины. Это позволит отследить движение каждого автомобиля от кадра к кадру.
  • Третий шаг — установить, какие места заняты, а какие нет, для чего нужно объединить сведения, полученные на первом и втором шагах.
  • Наконец, когда место освобождается, нужно отправить уведомление. Это мы сможем сделать, отследив изменение положения машины между кадрами.

Теперь же погрузимся в детали!

Распознаём места для парковки

Итак, вот вид с камеры:

Я решил проблему с парковкой у дома с помощью машинного обучения

Нам нужно исследовать это изображение и получить список участков, пригодных для стоянки.

Места для парковки отмечены жёлтыми линиями
Места для парковки отмечены жёлтыми линиями

Проще всего задать расположение каждого места вручную, но тогда, стоит нам, скажем, передвинуть камеру, придётся писать код заново. Это неудобно, поэтому давайте автоматизируем и этот процесс.

С одной стороны, можно ориентироваться по парковочным счётчикам, предположив, что место для парковки есть напротив каждого устройства.

Счётчики обведены жёлтым
Счётчики обведены жёлтым

Но тут есть ряд сложностей. Во-первых, не у каждого парковочного места есть счётчик (более того, мы зачастую ищем место, за которое не нужно платить). Во-вторых, расположение счётчика не указывает на точное расположение площадки для парковки, а только приближает к нему.

С другой стороны, можно построить модель, распознающую разметку.

Желтые отрезки показывают, где расположены метки, отделяющие одно место от другого
Желтые отрезки показывают, где расположены метки, отделяющие одно место от другого

Впрочем, и тут полно головной боли. Во-первых, полосы, размечающие места для парковки, в моём городе очень маленькие, их тяжело увидеть издалека; стало быть, и компьютеру будет непросто их разглядеть. Во-вторых, на улицах полно других линий и полос, поэтому отделить разметку парковочных мест от всей остальной будет сложно.

Когда перед вами сложная проблема, подумайте, можно ли подобраться к ней по другому пути, обойдя некоторые технические сложности. Что такое место для парковки? Место, где машина припаркована долго. А стоит ли вообще его распознавать? Почему бы просто не отыскать машины, которые долго не двигаются, и предположить, что они занимают парковочное место?

Иными словами, места для парковки — места, занятые стоящими автомобилями.

Нам не нужно искать собственно места для парковки, если мы можем определить, где стоят неподвижные машины
Нам не нужно искать собственно места для парковки, если мы можем определить, где стоят неподвижные машины

Распознаём машины

Существует немало техник машинного обучения, которые мы можем использовать для распознавания нужного объекта на картинке. Вот несколько самых расхожих алгоритмов, от «старой школы» к «новой»:

  • Гистограмма направленных градиентов. Это старая, не связанная с глубоким обучением техника работает довольно быстро, но не очень хорошо распознаёт машины с разных углов.

  • Свёрточная нейронная сеть. Этот подход точен, но не так эффективен, поскольку одно и то же изображение, чтобы выделить все машины, необходимо обработать несколько раз. И хотя такая сеть может с лёгкостью распознать автомобиль с разных углов, ей нужно гораздо больше данных для обучения, чем системе, строящейся на гистограмме.

  • Нейросеть с архитектурой Mask R-CNN, Faster R-CNN или YOLO, которая объединяет точность свёрточной сети с хорошим исполнением и находками, в разы ускоряющими процесс распознавания. Эта система будет работать относительно быстро (на графическом процессоре) при условии, что у нас есть много данных для обучения модели.

В сущности, мы ищем самое простое решение, которое приведёт к желаемому результату, требуя при этом наименьшее количество данных для обучения; кроме того, мы не стремимся задействовать новейший алгоритм. Тем не менее в нашем случае разумный выбор — Mask R-CNN, несмотря на её очевидную помпезность и новизну.

Архитектура Mask R-CNN спроектирована так, что очень эффективно, с вычислительной точки зрения, распознаёт объекты на всём изображении. Словом, она работает быстро. На современном графическом процессоре система способна распознавать объекты на видео в высоком разрешении, обрабатывая несколько кадров в секунду. Должно хватить.

К тому же Mask R-CNN выдаёт различную информацию об обнаруженных объектах. Большинство алгоритмов распознавания на выходе дают лишь рамку, помечающую объект. А Mask R-CNN не только укажет на расположение каждого, но и сделает контур (или маску).

Я решил проблему с парковкой у дома с помощью машинного обучения

Для обучения Mask R-CNN нужно много изображений с предметами, которые мы собираемся распознавать. Можно выйти на улицу, поснимать машины и затем разметить фотографии, но это займёт как минимум пару дней. К счастью, немало людей связаны с распознаванием автомобилей, поэтому существует несколько открытых баз данных с нужными снимками.

Так, в базе COCO целых 12 тысяч изображений с машинами, на которые наложены маски. Вот она-то нам и подходит лучше всего.

Пример изображения из COCO c размеченными объектами
Пример изображения из COCO c размеченными объектами

А так как многие работают с моделями для распознавания, опираясь именно на COCO, в открытом доступе есть и обученные сети. Поэтому, вместо того чтобы заниматься обучением самостоятельно, я могу пустить в ход модель, которая может распознавать машины сразу после установки. В этом проекте мы задействуем реализацию Mask R-CNN от Matterport.

Сразу после установки модель распознаёт на изображении с камеры объекты, заданные в COCO по умолчанию:

Я решил проблему с парковкой у дома с помощью машинного обучения

Как видно, модель обнаружила не только машины, но и светофоры и людей. Забавно, что дерево система определила как «растение в горшке».

У каждого объекта теперь есть четыре маркера:

  • Тип объекта. Модель, обученная на данных COCO, может распознавать 80 типов часто встречающихся объектов вроде машин или грузовиков. Полный список.
  • Точность распознавания. Чем выше показатель, тем увереннее модель в том, что она не ошиблась, распознавая предмет.
  • Двумерная рамка объекта.
  • Растровая маска, указывающая, какие пиксели внутри рамки принадлежат собственно объекту. Данными маски позволяют выделить и контур объекта.

Ниже — код для определения рамок машин на основе задействованной нами модели от Matterport вместе с библиотекой OpenCV.

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 OpenCV 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); её значение равно числу пикселей в области пересечения двух объектов, поделённому на количество пикселей, занятых этими объектами.

Я решил проблему с парковкой у дома с помощью машинного обучения

Так мы узнаем, насколько велико пересечение рамки места для парковки рамкой автомобиля. Получив это значение, не составит труда определить, занято ли место. Если показатель мал, машина едва ли занимает парковочное место, наоборот — автомобиль занимает его большую часть, поэтому можно с уверенностью сказать: место занято.

А так как пересечение над соединением довольно часто используется в компьютерном зрении, оно будет реализовано в библиотеках, которые вы используйте. И действительно, библиотека Mask 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.] ]

В этом двумерном массиве каждый ряд обозначает одну из рамок места для стоянки, а каждая колонка — насколько сильно машины её пересекают. Если показатель равен единице, машина занимает участок полностью, если, например, 0,02 — машина только касается его.

Чтобы отделить незанятые места, нужно лишь проверить каждый ряд в массиве. Если все показатели равны нулю или очень малы, место с большой вероятностью не занято.

Помните, однако, что с потоковым видео распознавание не всегда работает хорошо. Несмотря на высокую точность Mask R-CNN, система время от времени упускает одну-две машины в кадре. Поэтому, перед тем как отмечать парковочное место пустым, нужно убедиться, что оно остаётся таким на протяжении небольшого промежутка времени. Хватит 5–10 кадров подряд.

Это не даст системе ошибиться со свободным местом после сбоя на одном кадре. Но как только становится ясным, что хотя бы одно место не занято в течение нескольких кадров, можно отправлять сообщение.

Отправляем уведомление

Последний шаг в цепочке — отправка SMS-сообщения.Тут на помощь приходит Twilio, популярный API, позволяющий отправлять SMS-сообщения едва ли не с любого языка программирования.

Разумеется, если вы предпочитаете другой сервис, можете использовать его. Просто мне Twilio первым приходит в голову.

Чтобы подключить Twilio, создайте пробную учётную запись и номер телефона, а также получите данные вашего аккаунта. Затем нужно установить клиентскую библиотеку:

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 OpenCV 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+, Matterport Mask R-CNN и OpenCV. Отмечу: я нарочно держал код максимально «голым». Так, предполагается, что все машины в первом кадре припаркованы.

Не бойтесь подстраивать код под разные ситуации. Заменив идентификаторы объектов, вы найдёте коду совершенно иное применение.

Представьте, что вы работаете на горнолыжном курорте. Пара поправок, и система будет самостоятельно распознавать вылетающих на рампе сноубордистов и делать серии снимков хороших прыжков. А если вы работаете в охотничьем заповеднике, можно сделать так, чтобы система считала пробегающих зебр. Веселитесь!

5858
50 комментариев

Чо он врет? Нет у него никаких друзей!

32

Я конечно дилетант, но если решать исключительно его локальную задачу, с данного ракурса проще было отслеживать сегмент видимого бардюра у тротуара и не заморачиваться на контурах машин и сложных алгоритмах. Но так бывает, хочется иногда пожарить яичницу с помощью лазера...

4

Ответ в самом тексте. Ручная оптимизация под текущую картинку уничтожит саму идею проекта.
В данной постановке можно включить камеру, направить ее на любой нужный участок и через день-два она начнет работать сама. А твоя система будет работать только там, где камера смотрит на бордюр. Другими словами эта система без переделок будет работать дома, в офисе, при смене работы или переезде фирмы, при пересадке внутри офиса, у друзей или на съемной квартире подружки, а твоя система потребует каждый раз полной переделки...

8

По хорошему система должна давать информацию по запросу о кол-ве свободных мест в данный момент (например, стоит ли вообще заезжать во двор, чтобы припарковаться). А ещё давать статистику в какое время больше парковочных мест. Можно спланировать своё рабочее время, исходя из этих данных.

5

Сомнительная история в плане выгоды, т.к. между получением СМС, реакцией на него и реальной возможностью занять парковочное место слишком большой промежуток времени. Обычно в плотной линии парковки если кто-то сваливает то не проходит и минуты чтобы не заехал другой водитель.

3

Просто смотри, сколько там свободных мест. Если одно, глупо на это расчитывать, но если их пять - вполне.

1

Хорошая штука. Перспективная.
Можно, например, отслеживать появление первого круассана в пекарне напротив, или почтальона...
Я в восторге!

2