Пример обработки изображения с помощью YOLACT
Пример обработки изображения с помощью YOLACT

Приветствую всех читателей Хабра! Сегодня я хочу поделиться с вами своим опытом запуска YOLACT на edge-устройстве RockChip. Несмотря на то, что процесс запуска занял больше времени, чем я ожидал, я решил поделиться с вами своими наработками, чтобы помочь другим разработчикам, которые могут столкнуться с той же задачей. В конце концов я нашёл способ запуска yolact, который позволил достичь высокой производительности и качества модели. Надеюсь, что мой опыт будет полезен для вас и поможет вам избежать ошибок, которые я совершил. Приятного чтения!

Так уж вышло, что задачам классификации изображений, детекции и сегментации объектов на изображениях посвящено множество статей, обзоров и мануалов. Вообще, тема алгоритмов компьютерного зрения является интересной. При этом, когда речь заходит об instance-segmentation, авторы ограничиваются обзором Mask-RCNN и Fast R-CNN, сетей которые вышли более семи лет назад. Запуск в реал‑тайме этих сетей в принципе невозможен даже на мощном железе, что уж говорить об «одноплатниках». Пытаясь восполнить этот пробел и имея под рукой мощный и дешевый «firefly» я натолкнулся на yolact.

Немного о самой yolact

Статья «YOLACT. Real-time Instance Segmentation» вышла в 2019 году. И на тот момент сетка была в четыре раза производительней других алгоритмов, решающих подобную задачу (33 fps против 8). При этом точность (mAp) у yolact была примерно на 17 процентов ниже чем у знаменитой Mask-RCNN. Авторы заявляют, что yolact это первая instance-segmentation сеть работающая в "реал-тайме", что весьма и весьма впечатляет.

Нужно сказать, что instance-segmentation является наиболее интересной задачей сегментации, так как ближе всего повторяет процесс, который происходит у нас в мозгу, когда мы смотрим на окружающие объекты. Мы видим не только границы объектов, но и понимаем к какому классу эти объекты принадлежат. Yolact делает это за счёт "пирамиды свёрточных слоёв", выделяющих карты признаков разного масштаба, в основании. И Protonet, использующейся для генерации k-масок (k = 32). Как пишут авторы, архитектура сети была основана на RetinaNet + FPN.

Немного о RockChip

Firefly RK3588 имеет на борту нейропроцессор (NPU) с тремя ядрами, мощностью до 6 TOPS. И для того чтобы использовать весь его потенциал (запустить инференс на NPU) необходимо перевести модель в определенный формат (граф) - rknn, а веса модели сжать (квантизировать) до точности 16 или 8 бит. Сделать это можно с помощью небольшого скрипта, пример которого находится в официальном репозитории firefly. Конвертация в rknn-формат может идти напрямую из model.pt, а также из model.onnx.

Обзор yolact-проектов

dbolya. В интернете существует несколько репозиториев с реализацией yolact. В первую очередь - это репозиторий от авторов оригинальной статьи: dbolya. Проект не отличается дружелюбностью к пользователю (несмотря на объемный "Readme"), но запускается из коробки. Большим минусом данного проекта является невозможность сконвертировать модель в onnx формат. Автор признается, что не собирается переписывать модель, чтобы добавить возможность конвертации в onnx, так как это снижает её производительность. Конвертация напрямую в rknn также невозможна. Поэтому, если вы собираетесь запускать сегментацию на компьютере, сервере и т. п. можно использовать этот проект, а дальше можно не читать.

Ma-Dan. К счастью, существует "форк" yolact от Ma-Dan, в котором из класса модели выкинуты "ненужные" декораторы и всякие прочие jit-компиляторы, что позволяет сконвертировать её в onnx формат. Но у Ma-Dan модели есть странный выход (Рис. 1), из-за чего конвертация в rknn-формат, необходимый для запуска на firefly, не проходит. Наверное, этот "output" можно удалить и всё заработает, но я этого не проверял.

Рис.1. Странный, ни с чем не связанный (в отличии от остальных 4-х) выход сети.
Рис.1. Странный, ни с чем не связанный (в отличии от остальных 4-х) выход сети.

