Определение свободного парковочного места с помощью Computer Vision

Модель детектирования свободного парковочного места

Модель детектирования свободного парковочного места

Всем привет! Это моя первая статья на Хабр (поэтому не судите строго).

Дело было так: смотрел я как-то в окно и увидел, как человек сидит в машине на парковке и ждет, когда освободится парковочное место. Бывает, что и я сижу в машине и жду, когда же можно будет припарковать своего верного коня. И тут я подумал, а почему бы не подключить Компьютерное Зрение для этого? Зачем я учился разработке нейросетей, если не могу заставить компьютер работать вместо меня?

Изначально идея заключалась в следующем: Модель на базе компьютерного зрения должна через веб-камеру, установленную дома, отслеживать освободившиеся места на парковке и информировать через telegram-бота если такое место появится. Работать будем на Python.

Итак, ТЗ для меня от меня сформулировано, теперь за дело!

Первое с чем необходимо было определиться, это решить, какую модель детектирования объектов использовать. Сначала мой выбор пал на Fast R-СNN. Модель показывала хорошее качество детектирования. Однако после нескольких дней прокрастинации обдумывания реализации я решил воспользоваться более современными и интересными методами и подключить детектор от YOLO (взял не самую новую 4 версию).

YOLO4

YOLO4

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

#Библиотеки
import cv2
import numpy as np
import pandas as pd
from art import tprint
import matplotlib.pylab as plt
import requests

1) Подключаем камеру с помощью библиотеки CV. Разработку я делал на заранее записанном видео, но если работаем с веб-камерой, то необходимо просто передать cv2.VideoCapture () цифру ноль. Далее работаем с каждым кадром (берем каждый кадр видео и прогоняем его через нашу модель).

#Инициализируем работу с видео
video_capture = cv2.VideoCapture(video_path)

#Пока не нажата клавиша q функция будет работать
while video_capture.isOpened():
    
    ret, image_to_process = video_capture.read()

    #Препроцессинг изображения и работа YOLO
    height, width, _ = image_to_process.shape
    blob = cv2.dnn.blobFromImage(image_to_process, 1 / 255, (608, 608),
                                 (0, 0, 0), swapRB=True, crop=False)
    net.setInput(blob)
    outs = net.forward(out_layers)
    class_indexes, class_scores, boxes = ([] for i in range(3))

    #Обнаружение объектов в кадре
    for out in outs:
        for obj in out:
            scores = obj[5:]
            class_index = np.argmax(scores)

2) Следующий шаг: работа YOLO детектора. YOLO может детектировать 80 объектов, но нам нужны только машины, поэтому отсекаем всё лишнее. Берем только Bounding Boxes необходимых объектов класса car.

            #В классе 2 (car) только автомобили
            if class_index == 2: 
                class_score = scores[class_index]
                if class_score > 0:
                    center_x = int(obj[0] * width)
                    center_y = int(obj[1] * height)
                    obj_width = int(obj[2] * width)
                    obj_height = int(obj[3] * height)
                    box = [center_x - obj_width // 2, center_y - obj_height // 2,
                            obj_width, obj_height]

                    #BB
                    boxes.append(box)
                    class_indexes.append(class_index)
                    class_scores.append(float(class_score))

Для информации: объекты которые может детектировать YOLO4 (в следующий раз буду детектировать жирафа верхом на сноуборде).

['person', 'bicycle', 'car', 'motorbike', 'aeroplane', 'bus',
'train', 'truck', 'boat', 'traffic light','fire hydrant', 'stop sign',
'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag',
'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite',
'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket',
'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 
'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 
'donut', 'cake', 'chair', 'sofa', 'pottedplant', 'bed', 'diningtable', 
'toilet', 'tvmonitor', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 
'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']

3) Теперь начинается творческая часть. Что мы подразумеваем под паковочными местами? Самое простое и логичное: взять места на которых стоят машины! То есть, под всеми машинами, которые определились в кадре, находятся парковочные места. Что ж, для начала подойдет такой подход (усложнить всегда успеем)

Парковочные места определенные в первом кадре

Парковочные места определенные в первом кадре

В переменную first_frame_parking_spaces запишем все BBoxes, которые определились в кадре. Это наши парковочные места (на удивление, при записи видео на парковке были свободные места, но по факту всё всегда занято). Парковочные места мы записали в переменную, которую не трогаем до самого конца работы программы (Они у нас высечены в камне, это наш золотой грааль, это салат оливье на новый год, которые нельзя трогать).

#ПЕРВЫЙ КАДР, ОПРЕДЕЛЯЕМ ПАРКОМЕСТА
if not first_frame_parking_spaces:
    #Предполагаем, что под каждой машиной будет парковочное место
    first_frame_parking_spaces = boxes
    first_frame_parking_score = class_scores

4) Теперь будем детектировать сами машины в кадре. Это уже динамическая часть отработки программы. Здесь и возникают основные сложности.

Как определить, что машина стоит на паркоместе? Сравнить пересечение их BoundingBoxes, то есть нам нужно использовать Intersection over Union (IoU).

IoU

IoU

Так как детектор отрабатывает поиск машин в рандомном порядке, то мы будем сравнивать пересечение всех паркомест со всеми машинами в кадре. Если машина в кадре пересекается с паркоместом, то IoU будет примерно 0.8–0.9, в остальных случаях 0.0, как-то так:

IoU = 0.83, место занято

IoU = 0.83, место занято

Тогда если машина уезжает, то максимальное пересечение BBox машины с BBox паркоместом будет уменьшаться и после определённого порога можно будет сказать об освободившемся паркоместе. Логично? Логично! Но… Тут возникает первая проблема…

