Русский
Русский
English
Статистика
Реклама

Yolov3

Из песочницы Breakout-YOLO знакомимся с шустрой object-detection моделью, играя в классический Арканоид

21.06.2020 14:07:34 | Автор: admin


Всем привет! Весенний семестр для некоторых студентов 3-го курса ФУПМ МФТИ ознаменовался сдачей проектов по курсу Методы оптимизации. Каждый должен был выделить интересную для себя тему (или придумать свою) и воплотить её в жизнь в виде кода, научной статьи, численного эксперимента или даже бота в Telegram.


Жёстких ограничений на выбор темы не было, поэтому можно было дать разгуляться фантазии. You Only Live Once! воскликнул я, и решил использовать эту возможность, чтобы привнести немного огня в бессмертную классику.,


Выбор проекта




Всем хороша свобода выбора, кроме одного: надо определиться с этим самым выбором. Имелось много тем теоретических проектов, предложенных лектором и семинаристами, но меня они не заинтересовали. Всё-таки хотелось получить на выходе нечто, что можно потрогать руками, а не корпеть над статьями и математическими выкладками.


И тут я вспомнил, что год назад вместе с одногруппниками писал браузерный Арканоид на JavaScript. Почему бы не добавить в эту бородатую игрушку немного рок-н-ролла, а точнее модного нынче computer vision-а? Эта область ML представлялась мне довольно интересной и проект на эту тему стал бы прекрасной мотивацией для изучения.


Так и появилась игра Breakout с управлением жестами через веб-камеру, или выражаясь лаконичнее Breakout-YOLO, но обо всем по порядку.


Постановка задачи




Основной геймплей Арканоида заключается в отбивании шарика двигающейся платформой, управляемой мышью/тачпадом или клавиатурой:



Мне же хотелось перенести функцию управления на жесты пользователя. Управление платформой я утвердил следующее: её положение определяется положением некоторого жеста на видео с веб-камеры.


То есть естественным образом возникла задача real-time object-detection на вход алгоритму поступает кадр из видео, на выходе хотим иметь изображение с объектами, обведёнными в рамку (bounding box) прямо как на картинке ниже:



И все это нужно делать быстро, выдавая хотя бы 20-30 FPS, ведь никому не интересно играть в слайд-шоу. Не сомневаюсь, что причастные к CV люди по названию статьи догадались какую модель я выбрал, остальных же охотно приглашаю под следующий заголовок.


Tiny-YOLOv3




You Only Look Once


YOLO это популярная архитектура CNN для решения задачи object-detection:



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


За подробным описанием архитектуры отсылаю интересующегося читателя к оригинальной статье и её обзору от Deep Systems.


Better, Faster, Stronger


Одной из последних* версий YOLO является YOLOv3. От первоначальной модели эту отличает несколько новых фич: авторы отказались от fully-connected слоя в конце, таким образом остались только свёрточные слои. Для улучшения распознавания объектов разных размеров было добавлено 2 дополнительных выходных слоя, что напоминает концепцию feature pyramid.


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


Для того, чтобы регуляризовать сеть, к каждому свёрточному слою был прикручен batch normalization. Также был применён multi-scale training случайное изменение разрешения входящего изображения при обучении.


Детальное описание всех изменений можно найти в статьях про YOLOv2 и YOLOv3 и обзоре.


Я же взял Tiny-YOLOv3 уменьшенную версию этой модели без последнего слоя:



В ней используется слой пулинга вместо свёрточного слоя с страйдом 2. Всё это делает её быстрее и компактнее (~ 35 Мб), а значит более предпочтительнее для моей задачи.


* на момент выбора модели. С тех пор успела выйти YOLOv4 и нечто, называющее себя YOLOv5.


Работа с данными




Перейдём к самой рабоче-крестьянской части проекта.


Выбор жестов и датасета


Для управления игрой были выбраны следующие 4 жеста:



Вопрос: на чем обучать модель?


Первое, что пришло в голову взять датасет какого-нибудь жестового языка, например американского: ASL Alphabet, Sign Language MNIST и обучится на нем.
Но меня отпугнуло следующее:


  • В отсмотренных мною датасетах все фотографии были сделаны на однородном фоне
  • Не все выбранные мною жесты были представлены в датасетах
  • Я не нашел именно object detection датасета с bounding box-ами, так что в любом случае пришлось бы доразмечать данные

Модели же предстояло работать на разных ноутбуках с разными веб-камерами в условиях если не агрессивного фона и плохого освещения, то как минимум силуэта человека на заднем плане. По этой причине (а также из-за своей неосведомлённости в deep learning) я не мог обоснованно сказать, как YOLO обученная на этих данных поведет себя в бою. К тому же сроки предзащиты проекта поджимали права на ошибку не было.


Поэтому я посчитал правильным действовать наверняка и решил...



собрать и разметить свой датасет.


Не имей сто рублей, а имей сто друзей


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


Как для достижения большего разнообразия в датасете, так и для облегчения собственных страданий пришлось прибегнуть к бесплатному краудсорсингу помощи друзей. Я попросил каждого из этих восьми несчастных записать на веб-камеру 2-ух минутное видео с 4 жестами по 15 секунд на жест каждой рукой. Сам я сделал 4 таких видео в разной одежде и на разных фонах.


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


К концу 8-го часа разметки у меня имелось около 8000 изображений с аннотациями в .txt файлах, полное нежелание продолжать процесс и, наверное, парочка новых сидячих болезней. Поэтому, удовлетворившись проделанной работой, я пересчитал anchor box-ы, разделил данные на тест и трейн в жадном соотношении 1:9 и перешел к следующему этапу.


Обучение




Снаряжаемся видеокарточками


Времени до первого показа проекта оставалось всё меньше и меньше, поэтому я пошёл куда глаза глядят по пути наименьшего сопротивления и решил обучать модель в Darknet написанном на C и CUDA open-source фреймворке для создания и обучения нейронных сетей.


Вычислительных мощностей мне объективно не хватало из графических процессоров с поддержкой CUDA под рукой имелась только GeForce GT 740M (Compute Capability = 3.0), которой требовалось 10-12 часов для обучения Tiny-YOLOv3. Поэтому я стал счастливым обладателем подписки на Colab Pro с Tesla P100-PCIE-16GB под капотом, сократившей это время до 3 часов. Стоит отметить, что Colab уже давно и так бесплатно предоставляет всем желающим мощную Tesla T4, правда с ограничениями по времени использования. О них я узнал по всплывшей на экране надписи You cannot currently connect to a GPU due to usage limits in Colab, заигравшись с обучением YOLO на всяких кошечках-собачках, интереса ради.


