Этот пост является частью 12-месячного проекта ускоренного обучения Month to Master. На май: Моя цель - построить программную часть беспилотного автомобиля.

Вчера я придумал, как определять полосы движения на изображении дороги лицом вперед. Что ж ... Я по крайней мере понял, как запустить код, который мог бы это сделать.

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

Ниже приведен основной блок кода, который я использовал вчера. В частности, я скопировал основную функцию, которая называется «draw_lane_lines». По сути, функция - это блок кода, который принимает некоторый ввод (в данном случае фотографию), каким-то образом манипулирует вводом, а затем выводит манипуляцию (в данном случае строки полосы ).

Эта основная функция использует некоторые другие вспомогательные функции, определенные в другом месте кода, но эти вспомогательные функции в основном представляют собой немного более чистые способы использования готовых функций из библиотек, которые я загрузил вчера (например, OpenCV ).

def draw_lane_lines(image):
    imshape = image.shape
    
    # Greyscale image
    greyscaled_image = grayscale(image)
    
    # Gaussian Blur
    blurred_grey_image = gaussian_blur(greyscaled_image, 5)
    
    # Canny edge detection
    edges_image = canny(blurred_grey_image, 50, 150)
    
    # Mask edges image
    border = 0
    vertices = np.array([[(0,imshape[0]),(465, 320), (475, 320), 
    (imshape[1],imshape[0])]], dtype=np.int32)
    edges_image_with_mask = region_of_interest(edges_image, 
    vertices)
    
    # Hough lines
    rho = 2 
    theta = np.pi/180 
    threshold = 45    
    min_line_len = 40
    max_line_gap = 100 
    lines_image = hough_lines(edges_image_with_mask, rho, theta,  
    threshold, min_line_len, max_line_gap)
    # Convert Hough from single channel to RGB to prep for weighted
    hough_rgb_image = cv2.cvtColor(lines_image, cv2.COLOR_GRAY2BGR)
 
    # Combine lines image with original image
    final_image = weighted_img(hough_rgb_image, image)
    
    return final_image

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

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

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

Таким образом, на основе моего сегодняшнего исследования я попытаюсь объяснить следующую последовательность событий обработки изображения: Входное изображение → 1. Изображение в градациях серого, 2. Размытие по Гауссу, 3. Обнаружение контуров, 4. Изображение краев маски, 5. Горизонтальные линии → Выходные линии с переулками

Входное изображение

Вот начальное входное изображение.

Важно помнить, что изображение - это не что иное, как набор пикселей, расположенных в прямоугольнике. Этот конкретный прямоугольник составляет 960 пикселей на 540 пикселей.

Значение каждого пикселя представляет собой некоторую комбинацию красного, зеленого и синего цветов и представлено тройкой чисел, где каждое число соответствует значению одного из цветов. Значение каждого из цветов может находиться в диапазоне от 0 до 255, где 0 - полное отсутствие цвета, а 255 - 100% -ная интенсивность.

Например, белый цвет представлен как (255, 255, 255), а черный цвет представлен как (0, 0, 0).

Итак, это входное изображение можно описать 960 x 540 = 518 400 тройками чисел в диапазоне от (0, 0, 0) до (255, 255, 255).

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

1. Изображение в оттенках серого.

Первым шагом обработки является преобразование цветного изображения в оттенки серого, эффективно понижая цветовое пространство с трехмерного до одномерного. Намного проще (и эффективнее) управлять изображением только в одном измерении: это одно измерение представляет собой «темноту» или «интенсивность» пикселя, где 0 представляет черный, 255 представляет белый, а 126 представляет некоторый средний серый цвет. .

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

Например, вот цвет неба на исходной фотографии:

Его можно представить в пространстве RGB (красный, зеленый, синий) как (120, 172, 209).

Если я усредню эти значения вместе, я получу (120 + 172 + 209) / 3 = 167, или этот цвет в пространстве оттенков серого.

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

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

В одном из распространенных методов, называемых колометрическим преобразованием, используется эта взвешенная сумма: 0,2126 красный + 0,7152 зеленый + 0,0722 синий.

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

2. Размытие по Гауссу

Следующим шагом является размытие изображения с помощью Gaussian Blur.

Применяя небольшое размытие, мы можем удалить самую частую информацию (также известную как шум) из изображения, что даст нам «более плавные» цветовые блоки, которые мы сможем анализировать.