PINTO. Ещё одну версию yolact можно найти среди сотен других моделей у автора PINTO0309 (Честно, я не знаю чем занимается этот человек в свободное время, но подозреваю, что он живет на Венере, так как в сутках там ~5832 часа, иначе как объяснить его количество «contributions»). Всё что нужно - скачать репозиторий, скачать подготовленные автором модели и пост-процессы (в формате onnx) с помощью download.sh — скрипта. Поздравляю, вы на 90% приблизились к запуску yolact на edge-девайсе.

Рис.2. Результаты инференса yolact, продемонстрированные PINTO0309
Рис.2. Результаты инференса yolact, продемонстрированные PINTO0309

Тут есть несколько моментов. Во‑первых, нужно сконвертировать модель (например «yolact_base_54_800 000_550×550») в rknn формат. Цифры в конце названия модели означают размер входного изображения. Во‑вторых, нужно сконвертировать «postprocess550×550» в rknn формат. А это сделать невозможно не так просто — привет слои «reshape 1×30 963». Проще запустить постпроцесс с помощью onnxruntime. В‑третьих, то что у автора называется постпроцесс (postprocess550×550.onnx), на самом деле это лишь часть постпроцесса. Так, если сравнить его с оригинальным репозиторием (dbolya/yolact), то окажется что это часть класса Detect(), который выдаёт четыре аутпута: классы, боксы, скоры и маски (classes, boxes, scores, masks). Поэтому, для получения результата как на картинке (Рис.2), нужно добавить следующие строчки:

функция постпроцесса
def prep_display(results):
    def crop(bbox, shape):
        x1 = max(int(bbox[0] * shape[1]), 0)
        y1 = max(int(bbox[1] * shape[0]), 0)
        x2 = max(int(bbox[2] * shape[1]), 0)
        y2 = max(int(bbox[3] * shape[0]), 0)
        return (slice(y1, y2), slice(x1, x2))
    bboxes, scores, class_ids, masks = [], [], [], []
    for result, mask in zip(results[0][0], results[1]):
        bbox = result[:4].tolist()
        score = result[4]
        class_id = int(result[5])
        if threshold <= score:
            mask = np.where(mask > 0.5, class_id + 1, 0).astype(np.uint8)
            region = crop(bbox, mask.shape)
            cropped = np.zeros(mask.shape, dtype=np.uint8)
            cropped[region] = mask[region]
            bboxes.append(bbox)
            class_ids.append(class_id)
            scores.append(score)
            masks.append(cropped)
    return bboxes, scores, class_ids, masks

где "results" - это четыре тензора, полученных на выходе postprocess550x550.

и, наконец, отрисовываем результат:

функция рисующая маски и боксы на изображении
def onnx_draw(frame, bboxes, scores, class_ids, masks):
      colors = get_colors(len(COCO_CLASSES))
      frame_height, frame_width = frame.shape[0], frame.shape[1]
      # Draw masks
      if len(masks) > 0:
          mask_image = np.zeros(MASK_SHAPE, dtype=np.uint8)
          for mask in masks:
              color_mask = np.array(colors, dtype=np.uint8)[mask]
              filled = np.nonzero(mask)
              mask_image[filled] = color_mask[filled]
          mask_image = cv2.resize(mask_image, (frame_width, frame_height), cv2.INTER_NEAREST)
          cv2.addWeighted(frame, 0.5, mask_image, 0.5, 0.0, frame)
      # Draw boxes
      for bbox, score, class_id in zip(bboxes, scores, class_ids):
          x1, y1 = int(bbox[0] * frame_width), int(bbox[1] * frame_height)
          x2, y2 = int(bbox[2] * frame_width), int(bbox[3] * frame_height)
          color = colors[class_id + 1]
          frame = draw_box(frame, (x1, y1, x2, y2), color, class_id, score)
      return frame

