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

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

Но обо всем по порядку и для начала немного матчасти.

Сегментация


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

Вот неплохая статья, описывающая примитивные подходы.

Семантическая сегментация


Семантическая сегментация — разбиение изображения на объекты с определением типов этих объектов.

Выглядит это как-то так:



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

U-net


Самая известная нейросеть, изначально разработанная для медицины.
Первоисточник

Народ быстро сообразил, что подход можно использовать на все случаи жизни.

В интернете много статей, как готовить данные и обучать U-net сети:


Однако, готовой U-net сети, чтобы быстро взять и поэкспериментировать, я не нашел.

E-net


Более молодая и менее известная сеть. Разработана как раз для распознавания городских улиц.


Данные


Самые популярные датасеты, для сегментации улиц (на них изначально обучали E-net):


На этих же датасетах сейчас обучают и U-net.

Выбор реализации


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

Так что выбор был сделан в сторону наименьшего сопротивления.

Использование очень простое:
(Более всего беспокоит то, что сеть обучена на картинках размером 1024x512 — это во-первых больше, чем отдает камера на Raspberry, во-вторых требуемая производительность для обработки такого объема данных несколько смущает. В итоге, главная проблема окажется именно в этом).

Читаем нейросеть из файлов (в одном сама модель, в другом имена классов, в третьем — цвета).

def load_segment_model():
    try:
        classes = None
        with open(PiConf.SEGMENT_CLASSES) as f:
            classes = f.read().strip().split("\n")
        colors = None
        with open(PiConf.SEGMENT_COLORS) as f:
            colors= f.read().strip().split("\n")
        colors = [np.array(c.split(",")).astype("int") for c in colors]
        colors = np.array(colors, dtype="uint8")
        print("[INFO] loading model...")
        net = cv2.dnn.readNet(PiConf.SEGMENT_MODEL)
        return net, classes, colors
    except Exception as e:
        logging.exception("Cannot load segment model")
    return None, None, None

Сегментируем изображение, попутно размечая сегменты поверх оригинального изображения
(В моем случае все классы, кроме дороги, невидимы).

def segment_image(image_path, seg_net, seg_classes, seg_colors):

    image0 = cv2.imread(image_path)
    image = cv2.resize(image0, (1024, 512),interpolation=cv2.INTER_NEAREST)
    blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (1024, 512), 0, swapRB=True, crop=False)

    seg_net.setInput(blob)
    start = time.time()
    output = seg_net.forward()
    end = time.time()

    print("[INFO] inference took {:.4f} seconds".format(end - start))

    (numClasses, height, width) = output.shape[1:4]

    classMap = np.argmax(output[0], axis=0)

    mask = seg_colors[classMap]

    mask = cv2.resize(mask, (image0.shape[1], image0.shape[0]),interpolation=cv2.INTER_NEAREST)
    classMap = cv2.resize(classMap, (image0.shape[1], image0.shape[0]), interpolation=cv2.INTER_NEAREST)

    gmask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
    gmask = cv2.resize(gmask, (128, 64), interpolation=cv2.INTER_NEAREST)
    gmask = gmask[0:64,32:96]

    output = ((0.6 * image0) + (0.4 * mask)).astype("uint8")
    return output, gmask

Проверка


Берем готовые картинки с танка и натравливаем на них сегментирующую нейросеть.

1



Дорогой признана только левая часть тротуара.

Сжимаем картинку и берем из нее центр размером 64x64:
(Такой размер ожидается нейросетью, которая принимает решение о смене направления)



Нейросеть направления (по факту — классификатор) командует взять влево. Не очень правильно, но терпимо.

2



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



Классификатор предлагает ехать прямо.

3

Ситуция, когда робот оказался посреди тротуара.



Дорога распознана практически идеально.



Классификатор командает брать вправо (чтобы найти кромку дороги в следующий раз).

Применение


Поколдовав немного над прошивкой танка, заменил цветовой детектор дороги на сегментирующую нейросеть.

При запуске всего этого на Raspberry Pi первое, что вылезло — удручающая производительность.
На сегментацию одного изображения уходит по 6 секунд — за это время танк бодрой рысцой успевает проскочить все повороты.

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



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

Ссылки