Чтобы нащупать trade-off между точностью распознавания и быстродействием, я взял 4 модели: Tiny-YOLOv3 с размерами входного изображения 192x192, 256x256, 416x416 и XNOR Tiny-YOLOv3, в которой свёртки аппроксимируются двоичными операциями, что должно ускорять работу сети. Указанные Ёлки обучались в течение 8, 10, 12 и 16 тысяч итераций соответственно.


Метриками качества обученных моделей послужили mean Average Precision с IoU=0.5(далее обозначается как mAP@0,5) и FPS.


Результаты обучения



На графиках выше представлены зависимости значений лосс-функции на трейне и mAP@0.5 на тесте от номера итерации обучения.


Как видите, XNOR модели нездоровилось. Несмотря на финальный mAP@0.5 88%, при хорошем освещении она еле-еле распознавала пару жестов с минимальным confidence. FPS 15 также намекал, что что-то пошло не так. Это не могло не расстраивать, так как я возлагал на XNOR большие надежды. На момент написания этой статьи мне всё ещё не ясно, в чём была проблема, возможно я что-то напутал в .cfg файле.



Остальные же модели обучились приемлемо. Полученные значения FPS справедливы для следующей


конфигурации

ASUS Vivobook S15, i7 1.8GHz, GeForce MX250


В погоне за быстродействием для использования в игре я выбрал 192х192 модель.


Дабы не зависящий от размера входного изображения mAP@0.5 не вводил в заблуждение, надо сказать, что эта метрика в данной ситуации несколько обманчива.


Дело в том, что в собранном датасете жесты на фотографиях расположены примерно на одном расстоянии от камеры, поэтому модели переобучились на это расстояние. Это видно по тому, как 192х192 модель работает при сильном приближении/отдалении от веб-камеры она не умеет предсказывать очень большие/маленькие bounding box-ы, так как не видела таковых в датасете. Однако такое переобучение соответствует поставленной задаче распознаванию жестов на установленном расстоянии, удобном пользователю.


Таким образом, важно понимать, что mAP@0.5 здесь тесно связан с конкретной задачей, и на самом деле при уменьшении размера входного изображения модель теряет в точности распознавания очень близких/далеких к камере жестов, что, однако, практически не влияет на точность на тесте.


Let's play!




Немного про архитектуру


Для распознавания жестов в браузере я использовал библиотеку TensorFlow.js. Обученные веса сначала были сконвертированы в Keras, а затем преобразованы в TFJS Graph model.


Так выглядит демо-версия (загрузка может занять некоторое время):



Сама игра написана на Javascript, за основу был взят этот туториал. Её архитектура предельно проста и заключается лишь в перерисовке кадров с помощью setInterval. На некоторых девайсах наблюдался довольно низкий FPS, поэтому я установил модель распознавать жесты каждую 10-ую перерисовку.


Несмотря на это, всё равно могут быть заметны фризы в момент распознавания. Чтобы их избежать я планирую распараллелить распознавание и перерисовку с помощью Web Worker.


А вот теперь поиграем


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


Управление игрой:


  • Нажатие на кнопку Play/пауза в игре жест finger up
  • Перемещение курсора в меню/снятие с паузы жест circle
  • Перемещение платформы жест fist
  • Выстрел шариком с липкой платформы жест pistol

Поиграть можно здесь. Я рекомендую использовать FIrefox, но Chrome/Chromium также поддерживаются.


Так выглядит геймплей:



Вместо заключения




Внимательный читатель спросит: А причем тут собственно методы оптимизации? Да ни при чем! Но такая свобода выбора темы проекта дала стимул плотно начать разбираться в интересном разделе машинного обучения, и это здорово, на мой взгляд.


Sapere aude, хабровчане!


Полезные ссылки





P. S.


Всё это, как видится, лишь история о моём знакомстве с моделью YOLO и о её использовании. Если Вы знаете как надо было сделать, то приглашаю вместе с конструктивной критикой в комментарии, буду рад дельным рекомендациям.

Подробнее..

Обнаружение объектов с помощью YOLOv3 на Tensorflow 2.0

08.05.2021 14:13:54 | Автор: admin
Кадр из аниме "Жрица и медведь"Кадр из аниме "Жрица и медведь"

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

В данной статье мы узнаем о системе YOLO Object Detection и как реализовать подобную систему в Tensorflow 2.0

О YOLO:

Наша унифицированная архитектура чрезвычайно быстра. Базовая модель YOLO обрабатывает изображения в режиме реального времени со скоростью 45 кадров в секунду. Уменьшенная версия сети, Fast YOLO, обрабатывает аж 155 кадра в секунду

You Only Look Once: Unified, Real-Time Object Detection, 2015

Что такое YOLO?

YOLO это новейшая (на момент написания оригинальной статьи) система (сеть) обнаружения объектов. Она была разработана Джозефом Редмоном (Joseph Redmon). Наибольшим преимуществом YOLO над другими архитектурами является скорость. Модели семейства YOLO исключительно быстры и намного превосходят R-CNN (Region-Based Convolutional Neural Network) и другие модели. Это позволяет добиться обнаружения объектов в режиме реального времени.

На момент первой публикации (в 2016 году) по сравнению с другими системами, такими как R-CNN и DPM (Deformable Part Model), YOLO добилась передового значения mAP (mean Average Precision). С другой стороны, YOLO испытывает трудности с точной локализацией объектов. Однако в новой версии были внесены улучшения в скорости и точности системы.

Альтернативы (на момент публикации статьи): Другие архитектуры в основном использовали метод скользящего окна по всему изображению, и классификатор использовался для определенной области изображения (DPM). Также, R-CNN использовал метод предложения регионов (region proposal method). Описываемый метод сначала создает потенциальные bounding boxы. Затем, на области, ограниченные bounding boxами, запускается классификатор и следующее удаление повторяющихся распознаваний, и уточнение границ рамок.

YOLO переосмыслила задачу обнаружения объектов в задачу регрессии.Она идет от пикселей изображения к координатам bounding boxов и вероятностей классов. Тем самым, единая сверточная сеть предсказывает несколько bounding boxов и вероятности классов для содержания этих областей.

Теория

Так как YOLO необходимо только один взгляд на изображение, то метод скользящего окна не подходит в данной ситуации. Вместо этого, изображение будет поделено на сетку с ячейками размером S x S. Каждая ячейка может содержать несколько разных объектов для распознавания.

Во-первых, каждая ячейка отвечает за прогнозирование количества bounding boxов. Также, каждая ячейка прогнозирует доверительное значение (confidence value) для каждой области, ограниченной bounding boxом. Иными словами, это значение определяет вероятность нахождения того или иного объекта в данной области. То есть в случае, если какая-то ячейка сетки не имеет определенного объекта, важно, чтобы доверительное значение для этой области было низким.

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

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