Но есть и четвертый, самый неприятный момент — yolact_base_54_800000_550x550.onnx от PINTO нельзя обучить. А если у вас получится это сделать, то в случае если количество классов будет отличаться от исходных 80, то postprocess550x550.onnx перестанет работать. Так как размер входного тензора у postprocess550x550 фиксированный. PINTO пишет, что размер входных тензоров у onnx-моделей можно менять с помощью определённых программ, но заморачиваться с этим каждый раз когда вам нужно обучить модель на новые классы, вместо того, чтобы иметь нормально работающий постпроцесс, как-то не хочется.
Подводя итоги, если вы не собираетесь переучивать yolact на свои классы, а хотите быстро запустить модель для демонстрации, то можно обойтись репозиторием PINTO.

postprocess550x550

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

feiyuhuahuo. Последним и окончательным вариантом оказался Yolact_minimal. Сам проект как и модель является упрощенным вариантом оригинального проекта dbolya/yolact. Обучение модели, оценка точности и конвертация её в onnx-формат делаются в несколько строчек и изменением конфигов. Модель прекрасно конвертируется в fp16, а если использовать resnet101 в качестве backbone, то и квантуется до int8, правда точность после квантизации оставляет желать лучшего. Один из вариантов увеличения точности квантованной модели — это использование Quantization Aware Training.
Единственный минус — для запуска инференса на устройстве, необходимо перенести туда весь пост процесс, причем использовать можно только те функции, которые работают с numpy-массивами. После этого можно получить кое-какие результаты.

Скачиваем случайную картинку с интернета, обрезаем до нужного размера и скармливаем её нейронке. Смотрим на результат:

Рис. 3. Результат работы rknn-модели.
Рис. 3. Результат работы rknn-модели.

Видим, что результат пока не удовлетворительный. Куча ббоксов, маски перекрывают друг‑друга, странный «score». Как бы там ни было, уже что‑то появилось и с этим можно работать. «Score» больше единицы наводит на некоторые мысли. Сравнивая rknn модель и модель Yolact_minimal, например с помощью netron.app (как оказалось он прекрасно «кушает» rknn‑модели), можно увидеть, что выход у исходной сетки прежде чем быть отфильтрован по «nms_score_thre» проходит через функцию softmax (class_pred = F.softmax(class_pred, -1)). Понятно, значит при конвертации потерялся softmax, давайте добавлять:

def np_softmax(x):
    np_max = np.max(x, axis=1)
    sft_max = []
    for idx, pred in enumerate(x):
        e_x = np.exp(pred - np_max[idx])
        sft_max.append(e_x / e_x.sum())
    sft_max = np.array(sft_max)
    return sft_max
  
  # Обратите внимание, что softmax рассчитывается в цикле, 
  # для каждого вектора класса отдельно (что логично). 

После чего получаем симпатичный результат. И это на устройстве размером с пластиковую карту!

Рис. 4. Результат работы rknn-модели + softmax.
Рис. 4. Результат работы rknn-модели + softmax.

Всё работает как надо, и теперь можно выставлять уровень уверенности, порог который плохо распознанные объекты не должны пересекать.

Осталось обучить модель под ваши задачи. Как это сделать написано в репозитории Yolact_minimal. Если кратко, то главное получить JSON файл с разметкой в формате COCO (custom_ann.json). И вставить название нужных классов в конфиг.

Тренировка на 50-ти картинках занимает около 1,5 часов на одной карточке V100. Сеть работает только с изображениями размеры которых кратны 32. Мной были выбраны изображения 544 на 544px.

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

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

