День 7–9 — 05–07 декабря 2020 г. — Итоги

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

Прогресс

Я попытаюсь перечислить необходимые шаги на высоком уровне и максимально подробно описать наиболее важные из них. Я должен начать с того, что для этого модуля, а возможно, и для последующих, библиотеки, которые я буду использовать, — это NumPy и OpenCV. NumPy позволяет выполнять сложные многомерные массивы и математические операции с матрицами, а OpenCV — это библиотека функций, предназначенная для компьютерного зрения в реальном времени [ref], я использую ее в этом модуле для загрузки изображений, обработки и рендеринг.

Код этого модуля можно найти на моем GitHub, здесь.

Шаг 0 — Понимание того, что такое изображение

Важно отметить, что изображение представляет собой набор (список/массив) пикселей. Каждый пиксель имеет свой цвет, представленный в пространстве RGB (красный-зеленый-синий).

Учитывая изображение выше, его размеры составляют 2400 x 1600 пикселей, то есть ширина составляет 2400 пикселей в поперечнике, а высота — 1600 пикселей (по вертикали).

Каждый пиксель, как указано выше, представлен значением цвета RGB, например (37 114 184), которое является цветом одного пикселя неба на этом изображении. Каждое число представляет интенсивность цвета, которое оно представляет, по шкале от 0 до 255. Где 0 означает отсутствие интенсивности (черный), 255 означает высокую интенсивность (белый). Следовательно, черный — это (0, 0, 0), а белый — (255, 255, 255).

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

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

Шаг 1 — Загрузка и отображение

Во-первых, мне нужно импортировать NumPy и OpenCV в свое приложение:

импорт cv2

импортировать numpy как np

Так просто. Затем я мог бы использовать OpenCV в файле как cv2 и NumPy как np.

Загрузка изображения проста, я поместил образец изображения в тот же каталог, что и мой файл Python, и всего одной строкой кода, используя

image = cv2.imread(‘test_image.jpg’)

Чтобы отобразить его, я просто использую cv2.imshow(“result”, final_image)

Шаг 2 — Превращение изображения в оттенки серого

Этот шаг необходим, потому что, выполняя его, мы помогаем программе работать быстрее и эффективнее обнаруживать полосы, позже вы поймете, почему. Единственное, что я хотел бы здесь добавить, — это краткое объяснение того, что на самом деле представляет собой шкала серого. Он превращает цвет пикселя RGB (37 114 184) в одно число по шкале от 0 до 255, но содержит только одно значение. Например, синий цвет в цветовом коде выше, в оттенках серого, равен 112. Таким образом, в шкале серого новый цвет выглядит так (355).

Но как я получил 112 из (37 114 184)? Ну, это просто, это просто среднее значение трех интенсивностей цвета (37+114+184)/3. Круто, да? Итак, OpenCV делает это для каждого пикселя на входе изображения.

Вот код и результат вывода:

серый = cv2.cvtColor(изображение, cv2.COLOR_RGB2GRAY)

Шаг 3 — Размытие по Гауссу

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

blur = cv2.GaussianBlur(серый, (5, 5), 0)

OpenCV GussianBlur делает то, что написано на коробке, добавляет размытие к данному изображению. Второй аргумент — это количество пикселей (ширина, высота), из которых берется средний цвет. Итак, чтобы добавить размытие, требуется средний цвет для окружающих 5 пикселей всех размеров, в результате чего получается следующее:

Шаг 4 — Хитрое обнаружение

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

canny = cv2.Canny(blur, 50, 150)

Последние 2 аргумента являются пороговыми значениями. 50 означает, что цвета с интенсивностью ниже 50 отключаются (0/черный), а цвета выше 150 становятся белыми (255). Так что все становится буквально черно-белым:

Шаг 5 — Определение области

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

def region_of_interest(image):
# height — это первая ось из свойства формы
height = image.shape[0]
polygons = np.array([
[(319, высота), # точка B
(982, высота), # точка C
(554, 258)] # точка A
])
# рисуем маску той же формы, что и изображение
mask = np.zeros_like(image) # нули черного цвета
# заполняем многоугольник нашим треугольником, bg белого цвета
cv2.fillPoly(mask, полигонов, 255)
возврат маски

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

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

Это хитрое изображение на графике, помогающее нам увидеть его размеры. Как видите, высота обработанного изображения составляет 704 пикселя, а ширина ~1279 пикселов. Чтобы получить высоту, нам нужно получить доступ к свойству shape изображения. Это одномерный массив, который выглядит так:

(704, 1279, 3)

