- Вводная
- Уроки рисования
- Линия
- Прямоугольник
- Окружность
- Текст
- Функции в программировании
- Нарисуйте геометрические фигуры на изображениях с помощью OpenCV
- Нарисуйте линию:
- Нарисуйте прямоугольник:
- Нарисовать круг :
- Написание текста:
- Введение в OpenCV применительно к распознаванию линий дорожной разметки
- Принцип работы детектора
- Шаг 1: Предварительная обработка и векторизация
- Шаг 2: Обновление линий дорожной разметки
- Шаг 3: Рисование и обновление исходного изображения
- Что дальше?
- P.S. А теперь сломаем все!
Вводная
NumPy для Питона устанавливали здесь вместе с OpenCV.
NumPy — это самобытная библиотека, позволяющая удобно работать с большими структурированными объемами данных т.е. массивами, которые могут быть как одномерными
так и двух- и трех- и еще более многомерными.
Так как объект нашего курса — изображение, то применение NumPy здесь в основном следующее: изображение — это матрица пикселей (двумерный массив), а каждый пиксель представляется тремя цветами (для цветного изображения).
Один пиксель:
[R,G,B]
Строка изображения:
[ [R,G,B],[R,G,B],[R,G,B],[R,G,B], . ]
Все изображение:
[
[ [R,G,B],[R,G,B],[R,G,B],[R,G,B], . ],
[ [R,G,B],[R,G,B],[R,G,B],[R,G,B], . ],
[ [R,G,B],[R,G,B],[R,G,B],[R,G,B], . ],
…
[ [R,G,B],[R,G,B],[R,G,B],[R,G,B], . ]
]
При квадратном изображении со стороной в n пикселей имеем трехмерный массив n*n*3. NumPy даёт нам возможность создать эту структуру одной строкой:
Функция zeros() создает массив, заполненный нулями согласно аргументам:
- shape — размерность — в нашем случае это матрица 512 на 512, в которой каждый элемент — это тоже массив из 3х элементов
- dtype — тип данных, необязательный аргумент — по умолчанию применяет float (число с плавающей точкой типа 39.0), но мы указываем 8 бит значение, так как каждый из трех цветов (Red, Green, Blue) имеет градацию от 0 до 255 — это 256 значений — 2 в восьмой степени т.е. значение, кодируемое 8 битами.
Записанная в переменную img матрица как раз является стандартным представлением изображения в OpenCV, поэтому без проблем выводим черный квадрат на экран в новом окне:
Время экспериментов! Что будет, если каждый элемент нашего массива будет случайным числом от 0 до 255? Это легко проверить:
Конечно, есть вероятность, что на экран выведется пейзаж заката на Марсе, но на деле всегда получается просто белый шум:
Уроки рисования
Наверняка вы видели примеры работы OpenCV следующего вида:
В данном случае демонстрируется функция Распознавания объектов. Забегая вперед скажу, что в коде это представляет собой тоже буквально одну функцию, в которую аргументами передаются кадр и файл обученного алгоритма, однако наносить на выводимое изображение красивые рамки и надписи придется отдельно. Посмотрим как это делается.
Линия
Чтоб не мучать глаза, рисовать будем на старом черном фоне, но для тренировки навыков рекомендую самостоятельно окрасить рабочий фон в любой другой цвет или же вообще открыть готовое изображение из файловой системы.
Новая линия задается функцией объекта OpenCV:
в которую по порядку передаем следующие аргументы:
- img — переменная с OpenCV-изображением, в нее же запишется результат выполнения
- (0,0) — координаты начала
- (511,511) — координаты конца
- (255,0,0) — цвет, по умолчанию используется формат BGR, то есть Blue=255, а все остальные цвета нулевые — это будет чисто синий цвет
- 50 — толщина линии в пикселях
Прямоугольник
Строится функцией rectangle():
Со следующими аргументами:
- img — переменная с изображением-полотном
- координаты левой верхней точки
- координаты правой нижней точки
- цвет
- толщина линий, при отрицательном значении фигура закрашивается цветом линии
Окружность
Строится функцией circle():
Со следующими аргументами:
- img — переменная с изображением-полотном
- (447,63) — координаты центра окружности
- 63 — радиус окружности
- (255,0,0) — цвет
- -1 — заливка цветом окружности или толщина линии (>0)
Текст
Выводится функцией putText():
Со следующими аргументами:
- img — переменная с изображением-полотном
- ‘OpenCV’ — текст, который необходимо вывести
- (10,400) — координаты левой нижней точки курсора, откуда начнет печататься текст
- font — переменная с выбранным шрифтом
- 4 — размер шрифта
- (255,255,255) — цвет
- 3 — толщина линии
- cv2.LINE_AA — алгоритм отображения линии, кроме этого существуют LINE_4 и LINE_8 дающее худшее качество кривых линий
Выбор шрифтов не слишком разнообразен:
- FONT_HERSHEY_SIMPLEX
- FONT_HERSHEY_PLAIN
- FONT_HERSHEY_DUPLEX
- FONT_HERSHEY_COMPLEX
- FONT_HERSHEY_TRIPLEX
- FONT_HERSHEY_COMPLEX_SMALL
- FONT_HERSHEY_SCRIPT_SIMPLEX
- FONT_HERSHEY_SCRIPT_COMPLEX
Функции в программировании
Это объединения повторяющихся участков кода в единый неразрывный блок из нескольких строк в одном месте для удобного обращения к ним.
Рисуя фигуры мы как раз обращались к функциям, только их код находится в одном из файлов библиотеки, которую импортируем в начале скрипта.
Объявление новой функции происходит следующим образом:
Аргументов может и не быть, в этом случае остаются пустые скобки с двоеточием. Следующая строка после объявления функции пишется с отступом, увеличенным на единицу.
В Python окончание функции никак не обозначается, вместо этого идет пустая строка, и далее код продолжаем писать с отступом, какой был у строки объявления функции.
Переходим к практике — напишем функцию, выводящую рамки с надписями как в примере с распознаванием автомобилей. Назовем ее rect (rectangle — прямоугольник), а передавать ей будем следующие аргументы:
- img — переменную с изображением-полотном
- координату Х левого верхнего угла прямоугольника
- координату Y левого верхнего угла прямоугольника
- ширину
- высоту
- надпись (не будет являться обязательным аргументом)
Чтоб сделать аргумент необязательным при объявлении новой функции следует сразу задать его значение по умолчанию — в нашем случае это пустая строка. Если же при вызове функции этот аргумент будет указан, то его значение просто перепишется поверх дефолтного.
Далее нарисуем сам прямоугольник, благо все для этого у нас есть: полотно и координаты. Цвет и толщину рамки не позволим выбирать вообще — для упрощения, а то аргументов и так немало.
Координаты. Функция отрисовки требует ввода координат углов прямоугольника: с первым все понятно (x,y), а второй получим прибавив полученные размеры к координатам левого верхнего угла: (x+w,y+h).
Готово, уже можно пробовать рисовать рамки в промышленных масштабах только что написанной упрощенной функцией:
Текст. Является необязательным аргументом, поэтому дальше будем действовать через условие.
Если полученный аргумент не равен дефолтному (пустой строке) — попадаем в тело условия, где рисуем текст, иначе пропускаем это действие.
Располагать текст будет над рамкой слева, и прежде необходимо вывести фон, на котором текст будет хорошо читаться. Сделаем это с помощью того же прямоугольника, только на этот раз полностью закрасим его площадь. Но еще нужно получить его размеры:
Хнач равен этой же координате основной рамки: х
Yнач будет равен этой же координате основной рамки с вычетом высоты шрифта y-22
Хконц равен Хнач с прибавлением длины текста: x+l
Yконц высоте начала основной рамки, то есть y
Таким образом фон текста будем выводить командой:
Длину текста l вычислим получив количество символов стандартной функцией Питона len(), умножив полученное на примерную длину одного символа:
Остается поверх фона вывести сам текст. Координатами для текста как раз послужат начальные координаты основной рамки:
Вот теперь весь функционал функции rect(img,x,y,w,h,text=»») готов к проверке:
При желании можно поэкспериментировать с “примерными” коэффициентами, цветами и шрифтами.
Источник
Нарисуйте геометрические фигуры на изображениях с помощью OpenCV
OpenCV предоставляет множество функций рисования для рисования геометрических фигур и написания текста на изображениях. Давайте посмотрим на некоторые функции рисования и нарисуем геометрические фигуры на изображениях с помощью OpenCV.
Некоторые из функций рисования:
cv2.line() : Used to draw line on an image.
cv2.rectangle() : Used to draw rectangle on an image.
cv2.circle() : Used to draw circle on an image.
cv2.putText() : Used to write text on image.
Чтобы продемонстрировать использование вышеупомянутых функций, нам нужно изображение размером 400 X 400, заполненное сплошным цветом (в данном случае черным). Для этого мы можем использовать функцию numpy.zeroes для создания необходимого изображения.
# Python3 программа для рисования сплошным цветом
# изображение с помощью функции numpy.zeroes ()
import numpy as np
# Создание черного изображения с 3 каналами
# RGB и тип unsigned int
img = np.zeros(( 400 , 400 , 3 ), dtype = «uint8» )
cv2.imshow( ‘dark’ , img)
# Позволяет нам видеть изображение
# до принудительного закрытия
Выход :
Теперь давайте нарисуем несколько геометрических фигур на этом сплошном черном изображении.
Нарисуйте линию:
cv2.line(imageObjectName, (‘start_coordinates’), (‘end_coordinates’), (‘color_in_bgr’), ‘line_thickness’)
# Python3 программа для рисования линий
# форма на сплошном изображении
import numpy as np
# Создание черного изображения с 3 каналами
# RGB и тип unsigned int
img = np.zeros(( 400 , 400 , 3 ), dtype = «uint8» )
cv2.line(img, ( 20 , 160 ), ( 100 , 160 ), ( 0 , 0 , 255 ), 10 )
cv2.imshow( ‘dark’ , img)
# Позволяет нам видеть изображение
# до принудительного закрытия
Выход :
Нарисуйте прямоугольник:
cv2.rectangle(imageObjectName, (‘top_left_vertex_coordinates’), (‘lower_right_vertex_coordinates’), (‘stroke_color_in_bgr’), ‘stroke_thickness’)
# Python3 программа для рисования прямоугольника
# форма на сплошном изображении
import numpy as np
# Создание черного изображения с 3
# каналы RGB и тип unsigned int
img = np.zeros(( 400 , 400 , 3 ), dtype = «uint8» )
cv2.rectangle(img, ( 30 , 30 ), ( 300 , 200 ), ( 0 , 255 , 0 ), 5 )
cv2.imshow( ‘dark’ , img)
# Позволяет нам видеть изображение
# до принудительного закрытия
Выход :
Нарисовать круг :
cv2.circle(imageObjectName, (‘center_coordinates’), (‘circle_radius’), (‘color_in_bgr’), ‘stroke_thickness’)
# Python3 программа для рисования круга
# форма на сплошном изображении
import numpy as np
# Создание черного изображения с 3
# каналы RGB и тип unsigned int
img = np.zeros(( 400 , 400 , 3 ), dtype = «uint8» )
cv2.circle(img, ( 200 , 200 ), 80 , ( 255 , 0 , 0 ), 3 )
cv2.imshow( ‘dark’ , img)
# Позволяет нам видеть изображение
# до принудительного закрытия
Выход :
Написание текста:
cv2.putText(imageObjectName, ‘TextContent’, (‘text_starting_point_coordinates’), ‘fontToBeUsed’, ‘font_size’, (‘text_color’, ‘text_thickness’, ‘line_type’)
# Python3 программа для записи
# текст на сплошном изображении
import numpy as np
# Создание черного изображения с 3
# каналы RGB и тип unsigned int
img = np.zeros(( 400 , 400 , 3 ), dtype = «uint8» )
cv2.putText(img, ‘GeeksForGeeks’ , ( 50 , 50 ),
font, 0.8 , ( 0 , 255 , 0 ), 2 , cv2.LINE_AA)
cv2.imshow( ‘dark’ , img)
# Позволяет нам видеть изображение
# до принудительного закрытия
Выход :
Аппликации нанесения фигур на изображения:
- Рисование геометрических фигур может помочь нам выделить отдельные части изображения.
- Геометрические формы, такие как линия, могут помочь нам указать или идентифицировать определенные области на изображении.
- Написание текста на определенных областях изображений может добавить описание к этой области.
Источник
Введение в OpenCV применительно к распознаванию линий дорожной разметки
Привет, Хабр! Публикуем материал выпускника нашей программы Deep Learning и координатора программы по большим данным, Кирилла Данилюка о его опыте использования фреймворка компьютерного зрения OpenCV для определения линий дорожной разметки.
Некоторое время назад я начал программу от Udacity: “Self-Driving Car Engineer Nanodegree”. Она состоит из множества проектов по различным аспектам построения системы вождения на автопилоте. Представляю вашему вниманию мое решение к первому проекту: простой линейный детектор дорожной разметки. Чтобы понять, что в итоге получилось, посмотрите сначала видео:
Цель данного проекта — построить простую линейную модель для покадрового распознавания полос движения: на вход получаем кадр, серией трансформаций, о которых поговорим далее, обрабатываем его, получаем отфильтрованное изображение, которое можно векторизовать и обучить две независимых линейных регрессии: по одной для каждой полосы. Проект намеренно простой: только линейная модель, только хорошие погодные условия и видимость, только две линии разметки. Естественно, это не продакшн-решение, однако даже такой проект позволяет вдоволь наиграться с OpenCV, фильтрами и, в целом, помогает почувствовать, с какими трудностями сталкиваются разработчики автопилотов в автомобилях.
Принцип работы детектора
Процесс построения детектора состоит из трех основных шагов:
- Предобработка данных, фильтрация от шума и векторизация изображения.
- Обновление состояния линий дорожной разметки по данным из первого шага.
- Рисование обновленных линий и других объектов на исходном изображении.
Сначала на вход функции image_pipeline подается 3-канальное изображение формата RGB, которое затем фильтруется, преобразовывается, а внутри функции обновляются объекты Line и Lane . Затем поверх самого изображения рисуются все необходимые элементы, как показано ниже:
Я старался подходить к задаче в стиле ООП (в отличие от большинства аналитических задач): так, чтобы каждый из шагов получился изолированным от других.
Шаг 1: Предварительная обработка и векторизация
Первая стадия нашей работы хорошо знакома data scientist-ам и всем, кто работает с “сырыми” данными: сперва мы должны сделать предобработку данных, а затем векторизовать в понятный для алгоритмов вид. Общий пайплайн для предобработки и векторизации исходного изображения следующий:
В нашем проекте используется OpenCV — один из самых популярных фреймворков для работы с изображениями на пиксельном уровне с помощью матричных операций.
Сначала мы преобразуем исходное RGB-изображение в HSV — именно в этой цветовой модели удобно выделять диапазоны конкретных цветов (а нас интересуют оттенки жёлтого и белого для определения полос движения).
Обратите внимание на скриншот ниже: выделить «всё жёлтое» в RGB гораздо сложнее, чем в HSV.
После перевода изображения в HSV некоторые рекомендуют применить размытие по Гауссу, но в моём случае оно снизило качество распознавания. Следующая стадия — бинаризация (преобразование изображения в бинарную маску с интересующими нас цветами: оттенками желтого и белого).
Наконец, мы готовы векторизировать наше изображение. Применим два преобразования:
- Детектор границ Кэнни: алгоритм оптимального определения границ, который рассчитывает градиенты интенсивности изображения, а затем с помощью двух порогов удаляет слабые границы, оставляя искомые (мы используем (280, 360) ) как пороговые значения в функции canny .
- Преобразование Хафа: получив границы с помощью алгоритма Кэнни, мы можем соединить их с помощью линий. Я не хочу вдаваться в математику алгоритма — она достойна отдельного поста — эта ссылка или ссылка выше поможет вам, если вас заинтересовал метод. Главное, что, применив это преобразование, мы получаем набор линий, каждая из которых, после небольшой дополнительной обработки и фильтрации, становится экземпляром класса Line с известным углом наклона и свободным членом.
Очевидно, что верхняя часть изображения вряд ли будет содержать линии разметки, поэтому её можно не принимать в расчёт. Способов два: либо сразу закрасить верх нашей бинарной маски черным, либо подумать над более умной фильтрацией линий. Я выбрал второй способ: я посчитал, что всё, что находится выше линии горизонта, не может быть линией разметки.
Линию горизонта (vanishing point) можно определить по той точке, в которой сходится правая и левая полоса движения.
Шаг 2: Обновление линий дорожной разметки
Обновление линий дорожной разметки будет происходить с помощью функции update_lane(segments) в image_pipeline , которая на вход получает объекты segments с последнего шага (которые на самом деле являются объектами Line из преобразования Хафа).
Для того, чтобы облегчить процесс, я решил использовать ООП и представлять линии дорожной разметки как экземпляры класса Lane : Lane.left_line, Lane.right_line . Некоторые студенты ограничились добавлением объекта `lane` в глобальный неймспейс, но я не фанат глобальных переменных в коде.
Рассмотрим подробнее классы Lane и Line и их экземпляры:
Каждый экземпляр класса Line представляет собой отдельную линию: кусок дорожной разметки или просто любую линию, которая будет определена преобразованием Хафа, в то время как главная цель объектов класса Lane — выявлять, является ли данная линия сегментом дорожной разметки. Чтобы это сделать, будем руководствоваться следующей логикой:
- Линия не может быть горизонтальной и должна иметь умеренный уклон.
- Разница между уклонами линии дорожной разметки и линии-кандидата не может быть слишком высокой.
- Линия-кандидат не должна отстоять далеко от дорожной разметки, к которой она принадлежит.
- Линия-кандидат должна быть ниже горизонта
Таким образом, для определения принадлежности к линии разметки мы используем достаточно тривиальную логику: принимаем решения исходя из уклона линии и расстояния до разметки. Способ неидеальный, но он сработал для моих простых условий
Класс Lane является контейнером для левой и правой линии разметки (рефакторинг так и просится). В классе также представлено несколько методов, относящихся к работе с линиями разметки, самый важный из которых fit_lane_line . Для того, чтобы создать новую линию разметки, я представляю подходящие сегменты разметки в виде точек, а затем аппроксимирую их полиномом первого порядка (то есть линией) с помощью обычной функции numpy.polyfit
Стабилизация полученных линий дорожной разметки очень важна: исходное изображение очень зашумлено, а определение полос происходит покадрово. Любая тень или неоднородность дорожного покрытия сразу меняет цвет разметки на такой, который наш детектор определить не в состоянии… В процессе работы я использовал несколько способов стабилизации:
- Буферы. Полученная линия разметки запоминает N предыдущих состояний и последовательно добавляет состояние линии разметки на текущем кадре в буфер.
- Дополнительная фильтрация линий с учётом данных в буфере. Если после преобразования и очистки мы не смогли избавиться от шума в данных, то есть вероятность, что наша линия окажется выбросом, а, как мы знаем, линейная модель чувствительна к выбросам. Поэтому для нас принципиально высокое значение точности — даже в ущерб значительной потери полноты. Проще говоря, лучше отфильтровать правильную линию, чем добавить в модель выброс. Специально для таких случаев, я создал DECISION_MAT — матрицу “принятия решения”, которая решает, как соотнести текущий уклон линии и среднее по всем линиям в буфере.
Например, для DECISION_MAT = [ [ 0.1, 0.9] , [1, 0] ] мы рассматриваем выбор из двух решений: считать линию нестабильной (т.е. потенциальным выбросом), либо стабильной (ее наклон соответствует среднему наклону линий данной полосы в буфере плюс/минус пороговое значение). Если линия нестабильна, мы всё равно хотим не потерять её: она может нести информацию о реальном повороте дороги. Просто учитывать её мы будем с маленьким коэффициентом (в данном случае — 0.1) Для стабильной линии мы просто будем использовать ее текущие параметры без какого либо взвешивания по предыдущим данным.
Индикатор стабильности линии разметки в текущем кадре описывается объектами класса Lane : Lane.right_lane.stable и Lane.left_lane.stable , которые являются булевыми. Если хотя бы одна из данных переменных принимает значение False , я визуализирую это как красный полигон между двумя линиями (ниже вы сможете увидеть, как это выглядит).
В результате мы получаем достаточно стабильные линии:
Шаг 3: Рисование и обновление исходного изображения
Для того, чтобы линии были нарисованы корректно, я написал довольно простой алгоритм, который вычисляет координаты точки горизонта, о которой мы уже с вами говорили. В моем проекте данная точка нужна для двух вещей:
- Ограничить экстраполяцию линий разметки данной точкой.
- Отфильтровать все линии Хафа, находящиеся выше горизонта.
Для визуализации всего процесса определения полос, я сделал небольшое image augmentation :
Как видно из кода, я накладываю на исходное видео два изображения: одно с бинарной маской, второе — с прошедшими все наши фильтры линиями Хафа (трансформированными в точки). На само исходное видео я накладываю две полосы движения (линейная регрессия над точками из предыдущего изображения). Зелёный прямоугольник — индикатор наличия «нестабильных» линий: при их наличии он становится красным. Использование такой архитектуры позволяет достаточно легко менять и комбинировать кадры, которые будут высвечиваться в качестве дашборда, позволяя одновременно визуализировать множество компонентов и все это — без каких либо значительных изменений в исходном коде.
Что дальше?
Данный проект еще очень далек от завершения: чем больше я работаю над ним, тем больше вещей, требующих улучшения, я нахожу:
- Сделать детектор нелинейным, чтобы он мог с успехом работать, к примеру, в горах, где повороты на каждом шагу.
- Сделать проекцию дороги как «вид сверху» — это значительно упростит определение полос.
- Распознавание дороги. Было бы замечательно распознавать не только разметку, но и саму дорогу, что значительно облегчит работу детектора.
Весь исходный код проекта доступен на GitHub по ссылке.
P.S. А теперь сломаем все!
Конечно, в этом посте должна быть и забавная часть. Давайте посмотрим, как жалок становится детектор на горной дороге с частыми сменами направления и освещённости. Сначала всё, вроде бы, нормально, но в дальнейшем ошибка в определении полос накапливается, и детектор перестаёт успевать следить за ними:
А в лесу, где свет меняется очень быстро, наш детектор полностью провалил задание:
Кстати, один из следующих проектов — сделать нелинейный детектор, который как раз и справится с «лесным» заданием. Следите за новыми постами!
Источник