Введение

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

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

При том что сама технология является зрелой, рынок ее применения находится на начальной стадии развития, но благодаря преимуществам компьютерного зрения он имеет потенциал роста. По данным 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 

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

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


  1. dimnsk
    15.10.2022 11:12

    >>> "то можно придумать много сфер применения:"

    и все сферы применения именно придуманные
    это так не работает
    от задач применение, а не от придумывания