Недавно я завершил четвертый проект программы Udacity Self-Driving Car, в котором мы рисуем линии полос на предоставленном нам видео.

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

  • Компьютерное зрение - темное искусство
  • У Jupyter Notebooks есть цель, но они не должны использоваться для вашего реального кода.
  • Линтеры потрясающие

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

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

Но давайте начнем с самого начала. Во-первых, цели этого проекта заключались в следующем:

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

Калибровка

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

Калибровка состоит из двух основных этапов. Во-первых, мы используем функцию в OpenCV под названием findChessboardCorners. Мы пропускаем все изображения через эту функцию, давая ей ожидаемое количество углов. Затем для каждого успешного вызова этой функции мы добавляем точки, которые мы получаем, а также набор «нормализованных» углов. Имея их в руках, мы, наконец, вызываем calibrateCamera в OpenCV и получаем матрицу и коэффициенты искажения, которые нам понадобятся позже, чтобы вызвать cv2.undistort для выпрямления изображений.

Создание двоичного образа

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

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

Проблема здесь в том, что как только вы получите что-то, что кажется работающим, вы столкнетесь с частью видео, где оно просто развалится. Либо он будет в тени, либо дорожное покрытие действительно светлое и резко падает контраст. Потом вы исправите это, и что-то другое перестанет работать. Это была вечная игра в «Ударь крота».

Для экспериментов я использовал блокнот Jupyter (iPython). В конце концов, я устал слепо менять значения и повторно запускать ячейку кода и начал использовать iPython Widgets. Они очень полезны для такого рода вещей, когда вы пытаетесь настроить значения и увидеть результат. Я их очень рекомендую. В итоге я установил несколько ползунков, чтобы поиграть со значениями.

Я перепробовал столько перестановок, что сбился со счета. В какой-то момент я подумал, что у меня есть решение в использовании Sobel для обнаружения линий в сочетании с установлением пороговых значений цвета с помощью операции AND. Блестяще! Только не вышло. Я бы встретил еще одно место, где он развалился.

Одна из проблем с обнаружением Sobel заключается в том, что оно работает с изображением в градациях серого. Если вы сделаете полутоновое изображение светлой дороги, желтая линия в основном исчезнет, ​​поэтому она ничего не улавливает. Это означало, что я должен использовать пороговое значение для желтых линий и ИЛИ это в моем результирующем изображении.

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

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

Перейти к деформации!

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

Это поможет нам позже определить кривизну полосы движения.

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

Мы выполняем преобразование с помощью cv2.getPerspectiveTransform(). Исходный прямоугольник представляет собой трапецию, покрывающую полосу движения, а в результате получается прямоугольник, который вы видите наверху справа.

Обнаружение

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

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

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

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

Сложный вариант

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

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

Как только я цеплялся за линию, я сразу же вычислял полиномиальную аппроксимацию. Затем я бы использовал эту линию, чтобы контролировать свои движения. Кроме того, я бы начал с самого простого полигона, который мог (линия, если позволяли точки), и использовал его. У меня был метод, который помог выбрать наиболее подходящий многочлен для прямой. Я бы стал более сложным и посмотрел, существенно ли улучшилась посадка. Если нет, оставьте более простой. Проблема здесь в том, что иногда пиксели заставляли меня улетать в каком-то сумасшедшем направлении. Поэтому я также добавил понятие «смотреть, где (если вообще) линия пересекает прямоугольник, в котором я сканировал. Я бы следил либо за линией, либо за направлением пересечения, в зависимости от того, что даст больше пикселей. Затем я переставлял линию и продолжал так.

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

Но я все еще думаю, что здесь есть кое-что, к чему я, возможно, захочу когда-нибудь вернуться в свое обильное свободное время.

Как узнать, когда что-то пойдет не так

Все это отлично работает, когда у вас есть красивые чистые линии. Но в большинстве случаев это не так.