Давайте подробней опишем вывод модели.

В YOLO используются anchor boxes (якорные рамки / фиксированные рамки) для прогнозирования bounding boxов. Идея anchor boxов сводится к предварительному определению двух различных форм. И таким образом, мы можем объединить два предсказания с двумя anchor boxами (в целом, мы могли бы использовать даже большее количество anchor boxов). Эти якоря были рассчитаны с помощью датасета COCO (Common Objects in Context) и кластеризации k-средних (K-means clustering).

У нас есть сетка, где каждая ячейка предсказывает:

  • Для каждого bounding box'а:

    • 4 координаты (tx , ty , tw , th)

    • 1 objectness error (ошибка объектности), которая является показателем уверенности в присутствии того или иного объекта

  • Некоторое количество вероятностей классов

Если же присутствует некоторое смещение от верхнего левого угла на cx , cy то прогнозы будут соответствовать:

b_{x} = \sigma(t_{x}) + c_{x}\\ b_{y} = \sigma(t_{y}) + c_{y}\\ b_{w} = p_{w}e^{t_{w}}\\ b_{h} = p_{h}e^{t_{h}}

где pw (ширина) и ph (высота) соответствуют ширине и высоте bounding box'а. Вместо того, чтобы предугадывать смещение как в прошлой версии YOLOv2, авторы прогнозируют координаты местоположения относительно местоположения ячейки.

Этот вывод является выводом нашей нейронной сети. В общей сложности здесьS x S x [B * (4+1+C)] выводов, где B это количество bounding box'ов, которое может предсказать ячейка на карте объектов, C это количество классов, 4 для bounding box'ов, 1 для objectness prediction (прогнозирование объектности). За один проход мы можем пройти от входного изображения к выходному тензору, который соответствует обнаруженным объектам на картинке. Также стоит отметить, что YOLOv3 прогнозирует bounding box'ы в трех разных масштабах.

Теперь, если мы возьмем вероятность и умножим их на доверительные значения, мы получим все bounding box'ы, взвешенные по вероятности содержания этого объекта.

Простое нахождение порогового значения избавит нас от прогнозов с низким доверительным значением. Для следующего шага важно определить метрику IoU (Intersection over Union / Пересечение над объединением). Эта метрика равняется соотношению площади пересекающихся областей к площади областей объединенных.

После этого все равно могут остаться дубликаты, и чтобы от них избавиться нужно использовать подавление не-максимумов (non-maximum suppression). Подавление не-максимумов заключается в следующем: алгоритм берёт bounding box с наибольшей вероятностью принадлежности к объекту, затем, среди остальных граничащих bounding box'ов с данной области, возьмёт один с наивысшим IoU и подавляет его.

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

Yolov3Yolov3

Мы также рекомендуем прочитать следующие статьи о YOLO:

Реализация в Tensorflow

Первым шагом в реализации YOLO это подготовка ноутбука и импортирование необходимых библиотек. Целиком ноутбук с кодом вы можете на Github или Kaggle:

Следуя этой статье, мы сделаем полную сверточную сеть (fully convolutional network / FCN) без обучения. Для того, чтобы применить эту сеть для определения объектов, нам необходимо скачать готовые веса от предварительно обученной модели. Эти веса были получены от обучения YOLOv3 на датасете COCO (Common Objects in Context). Файл с весами можно скачать по ссылке официального сайта.

# Создаем папку для checkpoint'ов с весами.# !mkdir checkpoints# Скачиваем файл с весами для YOLOv3 с официального сайта.# !wget https://pjreddie.com/media/files/yolov3.weights# Импортируем необходимые библиотеки.import cv2import numpy as np import tensorflow as tf from absl import loggingfrom itertools import repeatfrom PIL import Imagefrom tensorflow.keras import Modelfrom tensorflow.keras.layers import Add, Concatenate, Lambdafrom tensorflow.keras.layers import Conv2D, Input, LeakyReLUfrom tensorflow.keras.layers import MaxPool2D, UpSampling2D, ZeroPadding2Dfrom tensorflow.keras.regularizers import l2from tensorflow.keras.losses import binary_crossentropyfrom tensorflow.keras.losses import sparse_categorical_crossentropyyolo_iou_threshold = 0.6 # Intersection Over Union (iou) threshold.yolo_score_threshold = 0.6 # Score threshold.weightyolov3 = 'yolov3.weights' # Путь до файла с весами.size = 416 # Размер изображения. checkpoints = 'checkpoints/yolov3.tf' # Путь до файла с checkpoint'ом.num_classes = 80 # Количество классов в модели.# Список слоев в YOLOv3 Fully Convolutional Network (FCN).YOLO_V3_LAYERS = [    'yolo_darknet',    'yolo_conv_0',    'yolo_output_0',    'yolo_conv_1',    'yolo_output_1',    'yolo_conv_2',    'yolo_output_2']

По причине того, что порядок слоев в Darknet (open source NN framework) и tf.keras разные, то загрузить веса с помощью чистого функционального API будет проблематично. В этом случае, наилучшим решением будет создание подмоделей в keras. TF Checkpoints рекомендованы для сохранения вложенных подмоделей и они официально поддерживаются Tensorflow.

# Функция для загрузки весов обученной модели.def load_darknet_weights(model, weights_file):    wf = open(weights_file, 'rb')    major, minor, revision, seen, _ = np.fromfile(wf, dtype=np.int32, count=5)    layers = YOLO_V3_LAYERS    for layer_name in layers:        sub_model = model.get_layer(layer_name)        for i, layer in enumerate(sub_model.layers):            if not layer.name.startswith('conv2d'):                continue            batch_norm = None            if i + 1 < len(sub_model.layers) and \                sub_model.layers[i + 1].name.startswith('batch_norm'):                    batch_norm = sub_model.layers[i + 1]            logging.info("{}/{} {}".format(                sub_model.name, layer.name, 'bn' if batch_norm else 'bias'))                        filters = layer.filters            size = layer.kernel_size[0]            in_dim = layer.input_shape[-1]            if batch_norm is None:                conv_bias = np.fromfile(wf, dtype=np.float32, count=filters)            else:                bn_weights = np.fromfile(wf, dtype=np.float32, count=4*filters)                bn_weights = bn_weights.reshape((4, filters))[[1, 0, 2, 3]]            conv_shape = (filters, in_dim, size, size)            conv_weights = np.fromfile(wf, dtype=np.float32, count=np.product(conv_shape))            conv_weights = conv_weights.reshape(conv_shape).transpose([2, 3, 1, 0])            if batch_norm is None:                layer.set_weights([conv_weights, conv_bias])            else:                layer.set_weights([conv_weights])                batch_norm.set_weights(bn_weights)    assert len(wf.read()) == 0, 'failed to read weights'    wf.close()

