Хабрахабр

[Перевод] Ищем свободное парковочное место с Python

image

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

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

image

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

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

Декомпозируем задачу

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

Вот, как я разбил свою задачу:

image

На вход конвейера поступает видеопоток с веб-камеры, направленной в окно:

image

Через конвейер мы будем передавать каждый кадр видео, по одному за раз.

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

Это позволит нам отслеживать движение каждой машины от кадра к кадру. Затем на каждом кадре нужно найти все машины.

Для этого нужно совместить результаты первых двух шагов. Третьим шагом нужно определить, какие места заняты машинами, а какие — нет.

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

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

Распознаём парковочные места

Вот, что видит наша камера:

image

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

image

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

Как вариант, можно искать на изображении паркометры и предположить, что рядом с каждым из них есть парковочное место:

image

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

Другая идея заключается в создании модели распознавания объектов, которая ищет метки парковочного места, нарисованные на дороге:

image

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

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

Другими словами, парковочные места расположены там, где подолгу стоят машины:

image

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

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

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

  • Можно обучить детектор на основе HOG (Histogram of Oriented Gradients, гистограммы направленных градиентов) и пройтись им по всему изображению, чтобы найти все машины. Этот старый подход, не использующий глубокое обучение, работает относительно быстро, но не очень хорошо справляется с машинами, расположенными по-разному.
  • Можно обучить детектор на основе CNN (Convolutional Neural Network, свёрточная нейронная сеть) и пройтись им по всему изображению, пока не найдём все машины. Этот подход работает точно, но не так эффективно, так как нам нужно просканировать изображение несколько раз с помощью CNN, чтобы найти все машины. И хотя так мы сможем найти машины, расположенные по-разному, нам потребуется гораздо больше обучающих данных, чем для HOG-детектора.
  • Можно использовать новый подход с глубоким обучением вроде Mask R-CNN, Faster R-CNN или YOLO, который совмещает в себе точность CNN и набор технических хитростей, сильно повышающих скорость распознавания. Такие модели будут работать относительно быстро (на GPU), если у нас есть много данных для обучения модели.

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

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

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

image

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

В этом датасете находится более 12 000 изображений с уже размеченными машинами. Один из них — популярный датасет СОСО(сокращение для Common Objects In Context), в котором есть изображения, аннотированные масками объектов. Вот пример изображения из датасета:

image

Такие данные отлично подходят для обучения модели на основе Mask R-CNN.

Мы не первые, кому захотелось обучить свою модель с помощью датасета COCO — многие люди уже сделали это до нас и поделились своими результатами. Но придержите коней, есть новости ещё лучше! Для нашего проекта мы воспользуемся open-source моделью от Matterport. Поэтому вместо обучения своей модели мы можем взять готовую, которая уже может распознавать машины.

Если дать на вход этой модели изображение с камеры, вот что мы получим уже «из коробки»:

image

Забавно, что дерево она распознала как комнатное растение. Модель распознала не только машины, но и такие объекты, как светофоры и люди.

Для каждого распознанного объекта модель Mask R-CNN возвращает 4 вещи:

  • Тип обнаруженного объекта (целое число). Предобученная модель COCO умеет распознавать 80 разных часто встречающихся объектов вроде машин и грузовиков. С их полным списком можно ознакомиться здесь.
  • Степень уверенности в результатах распознавания. Чем выше число, тем сильнее модель уверена в правильности распознавания объекта.
  • Ограничивающая рамка для объекта в форме XY-координат пикселей на изображении.
  • «Маска», которая показывает, какие пиксели внутри ограничивающей рамки являются частью объекта. С помощью данных маски можно найти контур объекта.

Ниже показан код на Python для обнаружения ограничивающей рамки для машин с помощью предобученной модели Mask R-CNN и OpenCV:

