Как компьютерное зрение и распознание жестов может пригодиться бизнесу

от автора

Введение

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

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

При том что сама технология является зрелой, рынок ее применения находится на начальной стадии развития, но благодаря преимуществам компьютерного зрения он имеет потенциал роста. По данным Mordor Intelligence, совокупный годовой темп роста в ближайшие пять лет может составить 7,36 %.

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

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

  • оценка качества обслуживания/сервиса (палец вверх, палец вниз);

  • виртуальные надписи/подписи воображаемым маркером;

  • виртуальные кнопки в компьютерных играх;

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

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

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

На Habr ранее описывался набор данных HaGRID, который содержит информацию о 18 жестах рук и примерно по 30 000 экземпляров изображений на каждый жест. В наборе есть как более интуитивно понятные жесты, например «большой палец вверх», так и менее понятные — «указательный палец вверх». В итоге мы приняли решение поработать с детектированием и распознаванием до 10 наиболее интуитивных жестов. Наличие такого качественного набора данных как бы подталкивало пойти проторенной дорогой — обучать нейронную сеть на базе HaGRID, тем более что примеры обученных моделей также представлены в статье. Но начали мы с анализа альтернатив — готовых движков для отслеживания рук. 

Исследование доступных решений

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

В результате изучения открытых источников выявили следующие движки для отслеживания рук:

Решение

Краткое описание

Плюсы

Минусы

MediaPipe

Решение для отслеживания рук и пальцев с высокой точностью. Ссылка: https://google.github.io/mediapipe/solutions/hands.html 

21 3D-метка на руке

Наличие поддержки Python

Не поддерживаются позы рук

Yoha

Движок для отслеживания рук. Механизм развивается вокруг конкретных поз рук, которые пользователи/разработчики находят полезными. Ссылка: https://github.com/handtracking-io/yoha 

Поддерживаемые позы рук:

Щипок (касание указательным и большим пальцами)

Кулак

21 2D-метка на руке

Как язык программирования доступен только JavaScript

По итогам исследования выбор оказался достаточно невелик. Можно сказать, что выбор без выбора, так как из двух решений только одно поддерживает требуемый язык программирования. Кроме того, MediaPipe поддерживает детекцию 21 метки для точек крепления фаланг руки в 3D-пространстве (есть координаты X, Y, Z), в то время как Yoha работает лишь в 2D пространстве (координаты X и Y). Координата Z расширяет диапазон возможностей использования решения за счет позиционирования руки в 3D пространстве, например для отслеживания поворотов кистей рук.

Обзор базовых возможностей MediaPipe ранее публиковался на Habr.

Разработка собственного решения

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

Для запуска «вещания» со стороны сервера используется следующий код:

CLIENTS = set()   async def handler(websocket):     CLIENTS.add(websocket)     try:         await websocket.wait_closed()     finally:         CLIENTS.remove(websocket)         logging.debug("Websocket was removed")   async def broadcast(message):     for websocket in CLIENTS.copy():         try:             await websocket.send(message)         except websockets.ConnectionClosed:             logging.info("Websocket Connection Closed")

Чтобы отсылать сообщения о распознанных жестах на сторону потребителя всем пользователям, запишем все соединения в глобальную переменную CLIENTS и далее CLIENTS.add().

Сопрограмма wait_closed() идентична атрибуту closed(), за исключением того, что ее можно ожидать. Это может облегчить нам обнаружение завершения соединения, независимо от его причины, в задачах, которые взаимодействуют с WebSocket-соединением.

Далее создадим функцию для запуска сообщений со стороны сервера в онлайн-режиме.

Теперь поработаем над созданием самого сообщения с распознанным жестом руки:

async def broadcast_messages():     video_stream = cv2.VideoCapture(0)  # Or http://<ip>:<port>/video     palm_detector = PalmDetection()      with palm_detector.get_palm_ibject() as hands:          while video_stream.isOpened():             success, image = video_stream.read()             if not success:                 logging.error("Ignoring empty camera frame.")                 # If loading a video, use 'break' instead of 'continue'.                 continue              if cv2.waitKey(1) & 0xFF == ord('q'):                 logging.info("KeyboardInterrupt.")                 break

Для создания объекта hands используется следующий код:

class PalmDetection:          def __init__(self):         self.mp_hands = mp.solutions.hands         self.mp_drawing = mp.solutions.drawing_utils         self.mp_drawing_styles = mp.solutions.drawing_styles      def get_palm_ibject(         self,         complexity=0,         detection_level=0.5,         tracking_level=0.5,         hands_count=2     ):         model = self.mp_hands.Hands(             model_complexity = complexity,             min_detection_confidence = detection_level,             min_tracking_confidence = tracking_level,             max_num_hands=hands_count         )         return model

Метод get_palm_object() задает параметры возвращаемой модели, подробнее о них можно почитать в документации к проекту.

Далее нам необходимо захватить видеопоток, с чем справляется библиотека opencv-python — cv2.VideoCapture(). При создании экземпляра этого класса передадим в него идентификатор устройства захвата видеопотока. Для стандартной веб-камеры ноутбука идентификатор оказался равен 0. Но если вы подключите к ноутбуку внешнюю камеру и заходите сделать захват с нее, то идентификатор потребуется изменить на 1. Также мы тестировали данное решение на ноутбуке с операционной системой Linux, где подходящий идентификатор оказался -1. Для просмотра списка устройств видеозахвата в Linux также можно попробовать воспользоваться специальной утилитой:

sudo apt-get install v4l-utils  v4l2-ctl --list-devices
with palm_detector.get_palm_ibject() as hands:      while video_stream.isOpened():         success, image = video_stream.read()         if not success:             logging.error("Ignoring empty camera frame.")             # If loading a video, use 'break' instead of 'continue'.             continue

Достаточно много проблем доставляет захват видеопотока, если разработка ведется с опорой на WSL2. Поддержка подключения USB-устройств в WSL изначально недоступна, поэтому нам потребуется установить проект с открытым кодом usbipd-win. 

Альтернативное решение — превращение стандартной веб-камеры или камеры смартфона при помощи соответствующего программного обеспечения в передающее устройство. Например, Android-приложение IP Webcam позволяет это сделать. Используя IP Webcam, в параметры cv2.VideoCapture() передадим url в формате:

url = "http://<ipaddress>:<port>/video"

# Change color before results to improve performance. try:     IMT(image).change_color(cv2.COLOR_BGR2RGB)     results = hands.process(image)     IMT(image).change_color(cv2.COLOR_RGB2BGR) except Exception as error_message:     logging.error(error_message)  if results.multi_hand_landmarks:     for hand_landmarks in results.multi_hand_landmarks:         # Drawing palms landmarks.         palm_detector.drawing_palms(image, hand_landmarks)                  # Action recognition.         try:             gesture_recognition = HandGestureRecognition(hand_landmarks)             palm_gesture = gesture_recognition.gesture_to_action()             palm_gesture_code = gesture_dictionary[palm_gesture]         except Exception as error_message:             logging.error(error_message)          await asyncio.sleep(0)         message = palm_gesture_code  # Gesture recognition output         await broadcast(json.dumps({"gesture": message}))

В первом блоке try / except происходит попытка двойного преобразования цветовой палитры изображения. Сначала из BGR -> RGB. Именно на такой цветовой палитре mediapipe показывает лучшую производительность и точность детектирования рук hands.process(image), после чего происходит обратное преобразование в привычный человеческому глазу формат RGB -> BGR. Но если вы не планируете выводить видеопоток для просмотра, второе преобразование делать не обязательно.

Во втором блоке try / except происходит попытка определить сочетание 3D-меток, распознанных на руке, как некий жест HandGestureRecognition(hand_landmarks).

Например, жест «Палец вверх», он же «Класс», предполагает, что точки на большом пальце находятся выше точек на других пальцах. Кроме того, крайняя точка большого пальца находится выше, чем точка предыдущей фаланги большого пальца, и так далее.

Запуск всего потока происходит в бесконечном цикле на заданном порту.

async def main():     async with websockets.serve(handler, "0.0.0.0", 8765):         await broadcast_messages()  # runs forever

Теперь посмотрим на само программирование жестов на примере жестов: «Класс», «Победа» и «Все хорошо, OK»

Для получения ключевых объектов на руке нам поможет следующая функция:

def _get_keypoints(self):     """Returns the coordinates of 21 points recognized on the palm in 3D.     """     keypoints = []          for data_point in self.landmarks_list.landmark:         keypoints.append({             'x': data_point.x,             'y': data_point.y,             'z': data_point.z         })      return keypoints

Будем вызывать ее как метод класса HandGestureRecognition.gesture_to_action(), передав ему предварительно список всех точек landmark_list в момент конструирования из results.multi_hand_landmarks:

class HandGestureRecognition:          def __init__(self, landmarks_list: NormalizedLandmarkList):         self.landmarks_list = landmarks_list      def gesture_to_action(self):          result: str = ''         keypoints = self._get_keypoints()

Каждая точка по осям X, Y, Z может быть задана следующим образом:

# X axes keypoint_0x = keypoints[0]['x'] keypoint_4x = keypoints[4]['x'] keypoint_6x = keypoints[6]['x']

При этом индексы списка соответствуют порядковым номерам точек на руке. Это подробно и наглядно описано в документации к проекту.

# 1 Жест «Класс»

if (     keypoint_4y < keypoint_3y     and keypoint_5y < keypoint_9y     and keypoint_8x > keypoint_7x     and keypoint_12x > keypoint_11x      and keypoint_16x > keypoint_15x      and keypoint_20x > keypoint_19x      and keypoint_8x < keypoint_0x      and keypoint_12x < keypoint_0x      and keypoint_16x < keypoint_0x      and keypoint_20x < keypoint_0x  ):     result = 'thumbs Up'

# Жест «Победа»

elif (     keypoint_8y < keypoint_5y     and keypoint_12y < keypoint_9y     and keypoint_16y > keypoint_14y     and keypoint_20y > keypoint_18y ):     result = 'Victory'

# Жест «Все хорошо»  

elif (     keypoint_20x > keypoint_16x     and keypoint_16x > keypoint_12x     and keypoint_8y > keypoint_6y     and keypoint_0y > keypoint_1y     and keypoint_12y < keypoint_11y     and keypoint_16y < keypoint_15y     and keypoint_20y < keypoint_19y ):     result = 'Its okay'

 Результат и выводы

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

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

Особенности получившегося решения:

  • Надежное распознавание кисти руки на расстоянии 1-4 метра.

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

  • Возможность быстрого и гибкого программирования сценариев в ответ на тот или иной жест руки:

    • отображение надписей, картинок в видеопотоке;

    • отправка сообщений;

    • запуск того или иного оборудования и управление им;

    • другие сценарии.

P.S. Одним из неочевидных открытий при работе над проектом стало то, что команда приняла и посчитала вполне удобным стандартизировать подход к форматированию Python-кода с применением проекта Black (ссылка на код проекта). Стандарты форматирования данного проекта вполне жестко и бескомпромиссно позволяют «причесать» одинарные или двойные кавычки, места отступов при переносе строк и другие моменты, в которых часто возникают свободные толкования между разработчиками.

Контакты разработчика: dmitry.hodykin@itentika.ru 

На код проекта можно посмотреть по ссылке.


ссылка на оригинал статьи https://habr.com/ru/company/itentika/blog/693082/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *