Распознавание жестов — увлекательная область компьютерного зрения, которая в последние годы привлекла значительное внимание. Он имеет множество приложений в таких областях, как игры, робототехника, взаимодействие человека с компьютером и системы безопасности. В этом проекте мы построим модель, которая сможет правильно предсказать пять жестов из набора данных, содержащего сотни видео.
Здесь мы собираемся использовать два типа архитектуры модели:
- Трехмерная сверточная сеть (Conv3D): трехмерные свертки являются естественным расширением двумерных сверток. Так же, как и в 2D-преобразовании, вы перемещаете фильтр в двух направлениях (x и y), в 3D-преобразовании вы перемещаете фильтр в трех направлениях (x, y и z). В этом случае входом в 3D-конверсию является видео (которое представляет собой последовательность из 30 изображений RGB). Если мы предположим, что форма каждого изображения составляет, например, 100x100x3, видео становится четырехмерным тензором формы 100x100x3x30, что можно записать как (100x100x30)x3, где 3 — количество каналов. Следовательно, выводя аналогию из двумерных сверток, где двумерное ядро/фильтр (квадратный фильтр) представлено как (fxf)xc, где f — размер фильтра, а c — количество каналов, трехмерное ядро/фильтр («кубический» фильтр) представляется как (fxfxf)xc (здесь c = 3, поскольку входные изображения имеют три канала). Этот кубический фильтр теперь будет «3D-свертываться» на каждом из трех каналов тензора (100x100x30).
- Свертки + RNN:сеть conv2D извлекает вектор признаков для каждого изображения, а затем последовательность этих векторов признаков передается в сеть на основе RNN. Результатом RNN является обычный softmax (для такой задачи классификации, как эта).
НАБОР ДАННЫХ
Данные для обучения состоят из нескольких сотен видеороликов, отнесенных к одному из пяти классов:
- Смахивание вправо: движение руки в правильном направлении.
2. Свайп влево: движение руки влево.
3. Большой палец вверх: указательный палец вверх.
4. Большой палец вниз: указательный палец вниз.
5. Стоп: показ руки с раскрытой ладонью.
Каждое видео (обычно продолжительностью 2–3 секунды) разделено на последовательность из 30 кадров (изображений). Данные содержат папку «train» и «val» с двумя файлами CSV для двух папок. Эти папки, в свою очередь, разделены на подпапки, где каждая подпапка представляет собой видео определенного жеста. Каждая подпапка, то есть видео, содержит 30 кадров (или изображений). Обратите внимание, что все изображения в определенной подпапке с видео имеют одинаковые размеры, но разные видео могут иметь разные размеры. В частности, видео имеет два типа размеров — 360 x 360 или 120 x 160 (в зависимости от веб-камеры, используемой для записи видео).
Каждая строка CSV-файла представляет собой одно видео и содержит три основных элемента информации: имя подпапки, содержащей 30 изображений видео, имя жеста и цифровую метку (от 0 до 4) видео.
Наша задача — обучить модель в папке «train», которая хорошо работает и в папке «val» (как это обычно делается в проектах ML). Чтобы начать процесс построения модели, вам сначала нужно получить данные в хранилище. Для того, чтобы получить данные о хранилище, перейдите по ссылке ниже:
https://drive.google.com/uc?id=1ehyrYBQ5rbQQe6yL4XbLWe3FMvuVUGiL
Предварительная обработка
импортировать необходимые библиотеки:
import numpy as np import math import os from imageio import imread from skimage.transform import resize from skimage.io import imread, imshow import matplotlib.pyplot as plt import datetime import os import warnings import cv2 from tensorflow import keras import tensorflow as tf warnings.filterwarnings("ignore")
В этом блоке вы читаете имена папок для обучения и проверки. Вы также устанавливаете batch_size
здесь. Обратите внимание, что вы устанавливаете размер пакета таким образом, чтобы вы могли использовать GPU на полную мощность. Вы продолжаете увеличивать размер партии, пока машина не выдаст ошибку.
train_doc = np.random.permutation(open('/content/Project_data/train.csv').readlines()) val_doc = np.random.permutation(open('/content/Project_data/val.csv').readlines()) batch_size = 26
Генератор
Это одна из самых важных частей кода. Приведена общая структура генератора. В генераторе вы собираетесь предварительно обработать изображения, поскольку у вас есть изображения двух разных размеров, а также создать пакет видеокадров. Вы должны поэкспериментировать с img_idx
, y
, z
и нормализацией, чтобы получить высокую точность.
def generator(source_path, folder_list, batch_size): print( 'Source path = ', source_path, '; batch size =', batch_size) img_idx = [0,1,2,4,6,8,10,12,14,16,18,20,22,24,26,27,28,29] while True: t = np.random.permutation(folder_list) num_batches = int(len(t)/batch_size) for batch in range(num_batches): batch_data = np.zeros((batch_size,18,120,120,3)) batch_labels = np.zeros((batch_size,5)) for folder in range(batch_size): imgs = os.listdir(source_path+'/'+ t[folder + (batch*batch_size)].split(';')[0]) for idx,item in enumerate(img_idx): image = imread(source_path+'/'+ t[folder + (batch*batch_size)].strip().split(';')[0]+'/'+imgs[item]).astype(np.float32) if image.shape[1] == 160: image = resize(image[:,20:140,:],(120,120)).astype(np.float32) else: image = resize(image,(120,120)).astype(np.float32) batch_data[folder,idx,:,:,0] = image[:,:,0] - 104 batch_data[folder,idx,:,:,1] = image[:,:,1] - 117 batch_data[folder,idx,:,:,2] = image[:,:,2] - 123 batch_labels[folder, int(t[folder + (batch*batch_size)].strip().split(';')[2])] = 1 yield batch_data, batch_labels if (len(t)%batch_size) != 0: batch_data = np.zeros((len(t)%batch_size,18,120,120,3)) batch_labels = np.zeros((len(t)%batch_size,5)) for folder in range(len(t)%batch_size): imgs = os.listdir(source_path+'/'+ t[folder + (num_batches*batch_size)].split(';')[0]) for idx,item in enumerate(img_idx): image = imread(source_path+'/'+ t[folder + (num_batches*batch_size)].strip().split(';')[0]+'/'+imgs[item]).astype(np.float32) if image.shape[1] == 160: image = resize(image[:,20:140,:],(120,120)).astype(np.float32) else: image = resize(image,(120,120)).astype(np.float32) batch_data[folder,idx,:,:,0] = image[:,:,0] - 104 batch_data[folder,idx,:,:,1] = image[:,:,1] - 117 batch_data[folder,idx,:,:,2] = image[:,:,2] - 123 batch_labels[folder, int(t[folder + (num_batches*batch_size)].strip().split(';')[2])] = 1 yield batch_data, batch_labels
Обратите внимание, что видео представлено выше в генераторе как (количество изображений, высота, ширина, количество каналов). Учитывайте это при создании архитектуры модели.
curr_dt_time = datetime.datetime.now() train_path = '/content/Project_data/train' val_path = '/content/Project_data/val' num_train_sequences = len(train_doc) print('# training sequences =', num_train_sequences) num_val_sequences = len(val_doc) print('# validation sequences =', num_val_sequences) num_epochs = 30 print ('# epochs =', num_epochs)
Построение модели
- Конв3D:
from keras.models import Sequential from keras.layers import Dense, GRU, Dropout, Flatten, BatchNormalization, Activation from keras.layers.convolutional import Conv3D, MaxPooling3D from keras.callbacks import ModelCheckpoint, ReduceLROnPlateau from keras import optimizers model = Sequential() model.add(Conv3D(64, (3,3,3), strides=(1,1,1), padding='same', input_shape=(18,120,120,3))) model.add(BatchNormalization()) model.add(Activation('elu')) model.add(MaxPooling3D(pool_size=(2,2,1), strides=(2,2,1))) model.add(Conv3D(128, (3,3,3), strides=(1,1,1), padding='same')) model.add(BatchNormalization()) model.add(Activation('elu')) model.add(MaxPooling3D(pool_size=(2,2,2), strides=(2,2,2))) model.add(Conv3D(256, (3,3,3), strides=(1,1,1), padding='same')) model.add(BatchNormalization()) model.add(Activation('elu')) model.add(MaxPooling3D(pool_size=(2,2,2), strides=(2,2,2))) model.add(Conv3D(256, (3,3,3), strides=(1,1,1), padding='same')) model.add(BatchNormalization()) model.add(Activation('elu')) model.add(MaxPooling3D(pool_size=(2,2,2), strides=(2,2,2))) model.add(Flatten()) model.add(Dropout(0.5)) model.add(Dense(512, activation='elu')) model.add(Dropout(0.5)) model.add(Dense(5, activation='softmax'))
Теперь, когда вы написали модель, следующим шагом будет compile
модель. Когда вы распечатаете summary
модели, вы увидите общее количество параметров, которые вам нужно обучить.
sgd = optimizers.SGD(lr=0.001, decay=1e-6, momentum=0.7, nesterov=True) model.compile(optimizer=sgd, loss='categorical_crossentropy', metrics=['categorical_accuracy']) print (model.summary())
Давайте создадим train_generator
и val_generator
, которые будут использоваться в .fit_generator
.
train_generator = generator(train_path, train_doc, batch_size) val_generator = generator(val_path, val_doc, batch_size) model_name = 'model_init' + '_' + str(curr_dt_time).replace(' ','').replace(':','_') + '/' if not os.path.exists(model_name): os.mkdir(model_name) filepath = model_name + 'model-{epoch:05d}-{loss:.5f}-{categorical_accuracy:.5f}-{val_loss:.5f}-{val_categorical_accuracy:.5f}.h5' checkpoint = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=False, save_weights_only=False, mode='auto', period=1) LR = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, verbose=1, mode='min', epsilon=0.0001, cooldown=0, min_lr=0.00001) callbacks_list = [checkpoint, LR]
steps_per_epoch
и validation_steps
используются методом fit
для определения количества вызовов next(), которые необходимо сделать.
if (num_train_sequences%batch_size) == 0: steps_per_epoch = int(num_train_sequences/batch_size) else: steps_per_epoch = (num_train_sequences//batch_size) + 1 if (num_val_sequences%batch_size) == 0: validation_steps = int(num_val_sequences/batch_size) else: validation_steps = (num_val_sequences//batch_size) + 1
Давайте теперь подгоним модель. Это начнет обучение модели, и с помощью контрольных точек вы сможете сохранить модель в конце каждой эпохи.
model.fit_generator(train_generator, steps_per_epoch=steps_per_epoch, epochs=num_epochs, verbose=1, callbacks=callbacks_list, validation_data=val_generator, validation_steps=validation_steps, class_weight=None, workers=1, initial_epoch=0)
…
Через 30 эпох: точность обучения – 0,9502, точность проверки – 0,8200.
2. CNN(VGG16) + RNN(двунаправленный LSTM):
#model from keras.models import Sequential, Model from keras.layers import Dense, GRU, Dropout, Flatten, TimeDistributed, Bidirectional, LSTM from keras.callbacks import ModelCheckpoint, ReduceLROnPlateau from keras import optimizers from keras.applications.vgg16 import VGG16 base_model = VGG16(include_top=False, weights='imagenet', input_shape=(120,120,3),pooling='avg') x = base_model.output x = Flatten()(x) #x.add(Dropout(0.5)) features = Dense(64, activation='relu')(x) conv_model = Model(inputs=base_model.input, outputs=features) for layer in base_model.layers: layer.trainable = False model_sec = Sequential() model_sec.add(TimeDistributed(conv_model, input_shape=(18,120,120,3))) model_sec.add(Bidirectional(LSTM(64, return_sequences=True))) model_sec.add(GRU(32)) model_sec.add(Dropout(0.5)) model_sec.add(Dense(128, activation='relu')) model_sec.add(Dense(5, activation='softmax')) sgd = optimizers.Adam(lr=0.001, decay=1e-6) model_sec.compile(optimizer=sgd, loss='categorical_crossentropy', metrics=['categorical_accuracy']) print (model_sec.summary())
train_generator = generator(train_path, train_doc, batch_size) val_generator = generator(val_path, val_doc, batch_size) model_name = 'model_init_conv_lstm' + '_' + str(curr_dt_time).replace(' ','').replace(':','_') + '/' if not os.path.exists(model_name): os.mkdir(model_name) filepath = model_name + 'model-{epoch:05d}-{loss:.5f}-{categorical_accuracy:.5f}-{val_loss:.5f}-{val_categorical_accuracy:.5f}.h5' checkpoint = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=False, save_weights_only=False, mode='auto', period=1) LR = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, verbose=1, mode='min', epsilon=0.0001, cooldown=0, min_lr=0.00001) callbacks_list = [checkpoint, LR] if (num_train_sequences%batch_size) == 0: steps_per_epoch = int(num_train_sequences/batch_size) else: steps_per_epoch = (num_train_sequences//batch_size) + 1 if (num_val_sequences%batch_size) == 0: validation_steps = int(num_val_sequences/batch_size) else: validation_steps = (num_val_sequences//batch_size) + 1 model_sec.fit_generator(train_generator, steps_per_epoch=steps_per_epoch, epochs=num_epochs, verbose=1, callbacks=callbacks_list, validation_data=val_generator, validation_steps=validation_steps, class_weight=None, workers=1, initial_epoch=0)
…
Через 30 эпох: точность обучения – 0,9985, точность проверки – 0,8213.
3. CNN(Resnet50) + RNN(двунаправленный GRU):
#model from keras.models import Sequential, Model from keras.layers import Dense, GRU, Dropout, Flatten, TimeDistributed, Bidirectional, LSTM from keras.callbacks import ModelCheckpoint, ReduceLROnPlateau from keras import optimizers from keras.applications.resnet import ResNet50 base_model = ResNet50(include_top=False, weights='imagenet', input_shape=(120,120,3),pooling='max') x = base_model.output x = Flatten()(x) features = Dense(128, activation='relu')(x) conv_model = Model(inputs=base_model.input, outputs=features) for layer in base_model.layers: layer.trainable = False model_three = Sequential() model_three.add(TimeDistributed(conv_model, input_shape=(18,120,120,3))) model_three.add(Bidirectional(GRU(32, return_sequences=True))) model_three.add(GRU(16)) model_three.add(Dropout(0.5)) model_three.add(Dense(64, activation='relu')) model_three.add(Dense(5, activation='softmax')) sgd = optimizers.Adam(lr=0.001, decay=1e-6) model_three.compile(optimizer=sgd, loss='categorical_crossentropy', metrics=['categorical_accuracy']) print (model_three.summary()) train_generator = generator(train_path, train_doc, batch_size) val_generator = generator(val_path, val_doc, batch_size) model_name = 'model_init_conv_gru' + '_' + str(curr_dt_time).replace(' ','').replace(':','_') + '/' if not os.path.exists(model_name): os.mkdir(model_name) filepath = model_name + 'model-{epoch:05d}-{loss:.5f}-{categorical_accuracy:.5f}-{val_loss:.5f}-{val_categorical_accuracy:.5f}.h5' checkpoint = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=False, save_weights_only=False, mode='auto', period=1) LR = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, verbose=1, mode='min', epsilon=0.0001, cooldown=0, min_lr=0.00001) callbacks_list = [checkpoint, LR] if (num_train_sequences%batch_size) == 0: steps_per_epoch = int(num_train_sequences/batch_size) else: steps_per_epoch = (num_train_sequences//batch_size) + 1 if (num_val_sequences%batch_size) == 0: validation_steps = int(num_val_sequences/batch_size) else: validation_steps = (num_val_sequences//batch_size) + 1 model_three.fit_generator(train_generator, steps_per_epoch=steps_per_epoch, epochs=num_epochs, verbose=1, callbacks=callbacks_list, validation_data=val_generator, validation_steps=validation_steps, class_weight=None, workers=1, initial_epoch=0)
…
Через 30 эпох: точность обучения – 0,9759, точность проверки – 0,8300.
Следовательно, выбор model_three: CNN(Resnet50) + RNN(двунаправленный GRU) в качестве нашей окончательной модели. Давайте сделаем прогнозы, используя эту модель, для одной видеопоследовательности.
Прогноз
Загрузка папки с видео жестом большого пальца вниз и прогнозирование с помощью model_three:
#preprocessing single video sequence to make prediction upon img_idx = [0,1,2,4,6,8,10,12,14,16,18,20,22,24,26,27,28,29] video=[] imgs = os.listdir('/content/Project_data/train/WIN_20180907_15_38_35_Pro_Thumbs Down_new'.split(';')[0]) for idx,item in enumerate(img_idx): image = imread('/content/Project_data/train/WIN_20180907_15_38_35_Pro_Thumbs Down_new'.strip().split(';')[0]+'/'+imgs[item]).astype(np.float32) if image.shape[1] == 160: image = resize(image[:,20:140,:],(120,120)).astype(np.float32) else: image = resize(image,(120,120)).astype(np.float32) image[:,:,0] -= 104 image[:,:,1] -= 117 image[:,:,2] -= 123 video.append(image) video = np.expand_dims(np.array(video), axis=0) model_three.predict(video)
Вывод: массив([[1.6398988e-04, 1.0931127e-04, 2.5824511e-03, 8.9063227e-01, 1.0651199e-01]], dtype=float32)
модель, предсказывающая видео жеста «большой палец вниз» как «большой палец вниз» (категория: 4).
Заключение
Проект распознавания жестов, обсуждаемый в этом блоге, показал большой потенциал для практического применения. Благодаря использованию методов глубокого обучения можно разработать систему, которая может точно распознавать и классифицировать различные жесты рук в режиме реального времени.
В проекте использовалась модель глубокого обучения, которая была обучена на наборе данных видео жестов рук. Затем модель была доработана с использованием различных архитектур и методов трансфертного обучения, что повысило ее точность и сократило время обучения.
Одно из наиболее многообещающих применений этой технологии находится в области взаимодействия человека с компьютером, где ее можно использовать для управления устройствами и интерфейсами с помощью жестов рук вместо традиционных методов ввода, таких как клавиатура и мышь. Это потенциально может сделать компьютеры и другие устройства более доступными и интуитивно понятными для пользователей с ограниченными возможностями или ограниченной подвижностью.