import numpy as np
import cv2
import mrcnn.config
import mrcnn.utils
from mrcnn.model import MaskRCNN
from pathlib import Path # Конфигурация, которую будет использовать библиотека Mask-RCNN.
class MaskRCNNConfig(mrcnn.config.Config): NAME = "coco_pretrained_model_config" IMAGES_PER_GPU = 1 GPU_COUNT = 1 NUM_CLASSES = 1 + 80 # в датасете COCO находится 80 классов + 1 фоновый класс. DETECTION_MIN_CONFIDENCE = 0.6 # Фильтруем список результатов распознавания, чтобы остались только автомобили.
def get_car_boxes(boxes, class_ids): car_boxes = [] for i, box in enumerate(boxes): # Если найденный объект не автомобиль, то пропускаем его. if class_ids[i] in [3, 8, 6]: car_boxes.append(box) return np.array(car_boxes) # Корневая директория проекта.
ROOT_DIR = Path(".") # Директория для сохранения логов и обученной модели.
MODEL_DIR = ROOT_DIR / "logs" # Локальный путь к файлу с обученными весами.
COCO_MODEL_PATH = ROOT_DIR / "mask_rcnn_coco.h5" # Загружаем датасет COCO при необходимости.
if not COCO_MODEL_PATH.exists(): mrcnn.utils.download_trained_weights(COCO_MODEL_PATH) # Директория с изображениями для обработки.
IMAGE_DIR = ROOT_DIR / "images" # Видеофайл или камера для обработки — вставьте значение 0, если нужно использовать камеру, а не видеофайл.
VIDEO_SOURCE = "test_images/parking.mp4" # Создаём модель Mask-RCNN в режиме вывода.
model = MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=MaskRCNNConfig()) # Загружаем предобученную модель.
model.load_weights(COCO_MODEL_PATH, by_name=True) # Местоположение парковочных мест.
parked_car_boxes = None # Загружаем видеофайл, для которого хотим запустить распознавание.
video_capture = cv2.VideoCapture(VIDEO_SOURCE) # Проходимся в цикле по каждому кадру.
while video_capture.isOpened(): success, frame = video_capture.read() if not success: break # Конвертируем изображение из цветовой модели BGR (используется OpenCV) в RGB. rgb_image = frame[:, :, ::-1] # Подаём изображение модели Mask R-CNN для получения результата. results = model.detect([rgb_image], verbose=0) # Mask R-CNN предполагает, что мы распознаём объекты на множественных изображениях. # Мы передали только одно изображение, поэтому извлекаем только первый результат. r = results[0] # Переменная r теперь содержит результаты распознавания: # - r['rois'] — ограничивающая рамка для каждого распознанного объекта; # - r['class_ids'] — идентификатор (тип) объекта; # - r['scores'] — степень уверенности; # - r['masks'] — маски объектов (что даёт вам их контур). # Фильтруем результат для получения рамок автомобилей. car_boxes = get_car_boxes(r['rois'], r['class_ids']) print("Cars found in frame of video:") # Отображаем каждую рамку на кадре. for box in car_boxes: print("Car:", box) y1, x1, y2, x2 = box # Рисуем рамку. cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 1) # Показываем кадр на экране. cv2.imshow('Video', frame) # Нажмите 'q', чтобы выйти. if cv2.waitKey(1) & 0xFF == ord('q'): break # Очищаем всё после завершения.
video_capture.release()
cv2.destroyAllWindows()

После запуска этого скрипта на экране появится изображение с рамкой вокруг каждой обнаруженной машины:

image

Также в консоль будут выведены координаты каждой машины:

Cars found in frame of video:
Car: [492 871 551 961]
Car: [450 819 509 913]
Car: [411 774 470 856]

Вот мы и научились распознавать машины на изображении.

Распознаём пустые парковочные места

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

Проблема в том, что рамки машин частично перекрывают друг друга:

image

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

IoU можно найти, посчитав количество пикселей, где пересекаются два объекта, и разделить на количество пикселей, занимаемых этими объектами: Мы воспользуемся мерой под названием Intersection Over Union (отношение площади пересечения к сумме площадей) или IoU.

image

Это позволит легко определить, свободна ли парковка. Так мы сможем понять, как сильно ограничивающая рамка машины пересекается с рамкой парковочного места. 15, значит, машина занимает малую часть парковочного места. Если значение IoU низкое, вроде 0. 6, то это значит, что машина занимает большую часть места и там нельзя припарковаться. А если оно высокое, вроде 0.

