Демонстрация использования компьютерного зрения в фитнесе
Я большой поклонник подхода с собственным весом и вообще тренировок, но я не люблю слишком много ходить в спортзал.
Кроме того, во время принудительной блокировки из-за коронавируса было бы полезно попробовать другой подход к фитнесу и тренировкам.
Поэтому я спросил себя: есть ли способ использовать машинное обучение в этой области? Могу ли я объединить две страсти, чтобы сделать что-нибудь полезное?
Одна из основных проблем заключается в том, чтобы проверить правильность упражнения, поэтому я провел несколько экспериментов, попробовал подход и обнаружил, что ...
Хорошо, не хочу ничего портить, просто продолжайте читать, чтобы узнать!
Сформулируйте проблему
Как всегда, давайте начнем с постановки проблемы. Мы хотим получить способ оценить правильность упражнения, используя видео в качестве входных данных.
Оптимальным вариантом должно быть использование потоковой передачи в реальном времени, но давайте пока будем простыми, используя файл, поскольку, в конце концов, мы хотим проверить подход, прежде чем создавать что-то поверх.
Итак, первым шагом должно быть видео с (надеюсь) правильным выполнением, поскольку его можно использовать в качестве основы для сравнения других.
Вот первое видео, снятое в моей подземной комнате усталости :)
Моя первая мысль заключалась в том, чтобы использовать 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)
Это все! Спасибо.