Всем привет,
Здесь я примерно объясняю свою реализацию функции draw_lines () для проекта 1 Self Driving Car Nanodegree от Udacity. Вот код, работающий с видео с заданием:
Для того же проекта, использующего фильтр Калмана, отметьте этот другой пост.
- Используйте глобальные переменные для передачи значений ключей из одного кадра в другой.
Сначала я объявляю эти переменные вне функции 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, это очень помогло.
Также спасибо за чтение и удачи в ваших собственных решениях!
Рауль.