Также хочу порекомендовать свой репозиторий для запуска yolact: GitHub. В нём находятся две rknn‑сетки: «yolact_550.rknn» и «yolact_544.rknn», и описаны два постпроцесса: ONNX и RKNN, для первой и второй модели, соответственно. Спасибо за внимание!

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


  1. le2
    09.09.2023 21:14
    +7

    пару лет назад в течение нескольких месяцев дома поковырял плату от firefly - 3399pro. Поделюсь своей картиной мира, возможно будет кому-то полезной. Буду рад, если кто-то мои тезисы опровергнет.
    - NPU - не про скорость, а про энергопотребление. (например разблокировку телефона по лицу без NPU делать глупо) Рилтаймовые сетки работают не медленнее на GPU и, внезапно, работают ещё быстрее на CPU при утилизации нескольких ядер с библиотекой XNNPACK.
    - практически все быстрее работает на GPU, если вам нужны ещё трансформации картинок в реалтайме. Потому что инференс это обычно только 50% времени цикла. (в этом и подхвох. Во всех статьях авторы сравнивают только время инференса, хотя рабочий цикл программы занимает больше времени. )

    -NPU всегда головняк. Процесс чипа разработки занимает несколько лет. Берутся актуальные на тот момент слои нейросетей и реализуются в железе. В момент когда NPU добирается до пользователя, то пользователю достается доступным древний TеnsorFlow1.3 с неактуальными возможностями. Неподдерживающиеся слои можно исполнять на CPU, но тогда вылезают транспортные расходы - нужно гонять данные до NPU и обратно раз 5-8 на один инференс. Смысл использования NPU в этом случае теряется.
    - NPU - всегда разрабатывается мелкими стартапами, к коим Рокчип и любой другой вендор не имеют отношения. Практически это означает что вам будет выдан бинарный запускаемый файл, который якобы может сконверчивать ONNX, TF lite и что-то еще в понятный для NPU формат RKNN. На практике вас будут ждать два варианта - вашу нейронку программу сконвертить не может либо может, но инференс будет не верный!
    - NPU через PCIe или однокристальный ничего не добавляет. Практически производительнсть можно оценить через внешний свисток с NPU через USB3.0. Времена будут близки.

    - на форуме файрфлай вам в лучшем случае ответит такой же бедолага как и вы. Файрфлай никакую поддержку железок не оказывает.
    - на китайском форуме сапорта NPU меня тупо кикнули, убив аккаунт, при попытке задать вопрос на английском (в английской версии).

    Тем не менее все проблемы я порешал, всё что хотел сделал. Для России, сейчас, Рокчип это, вероятно, лучший вариант для встраиваемых решений. Но для себя решил что никакого тайного знания железки на ARM не несут. 99% работ лучше решать на ПК с x86, а переходить в эбмеддед нужно при наличии бизнес-плана.


    1. 124bit
      09.09.2023 21:14

      Интересная статья и интересный комментарий


    1. Paskin
      09.09.2023 21:14
      +2

      Согласен с большинством утверждений. Единственное - NPU это (в теории) еще и про короткий путь от камеры до инференса, при условии синхронизации по кадрам. Это подход, который продвигает Nvidia в своих Jetson-ах, в которых ISP "ближе" к GPU/NPU чем центральный процессор - а также Kendryte и некоторые другие компании.
      К сожалению, у них не получается снизить цену так, чтобы Jetson-ы можно было встраивать в камеры (все-таки 400+ долларов без обьектива за Advantech это очень много для массового рынка) - да и CSI не самый удобный интерфейс в смысле монтажа. А с USB или сетью - производительность сразу падает. Но для серверных применений Nvidia активно продвигает принцип "из сети - прямо в DLA", собственно за этим они Mellanox и покупали.
      Интел же пытаются идти тем путем, которым идут компании в областях обороны и других "небюджетных" решений - в которых и модели, и ISP, и пре/пост-процессинг тупо конвертируются в FPGA. Но получается пока неэкономично по питанию/теплу и сложно для использования.


    1. 120gramm
      09.09.2023 21:14

      Подскажите если отбросить вопрос энергопотребления что лучше выбрать по характеристикам цена/качество для inference моделей не связанных с компьютерным зрением (просто вычисления с плавающей точкой в режиме реального времени). cpu/gpu/stick? Модель не сложная (несколько признаков) но количество одновременно анализируемых параллельно объектов более 50.


      1. le2
        09.09.2023 21:14

        умозрительно такие вещи делать невозможно. Нужно просто взять и запустить вас софт на всех железках.
        Если ваша нейросеть с плавающей точкой и вы не можете ее отквантовать, то быстрее всего будет на GPU.
        GPU это +5..+10 долларов к цене чипа.
        NPU это такаже +5..+10 долларов к цене чипа.
        Производительность CPU можно примерно оценить по рассеиваемой мощности (TDP). Я проводил тесты на десятке железок с ARM на задачах машинного зрения. Какой-то усредненный алгоритм выдавал 3-4 fps на процессорах начального уровня,Распберри Zero и 25fps Распберри 4.