Комментарии (24)


  1. Germanjon
    27.06.2019 07:26

    Чисто теоретически, может быть записывать в память уже хорошо распознанные участки и не пытаться их распознать, а распознавать только «новое»?


    1. Stantin Автор
      27.06.2019 07:29

      А как отличить «хорошо распознанное» от «нового»?


      1. Germanjon
        27.06.2019 08:55

        То, что уже на прошлых кадрах было понято как дорога и препятствие. Не могу сформулировать мысль теоретически, попробую привести сильно упрощённо на конкретном примере:
        — Робот едет со скоростью 10 см/с.
        — Робот «видит» на 1 метр вперёд.
        — Робот распознал, что 50 сантиметров перед ним — точно (вероятность выше определённого порога) является ровной дороги.
        — Через секунду он фотографирует пространство впереди (глубиной 1 метр), но пытается распознать не всю фотографию, а только то пространство, которое не является точно распознанным (в данном примере — 40 сантиметров).


        1. Stantin Автор
          27.06.2019 14:02

          Предлагаете распознавать частично? Все равно большее значение имеет разрешение картинки.


      1. d1ss4pp34r
        27.06.2019 16:10

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

        Насчёт быстродейсвия — все сегментационные сети достаточно медленные, тем более на проце. Можно попробовать intel movidus stick, думаю, на нем получится пара fps, а то и больше. Либо попробовать раза в 4 уменьшить размер входа нейросети.
        А насчёт разницы между u- и e-net, думаю, для такого прототипа она вообще не принципиальна. Можно опять же поискать модификации с меньшим количеством слоев. Я когда-то решал задачу сегментации с помощью Segnet, находил годную cpu-реализацию (чуть больше информации тут )


        1. Stantin Автор
          27.06.2019 16:12

          Интересно, изучу


  1. nomhoi
    27.06.2019 17:15

    Танк маленький, камера слишком низко находится. Размер танка не соответствует размеру дороги.


    1. Stantin Автор
      27.06.2019 17:20

      Как это мешает?


      1. nomhoi
        27.06.2019 17:28

        Обученная модель подойдет только для такого размера машин.


        1. Stantin Автор
          27.06.2019 17:45

          Модели обычно обучают с большой вариативностью по размеру, положению, цвету итд.
          Так что низкую высоту камеры эта сеть должна решать (что она в общем-то и делает).


          1. nomhoi
            27.06.2019 18:00

            Модели обычно обучают с большой вариативностью по размеру, положению, цвету итд.

            Так делают все, кто обучает автопилоты?
            Это все равно что обычный автомобиль обучать по не размеченному плацу или аэродрому. Чему он там обучится? Не выезжать за пределы плаца или аэродрома?
            К тому же, обучать танк ездить только по дорогам — плохая идея :)


            1. Stantin Автор
              27.06.2019 18:10

              Пока только по тротуару, дальше — по мере роста потребностей.


              1. dkurt
                27.06.2019 23:23

                У ENet есть одна неприятная особенность, связанная с обучением на Cityscapes — на всех изображениях присутствует фрагмент капота Mercedes, отчего, можно заметить, сеть продолжает выделять на всех Ваших примерах полукруг как background внизу кадра.


                1. Stantin Автор
                  27.06.2019 23:36

                  Да, сомнительно вот здесь же нет


                  1. dkurt
                    29.06.2019 00:16

                    Ну как бы есть же, чем иначе объяснить влечение сети оставлять пиксели снизу, но не слева, например?



                    1. Stantin Автор
                      29.06.2019 00:35
                      +1

                      Может быть и ваша правда, я списывал это на неоднородность асфальта.


  1. xaoc80
    27.06.2019 19:05

    Я для своих экспериментов просто использовал вот этот проект github.com/kwotsin/TensorFlow-ENet
    Все работает из коробки с CamVid, при этом разрешение картинки можно задать в настройках (я потратил минут 20 на все), можно скачать предобученную модель.
    Можно использовть свой датасет.

    Но, если хотите высокий FPS возьмите модель Road Segmentation из openVino и используйте ее, она хорошо обучена и очень быстрая (у меня на одном ядре I7 работает в реалтайме, правда с Inference Engine). Не знаю, работает ли IE на вашей платформе, но если работает, то 3-4 fps должна выдать. Эта модель больше подходит для вашей задачи. Крмое этого, можете в модели уменьшить число классов до 2-х, этим вы сократите расходы времени CPU на обработку не нужных вам feature maps и тогда сеть будет работать быстрее. В репозитории OpenVino Zoo есть и варианты с int8 моделями, которые на CPU работают еще быстрее.


    1. Stantin Автор
      27.06.2019 19:19

      интересно, спасибо. Этот проект не попался изначально.


    1. dkurt
      27.06.2019 23:17

      Все же автор использует Raspberry, где ARM CPU. OpenVINO подойдёт разве что для запуска на Movidius (ну или попробовать OpenCV оттуда, вдруг забыли какие-то флаги оптимизации выставить). Поэтому вариант с уменьшением размеров картинки в несколько раз, как по мне, пока наиболее перспективный.


      1. Stantin Автор
        27.06.2019 23:44

        К сожалению, Интел так документирует свои продукты, как будто преследует цель, чтобы ими никто не пользовался. Так что про Road Segmentation сразу не понял.


        1. dkurt
          29.06.2019 00:08
          +1

          На всякий случай,
          Описания: https://github.com/opencv/open_model_zoo
          Сами модели: https://download.01.org/opencv/2019/open_model_zoo/R1/models_bin/


          1. Stantin Автор
            29.06.2019 00:36

            Будет что поизучать, спасибо


      1. xaoc80
        28.06.2019 00:27

        Пользуясь случаем хочу спросить насчет предобученных моделей Intel Zoo. У меня наблюдалось некоторое ускорение int8 моделей по сравнению с fp32. Там есть какие-то оптимизации для CPU вроде SIMD или у меня наблюдался эффект ускорения, связанный с уменьшением модели и более эффективным использованием кэша? На каких датасетах эти модели обучали? Можно ли взять модель, обучить на своем, а потом при помощи Model Optimizer сделать оптимизированную модель и запустить на IE?


        1. dkurt
          29.06.2019 00:03

          Я бы сказал, что ускорение даже должно быть. То, что вес модели меньше в 4 раза — это хорошо, но не главное.
          Модели обучаются в FP32 режиме — для создания INT8 веса квантизуют, с учетом активаций нейронов (назвали калибровкой). Потребуется 1-2 тысячи картинок-примеров из вашей задачи, чтобы собрать статистики.