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

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

  1. Трехмерная сверточная сеть (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).
  2. Свертки + RNN:сеть conv2D извлекает вектор признаков для каждого изображения, а затем последовательность этих векторов признаков передается в сеть на основе RNN. Результатом RNN является обычный softmax (для такой задачи классификации, как эта).

НАБОР ДАННЫХ

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

  1. Смахивание вправо: движение руки в правильном направлении.

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)

Построение модели

  1. Конв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).

Заключение

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

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

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