На этом же этапе, мы должны определить функцию для расчета IoU. Мы используем batch normalization (пакетная нормализация) для нормализации результатов, чтобы ускорить обучение. Так как tf.keras.layers.BatchNormalization работает не очень хорошо для трансферного обучения (transfer learning), то мы используем другой подход.

# Функция для расчета IoU.def interval_overlap(interval_1, interval_2):    x1, x2 = interval_1    x3, x4 = interval_2    if x3 < x1:        return 0 if x4 < x1 else (min(x2,x4) - x1)    else:        return 0 if x2 < x3 else (min(x2,x4) - x3)def intersectionOverUnion(box1, box2):    intersect_w = interval_overlap([box1.xmin, box1.xmax], [box2.xmin, box2.xmax])    intersect_h = interval_overlap([box1.ymin, box1.ymax], [box2.ymin, box2.ymax])    intersect_area = intersect_w * intersect_h    w1, h1 = box1.xmax-box1.xmin, box1.ymax-box1.ymin    w2, h2 = box2.xmax-box2.xmin, box2.ymax-box2.ymin    union_area = w1*h1 + w2*h2 - intersect_area    return float(intersect_area) / union_area class BatchNormalization(tf.keras.layers.BatchNormalization):    def call(self, x, training=False):        if training is None: training = tf.constant(False)        training = tf.logical_and(training, self.trainable)        return super().call(x, training)# Определяем 3 anchor box'а для каждой ячейки.   yolo_anchors = np.array([(10, 13), (16, 30), (33, 23), (30, 61), (62, 45),                        (59, 119), (116, 90), (156, 198), (373, 326)], np.float32) / 416yolo_anchor_masks = np.array([[6, 7, 8], [3, 4, 5], [0, 1, 2]])

В каждом масштабе мы определяем 3 anchor box'а для каждой ячейки. В нашем случае если маска будет:

  • 0, 1, 2 означает, что будут использованы первые три якорные рамки

  • 3, 4 ,5 означает, что будут использованы четвертая, пятая и шестая

  • 6, 7, 8 означает, что будут использованы седьмая, восьмая, девятая

# Функция для отрисовки bounding box'ов.def draw_outputs(img, outputs, class_names, white_list=None):    boxes, score, classes, nums = outputs    boxes, score, classes, nums = boxes[0], score[0], classes[0], nums[0]    wh = np.flip(img.shape[0:2])    for i in range(nums):        if class_names[int(classes[i])] not in white_list:            continue        x1y1 = tuple((np.array(boxes[i][0:2]) * wh).astype(np.int32))        x2y2 = tuple((np.array(boxes[i][2:4]) * wh).astype(np.int32))        img = cv2.rectangle(img, x1y1, x2y2, (255, 0, 0), 2)        img = cv2.putText(img, '{} {:.4f}'.format(            class_names[int(classes[i])], score[i]),            x1y1, cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0, 0, 255), 2)    return img

Теперь пришло время для реализации YOLOv3. Идея заключается в том, чтобы использовать только сверточные слои. Так как их здесь 53, то самым простым способом является создание функции, в которую мы будем передавать важные параметры, меняющиеся от слоя к слою.

Остаточные блоки (Residual Blocks) в диаграмме архитектуры YOLOv3 применяются для изучения признаков. Остаточный блок содержит в себе несколько сверточных слоев и дополнительные связи для обхода этих слоев.

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