Где 704 — высота, а 1279, как вы уже догадались, ширина в нашем случае (3 — глубина).

Итак, нам нужно нарисовать треугольник в качестве маски, как тот, что слева.

Три точки, A, B и C, указаны в коде (координаты). Мы используем эти координаты, чтобы сделать массив, который мы передаем в функцию OpenCV fillPoly, немного ниже. Перед этим мы делаем еще одну вещь. Создаем макс. И делаем мы это, создавая точную копию изображения, но полностью черного цвета. np.zeros_like создает идентичный массив, такой как изображение, но с нулями вместо всех цветовых кодов (ноль означает отсутствие интенсивности, поэтому цвет черный, помните?). Затем мы используем маску вместе с нашим новым массивом, чтобы заполнить маску нарисованным многоугольником. Результат пока выглядит так:

Шаг 6 — Побитовый и

Этот шаг ниже создает настоящий белый треугольник на черном фоне с такими же точными размерами, что и исходное изображение. Теперь маска передается в функцию cv2.bitwise_and(mask, image) вместе с исходным изображением, чтобы создать это:

Но что такое bitwise_and, спросите вы. Он принимает два аргумента: маску и исходное изображение (мы работаем только с массивами, поэтому, когда я говорю «изображение», я имею в виду не фактическое изображение, а цветовое представление RGB).

Затем он применяет оператор И к каждому элементу массива по сравнению с аналогом. На рисунке 1 вы можете видеть код цвета и его представление в двоичном виде. Черный цвет представлен числом 0, что в переводе на двоичное равно 0. Белый цвет — это 255, что в двоичном представлении равно 11111111. На рисунке 2 я применил свои навыки работы с Microsoft Paint, чтобы показать вам, как функция преобразует цвета в двоичном виде. . Затем последняя часть этого шага — применить конец к каждому элементу массива по сравнению с хитрым изображением. Под капотом это выглядит примерно так:

Данным цветом 75 (темно-серый) в двоичном формате является 1001011. 0 И 1001011 = 0. Поэтому, если этот серый цвет присутствует в областях, замаскированных как черный, результирующий цвет всегда будет черным.

Однако, если сравнить цвет 75 (1001011) с белым треугольником, вычисление будет выглядеть так: 1 AND 1001011 = 1.Это означает, что все внутри треугольника остается неизменным. , тогда как области снаружи становятся черными, вот чего мы достигли на этом шаге:

Шаг 7 — Линии Хафа

Теперь, когда наши полосы движения упрощены и ясны, мы можем использовать функцию OpenCV HoughLinesP, чтобы нарисовать линии, которые впоследствии можно использовать в качестве руководства для движения между ними, следующим образом:

cv2.HoughLinesP(cropped_image, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5)

Я должен начать этот шаг с объяснения того, что такое пространство Хафа. Каждая линия в декартовом пространстве представлена ​​​​точкой в ​​​​пространстве Хафа, благодаря следующему линейному уравнению:

y = mx + b

Где m называется наклон, а b — пересечение по оси Y.

Начнем с m. Это скорость, с которой наша линия растет или падает. Итак, его определение таково:

m = изменение по вертикали / изменение по горизонтали

or

m = (y2-y1)/(x2–x1)

b — это место, где линия пересекает ось Y, поэтому она называется пересечением по оси Y.

Я бы посоветовал посмотреть видео на YT об этом, его намного легче понять, когда у вас есть визуальные эффекты.

Двигаясь дальше, точка (точка) в декартовом пространстве представляется линией в пространстве Хафа. Это связано с тем, что через точку может проходить бесконечное количество линий. И так бесконечное количество точек Хафа образуют линию.

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

Итак, вернемся к нашей функции:

cv2.HoughLinesP(cropped_image, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5)

Первый аргумент — это обрезанное изображение, созданное на шаге 6. Второй аргумент — это размер ячейки, используемый для определения того, заслуживает ли обнаруженная линия того, чтобы считаться линией полосы движения. Третий аргумент — точность в градусах. Аргумент 4 — это порог, определяющий минимальное количество пересечений для рассматриваемой линии. Аргумент 5 — это просто переданный пустой массив. Шестой аргумент — это длина строки в пикселях, принятая на выходе. И последнее — это максимальный зазор между строками: максимальное расстояние в пикселях между сегментами, которое мы позволим соединить в один.

Наконец, в рамках этого шага я создал функцию для рисования линий на черном фоне:

