Всем привет! Это моя первая статья на Хабр (поэтому не судите строго).
Дело было так: смотрел я как-то в окно и увидел, как человек сидит в машине на парковке и ждет, когда освободится парковочное место. Бывает, что и я сижу в машине и жду, когда же можно будет припарковать своего верного коня. И тут я подумал, а почему бы не подключить Компьютерное Зрение для этого? Зачем я учился разработке нейросетей, если не могу заставить компьютер работать вместо меня?
Изначально идея заключалась в следующем: Модель на базе компьютерного зрения должна через веб-камеру, установленную дома, отслеживать освободившиеся места на парковке и информировать через telegram-бота если такое место появится. Работать будем на Python.
Итак, ТЗ для меня от меня сформулировано, теперь за дело!
Первое с чем необходимо было определиться, это решить, какую модель детектирования объектов использовать. Сначала мой выбор пал на Fast R-СNN. Модель показывала хорошее качество детектирования. Однако после нескольких дней прокрастинации обдумывания реализации я решил воспользоваться более современными и интересными методами и подключить детектор от YOLO (взял не самую новую 4 версию).
С выбором детектора покончено, с тяжелыми размышлениями о проекте тоже, можно начинать сборку.
#Библиотеки
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 будет примерно 0.8-0.9, в остальных случаях 0.0, как-то так:
Тогда если машина уезжает, то максимальное пересечение BBox машины с BBox паркоместом будет уменьшаться и после определённого порога можно будет сказать об освободившемся паркоместе. Логично? Логично! Но… Тут возникает первая проблема…
Если мы снимаем четко сверху, то вопросов нет, все будет так как описано выше. Но если под углом, то вот что происходит: так как BoundingBoxes от соседних машин могут пересекаться с соседними паркоместами, то в момент, когда одно из паркомест освобождается, модель не детектирует его полностью свободным, потому что одна из машин рядом пересекает одновременно два парковочных места (своё и освободившееся).
Вот что происходит, если мы посмотрим на это в цифрах:
Теперь вопрос: как «вытащить» нужное 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).
Настройка кода очень тонкая и, к сожалению, не универсальная. Уверен, при различных футожах будут возникать новые проблемы. В модели не реализовано определение нескольких парковочных мест одновременно, нет возможности определить пустые паркоместа на начальном кадре, и многое другое. Но сделана база и рассмотрены основные вопросы. Возможно при лучшей детекции на последних версиях YOLO часть вопросов можно будет откинуть (например, с неожиданным отключением детекции машины), однако, основную логику можно дорабатывать ещё долго, но уже в рамках коммерческих проектах.
Разработка этой версии заняло у меня три недели вялых ковыряний и два полных выходных усиленной разработки (плюс день на написание статьи).
На этом у меня всё! Надеюсь, был полезен этой статьей, буду благодарен комментариям и вопросам. В будущем планирую реализовать ещё несколько интересных проектов на основе Computer Vision и нейросетей.
Комментарии (30)
Alex-ok
30.05.2023 20:22+1Можно еще такую фичу прикрутить. Если авто уже стоит на парковке, но далековато - следить когда место поближе освободится, чтобы в случае чего выйти переставить. А то сосед мой весь вечер у окна по этому поводу дежурит.
csharpreader
30.05.2023 20:22+18Я смотрю, мир автовладельцев прямо соткан из приключений.
Alex-ok
30.05.2023 20:22Ну да, сейчас машину вечером удачно поставить тот еще квест.
csharpreader
30.05.2023 20:22+2Зачем тогда мучиться. Лет десять с женой ездим на такси. Из машины вышел, дверь захлопнул и не думаешь больше о ней ни секунды. И дешевле, чем владение своим автомобилем. И удобнее.
warhamster
30.05.2023 20:22+1И удобнее.
Если ездить не в часы пик и только туда, куда таксисты ездить не отказываются - тогда может быть. Иначе требуется очень много телодвижений, а результат достаточно непредсказуем, чтобы быть неприемлемым (ну, по крайней мере, в Новосибирске у яндекса так).
csharpreader
30.05.2023 20:22В Новосибирске таксисты особо одарённые, да. Например, однажды мне водитель предложил заехать забрать его жену «по пути» (учитывая, что поездка с приложения по Комфорту).
Geraclz
30.05.2023 20:22+1Попробуй добавить в систему функцию которая подсчитывает среднее время на освобождение парковочного места в зависимости от времени суток.
Пример: приезжаешь в час ночи, и отправляешь запрос боту о наличии свободных мест - он присылает ответ: 0 парковочных мест, ожидание более 1 часа времени ( типо в ночное время нет смысла ждать) а днем в зависимости от времени примерно можно определить
ret77876
30.05.2023 20:22Касательно ночи тоже интересный вопрос. Мне кажется, стандартная yolo 4 при плохом освещении будет плохо детектировать автомобили.
yesnogo
30.05.2023 20:22+1Или рассчитать вероятность появления свободного места в ближайшие 10(-15-20) мин. Учитывая, что большинство автовладельцев приезжают-уезжают в определенные промежутки времени, можно получить достаточно верный прогноз.
Mazepov Автор
30.05.2023 20:22Да, вот это интересно поисследовать. Можно собрать хорошую базу по времени и машинам (они во дворе всегда одни и те же, что логично) и сделать предиктор в какое время с какой вероятностью машина уедет.
Soorin
30.05.2023 20:22Подъезжая к дому, я всегда смотрю в камеры, установленные на окнах, чтоб понять, на какую сторону дома, и куда мне ехать. Чтоб уменьшить блики и непонятки, камеры давно вынесены на улицу и включены вcевозможные WDR и BLC. И... и вследствие неизбежной задержки при цифровой обработке и передаче сигнала от объектива камеры до экрана смартфона, зачастую я приезжаю к месту, куда уже кто-то паркуется (вариант - место занято незаметной даже человеку машиной "камуфляжного" грязно-серого цвета, мегабайком и т.п.)..
Аналитика ещё потратит время и ресурсы, сообщит о свободном месте, приедет автовладелец, а там - мопедик...
csharpreader
30.05.2023 20:22Вы так говорите, как будто аналитика – это бабушка, сидящая с термосом внутри банкомата и выдающая деньги. Секунду всё это происходит.
неизбежной задержки при цифровой обработке и передаче сигнала от объектива камеры до экрана смартфона
Ну, секунда этот лаг. Или у вас видеопоток на голубиную почту завязан? )
YegorP
30.05.2023 20:22У нас один провайдер (Интерсвязь) весь микрорайон и подъезды камерами облепил, причём доступными жителям в приложении. Лаг стримов - до 1 минуты. Голубиная почта?
Причём они прикрутили функционал определения парковочных мест на отдельных камерах похожим способом. И вот пока ты едешь к парковке, есть вполне ощутимая вероятность не успеть.
Понятно, что со своей камерой на балконе можно добиться меньшего лага. Но и соревноваться придётся за более узкий пятачок в поле зрения своей камеры.
csharpreader
30.05.2023 20:22Какие-то особо одарённые настройки, или особо одарённые админы настраивали. Или какое-то черезпопное кривое приложение.
У нас в компании 200 объектов от Калининграда до Хабаровска. Лаг видеопотока от силы 1 секунда.
aim
30.05.2023 20:22Что мы подразумеваем под паковочными местами?
занятно что большая часть того что там на фотках запарковано с нарушением. то есть подход простой, но в корне не верный.
Mazepov Автор
30.05.2023 20:22Можно добавить фичу по обнаружению неправильной парковки например, на газоне и помечать, что на этом месте нельзя парковаться (для добросовестных водителей), парковка с нарушением. Звучит интересно, можно попробовать реализовать такую штуку в этой программе. Спасибо за идею!)
YVGrinev
30.05.2023 20:22Две похожие статья уже были на хабре. Одна из них их песочницы, другая - перевод. Ещё была статья про целый сервис, который умеет автоматически учиться определять парковочные места на площадке перед вузом.
Почему такая старая архитектура взята? Давно есть v5, v6, v8
Mazepov Автор
30.05.2023 20:22Если честно, не могу ответить на вопрос про версию) Просто начал реализацию с 4 версии и не хотелось потом менять. В будущем попробую освоить более новую архитектуру.
safari2012
30.05.2023 20:22Я бы добавил отправку жалобы в ГИБДД на оленя, который паркуется на газоне...
csharpreader
30.05.2023 20:22Разработчик считает это парковочным местом, судя по скрину ))
Mazepov Автор
30.05.2023 20:22На самом деле так не считаю, просто модель так отработала) Чуть выше написал, что появилась идея добавить обнаружение неправильной парковки на газоне и исключать это место из списка парковочных.
Dynasaur
30.05.2023 20:22YOLO (взял не самую новую 4 версию)
Интересно почему и как выбрали четвёрку при наличии восьмой версии, которая вроде как эффективнее?
Mazepov Автор
30.05.2023 20:22Чуть выше уже писал, что у меня нет ответа на этот вопрос))) Почему то начал делать на 4 версии, а потом уже не стал менять... Следующие проекты буду пытаться реализовывать на последних версиях.
dcooder
30.05.2023 20:22С точки зрения практической пользы есть проблемы: чтобы пойти домой тебе в любом случае нужно где-то припарковать автомобиль. То есть либо кого-то подпереть, либо встать далеко от дома. В первом случае возникает риск того, что подпертый тобой автомобиль захочет уехать раньше, чем на парковке освободиться место. Во втором случае тебе по сигналу нейросети придется ножками топать до того места, куда ты припарковал авто (какая разница топать сейчас чтобы переставить машину или завтра утром?). И не факт, что за это время место уже кто-нибудь не займет.
Поэтому такая фича была бы полезна как опция автопилота, но в случае с опцией автопилота разумнее отслеживать не освобождение мест на парковке, а момента, когда подпертый тобой автомобиль захочет выехать, чтобы автопилот его выпустил, ну и возможно встал на его место.
Опять же, ваша модель скорее всего подойдет для каких-нибудь парковок в торговых центрах. Вешается табло на въезде, где отображается количество свободных мест. Если свободных мест нет, то водители будут это видеть и не будут заезжать на перегруженную парковку. Хотя для этой задачи есть более простое решение с подсчетом количества заехавших и выехавших машин. Но у вашего решения есть небольшое преимущество: свободное место на табло появится как только машина освободила парковочное место. А в варианте с подсчетом только когда машина выехала за территорию парковки. Но количество заехавших все равно придется считать, чтобы считать освободившееся место занятым после въезда автомобиля на территорию парковки, а то заедут две машины на одно свободное место и устроят мордобой )
Wesha
Раз все машины стоят под углом — не проще было сразу закодить не прямоугольник, а параллелограм?
berng
Yolo c параллелограммами не работает - надо переделывать архитектуру и переписывать кучу метрик и кода. Попробуйте, если получится - вам куча программеров благодарна будет, эта задача давно висит.