def DarknetConv(x, filters, size, strides=1, batch_norm=True):    if strides == 1:        padding = 'same'    else:        x = ZeroPadding2D(((1, 0), (1, 0)))(x)        padding = 'valid'    x = Conv2D(filters=filters, kernel_size=size,              strides=strides, padding=padding,              use_bias=not batch_norm, kernel_regularizer=l2(0.0005))(x)    if batch_norm:        x = BatchNormalization()(x)        x = LeakyReLU(alpha=0.1)(x)    return xdef DarknetResidual(x, filters):    previous = x    x = DarknetConv(x, filters // 2, 1)    x = DarknetConv(x, filters, 3)    x = Add()([previous , x])    return xdef DarknetBlock(x, filters, blocks):    x = DarknetConv(x, filters, 3, strides=2)    for _ in repeat(None, blocks):        x = DarknetResidual(x, filters)           return xdef Darknet(name=None):    x = inputs = Input([None, None, 3])    x = DarknetConv(x, 32, 3)    x = DarknetBlock(x, 64, 1)    x = DarknetBlock(x, 128, 2)    x = x_36 = DarknetBlock(x, 256, 8)    x = x_61 = DarknetBlock(x, 512, 8)    x = DarknetBlock(x, 1024, 4)    return tf.keras.Model(inputs, (x_36, x_61, x), name=name)  def YoloConv(filters, name=None):    def yolo_conv(x_in):        if isinstance(x_in, tuple):            inputs = Input(x_in[0].shape[1:]), Input(x_in[1].shape[1:])            x, x_skip = inputs            x = DarknetConv(x, filters, 1)            x = UpSampling2D(2)(x)            x = Concatenate()([x, x_skip])        else:            x = inputs = Input(x_in.shape[1:])        x = DarknetConv(x, filters, 1)        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, filters, 1)        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, filters, 1)        return Model(inputs, x, name=name)(x_in)    return yolo_conv  def YoloOutput(filters, anchors, classes, name=None):    def yolo_output(x_in):        x = inputs = Input(x_in.shape[1:])        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, anchors * (classes + 5), 1, batch_norm=False)        x = Lambda(lambda x: tf.reshape(x, (-1, tf.shape(x)[1], tf.shape(x)[2],                                        anchors, classes + 5)))(x)        return tf.keras.Model(inputs, x, name=name)(x_in)    return yolo_outputdef yolo_boxes(pred, anchors, classes):    grid_size = tf.shape(pred)[1]    box_xy, box_wh, score, class_probs = tf.split(pred, (2, 2, 1, classes), axis=-1)    box_xy = tf.sigmoid(box_xy)    score = tf.sigmoid(score)    class_probs = tf.sigmoid(class_probs)    pred_box = tf.concat((box_xy, box_wh), axis=-1)    grid = tf.meshgrid(tf.range(grid_size), tf.range(grid_size))    grid = tf.expand_dims(tf.stack(grid, axis=-1), axis=2)    box_xy = (box_xy + tf.cast(grid, tf.float32)) /  tf.cast(grid_size, tf.float32)    box_wh = tf.exp(box_wh) * anchors    box_x1y1 = box_xy - box_wh / 2    box_x2y2 = box_xy + box_wh / 2    bbox = tf.concat([box_x1y1, box_x2y2], axis=-1)        return bbox, score, class_probs, pred_box

Теперь определим функцию подавления не-максимумов.

def nonMaximumSuppression(outputs, anchors, masks, classes):    boxes, conf, out_type = [], [], []    for output in outputs:        boxes.append(tf.reshape(output[0], (tf.shape(output[0])[0], -1, tf.shape(output[0])[-1])))        conf.append(tf.reshape(output[1], (tf.shape(output[1])[0], -1, tf.shape(output[1])[-1])))        out_type.append(tf.reshape(output[2], (tf.shape(output[2])[0], -1, tf.shape(output[2])[-1])))    bbox = tf.concat(boxes, axis=1)    confidence = tf.concat(conf, axis=1)    class_probs = tf.concat(out_type, axis=1)    scores = confidence * class_probs      boxes, scores, classes, valid_detections = tf.image.combined_non_max_suppression(        boxes=tf.reshape(bbox, (tf.shape(bbox)[0], -1, 1, 4)),        scores=tf.reshape(            scores, (tf.shape(scores)[0], -1, tf.shape(scores)[-1])),        max_output_size_per_class=100,        max_total_size=100,        iou_threshold=yolo_iou_threshold,        score_threshold=yolo_score_threshold)      return boxes, scores, classes, valid_detections

Основная функция:

def YoloV3(size=None, channels=3, anchors=yolo_anchors,            masks=yolo_anchor_masks, classes=80, training=False):    x = inputs = Input([size, size, channels])    x_36, x_61, x = Darknet(name='yolo_darknet')(x)    x = YoloConv(512, name='yolo_conv_0')(x)    output_0 = YoloOutput(512, len(masks[0]), classes, name='yolo_output_0')(x)    x = YoloConv(256, name='yolo_conv_1')((x, x_61))    output_1 = YoloOutput(256, len(masks[1]), classes, name='yolo_output_1')(x)    x = YoloConv(128, name='yolo_conv_2')((x, x_36))    output_2 = YoloOutput(128, len(masks[2]), classes, name='yolo_output_2')(x)    if training:        return Model(inputs, (output_0, output_1, output_2), name='yolov3')    boxes_0 = Lambda(lambda x: yolo_boxes(x, anchors[masks[0]], classes),                  name='yolo_boxes_0')(output_0)    boxes_1 = Lambda(lambda x: yolo_boxes(x, anchors[masks[1]], classes),                  name='yolo_boxes_1')(output_1)    boxes_2 = Lambda(lambda x: yolo_boxes(x, anchors[masks[2]], classes),                  name='yolo_boxes_2')(output_2)    outputs = Lambda(lambda x: nonMaximumSuppression(x, anchors, masks, classes),                  name='nonMaximumSuppression')((boxes_0[:3], boxes_1[:3], boxes_2[:3]))    return Model(inputs, outputs, name='yolov3')

Функция потерь:

def YoloLoss(anchors, classes=80, ignore_thresh=0.5):    def yolo_loss(y_true, y_pred):        pred_box, pred_obj, pred_class, pred_xywh = yolo_boxes(            y_pred, anchors, classes)        pred_xy = pred_xywh[..., 0:2]        pred_wh = pred_xywh[..., 2:4]        true_box, true_obj, true_class_idx = tf.split(            y_true, (4, 1, 1), axis=-1)        true_xy = (true_box[..., 0:2] + true_box[..., 2:4]) / 2        true_wh = true_box[..., 2:4] - true_box[..., 0:2]        box_loss_scale = 2 - true_wh[..., 0] * true_wh[..., 1]        grid_size = tf.shape(y_true)[1]        grid = tf.meshgrid(tf.range(grid_size), tf.range(grid_size))        grid = tf.expand_dims(tf.stack(grid, axis=-1), axis=2)        true_xy = true_xy * tf.cast(grid_size, tf.float32) - \            tf.cast(grid, tf.float32)        true_wh = tf.math.log(true_wh / anchors)        true_wh = tf.where(tf.math.is_inf(true_wh),                      tf.zeros_like(true_wh), true_wh)        obj_mask = tf.squeeze(true_obj, -1)        true_box_flat = tf.boolean_mask(true_box, tf.cast(obj_mask, tf.bool))        best_iou = tf.reduce_max(intersectionOverUnion(            pred_box, true_box_flat), axis=-1)        ignore_mask = tf.cast(best_iou < ignore_thresh, tf.float32)        xy_loss = obj_mask * box_loss_scale * \            tf.reduce_sum(tf.square(true_xy - pred_xy), axis=-1)        wh_loss = obj_mask * box_loss_scale * \            tf.reduce_sum(tf.square(true_wh - pred_wh), axis=-1)        obj_loss = binary_crossentropy(true_obj, pred_obj)        obj_loss = obj_mask * obj_loss + \            (1 - obj_mask) * ignore_mask * obj_loss        class_loss = obj_mask * sparse_categorical_crossentropy(            true_class_idx, pred_class)        xy_loss = tf.reduce_sum(xy_loss, axis=(1, 2, 3))        wh_loss = tf.reduce_sum(wh_loss, axis=(1, 2, 3))        obj_loss = tf.reduce_sum(obj_loss, axis=(1, 2, 3))        class_loss = tf.reduce_sum(class_loss, axis=(1, 2, 3))        return xy_loss + wh_loss + obj_loss + class_loss    return yolo_loss

Функция "преобразовать цели" возвращает кортеж из форм:

(    [N, 13, 13, 3, 6],    [N, 26, 26, 3, 6],    [N, 52, 52, 3, 6])

Где N число меток в пакете, а число 6 означает [x, y, w, h, obj, class] bounding box'а.

@tf.functiondef transform_targets_for_output(y_true, grid_size, anchor_idxs, classes):    N = tf.shape(y_true)[0]    y_true_out = tf.zeros(      (N, grid_size, grid_size, tf.shape(anchor_idxs)[0], 6))    anchor_idxs = tf.cast(anchor_idxs, tf.int32)    indexes = tf.TensorArray(tf.int32, 1, dynamic_size=True)    updates = tf.TensorArray(tf.float32, 1, dynamic_size=True)    idx = 0    for i in tf.range(N):        for j in tf.range(tf.shape(y_true)[1]):            if tf.equal(y_true[i][j][2], 0):                continue            anchor_eq = tf.equal(                anchor_idxs, tf.cast(y_true[i][j][5], tf.int32))            if tf.reduce_any(anchor_eq):                box = y_true[i][j][0:4]                box_xy = (y_true[i][j][0:2] + y_true[i][j][2:4]) / 2                anchor_idx = tf.cast(tf.where(anchor_eq), tf.int32)                grid_xy = tf.cast(box_xy // (1/grid_size), tf.int32)                indexes = indexes.write(                    idx, [i, grid_xy[1], grid_xy[0], anchor_idx[0][0]])                updates = updates.write(                    idx, [box[0], box[1], box[2], box[3], 1, y_true[i][j][4]])                idx += 1    return tf.tensor_scatter_nd_update(        y_true_out, indexes.stack(), updates.stack())def transform_targets(y_train, anchors, anchor_masks, classes):    outputs = []    grid_size = 13    anchors = tf.cast(anchors, tf.float32)    anchor_area = anchors[..., 0] * anchors[..., 1]    box_wh = y_train[..., 2:4] - y_train[..., 0:2]    box_wh = tf.tile(tf.expand_dims(box_wh, -2),                    (1, 1, tf.shape(anchors)[0], 1))    box_area = box_wh[..., 0] * box_wh[..., 1]    intersection = tf.minimum(box_wh[..., 0], anchors[..., 0]) * \    tf.minimum(box_wh[..., 1], anchors[..., 1])    iou = intersection / (box_area + anchor_area - intersection)    anchor_idx = tf.cast(tf.argmax(iou, axis=-1), tf.float32)    anchor_idx = tf.expand_dims(anchor_idx, axis=-1)    y_train = tf.concat([y_train, anchor_idx], axis=-1)    for anchor_idxs in anchor_masks:        outputs.append(transform_targets_for_output(            y_train, grid_size, anchor_idxs, classes))        grid_size *= 2    return tuple(outputs) # [x, y, w, h, obj, class]def preprocess_image(x_train, size):    return (tf.image.resize(x_train, (size, size))) / 255

Теперь мы можем создать нашу модель, загрузить веса и названия классов. В COCO датасете их 80.

yolo = YoloV3(classes=num_classes)load_darknet_weights(yolo, weightyolov3)yolo.save_weights(checkpoints)class_names =  ["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck",    "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench",    "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe",    "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",    "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",    "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl",    "banana","apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut",    "cake","chair", "sofa", "pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop",     "mouse","remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",    "refrigerator","book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"]def detect_objects(img_path, white_list=None):    image = img_path     # Путь к изображению.    img = tf.image.decode_image(open(image, 'rb').read(), channels=3)    img = tf.expand_dims(img, 0)    img = preprocess_image(img, size)    boxes, scores, classes, nums = yolo(img)    img = cv2.imread(image)    img = draw_outputs(img, (boxes, scores, classes, nums), class_names, white_list)    cv2.imwrite('detected_{:}'.format(img_path), img)    detected = Image.open('detected_{:}'.format(img_path))    detected.show()    detect_objects('test.jpg', ['bear'])

Итог

В этой статье мы поговорили об отличительных особенностях YOLOv3 и её преимуществах перед другими моделями. Мы рассмотрели способ реализации с использованием TensorFlow 2.0 (TF должен быть не менее версией 2.0).

Ссылки

Подробнее..

Как работает Object Tracking на YOLO и DeepSort

10.08.2020 00:11:52 | Автор: admin
Object Tracking очень интересное направление, которое изучается и эволюционирует не первый десяток лет. Сейчас многие разработки в этой области построены на глубоком обучении, которое имеет преимущество над стандартными алгоритмами, так как нейронные сети могут аппроксимировать функции зачастую лучше.

Но как именно работает Object Tracking? Есть множество Deep Learning решений для этой задачи, и сегодня я хочу рассказать о распространенном решении и о математике, которая стоит за ним.

Итак, в этой статье я попробую простыми словами и формулами рассказать про:

  • YOLO отличный object detector
  • Фильтры Калмана
  • Расстояние Махаланобиса
  • Deep SORT


YOLO отличный object detector

Сразу нужно сделать очень важную пометку, которую нужно запомнить Object Detection это не Object Tracking. Для многих это не будет новостью, но часто люди путают эти понятия. Простыми словами:

Object Detection это просто определение объектов на картинке/кадре. То есть алгоритм или нейронная сеть определяют объект и записывают его позицию и bounding boxes (параметры прямоугольников вокруг объектов). Пока что речи о других кадрах не идет, и алгоритм работает только с одним.

Пример:



Object Tracking здесь совсем другое дело. Здесь задача не просто определить объекты на кадре, но еще и связать информацию с предыдущих кадров таким образом, чтобы не терять объект, или сделать его уникальным.

Пример:



То есть Object Tracker включает в себя Object Detection для определения объектов, и другие алгоритмы для понимания какой объект на новом кадре принадлежит какому из предыдущего кадра.

Поэтому Object Detection играет очень важную роль в задаче трэкинга.

Почему YOLO? Да потому что YOLO считается эффективнее многих других алгоритмов для определения объектов. Вот небольшой график для сравнения от создателей YOLO:



Здесь мы рассматриваем YOLOv3-4, поскольку это самые последние версии и они эффективнее предыдущих.

Архитектуры разных Object Detectors

Итак, существует несколько архитектур нейронных сетей, созданных для определения объектов. Они в основном разделяются на двухуровневые, такие как RCNN, fast RCNN и faster RCNN, и одноуровневые, такие как YOLO.

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

Обычно это выглядит так (для faster RCNN, которая является самой быстрой из перечисленных двухуровневых систем):

  1. Подается картинка/кадр на вход
  2. Кадр прогоняется через CNN для формирования feature maps
  3. Отдельной нейронной сетью определяются регионы с высокой вероятностью нахождения в них объектов
  4. Дальше эти регионы с помощью RoI pooling сжимаются и подаются в нейронную сеть, определяющую класс объекта в регионах




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

В чем же крутость YOLO? В том, что эта архитектура не имеет двух проблем свыше, и она доказала неоднократно свою эффективность.

Вообще архитектура YOLO в первых блоках не сильно отличается по логике блоков от других детекторов, то есть на вход подается картинка, дальше создаются feature maps с помощью CNN (правда в YOLO используется своя CNN под названием Darknet-53), затем эти feature maps определенным образом анализируются (об этом чуть позже), выдавая на выходе позиции и размеры bounding boxes и классы, которым они принадлежат.



Но что такое Neck, Dense Prediction и Sparse Prediction?

С Sparse Prediction мы разобрались немного ранее это просто повторение того, как двухуровневые алгоритмы работают: определяют по отдельности регионы и затем классифицируют эти регионы.

Neck (или шея) это отдельный блок, который создан для того, чтобы агрегировать информацию от отдельных слоев с предыдущих блоков (как показано на рисунке выше) для увеличения аккуратности предсказания. Если Вас заинтересовало это можете погуглить термины Path Aggregation Network, Spatial Attention Module и Spatial Pyramid Pooling.

И, наконец, то, что отличает YOLO от всех других архитектур блок под названием (на нашей картинке выше) Dense Prediction. На нем мы сфокусируемся чуть сильнее, потому что это очень интересное решение, которое как раз позволило YOLO вырваться в лидеры по эффективности определения объектов.

YOLO (You Only Look Once) несет в себе философию смотреть на картинку один раз, и за этот один просмотр (то есть один прогон картинки через одну нейронную сеть) делать все необходимые определения объектов. Как это происходит?

Итак, на выходе от работы YOLO мы обычно хотим вот это:



Что делает YOLO когда учится на данных (простыми словами):

Шаг 1: Обычно картинки решейпят под размер 416x416 перед началом обучения нейронной сети, чтобы можно было их подавать батчами (для ускорения обучения).

Шаг 2: Делим картинку (пока что мысленно) на клетки размером axa. В YOLOv3-4 принято делить на клетки размером 13x13 (о различных скейлах поговорим чуть позже, чтобы было понятнее).



Теперь фокусируемся на эти клеточках, на которые мы разделили картинку/кадр. Такие клетки, которые называются grid cells, лежат в основе идеи YOLO. Каждая клетка является якорем, к которому прикрепляются bounding boxes. То есть вокруг клетки рисуются несколько прямоугольников для определения объекта (поскольку непонятно, какой формы прямоугольник будет наиболее подходящим, их рисуют сразу несколько и разных форм), и их позиции, ширина и высота вычисляются относительно центра этой клетки.



Как же рисуются эти прямоугольники (bounding boxes) вокруг клетки? Как определяется их размер и позиция? Здесь в борьбу вступает техника anchor boxes (в переводе якорные коробки, или якорные прямоугольники). Они задаются в самом начале либо самим пользователем, либо их размеры определяются исходя из размеров bounding boxes, которые есть в датасете, на котором будет тренироваться YOLO (используется K-means clustering и IoU для определения самых подходящих размеров). Обычно задают порядка 3 различных anchor boxes, которые будут нарисованы вокруг (или внутри) одной клетки:



Зачем это сделано? Сейчас все будет понятно, так как мы обсудим то, как YOLO обучается.

Шаг 3. Картинка из датасета прогоняется через нашу нейронную сеть (заметим, что кроме картинки в тренировочном датасете у нас должны быть определенны позиции и размеры настоящих bounding boxes для объектов, которые есть на ней. Это называется аннотация и делается это в основном вручную).

Давайте теперь подумаем, что нам нужно получить на выходе.

Для каждой клетки, нам нужно понять две принципиальные вещи:

  1. Какой из anchor boxes, из 3 нарисованных вокруг клетки, нам подходит больше всего и как его можно немного подправить для того, чтобы он хорошо вписывал в себя объект
  2. Какой объект находится внутри этого anchor box и есть ли он вообще


Какой же должен быть тогда output у YOLO?

1. На выходе для каждой клетки мы хотим получить:



2. Output должен включать в себя вот такие параметры:



Как определяется objectness? На самом деле этот параметр определяется с помощью метрики IoU во время обучения. Метрика IoU работает так:



В начале Вы можете выставить порог для этой метрики, и если Ваш предсказанный bounding box будет выше этого порога, то у него будет objectness равной единице, а все остальные bounding boxes, у которых objectness ниже, будут исключены. Эта величина objectness понадобится нам, когда мы будем считать общий confidence score (на сколько мы уверены, что это именно нужный нам объект расположен внутри предсказанного прямоугольника) у каждого определенного объекта.

А теперь начинается самое интересное. Представим, что мы создатели YOLO и нам нужно натренировать ее на то, чтобы распознавать людей на кадре/картинке. Мы подаем картинку из датасета в YOLO, там происходит feature extraction в начале, а в конце у нас получается CNN слой, который рассказывает нам о всех клеточках, на которые мы разделили нашу картинку. И если этот слой рассказывает нам неправду о клеточках на картинке, то у нас должен быть большой Loss, чтобы потом его уменьшать при подаче в нейронную сеть следующих картинок.

Чтобы было совсем понятно, есть очень простая схема с тем, как YOLO создает этот последний слой:



Как мы видим из картинки, этот слой, размером 13x13 (для картинок изначального размера 416x416) для того, чтобы рассказывать про каждую клетку на картинке. Из этого последнего слоя и достается информация, которую мы хотим.

YOLO предсказывает 5 параметров (для каждого anchor box для определенной клетки):



Чтобы было легче понять, есть хорошая визуализация на эту тему:



Как можно понять их этой картинки, задача YOLO максимально точно предсказать эти параметры, чтобы максимально точно определять объект на картинке. А confidence score, который определяется для каждого предсказанного bounding box, является неким фильтром для того, чтобы отсеять совсем неточные предсказания. Для каждого предсказанного bounding box мы умножаем его IoU на вероятность того, что это определенный объект (вероятностное распределение рассчитывается во время обучения нейронной сети), берем лучшую вероятность из всех возможных, и если число после умножения превышает определенный порог, то мы можем оставить этот предсказанный bounding box на картинке.

Дальше, когда у нас остались только предсказанные bounding boxes с высоким confidence score, наши предсказания (если их визуализировать) могут выглядеть примерно вот так:



Мы можем теперь использовать технику NMS (non-max suppression), чтобы отфильтровать bounding boxes таким образом, чтобы для одного объекта был только один предсказанный bounding box.



Нужно также знать, что YOLOv3-4 предсказывают на 3-х разных скейлах. То есть картинка делится на 64 grid cells, на 256 клеток и на 1024 клетки, чтобы также видеть маленькие объекты. Для каждой группы клеток алгоритм повторяет необходимые действия во время предсказания/обучения, которые были описаны сверху.

В YOLOv4 было использовано много техник для увеличения точности модели без сильной потери скорости. Но для самого предсказания оставили Dense Prediction таким же, как и у YOLOv3. Если Вы интересуетесь тем, что же такого магического сделали авторы, чтобы поднять так точность не теряя скорости, есть отличная статья, написанная про YOLOv4.

Надеюсь мне удалось немного донести то, как работает YOLO в целом (точнее последние две версии, то есть YOLOv3 и YOLOv4), и в Вас это пробудит желание воспользоваться этой моделью в будущем, или узнать про ее работу чуть подробнее.

Раз мы разобрались с, пожалуй, лучшей нейронной сетью для Object Detection (если оценивать скорость/качество), давайте наконец перейдем к тому, как же нам связывать информацию о наших определенных YOLO объектов между кадрами видео. Как программа может понимать, что человек на предыдущем кадре это тот же человек, что и на новом?

Deep SORT

Для понимания этой технологии, следует сначала разобраться с парой математических аспектов расстояние Махалонобиса и фильтр Калмана.

Расстояние Махалонобиса

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




Допустим, у нас есть две переменные X1 и X2. Для каждой из них у нас есть много измерений.



Теперь, допустим, у нас появилось 2 новых измерения:



Как понять, какое из этих двух значений наиболее подходит для нашего распределения? На глаз все очевидно точка 2 нам подходит. Но вот евклидово расстояние до среднего значения у обоих точек одинаково. Соответственно, простое евклидово расстояние до среднего значения нам не подойдет.

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

С этим как раз справляется расстояние Махалонобиса. Поскольку в обычно датасетах переменных больше чем двух, вместо корреляции мы будем использовать ковариационную матрицу:



Что на самом деле делает расстояние Махалонобиса:
  • Избавляется от ковариации переменных
  • Делает дисперсию (variance) переменных равной 1
  • После этого использует обычное евклидово расстояние для трансформированных данных


Посмотрим на формулу, как вычисляется расстояние Махалонобиса:



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


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

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

Фильтр Калмана

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

Фильтр Калмана
Мы разберем сейчас одномерный пример, чтобы просто понять как работает фильтр Калмана для одной переменной. Для нескольких переменных алгоритм никак не отличается, правда там будут задействованы матрицы, о которых я кратко упомяну в процессе.

Допустим у нас есть термометр и чашка с водой, температуру которой мы хотим измерить. У термометра есть своя погрешность в 4 градуса по Цельсию. Допустим, настоящая температура воды в чашке порядка 72 градусов.

Фильтр Калмана считает 3 принципиально важные вещи:

1) Усиление Калмана (Kalman Gain):



Сразу прошу прощения за картинки, у Хабра какие-то проблемы с формулами (или я не до конца разобрался почему они у меня не отображаются).

2) Оцениваем значение нужной нам переменной на ЭТОМ этапе, с учетом посчитанного нами усиления Калмана и показаний с датчиков (в нашем случае, показание термометра), а также значений с предыдущего этапа, который происходил в прошлом.



3) Оцениваем НОВУЮ ошибку (неопределенность), чтобы применить ее на следующем этапе:



Таким образом, весь алгоритм может выглядеть вот так:



Итак, допустим мы задали изначальную температуру 69 градусов (прикинули примерно), с ошибкой в 2 градуса. Тогда, зная, что у термометра погрешность примерно 4 градуса (ага, супер точный термометр, ну это в моей Вселенной), наше значение KG по формуле (1) будет равно 2/(2+4) = 0.33. Дальше мы можем легко измерить наше новое значение воды (более точное), вставив термометр в воду и померив температуру. Получилось 70 градусов (допустим), и теперь мы можем более точно оценить температуру воды в стакане. Она будет равна по формуле (2), где нам теперь известно все, 68+0.33(70-68)=68.66. А наша новая ошибка по формуле (3) будет равна (1-0.33)2 = 1.32. Теперь на следующих шагах мы будем применять соответсвенно вычисленные нами новые значения для ошибки, температуры воды, и будем смотреть на новые показания прибора, когда мы его заново опустим в воду. График будет выглядеть примерно вот так:



Это потрясающе, как фильтр Калмана может избавлять нас от шума и помогать находить настоящее значение!

Если же у нас есть несколько величин, которые мы хотим использовать для предсказания, то мы будем использовать матрицы, а алгоритм не поменяется, правда немного изменится то, как мы считаем определенные величины (об этом вы можете прочитать в разных постах на Хабре о фильтре Калмана, одну из них я прикреплял выше).