def display_lines(image, lines):
line_image = np.zeros_like(image) # рисуем черный фон
если lines не равно None: # если обнаружены какие-либо линии Хафа
для строки в строках: # выполнить итерацию по каждой из них
"""
Примечание: каждая строка в строках представляет собой двумерный массив. Изменение формы превращает его в одномерный
массив или еще лучше для каждой строки. , сохраните координаты в соответствующих
переменных.
"""
x1, y1, x2, y2 = line.reshape(4)
"""
Используйте координаты для рисования линии.
arg1: фон
arg2: начальная точка отрезка (координаты x и y)
arg3: конечная точка отрезка (координаты x и y) )
arg4: цвет линии
arg5: толщина линии
«»»
cv2.line(line_image, (x1, y1), (x2, y2), (255 , 0 , 0), 10)
вернуть line_image

Мы делаем это, сначала рисуя черный фон, здесь ничего нового. Затем перебираем массив строк, обнаруженных ранее. Затем я беру линию (на каждой итерации) и с помощью функции изменения формы сохраняю ее координаты в 4 локальные переменные (x1, x2, y1, y2). Следующая часть кода не требует пояснений: я рисую линию поверх черного фона, сгенерированного над циклом while. Таким образом, мы получаем кучу таких строк:

Шаг 8 — Оптимизация

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

Сначала мы определяем пару массивов для хранения наклона и y-пересечения для каждой стороны:

left_fit = []
right_fit = []

Затем мы перебираем каждую строку, определенную на шаге 7:

для строки в строке:

Снова берем координаты линии (через каждую итерацию):

x1, y1, x2, y2 = line.reshape(4)

Затем мы вычисляем значения наклона и точки пересечения по оси Y (с учетом координат линии), используя следующую функцию:

parameters = np.polyfit((x1, x2), (y1, y2), 1)

Это возвращает массив, где первый элемент — это наклон, а второй — точка пересечения по оси y. Мы сохраняем их следующим образом:

наклон, точка пересечения = параметры[0], параметры[1]

Затем мы решаем, относится ли линия к левой или правой стороне. Если наклон линии отрицателен, это означает, что она относится к левой стороне. Опять же, видео лучше объясняет это, чем я сам. Мы применяем эту логику ниже:

если наклон ‹ 0:
left_fit.append([наклон, точка пересечения])
else:
right_fit.append([наклон, точка пересечения])

Когда итерация завершена, мы получаем 2 массива, содержащих набор наклонов и y-пересечений для каждой из обнаруженных линий. Затем вне итерации мы вычисляем средний наклон и точку пересечения по оси y для каждой из сторон, например:

left_fit_average = np.average(left_fit, axis=0)
right_fit_average = np.average(right_fit, axis=0)

Это довольно легко понять, он вычисляет средние значения по оси 0, то есть по вертикали заданных массивов: [[1, .78], [1.34, .12]] равно (1 + 1,34)/2 и (. 78 + 0,12)/2.

Получив их, мы определяем левую и правую линии следующим образом:

left_line = make_coordinates(image, left_fit_average)
right_line = make_coordinates(image, right_fit_average)

Функция make_coordinates создана мной, и она просто превращает наклон и точку пересечения с координатой Y обратно в координаты, используя y = mx + b.

наклон, пересечение = line_parameters
y1 = image.shape[0]
y2 = int(y1*3/5)
x1 = int((y1-пересечение)/наклон)
x2 = int((y2 -intercept)/slope)
return np.array([x1, y1, x2, y2])

Во-первых, мы берем наклон и y-пересечение для переданной линии. Затем мы берем y1 (это просто высота изображения). Затем мы вычисляем y2 (примерно ближе к середине изображения, где заканчивается дорога). Затем x1 и x2 рассчитываются по обратной формуле Хафа: x= (y-b)/m. наконец, мы объединяем все эти координаты в массив, который мы рисуем поверх нашего изображения, чтобы получить это:

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

Шаг 9 — Функциональность видео

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

cap = cv2.VideoCapture(‘test2.mp4’)

Я помещаю видео в тот же каталог, что и мой файл Python. Затем сослаться на него, используя указанную выше функцию.

в то время как cap.isOpened():
_, frame = cap.read()
if render(frame, 1):
break
cap.release()< br /> cv2.destroyAllWindows()

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

Наш конечный результат таков:

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

Мысли

Этот модуль был довольно хорошим введением в мощь NumPy и OpenCV. Многому научился и определенно расширил свои знания в области компьютерного зрения (от 0 до 1 как минимум).

Следующий

Следующий модуль теперь называется «Восприятие». Жду с нетерпением, завтра вернусь с обновлениями.