В нашей библиотеке Mask R-CNN она реализована в виде функции mrcnn.utils.compute_overlaps(). Поскольку IoU используется довольно часто в компьютерном зрении, в соответствующих библиотеках с большой вероятностью есть реализация этой меры.

Если у нас есть список ограничивающих рамок для парковочных мест, то добавить проверку на наличие машин в этих рамках можно, добавив всего строку-другую кода:

# Фильтруем результат для получения рамок автомобилей. car_boxes = get_car_boxes(r['rois'], r['class_ids']) # Смотрим, как сильно машины пересекаются с известными парковочными местами. 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 означает, что всё место полностью занято машиной, а низкое значение вроде 0. Результат 1. 02 говорит о том, что машина немного влезла на место, но на нём ещё можно припарковаться.

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

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

Отправляем SMS

Последняя часть нашего конвейера — отправка SMS-уведомления при появлении свободного парковочного места.

Twilio — это популярный API, который позволяет отправлять SMS из практически любого языка программирования с помощью всего нескольких строк кода. Отправить сообщение из Python очень легко, если использовать Twilio. Я никак не связан с Twilio, просто это первое, что приходит на ум. Конечно, если вы предпочитаете другой сервис, то можете использовать и его.

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

$ pip3 install twilio

После этого используйте следующий код для отправки сообщения:

from twilio.rest import Client # Данные аккаунта Twilio.
twilio_account_sid = 'Ваш Twilio SID'
twilio_auth_token = 'Ваш токен аутентификации Twilio'
twilio_source_phone_number = 'Ваш номер телефона Twilio' # Создаём объект клиента Twilio.
client = Client(twilio_account_sid, twilio_auth_token) # Отправляем SMS.
message = client.messages.create( body="Тело сообщения", from_=twilio_source_phone_number, to="Ваш номер, куда придёт сообщение"
)

Однако нужно сделать так, чтобы сообщение не отправлялось на каждом кадре, где видно свободное место. Чтобы добавить возможность отправки сообщений в наш скрипт, просто скопируйте туда этот код. Поэтому у нас будет флаг, который в установленном состоянии не даст отправлять сообщения в течение какого-то времени или пока не освободится другое место.