Опять же, математика, лежащая в основе размытия по Гауссу, очень проста: размытие просто требует большего количества средних пикселей (этот процесс усреднения представляет собой тип свертки ядра, что является излишне причудливым названием для того, что я ' м собираюсь объяснить).

В основном, чтобы создать размытие, вы должны выполнить следующие шаги:

  1. Выберите пиксель на фотографии и определите его значение.
  2. Найдите значения для локальных соседей выбранного пикселя (мы можем произвольно определить размер этой «локальной области», но обычно она довольно мала)
  3. Возьмите значение исходного пикселя и соседних пикселей и усредните их вместе, используя некоторую систему взвешивания.
  4. Заменить значение исходного пикселя выведенным усредненным значением
  5. Сделайте это для всех пикселей

Этот процесс, по сути, означает «сделать все пиксели более похожими на пиксели поблизости», что интуитивно звучит как размытие.

Для размытия по Гауссу мы просто используем распределение по Гауссу (т. Е. Колоколообразную кривую) для определения весов на шаге 3 выше. Это означает, что чем ближе пиксель к выбранному пикселю, тем больше его вес.

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

3. Обнаружение хитрых краев

Теперь, когда у нас есть изображение с оттенками серого и размытым по Гауссу, мы попытаемся найти все края на этой фотографии.

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

Например, есть четкая граница между серой дорогой и пунктирной белой линией, поскольку серая дорога может иметь значение примерно 126, белая линия имеет значение, близкое к 255, и постепенного перехода между этими значениями нет. .

Опять же, фильтр Canny Edge Detection использует очень простую математику для поиска ребер:

  1. Выберите пиксель на фото
  2. Определите значение для группы пикселей слева и группы пикселей справа от выбранного пикселя.
  3. Возьмите разницу между этими двумя группами (т.е. вычтите значение одной из другой).
  4. Измените значение выбранного пикселя на значение разницы, вычисленное на шаге 3.
  5. Сделайте это для всех пикселей.

Итак, представьте, что мы смотрим только на один пиксель слева и справа от выбранного пикселя, и представьте, что это значения: (Левый пиксель, выбранный пиксель, правый пиксель) = (133, 134, 155). Затем мы вычислили бы разницу между правым и левым пикселями, 155–133 = 22, и установили бы новое значение выбранного пикселя равным 22.

Если выбранный пиксель является краем, разница между левым и правым пикселями будет большим (ближе к 255) и, следовательно, будет отображаться как белый цвет на выводимом изображении. Если выбранный пиксель не является краем, разница будет близка к 0 и будет отображаться черным цветом.

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

Эти различия называются градиентами, и мы можем вычислить общий градиент, по сути, используя теорему Пифагора для сложения отдельных вкладов от вертикали и горизонтальные градиенты. Другими словами, мы можем сказать, что общий градиент² = вертикальный градиент² + горизонтальный градиент².

Так, например, допустим, вертикальный градиент = 22 и горизонтальный градиент = 143, тогда общий градиент = sqrt (22² + 143²) = ~ 145.

Результат выглядит примерно так…

Фильтр Canny Edge Detection теперь выполняет еще один шаг.

Вместо того, чтобы просто отображать все кромки, фильтр "Обнаружение каннибров" пытается идентифицировать важные кромки.

Для этого мы устанавливаем два порога: высокий порог и низкий порог. Допустим, высокий порог равен 200, а нижний порог - 150.

Для любого общего градиента, значение которого превышает верхний порог 200, этот пиксель автоматически считается краем и преобразуется в чистый белый цвет (255). Для любого общего градиента, значение которого меньше нижнего порога 155, этот пиксель автоматически считается «не краем» и преобразуется в чистый черный цвет (0).

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

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

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

4. Скрыть края изображения

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

Получаем это…

Кажется, что это довольно агрессивная и самонадеянная маска, но это то, что сейчас написано в исходном коде. Итак, продолжаем ...

5. Грубые строки

Последний шаг - использовать преобразование Хафа, чтобы найти математическое выражение для линий полос движения.

Математика, лежащая в основе преобразования Хафа, немного сложнее, чем все средневзвешенные вычисления, которые мы делали выше, но лишь незначительно.

Вот основная концепция:

Уравнение для прямой имеет вид y = mx + b, где m и b - константы, которые представляют наклон линии и точку пересечения оси y линии соответственно.

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

Затем мы перемещаемся по этому пространству m и b, и для каждой пары (m, b) мы можем определить уравнение для конкретной линии вида y = mx + b. На этом этапе мы хотим протестировать эту строку, поэтому мы находим все пиксели, которые лежат на этой линии на фотографии, и просим их проголосовать, если это хорошее предположение. для полосы движения или нет. Пиксель голосует «да», если он белый (он же часть края), и голос «нет», если он черный.

Пара (m, b), получившая наибольшее количество голосов (или в данном случае две пары, получившие наибольшее количество голосов), определяется как две линии дорожек.

Вот результат преобразования Хафа ...

Я пропускаю ту часть, где вместо использования формулы y = mx + b для представления линии преобразование Хафа использует представление в стиле полярных координат / тригонометрического стиля, в котором используются rho и theta в качестве двух параметров.

Это различие не очень важно (для нашего понимания), поскольку пространство все еще параметризуется в 2-х измерениях, и логика точно такая же, но это тригонометрическое представление действительно помогает с тем фактом, что мы не можем выразить полностью вертикальное линии с уравнением y = mx + b.

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

Окончательный результат

И мы закончили.

Программа выводит два параметра для описания двух полос движения (что интересно, эти выходные данные преобразуются обратно в параметризацию m, b). Программа также предоставляет координаты конечных точек каждой полосы движения.

Линия переулка 1

Угловой коэффициент: -0,740605727717; Перехват: 664.075746144

Пункт первый: (475, 311) Пункт два: (960, 599)

Линия переулка 2

Коэф: -0,740605727717; Перехват: 664.075746144

Первая точка: (475, 311) Вторая точка: (0, 664)

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

Прочтите следующий пост. Прочтите предыдущий пост.

Макс Дойч - навязчивый ученик, создатель продукта, подопытный кролик в Месяце до мастера и основатель Openmind.

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