Всем привет,

Здесь я примерно объясняю свою реализацию функции draw_lines () для проекта 1 Self Driving Car Nanodegree от Udacity. Вот код, работающий с видео с заданием:

Для того же проекта, использующего фильтр Калмана, отметьте этот другой пост.

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

Сначала я объявляю эти переменные вне функции draw_lines () в глобальной области видимости и инициализирую их значением «Inf».

l_weighted_m = np.Inf
r_weighted_m = np.Inf
l_weighted_x1 = np.Inf
l_weighted_x2 = np.Inf
r_weighted_x1 = np.Inf
r_weighted_x2 = np.Inf

Внутри функции draw_lines () у меня есть:

global l_weighted_m
global r_weighted_m
global l_weighted_x1
global l_weighted_x2
global r_weighted_x1
global r_weighted_x2

Чтобы они распознавались как глобальные объекты внутри локальной области видимости функции.

2. Сгруппируйте точки линий по наклону

# Group points of lines according to +/- slope and between a range
    for line in lines:
        for x1,y1,x2,y2 in line:
            slope = (y2-y1)/(x2-x1) 
            if slope < -0.4 and slope > -0.9: #right line, negative slope
                left_points.append((x1,y1))
                left_points.append((x2,y2))
            elif slope > 0.4 and slope < 0.9: #left line, positive slope
                right_points.append((x1,y1))
                right_points.append((x2,y2))

Я нашел диапазон наклона, который использую здесь, вычислив и распечатав наклон каждой линии в «линиях»…

3. Применить fitLine

Для левой строки у меня есть:

[l_vecx, l_vecy, l_pointx, l_pointy] = cv2.fitLine(np.array(left_points), cv2.DIST_L12, param, reps, aeps)
... # Same for right_points

Первые два возвращаемых значения [l_vecx, l_vecy] соответствуют нормализованному вектору, коллинеарному с подогнанной линией, последние два значения [l_pointx, l_pointy] являются координатами точки в этой строке.

Если вы хотите быстро просмотреть строку, вы можете использовать этот код:

x = l_pointx + 100*l_vecx
y = l_pointy + 100*l_vecy

Примените ту же идею к правой линии.

4. Нарисуйте линии

Вот как я делаю это для левой линии, опять же, тот же подход для правой:

## ----------------- Draw left line ------------------------
    l_m = l_vecy/l_vecx # Calculate new slope from normalized vector
    
    if l_m < -0.5 and l_m > -0.8: # Calculate m, x1 and x2 only if m is in range
        if l_weighted_m != np.Inf:  
            l_weighted_m = l_weighted_m*(1-NEW_M_WEIGHT) + l_m*NEW_M_WEIGHT # Calculate weighted slope
        else:
            l_weighted_m = l_m # Only the first time, when weighted_m is np.Inf
l_x1 = (l_y1 - l_pointy)/l_weighted_m + l_pointx
        if l_weighted_x1 != np.Inf:
            l_weighted_x1 = l_weighted_x1*(1-NEW_X_WEIGHT) + l_x1*NEW_X_WEIGHT # Calculate weighted x1
        else:
            l_weighted_x1 = l_x1 # Only the first time, when weighted_x1 is np.Inf
l_x2 = (l_y2 - l_pointy)/l_weighted_m + l_pointx
        if l_weighted_x2 != np.Inf:
            l_weighted_x2 = l_weighted_x2*(1-NEW_X_WEIGHT) + l_x2*NEW_X_WEIGHT # Calculate weighted x2
        else:
            l_weighted_x2 = l_x2 # Only the first time, when weighted_x2 is np.Inf
    
    cv2.line(img, (l_weighted_x2, l_y2), (l_weighted_x1, l_y1), (255,0,255), 10)

Я использую значения NEW_M_WEIGHT около 0,2 ~ 0,3.

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

Вы также можете найти диапазон, напечатав l_m после его расчета.

Цифровой фильтр нижних частот первого порядка

Вот то, что я знаю как очень простой «цифровой фильтр нижних частот первого порядка»:

y_fil(k) = α * y_fil(k-1) + (1 — α) * y(k)

Известный также как фильтр «бесконечной импульсной характеристики (БИХ)», если я хорошо помню.

Вот что я реализовал, чтобы сгладить резкие движения линий:

l_weighted_m = l_weighted_m*(1-NEW_M_WEIGHT) + l_m*NEW_M_WEIGHT # Calculate weighted slope

Как вы можете видеть в коде, я фильтрую наклон и две координаты «x» для каждой строки, координаты «y» остаются постоянными.

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

Интуитивно фильтр придает большее значение историческому шаблону значений, имевших место в прошлом, и гораздо меньше фактическому чтению. В некотором смысле переменная l_weighted_m хранит записи всех прошлых хороших показаний и дает этому больший перевес, чем новое фактическое показание. С другой стороны, «средневзвешенное значение» (сумма с весами) - это способ включения фактического считанного значения в нашу историческую запись для будущих измерений.

Детектор Canny Edge

В некоторых случаях вам может потребоваться снизить значение high_threshold по умолчанию со 150 до, возможно, 100, хотя у меня это сработало со значением по умолчанию 150.

Обработка видео после предыдущего

Перед обработкой другого клипа я всегда вызываю эту функцию:

reset_weighted_values() # Start processing new video with ‘Inf’ weighted values

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

# Function to reset weighted values persistent between frames
# to apply before processing a different video
def reset_weighted_values():
 global l_weighted_m
 global r_weighted_m
 global l_weighted_x1
 global l_weighted_x2
 global r_weighted_x1
 global r_weighted_x2
 
 l_weighted_m = np.Inf
 r_weighted_m = np.Inf
 l_weighted_x1 = np.Inf
 l_weighted_x2 = np.Inf
 r_weighted_x1 = np.Inf
 r_weighted_x2 = np.Inf

Заключение

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

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

Спасибо всем, кто публиковал вопросы и ответы на форумах, в Slack и Facebook, это очень помогло.

Также спасибо за чтение и удачи в ваших собственных решениях!

Рауль.