Запись №1 в моей серии «Дневник машиниста-самоучки».

Четверг, 12 апреля 2018 г. Готовы начать проект №1: Обнаружение полосы движения.

Посмотрите статью №2 из этой серии!

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

Отправка проекта Udacity включает четыре шага: анализ рубрики, написание кода, написание отзыва и отправка заявки. Рубрика дается нам с конкретными критериями, которым должен соответствовать наш код. Код написан в записной книжке Python 3 Jupyter. Запись - это просто текстовая запись, объясняющая, как код соответствует каждой спецификации и процессу написания кода. Отправка может быть либо ZIP-файлом, либо ссылкой на репозиторий Github. Для целей этой серии дневников все мои проекты и материалы будут открываться в моем профиле Github и ссылаться на них внизу каждого сообщения.

Начало проекта

Базовое репо для проекта находится по адресу https://github.com/udacity/CarND-LaneLines-P1. Я рекомендую клонировать его и следовать инструкциям по настройке в правильной среде.

Я перешел на https://github.com/dhruvshah1214/CarND-LaneLines-P1. Это публичный репозиторий моего кода для первого проекта.

Я зашел в терминал, клонировал репозиторий, который я разветвил, настроил среду conda, активировал ее, открыл блокнот Jupyter и начал писать код!

Написание конвейера

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

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

Я начал с написания конвейера. Вот что у меня получилось:

Вот результат этого кода:

Фух! Багов нет. Ну это впервые! :)

Но при более внимательном рассмотрении обнаружилось кое-что странное. Есть еще несколько лишних, нежелательных линий. Вероятно, это потому, что линии такие маленькие; Поскольку получение всех необходимых настроек пороговых значений для таких мелких деталей в 2D-изображении сложно и редко возможно, понижение верхней границы y для интересующей области отфильтрует эти нежелательные линии. Кроме того, на левом маркере полосы мы не ловим все линии полосы движения, поэтому нам придется настроить параметр mininum_line_length, который принимает функция hough_lines().

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

img_roi = region_of_interest(img_edges, np.array([[
     (125,imshape[0]),
     (imshape[1]/2–25, imshape[0]/2 + 50), 
     (imshape[1]/2 + 25, imshape[0]/2 + 50), 
     (imshape[1] - 70,imshape[0]) 
 ]], dtype=np.int32))

Параметры hough_lines() были изменены на:

lines = hough_lines(img_roi, 1, np.pi/180, 1, 5, 1)

Новый и улучшенный вывод:

Намного лучше.

Следующей задачей было отредактировать метод draw_lines(), чтобы на изображении рисовались только две линии: левая и правая.

Я решил различать линии по их наклону и поместил точки для каждого сегмента в четыре разных списка: список левой линии x, список левой линии y и две точки для правой линии, x и y. Мне также нужно было бы экстраполировать всю строку из списка точек. Я написал другой метод, draw_full_line(), который брал изображение, значения x и значения y, и использовал функцию numpy polyfit(), чтобы выполнить своего рода «линейную регрессию» и подогнать все точки к многочлену 1-й степени (известному некоторым как линия! ).

Код для метода:

def draw_full_line(img, x_list, y_list, color=[255, 0, 0], thickness=3):
    if len(x_list) == 0 or len(x_list) != len(y_list): 
        return
    
    line = np.polyfit(x_list, y_list, 1) 
    
    m = line[0]
    b = line[1]
    
    maxY = img.shape[0]
    maxX = img.shape[1]
    
    #take the lowest point on the image, and use the linear equation forumla to solve for its x
    y1 = maxY
    x1 = int((y1 - b)/m)
    
    # take an arbitrary point near the middle of the img
    y2 = int((maxY/2) + 60)
    # solve for x
    x2 = int((y2 - b)/m)
    
    cv2.line(img, (x1, y1), (x2, y2), color, thickness)

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

Настройка параметров

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

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

Размытие по Гауссу принимает положительное, нечетное целое числоkernel_size,, с которым вам нужно поиграть, чтобы найти то, что работает лучше всего. Я попробовал 1, 3, 5, 9, 11, 15, 17 (и проверьте результат обнаружения края (см. Следующий раздел). Чем больше значение kernel_size, тем более размытым становится изображение.

Я начал с 5, работал до 17, затем снова снизился до 13, а затем переключился на редактирование других параметров, а после работал до 1.

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

Обнаружение хитрого края

Из Википедии:

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

В итоге:

  • Если градиент пикселя (разница между собой и окружением) выше верхнего порога, пиксель принимается как край
  • Если значение градиента пикселя ниже нижнего порога, оно отклоняется.
  • Если градиент пикселя находится между двумя пороговыми значениями, он будет принят только в том случае, если он связан с пикселем, превышающим верхний порог.
  • Кэнни рекомендовала соотношение верхних и нижних частот от 2: 1 до 3: 1.

Производная функции f (x) показывает, насколько быстро f (x) изменяется относительно x.

Вкратце, градиент - это производная от нескольких переменных; градиент функции f (x, y) - это скорость изменения f относительно x и y. Для черного пикселя (значение 0) в точке (0,0) [поэтому для точки f (0,0) = 0] рядом с белым пикселем (значение 255) в точке (1, 1) [f (1 , 1) = 255] градиент f (x, y) рядом с (1, 1) будет большим, потому что окружающие пиксели темные и изменение значения пикселя по сравнению с его окружением велико.

Я рекомендую сначала установить low_threshold на ноль, а затем отрегулировать high_threshold. Если high_threshold слишком высокий, вы не найдете краев. Если high_threshold слишком низко, вы обнаружите слишком много краев. Как только вы найдете хороший high_threshold, отрегулируйте low_threshold, чтобы отбросить слабые края (шумы), связанные с сильными краями.

Hough Lines

Параметры HoughLinesP довольно просты:

  • rho и theta - это расстояние и угловое разрешение нашей сетки в пространстве Хафа. Подробнее о Hough Space здесь и Hough Transforms на OpenCV.
  • threshold: возвращаются только те линии, которые получили достаточно голосов (количество пересечений на ячейку сетки).
  • minLineLength: Минимальная длина строки (пикселей). Более короткие отрезки линии отфильтровываются.
  • maxLineGap: Максимально допустимый зазор (в пикселях) между точками на одной линии для их связывания.

Видео с конечного продукта:

Выводы и дальнейшие размышления

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

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

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

Мой код можно найти в моем репозитории Github здесь.