В предыдущем рассказе я рассказывал про PiTanq - построенный мною робот-танк. Тогда большая цель - сделать его автономным. Меня вдохновил Курс Udacity для беспилотных автомобилей, и первая задача этого курса - распознавать полосы на дорогах.

Поскольку мой робот находится довольно далеко от дороги, я наклеил на пол белую ленту.

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

Общее решение этой задачи будет выглядеть так:

Фильтровать изображение по цвету

Курсовое задание Udacity требует обнаружения как желтых, так и белых линий. Для этого они используют преобразование HSV или HLS. Мне нужно обнаружить только белую линию, поэтому я решил использовать только фильтр оттенков серого.

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

Обнаружить линию

Затем, применив детектор края Canny, я получил вот это изображение (внутри интересующей области):

Используя детектор линий Hough, я обнаружил несколько странных линий:

Сделать правила обнаружения более строгими:

Плохо с точки зрения актуальности (да и надежности тоже).

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

Проблема со светом

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

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

def balance_pic(image):
    global T
    ret = None
    direction = 0
    for i in range(0, tconf.th_iterations):
        rc, gray = cv.threshold(image, T, 255, 0)
        crop = Roi.crop_roi(gray)
        nwh = cv.countNonZero(crop)
        perc = int(100 * nwh / Roi.get_area())
        if perc > tconf.white_max:
            if T > tconf.threshold_max:
                break
            if direction == -1:
                ret = crop
                break
            T += 10
            direction = 1
        elif perc < tconf.white_min:
            if T < tconf.threshold_min:
                break
            if  direction == 1:
                ret = crop
                break
            T -= 10
            direction = -1
        else:
            ret = crop
            break  
    return ret

Принимайте решения по вождению

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

Определите действия поворота (при необходимости):

def check_shift_turn(angle, shift):
    turn_state = 0
    if angle < tconf.turn_angle or angle > 180 - tconf.turn_angle:
        turn_state = np.sign(90 - angle)
    shift_state = 0
    if abs(shift) > tconf.shift_max:
        shift_state = np.sign(shift)
    return turn_state, shift_state
def get_turn(turn_state, shift_state):
    turn_dir = 0
    turn_val = 0
    if shift_state != 0:
        turn_dir = shift_state
        turn_val = tconf.shift_step if shift_state != turn_state else tconf.turn_step
    elif turn_state != 0:
        turn_dir = turn_state
        turn_val = tconf.turn_step
    return turn_dir, turn_val

Самостоятельное вождение:

while(True):
            a, shift = get_vector()
            if a is None:
              # there is some code omitted related to line finding
              break
            turn_state, shift_state = check_shift_turn(a, shift)
            turn_dir, turn_val = get_turn(turn_state, shift_state)
            if turn_dir != 0:
                turn(turn_dir, turn_val)
            else:
                time.sleep(tconf.straight_run)

Результаты

Есть визуальная информация об отладке:

Настройки алгоритма

## Picture settings
# initial grayscale threshold
threshold = 120
# max grayscale threshold
threshold_max = 180
#min grayscale threshold
threshold_min = 40
# iterations to find balanced threshold
th_iterations = 10
# min % of white in roi
white_min=3
# max % of white in roi
white_max=12
## Driving settings
# line angle to make a turn
turn_angle = 45
# line shift to make an adjustment
shift_max = 20
# turning time of shift adjustment
shift_step = 0.125
# turning time of turn
turn_step = 0.25
# time of straight run
straight_run = 0.5
# attempts to find the line if lost
find_turn_attempts = 5
# turn step to find the line if lost
find_turn_step = 0.2
# max N of iterations of the whole tracking
max_steps = 100

Код

Исходный код доступен на Github.

Ссылки

Сайт PiTanq

История создания PiTanq

Код PiTanq на Github