Если мы снимаем четко сверху, то вопросов нет, все будет так как описано выше. Но если под углом, то вот что происходит: так как BoundingBoxes от соседних машин могут пересекаться с соседними паркоместами, то в момент, когда одно из паркомест освобождается, модель не детектирует его полностью свободным, потому что одна из машин рядом пересекает одновременно два парковочных места (своё и освободившееся).

Максимальное IoU=0.35

Максимальное IoU=0.35

Вот что происходит, если мы посмотрим на это в цифрах:

Уменьшающееся значение IoU это уезжающая машина, а IoU=0.35 это машина стоящая рядом

Уменьшающееся значение IoU это уезжающая машина, а IoU=0.35 это машина стоящая рядом

Теперь вопрос: как «вытащить» нужное IoU и сказать модели, что это именно наша машина? Сделаем несколько фильтров. Смысл такой: Первая фильтрация — берем все, что по IoU меньше 0.4 и больше 0 (защита от внезапного отключения детекции — отсутствие BoundingBox машины в модели при фактическом присутствии машины в кадре). Во второй фильтрации отсечем варианты, при которых пересечение по IoU меньше 0.15, таким образом мы можем в динамике сравнивая результаты IoU, определить, что у нас появился BoundingBox, который вначале попал под первое условие, а потом началось выполнение второго условия. Далее начинаем считать кадры и если подряд (на протяжении 10 кадров) у нас выполняется оба условия, то значит это свободное место.

Возникает ещё одна проблема: чехарды кадров. Если внезапно у нас появляется BoundingBox который удовлетворяет первому условию, то у нас будет сбиваться счетчик кадров для BoundingBox, который удовлетворяет обоим условиям. Тут начинаются танцы с бубном. К сожалению, придется добавить ещё один (последний) фильтр, который будет отвечать за чехарду BBoxes и обнулять счётчик free_parking_timer. Эх, надеюсь при просмотре кода ниже станет яснее:)

#IoU
overlaps = compute_overlaps(np.array(parking_spaces), np.array(cars_boxes))

for parking_space_one, area_overlap in zip(parking_spaces, overlaps):
    
    max_IoU = max(area_overlap)
    sort_IoU = np.sort(area_overlap[area_overlap > 0])[::-1]      
    
    if free_parking_space == False:
        
        if 0.0 < max_IoU < 0.4:

            #Количество паркомест по условию 1: 0.0 < IoU < 0.4
            len_sort = len(sort_IoU)

            #Количество паркомест по условию 2: IoU > 0.15
            sort_IoU_2 = sort_IoU[sort_IoU > 0.15]
            len_sort_2 = len(sort_IoU_2)

            #Смотрим чтобы удовлятворяло условию 1 и условию 2
            if (check_det_frame == parking_space_one) & (len_sort != len_sort_2):
                #Начинаем считать кадры подряд с пустыми координатами
                free_parking_timer += 1

            elif check_det_frame == None:
                check_det_frame = parking_space_one

            else:
                #Фильтр от чехарды мест (если место чередуется, то "скачет")
                free_parking_timer_bag1 += 1
                if free_parking_timer_bag1 == 2:
                    #Обнуляем счётчик, если паркоместо "скачет"
                    check_det_frame = parking_space_one
                    free_parking_timer = 0

            #Если более 10 кадров подряд, то предполагаем, что место свободно
            if free_parking_timer == 10:
                #Помечаем свободное место
                free_parking_space = True
                free_parking_space_box = parking_space_one
                #Отрисовываем рамку парковочного места 
                x_free, y_free, w_free, h_free = parking_space_one

И вот когда все три условия соблюдены на протяжении 10 кадров, мы наконец-то можем пометить выбранный BBox как свободное парковочное место и переключить флаг free_parking_space в положение True.

Работа модели

Работа модели

Стоит сделать обратную вещь: если free_parking_space=True, но парковочное место занимают, то у нас опять нет свободного места :(

#Если место занимают, то помечается как отсутствие свободных мест
overlaps = compute_overlaps(np.array([free_parking_space_box]), 
                            np.array(cars_boxes))
for area_overlap in overlaps:                
    max_IoU = max(area_overlap)
    if max_IoU > 0.6:
        free_parking_space = False
        telegram_message = False

Осталось совсем немного — прикрутить telegram сервис по информированию. В этой статье я не буду описывать как это сделать, приведу лишь отрывок кода с реализацией необходимых функций.

TOKEN = "…"
chat_id = "…"
  
#Функция для отправки фото в telegram
def send_photo_file(chat_id, img):
    files = {'photo': open(img, 'rb')}
    requests.post(f'https://api.telegram.org/bot{TOKEN}/sendPhoto?chat_id={chat_id}', files=files)

#Функция для отправки сообщения в telegram
def send_telegram_message(message):
    requests.get(f'https://api.telegram.org/bot{TOKEN}/sendMessage?chat_id={chat_id}&text={message}').json()

На этом собственно говоря — всё! Полный код сборки, вы можете посмотреть на моей странице на GitHub (https://github.com/Mazepov/Parking_Space_Detector).

Работа программы с информированием через telegram

Работа программы с информированием через telegram

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

Разработка этой версии заняло у меня три недели вялых ковыряний и два полных выходных усиленной разработки (плюс день на написание статьи).

На этом у меня всё! Надеюсь, был полезен этой статьей, буду благодарен комментариям и вопросам. В будущем планирую реализовать ещё несколько интересных проектов на основе Computer Vision и нейросетей.

© Habrahabr.ru