Есть много причин, по которым обнаружение может пойти не так. У вас может не хватить пикселей для линии, чтобы даже решить попробовать полиномиальную аппроксимацию. Или это может дать вам действительно дурацкую полиномиальную подгонку из-за того, что линия слишком короткая (может быть, у кадра есть конец пунктирной линии внизу и ничего над ним), или слишком шумно (в этом случае подгонка может быть потянута в неправильном направлении). Как можно успешно обнаруживать эти ситуации и бороться с ними? Я перепробовал множество разных методов, чтобы попытаться выяснить, когда что-то пошло не так.

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

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

Я также пробовал около 3–4 других вариантов и техник, но, как уже упоминалось, в конце концов я просто остановился на производных.

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

Определение положения и кривизны полосы движения

В последней части этого проекта все это использовалось для определения вашего текущего положения полосы движения (где вы находились относительно линий) и кривизны дороги.

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

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

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

ym_per_pix = 3 / 88  # meters per pixel in y dimension
xm_per_pix = 3.7 / 630  # meters per pixel in x dimension

И я использовал это для масштабирования значений, которые использовал, чтобы создать числа, которые вы видите на изображении в начале этой страницы. Я показал левую и правую кривые, но на самом деле я подумал о том, чтобы показать кривую от центра машины, что, на мой взгляд, более полезно. Может, когда-нибудь я это исправлю. Я также должен сказать «лево / право» вместо положительных / отрицательных чисел, чтобы было понятнее.

Используйте правильную среду

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

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

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

Но со временем я писал основной код для обнаружения и т. Д., И я продолжал его менять, рефакторинг или переименовывать вещи, и вот тогда среда ноутбука перестала работать.

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

В конце концов, я сдался и просто использовал отдельный файл Python. Таким образом, он загружается каждый раз, и вы сразу же обнаруживаете такие ошибки. А если вы добавите к этому линтер, то у вас должно быть очень хорошо. Если вы используете Python 3.6, вы также можете воспользоваться преимуществами аннотаций типов, чтобы сделать ваш код более типобезопасным. Как только я попал в текстовый редактор Sublime, я мог использовать там Anaconda, линтер и ничего себе. Намного лучше. Я настоятельно рекомендую этот путь, даже если вы не используете линт, но начинаете с собственного файла python и вас не увлекает выполнение всего этого в записной книжке. Это позволит вам сохранить все волосы и сэкономить много времени.

Мой код

Код для этого доступен на моем гитхабе.

Я закончил тем, что создал объект, названный, как ни странно, «Процессор». Это главный драйвер всего, что есть в приложении. Он, в свою очередь, использует несколько объектов: Line и LineDetector. Line - это наше постоянное хранилище, в котором мы храним самые последние и наиболее подходящие строки, а также некоторую отладочную информацию, которую мы используем, когда хотим увидеть, что мы обнаруживаем, как я показал выше. LineDetector просто использует линии и выполняет необработанное обнаружение кадра.

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

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

У меня также есть режимы отладки, которые я использовал. Я всегда рекомендовал бы такие варианты. Мне удалось обработать только одно изображение, а также визуализировать «двоичную» версию видео, чтобы увидеть, как работает моя пороговая обработка цвета. Наконец, у меня был режим полной отладки, который сбрасывал исходный кадр, а также мой кадр обнаружения. В то же время это будет добавлено в файл CSV, чтобы я мог при желании изобразить полиномиальные коэффициенты или производные. Я вывожу исходный кадр, чтобы, если возникла проблема с кадром 96, я мог просто пропустить его в одиночку и поиграть с настройками в своей записной книжке или через скрипт Python.

Выводы

Я думаю, что самым большим выводом для меня в этом проекте было как можно быстрее перейти на настоящий Python в следующий раз. Слишком много времени было потеряно в Jupyter Notebook. В то же время замечательно использовать блокнот для таких вещей, как определение порога цвета с помощью iPython Widgets. Так что я узнал кое-что полезное для следующего раза и планирую хорошо использовать эти знания.

Надеюсь, эта страница вам чем-то помогла! Увидимся снова после Project 5 😀