DeepSORT наконец-то!

Итак, мы теперь знаем, что такое фильтр Калмана и расстояние Махалонобиса. Технология DeepSORT просто связывает эти два понятия между собой для того, чтобы переносить информацию от одного кадра к другому, и добавляет новую метрику, под названием appearance. Сначала с помощью object detection определяются позиция, размер и класс одного bounding box. Потом можно в принципе применить Венгерский алгоритм, чтобы связать определенные объекты с ID объектов, которые раньше были на кадре и отслеживаются с помощью фильтров Калмана и все будет супер, как в оригинальном SORT. Но технология DeepSORT позволяет улучшить точность определения и уменьшить количество переключений между объектами, когда, допустим, один человек на кадре загораживает ненадолго другого, и теперь человек, которого загородили, считается новым объектом. Как она это делает?

Она добавляет крутой элемент в свою работу так называемый внешний вид людей, которые появляются на кадре (appearance). Этот внешний вид был натренирован отдельной нейронной сетью, которая была создана авторами DeepSORT. Они использовали порядка 1,100,000 картинок с более 1000 разных людей, чтобы нейронная сеть правильно предсказывала В оригинальном SORT есть проблема так как там не используется внешний вид объекта, то по факту, когда объект что-то закрывает на несколько кадров (например, другой человек или колона внутри здания), то алгоритм затем присваивает другой ID этому человеку в следствие чего так называемая память об объектах у оригинального SORT довольно краткосрочная.

Итак, теперь объекты имеют два свойства их динамика движения и их внешний вид. Для динамики у нас есть показатели, которые фильтруются и предсказываются с помощью фильтра Калмана (u,v,a,h,u,v,a,h), где u,v это позиция предсказанного прямоугольника по X и Y, a это соотношение сторон предсказанного прямоугольника (aspect ratio), h высота прямоугольника, ну и производные по каждой величине. Для внешнего вида тренировали нейронную сеть, которая имела структуру:



И в конце выдавала feature vector, размером 128x1. А дальше, вместо того, чтобы считать дистанцию между определенными объектами с помощью YOLO, и объектами, за которыми мы уже следили на кадре, а потом приписывать определенный ID просто с помощью расстояния Махалонобиса, авторы создали новую метрику для подсчета расстояния, которая включает в себя как предсказания с помощью фильтров Калмана, так и косинусовое расстояние (cosine distance), как называют его иначе, коэффициент Отиаи.

В итоге расстояние от определенного YOLO объекта до предсказанного фильтром Калмана объекта (или объекта, который уже есть в числе тех, который наблюдался на предыдущих кадрах) равно:



Где Da это дистанция по внешней схожести, а Dk расстояние Махалонобиса. Дальше эта гибридная дистанция применяется в Венгерском алгоритме, чтобы как раз правильно отсортировать определенные объекты с имеющимися ID.

Таким образом, простая дополнительная метрика Da помогла создать новый, элегантный алгоритм DeepSORT, который применяется во многих проблемах и является довольно популярным в задаче Object Tracking.

Статья получилась довольно увесистой, спасибо тем, кто дочитал до конца! Надеюсь, мне удалось рассказать что-то новое и помочь Вам в понимании того, как работает Object Tracking на YOLO и DeepSORT.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru