Демонстрация использования компьютерного зрения в фитнесе

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

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

Поэтому я спросил себя: есть ли способ использовать машинное обучение в этой области? Могу ли я объединить две страсти, чтобы сделать что-нибудь полезное?

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

Хорошо, не хочу ничего портить, просто продолжайте читать, чтобы узнать!

Сформулируйте проблему

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

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

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

Вот первое видео, снятое в моей подземной комнате усталости :)

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

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

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

(Все необходимые шаги для настройки напишу позже, в Приложении)

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

Круто то, что в качестве вывода также можно получить файл json со всеми позициями, кадр за кадром, поэтому должно быть возможно получить альтернативное числовое представление упражнения.

Итак, выполнив некоторую вспомогательную функцию и используя Plotly, вот как выглядит это упражнение с учетом перемещений по оси y - пропуск оси x менее полезен с учетом положения камеры.

Назовем его ок1.

Отлично, следующий шаг - найти способ сравнить два разных исполнения, чтобы определить, есть ли существенные различия.

Давайте сначала проведем визуальное сравнение на основе этих показателей и назовем это выполнение «сбой1».

Сравним графики движений

Есть очевидные различия.

Давайте попробуем с другим неудачным исполнением («fail2»)

и давайте сравним с исходным правильным исполнением ok1

Давайте теперь попробуем сравнить два хороших результата (назовем его вторым «ок2»).

Кривые очень похожи, поэтому мы эмпирически проверили этот подход.

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

Оказывается, есть что-то под названием Динамическое искажение времени, которое можно использовать для измерения сходства между двумя временными последовательностями. Подробнее здесь

Есть ли реализация на Python? Конечно, используя tslearn.metrics

Итак, давайте посчитаем некоторые цифры

Кулак сравните «ok1» с самим собой

dtw_value for feature nose_y is 0.0 
dtw_value for feature right_shoulder_y is 0.0 
dtw_value for feature right_elbow_y is 0.0 
dtw_value for feature right_wrist_y is 0.0 
dtw_value for feature left_shoulder_y is 0.0 
dtw_value for feature left_elbow_y is 0.0 
dtw_value for feature left_wrist_y is 0.0 
dtw_value for feature right_hip_y is 0.0 
dtw_value for feature right_knee_y is 0.0 
dtw_value for feature right_ankle_y is 0.0 
dtw_value for feature left_hip_y is 0.0 
dtw_value for feature left_knee_y is 0.0 
dtw_value for feature left_ankle_y is 0.0 
dtw_value for feature right_eye_y is 0.0 
dtw_value for feature left_eye_y is 0.0 
dtw_value for feature right_ear_y is 0.0 
dtw_value for feature left_ear_y is 0.0 
dtw_value for feature background_y is 0.0

Таким образом, 0 значений - это максимальное сходство, а более низкий балл означает большее сходство.

Давайте теперь попробуем измерить ok1 и fail1

dtw_value for feature nose_y is 188.00378744123748
dtw_value for feature right_shoulder_y is 155.97642562435527
dtw_value for feature right_elbow_y is 156.39925059973916
dtw_value for feature right_wrist_y is 17.982641407757672
dtw_value for feature left_shoulder_y is 13.5329438534267
dtw_value for feature left_elbow_y is 158.0005797757085
dtw_value for feature left_wrist_y is 27.544745106825722
dtw_value for feature right_hip_y is 12.151614599714703
dtw_value for feature right_knee_y is 191.94638493339747
dtw_value for feature right_ankle_y is 223.23781654997444
dtw_value for feature left_hip_y is 263.0165952996121
dtw_value for feature left_knee_y is 195.8379463587177
dtw_value for feature left_ankle_y is 227.95958454954243
dtw_value for feature right_eye_y is 288.64055642788685
dtw_value for feature left_eye_y is 192.9321060365538
dtw_value for feature right_ear_y is 192.15753964939807
dtw_value for feature left_ear_y is 190.20149442225735
dtw_value for feature background_y is 189.09276308989186

Я нашел полезным принять общее значение, чтобы иметь более сжатую информацию, такую ​​как медиана

dtw_median : 189.6471287560746

Сравнение ok1 и fail2

dtw_value for feature nose_y is 65.28319682858675
dtw_value for feature right_shoulder_y is 38.87442004120449
dtw_value for feature right_elbow_y is 37.75683113715981
dtw_value for feature right_wrist_y is 18.907807197028447
dtw_value for feature left_shoulder_y is 19.50736795264806
dtw_value for feature left_elbow_y is 45.031636992674414
dtw_value for feature left_wrist_y is 36.101698713495466
dtw_value for feature right_hip_y is 13.248353503737741
dtw_value for feature right_knee_y is 39.45295418596681
dtw_value for feature right_ankle_y is 49.27277845829276
dtw_value for feature left_hip_y is 65.78598402395453
dtw_value for feature left_knee_y is 38.59586190254078
dtw_value for feature left_ankle_y is 44.54850474482842
dtw_value for feature right_eye_y is 64.17832564035923
dtw_value for feature left_eye_y is 50.02819053653649
dtw_value for feature right_ear_y is 50.233695101993064
dtw_value for feature left_ear_y is 45.21480605000976
dtw_value for feature background_y is 42.15576012017812
dtw_median : 43.35213243250327

Сравнение ok1 и ok2

dtw_value for feature nose_y is 16.023831603583467
dtw_value for feature right_shoulder_y is 11.24889546622242
dtw_value for feature right_elbow_y is 11.94796246520719
dtw_value for feature right_wrist_y is 20.509653605070962
dtw_value for feature left_shoulder_y is 19.65007578484111
dtw_value for feature left_elbow_y is 14.486468134089847
dtw_value for feature left_wrist_y is 7.208783392501132
dtw_value for feature right_hip_y is 14.17544715061928
dtw_value for feature right_knee_y is 25.759515076957445
dtw_value for feature right_ankle_y is 43.123581089700735
dtw_value for feature left_hip_y is 83.91171946754521
dtw_value for feature left_knee_y is 23.860467116131673
dtw_value for feature left_ankle_y is 44.80603683656928
dtw_value for feature right_eye_y is 91.27560108813313
dtw_value for feature left_eye_y is 31.263050533657154
dtw_value for feature right_ear_y is 25.735729785455852
dtw_value for feature left_ear_y is 12.39151408383979
dtw_value for feature background_y is 11.887661376402017
dtw_median : 20.079864694956036

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

В качестве эмпирической встречной проверки давайте попробуем с другими примерами, начиная с этого значения.

ok1 и check1 - ›медиана 82.22671018607622

ok2 и check2 - ›медиана 196.313312415643

ok и check3 - ›медиана 25.03920782168309

Кажется, что медиана ниже 30 может быть стартовым порогом.

Посмотрим на них на видео

Заключение

Это только начало этого эксперимента: если предположить, что это правильный подход, есть много открытых моментов, таких как:

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

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

ОБНОВЛЕНИЕ: я добавил другие функции, начиная с этого. Посмотрите здесь

Представьте себе рабочую станцию ​​с камерой, которая

  • узнавать вас, когда вы входите в него с распознаванием лица
  • загружает вашу «воду» (тренировка дня)
  • проверяет правильность выполнения упражнений, дает подсказки
  • сигнализирует о плохом исполнении тренеру, который присутствует или, возможно, посещает удаленный сеанс с десятками людей, позволяя ему / ей предпринять корректирующие действия.

Даже тренировку можно настроить «на лету» на основе предыдущих занятий и общего состояния человека.

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

А пока приятных тренировок и будьте в безопасности.

Приложение

Докер + OpenPose

Вместо того, чтобы напрямую устанавливать OpenPose со всеми необходимыми зависимостями, я выбрал подход Docker. Вы можете найти здесь изображение: https://hub.docker.com/r/garyfeng/docker-openpose/

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

Но перед его запуском нужно запускать контейнеры с помощью GPU, иначе OpenPose не запустится. Вот вся инструкция, как это сделать (с графическими процессорами Invidia): https://github.com/NVIDIA/nvidia-docker

Вы увидите в команде «Привилегированные» и -e DISPLAY = $ DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix части, которые используются для доступа к камере внутри контейнера, если она вам нужна. .

Перед запуском команды docker обязательно выполните:

xhost +

чтобы контейнер мог подключиться.

Затем просто запустите

docker run --privileged --gpus all -v <host path to share>:/data  -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -it garyfeng/docker-openpose:latest

Через некоторое время вы войдете в оболочку bash внутри контейнера.

Если вы посмотрите документацию OpenPose, там много параметров, но давайте посмотрим на пару примеров

build/examples/openpose/openpose.bin --face

Он должен включить камеру и начать обнаруживать ключевые точки на вашем лице.

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

build/examples/openpose/openpose.bin --video /data/<input file>  --write_video /data/<ouptut file> --no_display --write_keypoint_json /data/<folder with json output files>

Обратите внимание на папку «data», которая была смонтирована при запуске контейнера. Если вы его измените, обязательно адаптируйте его в соответствии с командой.

Код Python

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

import pandas as pd
import os
import numpy as np
def read_pose_values(path, file_name):
    try:
        path, dirs, files = next(os.walk(path))
        df_output = pd.DataFrame()
        for i in range(len(files)):
            if i <=9:
                pose_sample = pd.read_json(path_or_buf=path+'/' +  file_name + '_00000000000' + str(i) + '_keypoints.json', typ='series')
            elif i <= 99:
                pose_sample = pd.read_json(path_or_buf=path+'/' + file_name + '_0000000000' + str(i) + '_keypoints.json', typ='series')
            else:
                pose_sample = pd.read_json(path_or_buf=path+'/' + file_name + '_000000000' + str(i) + '_keypoints.json', typ='series')    
            df_output = df_output.append(pose_sample, ignore_index = True)
        return df_output
    except Exception as e:
        print(e)

Это используется для возврата DataFrame со всем json, найденным в пути вывода OpenPose json (будьте осторожны, он сломается, если есть более 1000 файлов - обязательно исправить :)

'''
Nose – 0, Neck – 1, Right Shoulder – 2, Right Elbow – 3, Right Wrist – 4,
Left Shoulder – 5, Left Elbow – 6, Left Wrist – 7, Right Hip – 8,
Right Knee – 9, Right Ankle – 10, Left Hip – 11, Left Knee – 12,
LAnkle – 13, Right Eye – 14, Left Eye – 15, Right Ear – 16,
Left Ear – 17, Background – 18
'''
from sklearn.preprocessing import MinMaxScaler
def transform_and_transpose(pose_data, label):
    output = pd.DataFrame()
    for i in range(pose_data.shape[0] -1):
        if len(pose_data.people[i]) > 0: 
            output = output.append(pd.DataFrame(pose_data.people[i][0]['pose_keypoints']).T)
# drop confidence detection
    for y in range(2,output.shape[1] ,3):
        output.drop(columns=[y], inplace=True
# rename columns
    output.columns = ['nose_x', 'nose_y', 'right_shoulder_x', 'right_shoulder_y', 'right_elbow_x', 'right_elbow_y',
                      'right_wrist_x', 'right_wrist_y', 'left_shoulder_x', 'left_shoulder_y', 'left_elbow_x', 'left_elbow_y',
                      'left_wrist_x', 'left_wrist_y', 'right_hip_x', 'right_hip_y', 'right_knee_x', 'right_knee_y',
                      'right_ankle_x', 'right_ankle_y', 'left_hip_x', 'left_hip_y', 'left_knee_x', 'left_knee_y',
                      'left_ankle_x', 'left_ankle_y', 'right_eye_x', 'right_eye_y', 'left_eye_x', 'left_eye_y',
                      'right_ear_x', 'right_ear_y', 'left_ear_x','left_ear_y','background_x', 'background_y']
 
    # interpolate 0 values
    output.replace(0, np.nan, inplace=True)
    output.interpolate(method ='linear', limit_direction ='forward', inplace=True)
return output

Здесь мы выполняем переименование столбцов на основе настройки COCO и базовой интерполяции, если есть 0 значений (например, когда нос находится за полосой подтягивания):

def model_exercise(json,name,label):
    df_raw = read_pose_values(json,name)
    return transform_and_transpose(df_raw,label)
df_exercise_1 = model_exercise('<path to json>','<file_name>','<label>')

Собирая все вместе, функция, используемая для получения последнего DataFrame.

Давайте теперь посмотрим на некоторые графики:

import plotly.graph_objects as go
from plotly.subplots import make_subplots
def plot_y_features(df):
    fig = make_subplots(rows=3, cols=6, start_cell="top-left")
    r = 1
    c = 1
    X = pd.Series(range(df.shape[0]))
    for feature in df.columns:
        if '_y' in feature:
            fig.add_trace(go.Scatter(x=X, y=df[feature], name=feature),
            row=r, col=c)
            fig.update_xaxes(title_text=feature, row=r, col=c)
            if c < 6:
                c = c + 1
            else:
                c = 1
                r = r + 1
    fig.update_layout(title_text="Exercise y-axis movements breakdown", width=2000, height=1000)
    fig.show()
plot_y_features(df_exercise_1)

Рисуем подсюжеты для всех позиций.

Теперь проведем сравнение двух упражнений:

def plot_comparison_y_features(df1,df2):
    fig = make_subplots(rows=3, cols=6, start_cell="top-left")
    r = 1
    c = 1
    X1 = pd.Series(range(df1.shape[0]))
    X2 = pd.Series(range(df2.shape[0]))
    for feature in df1.columns:
        if '_y' in feature:
            fig.add_trace(go.Scatter(x=X1, y=df1[feature], name=feature + '_ok'),row=r, col=c)
            fig.add_trace(go.Scatter(x=X2, y=df2[feature], name=feature + '_fail'),row=r, col=c)
            fig.update_xaxes(title_text=feature, row=r, col=c)
            if c < 6:
                c = c + 1
            else:
                c = 1
                r = r + 1
    fig.update_layout(title_text="Exercise y-axis movements breakdown comparison", width=2000, height=1000)
    fig.show()
plot_comparison_y_features(df_exercise_1, df_ok2)

Наконец, часть динамического искажения времени:

def evaluate_dtw(df1,df2,feature, plot=False):
    x1 = range(df1.shape[0])
    y1 = df1[feature].values
    x2 = range(df2.shape[0])
    y2 = df2[feature].values
   
    dtw_value = evaluate_dtw(df1[feature],df2[feature])
      print("dtw_value for feature {} is {}".format(feature,     dtw_value))
    return dtw_value
def evaluate_dtw_values(df1,df2,plot = False):
    dtw_values = []
    for feature in df1.columns:
        if '_y' in feature:
            dtw_values.append(dtw(df1,df2,feature,plot))
    return pd.DataFrame(dtw_values)

Это все! Спасибо.