Складываем всё воедино

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 # Конфигурация, которую будет использовать библиотека Mask-RCNN.
class MaskRCNNConfig(mrcnn.config.Config): NAME = "coco_pretrained_model_config" IMAGES_PER_GPU = 1 GPU_COUNT = 1 NUM_CLASSES = 1 + 80 # в датасете COCO находится 80 классов + 1 фоновый класс. DETECTION_MIN_CONFIDENCE = 0.6 # Фильтруем список результатов распознавания, чтобы остались только автомобили.
def get_car_boxes(boxes, class_ids): car_boxes = [] for i, box in enumerate(boxes): # Если найденный объект не автомобиль, то пропускаем его. if class_ids[i] in [3, 8, 6]: car_boxes.append(box) return np.array(car_boxes) # Конфигурация Twilio.
twilio_account_sid = 'Ваш Twilio SID'
twilio_auth_token = 'Ваш токен аутентификации Twilio'
twilio_phone_number = 'Ваш номер телефона Twilio'
destination_phone_number = 'Номер, куда придёт сообщение'
client = Client(twilio_account_sid, twilio_auth_token) # Корневая директория проекта.
ROOT_DIR = Path(".") # Директория для сохранения логов и обученной модели.
MODEL_DIR = ROOT_DIR / "logs" # Локальный путь к файлу с обученными весами.
COCO_MODEL_PATH = ROOT_DIR / "mask_rcnn_coco.h5" # Загружаем датасет COCO при необходимости.
if not COCO_MODEL_PATH.exists(): mrcnn.utils.download_trained_weights(COCO_MODEL_PATH) # Директория с изображениями для обработки.
IMAGE_DIR = ROOT_DIR / "images" # Видеофайл или камера для обработки — вставьте значение 0, если использовать камеру, а не видеофайл.
VIDEO_SOURCE = "test_images/parking.mp4" # Создаём модель Mask-RCNN в режиме вывода.
model = MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=MaskRCNNConfig()) # Загружаем предобученную модель.
model.load_weights(COCO_MODEL_PATH, by_name=True) # Местоположение парковочных мест.
parked_car_boxes = None # Загружаем видеофайл, для которого хотим запустить распознавание.
video_capture = cv2.VideoCapture(VIDEO_SOURCE) # Сколько кадров подряд с пустым местом мы уже видели.
free_space_frames = 0 # Мы уже отправляли SMS?
sms_sent = False # Проходимся в цикле по каждому кадру.
while video_capture.isOpened(): success, frame = video_capture.read() if not success: break # Конвертируем изображение из цветовой модели BGR в RGB. rgb_image = frame[:, :, ::-1] # Подаём изображение модели Mask R-CNN для получения результата. results = model.detect([rgb_image], verbose=0) # Mask R-CNN предполагает, что мы распознаём объекты на множественных изображениях. # Мы передали только одно изображение, поэтому извлекаем только первый результат. r = results[0] # Переменная r теперь содержит результаты распознавания: # - r['rois'] — ограничивающая рамка для каждого распознанного объекта; # - r['class_ids'] — идентификатор (тип) объекта; # - r['scores'] — степень уверенности; # - r['masks'] — маски объектов (что даёт вам их контур). if parked_car_boxes is None: # Это первый кадр видео — допустим, что все обнаруженные машины стоят на парковке. # Сохраняем местоположение каждой машины как парковочное место и переходим к следующему кадру. parked_car_boxes = get_car_boxes(r['rois'], r['class_ids']) else: # Мы уже знаем, где места. Проверяем, есть ли свободные. # Ищем машины на текущем кадре. car_boxes = get_car_boxes(r['rois'], r['class_ids']) # Смотрим, как сильно эти машины пересекаются с известными парковочными местами. overlaps = mrcnn.utils.compute_overlaps(parked_car_boxes, car_boxes) # Предполагаем, что свободных мест нет, пока не найдём хотя бы одно. free_space = False # Проходимся в цикле по каждому известному парковочному месту. for parking_area, overlap_areas in zip(parked_car_boxes, overlaps): # Ищем максимальное значение пересечения с любой обнаруженной # на кадре машиной (неважно, какой). max_IoU_overlap = np.max(overlap_areas) # Получаем верхнюю левую и нижнюю правую координаты парковочного места. y1, x1, y2, x2 = parking_area # Проверяем, свободно ли место, проверив значение IoU. if max_IoU_overlap < 0.15: # Место свободно! Рисуем зелёную рамку вокруг него. cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 3) # Отмечаем, что мы нашли как минимум оно свободное место. free_space = True else: # Место всё ещё занято — рисуем красную рамку. cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 1) # Записываем значение IoU внутри рамки. font = cv2.FONT_HERSHEY_DUPLEX cv2.putText(frame, f"", (x1 + 6, y2 - 6), font, 0.3, (255, 255, 255)) # Если хотя бы одно место было свободным, начинаем считать кадры. # Это для того, чтобы убедиться, что место действительно свободно # и не отправить лишний раз уведомление. if free_space: free_space_frames += 1 else: # Если всё занято, обнуляем счётчик. free_space_frames = 0 # Если место свободно на протяжении нескольких кадров, можно сказать, что оно свободно. if free_space_frames > 10: # Отображаем надпись SPACE AVAILABLE!! вверху экрана. font = cv2.FONT_HERSHEY_DUPLEX cv2.putText(frame, f"SPACE AVAILABLE!", (10, 150), font, 3.0, (0, 255, 0), 2, cv2.FILLED) # Отправляем сообщение, если ещё не сделали это. 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 # Показываем кадр на экране. cv2.imshow('Video', frame) # Нажмите 'q', чтобы выйти. if cv2.waitKey(1) & 0xFF == ord('q'): break # Нажмите 'q', чтобы выйти.
video_capture.release()
cv2.destroyAllWindows()

6+, Matterport Mask R-CNN и OpenCV. Для запуска того кода сначала нужно установить Python 3.

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

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

Первыми подобные статьи можно читать в телеграм-канале Нейрон (@neurondata)

Экспериментируйте! Всем знаний.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть