Reading Time: 11 minutes

Наверняка вам хочется применить Computer Vision для чего то крутого прямо сейчас ? И мы будем заниматься именно этим! Сегодня мы сделаем систему автопилота, очень упрощенную, для автомобилей, которая сможет определять что можно начинать движение на светофоре. Для написания системы нам потребуются OpenCV3 и Python3, плюс видеорегистратор, либо видео с него. В статье будет приведена ссылка на массив видео, с которым вы можете потренироваться. Итоговый алгоритм вы сможете применить даже на RaspberryPi, он будет очень быстрым и сможет работать даже на ней в реальном времени. Начнем!

Постановка задачи

Мы живем в России, а значит все очень любим видеорегистраторы. Именно такое видео мы и будем обрабатывать в нашей системе автопилота. Оно отличается тем, что угол обзора видео составляет до 170 градусов и разрешение видео составляет от 1280×720 до 1920×1080, с частотой кадров 30 либо 60 кадров в секунду. Примеры для тестов вы можете скачать по следующей ссылке: https://goo.gl/CziDCc Для понимания как это выглядит:

Пример видео с регистратора

Пример видео с регистратора

Наша задача будет в следующем: определить на каком кадре в видео сигнал светофора измениться с красного на зеленый. Сохранить найденный номер кадра и название соответствующего файла в файл. Далее мы посчитаем точность нашего алгоритма, выбрав тестовую и обучающие выборки. В конце мы добавим маленький бонус – научим нашу программу резко газовать на зеленый сигнал ?

Начинаем работу с видео

Это первый раз когда мы работаем с записанным видео на OpenCV, даже более того, записанным не в нашей программе, а со стороны. Если вы устанавливали сборку OpenCV с Anaconda, то скорее всего у вас не будет проблем с чтением данных файлов. Однако для всех, у кого возникнут проблемы, я предлагаю собрать OpenCV с пакетом FFMpeg. Данный пакет поддерживает большинство форматов как аудио- так и видеофайлов. Инструкцию по установке OpenCV с данным пакетом вы можете найти здесь и здесь.

Начнем работать с файлами, для начала мы будем все операции проводить только над одним файлом, в дальнейшем мы данную функцию сможем применить ко всем файлам в списке либо директории:

import numpy as np
import cv2 

video_file = 'akn.158.044.left.avi'     #указываем имя файла в переменной, 
cap = cv2.VideoCapture(video_file)      #для последующего автоматического перебора файлов

while(cap.isOpened()):
    ret, frame = cap.read()             #получаем кадр из видеопотока
    if frame is None:                   #проверка на корректность кадра
        break
    cv2.imshow("video_frame", frame)    #показываем кадр в opencv окне
    if cv2.waitKey(1) & 0xFF == ord('q'): 
        break                           #в случае нажатия клавиши q выходим из цикла
cap.release()
cv2.destroyAllWindows()
cv2.waitKey(1);                         #костыль для закрытия окна, может быть нужен в jupyter

Размерем подробнее код:

  • Строки 1-2: Добавляем пакеты с numpy и opencv в нашу программу, numpy необходим, так как opencv основывается на нем и все изображения представлены как массивы ndarray.
  • Строки 4-5: Открываем файл с записью работы видеорегистратора, мы сохранили название файла в специальную переменную, для дальнейшей автоматической обработки списка файлов
  • Строки 8-13: Получаем изображения из видеопотока и отрисовываем его средствами OpenCV, добавляем возможность выхода из приложения клавишей q
  • Строки 14-16: Закрываем все потоки и окна

Данный код просто открывает файл и показывает на экране, что тут может быть интересного? На самом деле мы запускаем этот код ради проверки того, что у вас нормально открывается видеофайлы нашего формата. Если у вас все запустилось без ошибок и вы увидели видео, переходим к следующей главе, если нет – попробуйте еще раз установить OpenCV3 с FFMpeg. Если проблемы не исчезнут, пишите в комментариях, решим их вместе.

Алгоритм для нахождения кадра с переключением сигнала

Приступаем к самому интересному – алгоритму нахождения переключения сигнала светофора с красного на зеленый. Я знаю, сейчас расцвет нейронных сетей, компании выпускают все более простые и удобные пакеты для глубокого обучения, такие как CatBoost, TensorFlow, Caffe. Даже последнее обновление OpenCV 3.3.0 в основном заключается в интеграции с основными библиотеками для ML. Но в данном случае мы сможем обойтись только самыми простыми средствами, такими как определение цвета, нахождение контуров и расстояний между точками. Мне кажется, уметь решать задачу просто – очень важный навык.

Приведу пример того, что мы хотим найти:

Пример светофора

Пример светофора

Опишем то, что происходит в кадре: в первых кадрах в верхней части изображения есть “красный круг” который в определенный момент времени пропадает и заменяется темным участком. В тоже время, спустя некоторое количество кадров, в нижней части появляется “зеленый” круг. Именно данный момент можно считать за искомый.

Теперь распишем это в терминах алгоритма для программы:

  • Находим все “красные” участки в кадре, кладем их все в массив, связанный с кадром
  • Находим в текущем кадре все пропавшие “красные” участки – добавляем в массив возможных красных сигналов светофора текущего кадра
  • Находим все “зеленые” участки в кадре также кладем их в массив
  • Находим все “зеленые” участки в кадре, которых не было в предыдущих кадрах – добавляем в массив возможных зеленых сигналов светофора текущего кадра
  • Проверяем все возможные сигналы, как красные так и зеленые, на то что они близки по форме к кругу
  • Делаем проверку на относительные размеры красных и зеленых кругов и на взаимное расположение.
  • Если находим удовлетворяющие нас пары – мы нашли переключение светофора!

Алгоритм выглядит очень просто, но дьявол кроется в мелочах ? Далее мы начнем показывать реализацию алгоритма, опишем шаги, в которых надо сделать хитрые действия и покажем как можно оптимизировать алгоритм.

Находим красные и зеленые круги

Начнем с первых пунктов – находим все “красные” участки в кадре. Как мы уже видели в уроке для этого используем пространство HSV. Первым делом переведем кадр в него:

frame_hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

Далее я специально поместил “красный” в ковычки. Мы не находим чистый красный цвет, который соответствует (255,0,0) в RGB. Причина этого – мы находимся в реальном мире, где красный сигнал светофора может иметь оттенок от оранжевого до фиолетового, в зависимости от того, как сработает автоматический выбор баланса белого в регистраторе. Светофоры, ко всему прочему, могут быть основаны на различных технологиях, от ламп накаливания до светодиодов, в каждом случае имея различный оттенок цвета.

Представим необходимые нам цвета на цветовом круге HSV:

HSV красный оттенок

HSV красный оттенок

На данном цветовом круге выделена область оттенков, которую мы решаем считать за “красный”. Выбор данной области на самом деле очень много значит, так как увеличение числа оттенков позволит охватить большее число различных вариаций светофоров, регистраторов и погодных условий. Но чрезмерное увеличение данной области приведет к случайным срабатываниям, что сведет ценность всей системы на нет.

Запишем приведенный диапазон в коде и найдем в изображении:

#красный цвет представляет из себя две области в пространстве HSV
lower_red = np.array([0, 85, 110], dtype = "uint8")
upper_red = np.array([15, 255, 255], dtype = "uint8")

#красный в диапазоне фиолетового оттенка
lower_violet = np.array([165, 85, 110], dtype = "uint8")
upper_violet = np.array([180, 255, 255], dtype = "uint8")

red_mask_orange = cv2.inRange(frame_hsv, lower_red, upper_red)        #применяем маску по цвету
red_mask_violet = cv2.inRange(frame_hsv, lower_violet, upper_violet)  #для красного таких 2

red_mask_full = red_mask_orange + red_mask_violet #полная масква предствавляет из себя сумму

Подробнее рассмотрим данный код:

  • Строки 2-3: В данных строках мы задаем границы для красного цвета, заметьте, что значения для оттенка (H) составляют от 0 до 15 единиц, хотя на круге значения составляют от 0 до 30 единиц. Такое преобразование необходимо потому, что шкала HSV в OpenCV3 лежит в значениях H(0-180) в отличии от шкалы в стандартном пространстве H(0-360). В дальнейшем если будете брать значение из онлайновых средств или редакторов, делайте такую поправку.
  • Строки 6-7: Мы выделяем еще один диапазон для красного цвета. Так надо сделать, потому что  оттенки красного лежат как в начале шкалы, где они ближе к оранжевому, так и в конце, ближе к фиолетовому. Для наших реальных светофоров нам необходимы оба диапазона.
  • Строки 9-10: Применяем маску по цвету стандартными средствами OpenCV3. Так как диапазона цветов два, необходимы две маски.
  • Строка 12: Итоговая маска представляет из себя сумму оранжевых и фиолетовых масок.

Проверим наш код на видео:

Красная маска

Красная маска

И отдельно на светофорах:

Светофоры с красной маской

Светофоры с красной маской

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

Решением этой проблемы – не только красные сигналы, но и зеленые, которые появляются на светофоре. Найдем их в нашем кадре:

#с зеленым все проще - он в центре диапазона
lower_green = np.array([40, 85, 110], dtype = "uint8")
upper_green = np.array([91, 255, 255], dtype = "uint8")
    
#применяем маску
green_mask = cv2.inRange(converted, lower_green, upper_green)

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

HSV зеленый диапазон

HSV зеленый диапазон

Проверим как данная маска выглядит на полном кадре:

Зеленая маска на кадре

Зеленая маска на кадре

Шума на ней гораздо меньше, это благодаря тому, что мы в городе ? на природе все становится гораздо хуже, но мы это и сами увидим. Самое приятное – мы видим наш сигнал зеленого светофора. Посмотрим как это выглядит на вырезанном фрагменте только со светофором:

Светофоры с зеленой маской

Светофоры с зеленой маской

Отлично! Мы четко видим требуемый зеленый сигнал. ?

Осталось дело за малым – связать все сигналы и сказать что пора ехать, светофор горит зеленым!

Добавляем в наш алгоритм время

Двигаемся по алгоритму дальше. Следующее действие, которое мы должны сделать – создать массивы с нашими найденными красными и зелеными областями и проследить когда один сигнал появляется а другой исчезает. Другими словами – мы должны добавить ось времени, на которой наш светофор выглядит следующим образом:

Временная развертка

Временная развертка

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

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

last_red_frame = None   #задаем пустые прошлые кадры
last_green_frame = None #красные и зеленые, для дальнейшей работы
while(cap.isOpened()):
    ret, frame = cap.read()
    if frame is None:
        break

    #рассчитываем зеленые и красные маски для текущего кадра
    frame_hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    red_mask_frame = red_mask(frame_hsv)
    green_mask_frame = green_mask(frame_hsv)
    
    #проверяем прошлые кадры, если они пустые - заполняем их текущими 
    #масками
    if last_red_frame is None:
        last_red_frame = red_mask_frame
    if last_green_frame is None:
        last_green_frame = green_mask_frame
    
    #здесь находиться логика по получению разницы в кадрах
    #заметьте, что порядок в действиях для зеленого и красного кадра различный, это надо из логики - красный гаснет, зеленый загорается
    red_dif_frame = (last_red_frame-red_mask_frame)
    green_dif_frame = (green_mask_frame-last_green_frame)
    
    #здесь мы делаем отсечку для изображений, так как в процессе вычитания мы получаем следующие цифры
    #255-0=0    0-0=0    0-255=1 мы боремся с последним случаем
    ret, red_dif_frame = cv2.threshold(red_dif_frame,127,255,cv2.THRESH_BINARY)
    ret, green_dif_frame = cv2.threshold(green_dif_frame,127,255,cv2.THRESH_BINARY)
    
    last_green_frame = green_mask_frame
    last_red_frame = red_mask_frame
    
    #отрисовываем оба кадра
    cv2.imshow("red_dif_frame", red_dif_frame)
    cv2.imshow("green_dif_frame", green_dif_frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
        
cap.release()
cv2.destroyAllWindows()
cv2.waitKey(1);

Покажем на рисунке что делаем данный код и разберем в нем интересные моменты

Схема работы разностной схемы в один кадр

Схема работы разностной схемы в один кадр

  • Строки 1-2: Мы объявляем переменные для прошлых кадров масок, красной и зеленой
  • Строки 9-11: Вычисляем маски, используя диапазоны цветов для красного и зеленого спектра
  • Строки 14-17: В первый раз мы инициализируем переменные текущими кадрами. Мы ничего не теряем от такого действия, так как считаем, что в первый кадр, если переключение и было, мы его не учитываем.
  • Строки 22-23: Рассчитываем разницу между кадрами. В случае с красным – мы хотим получить места, которые пропали в текущем кадре, поэтому мы вычитаем из прошлого кадра текущий. В случае с зеленым нам интересны места, которые появились. Таким образом, мы вычитаем из текущего кадра прошлый.
  • Строки 28-29: Проводим операцию по пороговой отрезке всех значений. Это необходимо произвести для того, чтобы устранить следующий эффект: когда мы вычитаем из пикселя, который имел значение 0, пиксель со значением 255, то мы получаем значение 1. Которое происходит их характера данных uint8. В нашем случае, необходимо такие значения сделать равными 0.

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

Итоги

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

В следующей главе мы доведем эту часть до хорошего результата. Введем проверки на “круглость” областей. Научимся определять пространственное и временное расстояние между объектами и событиями в кадре. Проведем тестирование нашего алгоритма, и поставим открытые вопросы по его улучшению. Чтобы не пропустить это – добавляйте сайт в избранное и следите за новостями. Спасибо и хорошего программирования!


5 Comments

Сергей · 27/07/2020 at 12:18

Отличная статья! Но где же продолжение? Просим, просим!

Дмитрий · 25/03/2020 at 23:22

Очень круто

Олег · 28/10/2019 at 23:32

Где же продолжение?

Anonymous · 01/11/2017 at 12:53

Спс! Достаточно просто и понятно.

Владимир · 24/09/2017 at 20:14

Кирилл, спасибо за интересную и полезную статью! Интересный подход к довольно не тривиальной и важной задаче. Буду с нетерпением ждать продолжения цикла и следить за обновлениями.

Leave a Reply to Дмитрий Cancel reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.