Recunoașterea gesturilor este un domeniu fascinant al vederii computerizate care a câștigat o atenție semnificativă în ultimii ani. Are numeroase aplicații în domenii precum jocurile, robotica, interacțiunea om-calculator și sistemele de securitate. În acest proiect, vom construi un model care poate prezice corect cinci gesturi dintr-un set de date care conține sute de videoclipuri.
Există două tipuri de arhitectură model pe care le vom folosi aici:
- Rețeaua convoluțională 3D (Conv3D): convoluțiile 3D sunt o extensie naturală a convoluțiilor 2D. La fel ca în conv. 2D, mutați filtrul în două direcții (x și y), în conv. 3D, mutați filtrul în trei direcții (x, y și z). În acest caz, intrarea într-o conv. 3D este un videoclip (care este o secvență de 30 de imagini RGB). Dacă presupunem că forma fiecărei imagini este de 100x100x3, de exemplu, videoclipul devine un tensor 4-D de formă 100x100x3x30 care poate fi scris ca (100x100x30)x3 unde 3 este numărul de canale. Prin urmare, derivând analogia din convoluțiile 2-D în care un nucleu/filtru 2-D (un filtru pătrat) este reprezentat ca (fxf)xc unde f este dimensiunea filtrului și c este numărul de canale, un nucleu/filtru 3-D (un filtru „cubic”) este reprezentat ca (fxfxf)xc (aici c = 3 deoarece imaginile de intrare au trei canale). Acest filtru cubic va „convolua 3D” pe fiecare dintre cele trei canale ale tensorului (100x100x30).
- Convoluții + RNN:Rețeaua conv2D va extrage un vector de caracteristici pentru fiecare imagine, iar o secvență a acestor vectori de caracteristici este apoi transmisă unei rețele bazate pe RNN. Ieșirea RNN este un softmax obișnuit (pentru o problemă de clasificare precum aceasta).
SET DE DATE
Datele de instruire constau în câteva sute de videoclipuri clasificate într-una dintre cele cinci clase:
- Glisare spre dreapta: mișcarea mâinii în direcția corectă.



2. Glisare la stânga: mișcarea mâinii în direcția stângă.



3. Degetul mare în sus: îndreptarea cu degetul mare în sus.



4. Degetul mare în jos: îndreptarea degetului mare în jos.



5. Oprire: arătarea mâinii cu palma deschisă.



Fiecare videoclip (de obicei 2-3 secunde) este împărțit într-o secvență de 30 de cadre (imagini). Datele conțin un folder „train” și „val” cu două fișiere CSV pentru cele două foldere. Aceste foldere sunt la rândul lor împărțite în subdosare în care fiecare subdosar reprezintă un videoclip al unui anumit gest. Fiecare subdosar, adică un videoclip, conține 30 de cadre (sau imagini). Rețineți că toate imaginile dintr-un anumit subdosar video au aceleași dimensiuni, dar videoclipuri diferite pot avea dimensiuni diferite. Mai exact, videoclipurile au două tipuri de dimensiuni – fie 360x360, fie 120x160 (în funcție de camera web folosită pentru înregistrarea videoclipurilor).
Fiecare rând al fișierului CSV reprezintă un videoclip și conține trei informații principale - numele subdosarului care conține cele 30 de imagini ale videoclipului, numele gestului și eticheta numerică (între 0-4) a videoclipului.
Sarcina noastră este să antrenăm un model în folderul „train” care funcționează bine și în folderul „val” (cum se face de obicei în proiectele ML). Pentru a începe procesul de construire a modelului, mai întâi trebuie să obțineți datele de pe stocarea dvs. Pentru a obține datele de pe stocare, accesați linkul de mai jos:
„https://drive.google.com/uc?id=1ehyrYBQ5rbQQe6yL4XbLWe3FMvuVUGiL”
Preprocesare
importați bibliotecile necesare:
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")
În acest bloc, citiți numele folderelor pentru instruire și validare. De asemenea, setați batch_size aici. Rețineți că setați dimensiunea lotului în așa fel încât să puteți utiliza GPU-ul la capacitate maximă. Continuați să creșteți dimensiunea lotului până când mașina afișează o eroare.
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
Generator
Aceasta este una dintre cele mai importante părți ale codului. Structura generală a generatorului a fost dată. În generator, veți preprocesa imaginile deoarece aveți imagini de 2 dimensiuni diferite și veți crea un lot de cadre video. Trebuie să experimentați cu img_idx, y,z și normalizarea astfel încât să obțineți o precizie ridicată.
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
Rețineți aici că un videoclip este reprezentat mai sus în generator ca (număr de imagini, înălțime, lățime, număr de canale). Luați în considerare acest lucru atunci când creați arhitectura modelului.
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)
Clădire model
- Conv3D:

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'))
Acum că ați scris modelul, următorul pas este să compile modelul. Când imprimați summary al modelului, veți vedea numărul total de parametri pe care trebuie să îi antrenați.
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())

Să creăm train_generator și val_generator care vor fi folosite în .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 și validation_steps sunt folosite de metoda fit pentru a decide numărul de apeluri next() pe care trebuie să le facă.
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
Haideți acum să potrivim modelul. Acest lucru va începe antrenamentul modelului și, cu ajutorul punctelor de control, veți putea salva modelul la sfârșitul fiecărei epoci.
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)

…

După 30 de epoci: Precizia trenului-0,9502, Precizia validării-0,8200.
2. CNN(VGG16) + RNN(LSTM bidirecțional):

#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)

…

După 30 de epoci: Precizia trenului-0,9985, Precizia validării-0,8213.
3. CNN (Resnet50) + RNN (GRU bidirecțional):

#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)

…

După 30 de epoci: Precizia trenului-0,9759, Precizia validării-0,8300.
Prin urmare, alegem model_three: CNN(Resnet50) + RNN(Bidirectional GRU)ca model final. Să facem predicții folosind acest model pe o singură secvență video.
Predicție
Încărcarea unui dosar video cu degetul mare în jos și efectuarea de predicții folosind 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)
Ieșire: matrice([[1.6398988e-04, 1.0931127e-04, 2.5824511e-03, 8.9063227e-01, 1.0651199e-01]], dtype=float32)
model care prezice degetul mare în jos gest video ca degetul mare în jos (categoria:4).
Concluzie
Proiectul de recunoaștere a gesturilor discutat în acest blog a arătat un mare potențial pentru aplicații practice. Prin utilizarea tehnicilor de învățare profundă, este posibil să se dezvolte un sistem care poate recunoaște și clasifica cu precizie diferite gesturi ale mâinii în timp real.
Proiectul a folosit un model de învățare profundă care a fost antrenat pe un set de date de videoclipuri cu gesturi ale mâinii. Modelul a fost apoi ajustat folosind diferite arhitecturi, tehnici de transfer de învățare, ceea ce i-a îmbunătățit acuratețea și a redus timpul de antrenament.
Una dintre cele mai promițătoare aplicații ale acestei tehnologii este în domeniul interacțiunii om-calculator, unde poate fi folosită pentru a controla dispozitive și interfețe folosind gesturi ale mâinii în loc de metodele tradiționale de introducere, cum ar fi tastaturile și șoarecii. Acest lucru ar putea face computerele și alte dispozitive mai accesibile și intuitive pentru utilizatorii cu dizabilități sau cu mobilitate limitată.