Rozpoznawanie gestów to fascynująca dziedzina widzenia komputerowego, która w ostatnich latach zyskała znaczną uwagę. Ma wiele zastosowań w takich obszarach, jak gry, robotyka, interakcja człowiek-komputer i systemy bezpieczeństwa. W tym projekcie zbudujemy model, który będzie w stanie poprawnie przewidzieć pięć gestów na podstawie zbioru danych zawierającego setki filmów.
Istnieją dwa rodzaje architektury modelu, które będziemy tutaj stosować:
- Sieć splotowa 3D (Conv3D): sploty 3D są naturalnym rozszerzeniem splotów 2D. Podobnie jak w konw. 2D przesuwasz filtr w dwóch kierunkach (x i y), w konw. 3D przesuwasz filtr w trzech kierunkach (x, y i z). W tym przypadku danymi wejściowymi konwersji 3D jest wideo (będące sekwencją 30 obrazów RGB). Jeśli założymy, że kształt każdego obrazu wynosi na przykład 100x100x3, wideo stanie się tensorem 4-D o kształcie 100x100x3x30, który można zapisać jako (100x100x30)x3, gdzie 3 to liczba kanałów. Stąd, wywodząc analogię ze splotów 2-D, w których jądro/filtr 2-D (filtr kwadratowy) jest reprezentowane jako (fxf)xc, gdzie f to rozmiar filtra, a c to liczba kanałów, jądro/filtr 3-D (filtr „sześcienny”) jest reprezentowany jako (fxfxf)xc (tutaj c = 3, ponieważ obrazy wejściowe mają trzy kanały). Ten sześcienny filtr będzie teraz „splatany 3D” na każdym z trzech kanałów tensora (100x100x30).
- Sploty + RNN:Sieć conv2D wyodrębni wektor cech dla każdego obrazu, a sekwencja tych wektorów cech zostanie następnie przekazana do sieci opartej na RNN. Dane wyjściowe RNN to zwykły softmax (dla problemu klasyfikacji takiego jak ten).
ZESTAW DANYCH
Dane szkoleniowe składają się z kilkuset filmów podzielonych na jedną z pięciu klas:
- Przesunięcie w prawo: przesuwanie ręki we właściwym kierunku.



2. Przesunięcie w lewo: ruch ręki w lewo.



3. Kciuki w górę: skierowanie kciuka w górę.



4. Kciuk w dół: skierowanie kciuka w dół.



5. Stop: pokazanie ręki z otwartą dłonią.



Każdy film (zwykle trwający 2–3 sekundy) jest podzielony na sekwencję 30 klatek (obrazów). Dane zawierają foldery „train” i „val” z dwoma plikami CSV dla obu folderów. Foldery te są z kolei podzielone na podfoldery, z których każdy podfolder reprezentuje film przedstawiający określony gest. Każdy podfolder, czyli film, zawiera 30 klatek (lub obrazów). Pamiętaj, że wszystkie obrazy w określonym podfolderze wideo mają te same wymiary, ale różne filmy mogą mieć różne wymiary. W szczególności filmy mają dwa rodzaje wymiarów — 360 x 360 lub 120 x 160 (w zależności od kamery internetowej używanej do nagrywania filmów).
Każdy wiersz pliku CSV reprezentuje jeden film i zawiera trzy główne informacje — nazwę podfolderu zawierającego 30 obrazów wideo, nazwę gestu i etykietę numeryczną (od 0 do 4) filmu.
Naszym zadaniem jest wytrenowanie modelu w folderze „train”, który działa dobrze również w folderze „val” (jak to zwykle ma miejsce w projektach ML). Aby rozpocząć proces budowania modelu, musisz najpierw pobrać dane ze swojego magazynu. Aby uzyskać dane na dysku kliknij poniższy link:
https://drive.google.com/uc?id=1ehyrYBQ5rbQQe6yL4XbLWe3FMvuVUGiL
Przetwarzanie wstępne
zaimportuj wymagane biblioteki:
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")
W tym bloku czytasz nazwy folderów do szkolenia i walidacji. Tutaj również ustawiasz batch_size. Pamiętaj, że rozmiar wsadu ustawiasz w taki sposób, aby móc w pełni wykorzystać możliwości procesora graficznego. Zwiększasz wielkość partii, aż maszyna zgłosi błąd.
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
Jest to jedna z najważniejszych części kodu. Podano ogólną konstrukcję generatora. W generatorze będziesz wstępnie przetwarzać obrazy, ponieważ masz obrazy o 2 różnych wymiarach, a także utworzysz partię klatek wideo. Musisz poeksperymentować z img_idx, y,z i normalizacją, aby uzyskać wysoką dokładność.
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
Należy zauważyć, że wideo jest reprezentowane powyżej w generatorze jako (liczba obrazów, wysokość, szerokość, liczba kanałów). Weź to pod uwagę podczas tworzenia architektury modelu.
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)
Budowanie modelu
- 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'))
Teraz, gdy już napisałeś model, następnym krokiem jest compile utworzenie modelu. Kiedy wydrukujesz summary modelu, zobaczysz całkowitą liczbę parametrów, które musisz wytrenować.
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())

Stwórzmy train_generator i val_generator, które będą użyte w .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 są używane przez metodę fit do określenia liczby wywołań next(), które należy wykonać.
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
Dopasujmy teraz model. Spowoduje to rozpoczęcie uczenia modelu i przy pomocy punktów kontrolnych będziesz mógł zapisać model na końcu każdej epoki.
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)

…

Po 30 epokach: dokładność pociągu – 0,9502, dokładność walidacji – 0,8200.
2. CNN(VGG16) + RNN(dwukierunkowy 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)

…

Po 30 epokach: dokładność pociągu – 0,9985, dokładność walidacji – 0,8213.
3. CNN(Resnet50) + RNN(dwukierunkowy 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)

…

Po 30 epokach: dokładność pociągu – 0,9759, dokładność walidacji – 0,8300.
Dlatego wybraliśmy model_three: CNN(Resnet50) + RNN(Dwukierunkowy GRU)jako nasz ostateczny model. Dokonajmy prognoz, korzystając z tego modelu dla pojedynczej sekwencji wideo.
Przewidywanie
Ładowanie folderu wideo za pomocą gestu kciuka w dół i przewidywania za pomocą 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)
Dane wyjściowe: tablica([[1.6398988e-04, 1.0931127e-04, 2.5824511e-03, 8.9063227e-01, 1.0651199e-01]], dtype=float32)
model przewidujący wideo gestu kciuka w dół jako kciuka w dół (kategoria: 4).
Wniosek
Projekt rozpoznawania gestów omawiany na tym blogu wykazał ogromny potencjał w praktycznych zastosowaniach. Dzięki zastosowaniu technik głębokiego uczenia się możliwe jest opracowanie systemu, który w czasie rzeczywistym potrafi dokładnie rozpoznawać i klasyfikować różne gesty dłoni.
W projekcie wykorzystano model głębokiego uczenia się, który został przeszkolony na zbiorze danych wideo zawierających gesty rąk. Następnie model udoskonalono przy użyciu różnych architektur i technik uczenia się transferowego, co poprawiło jego dokładność i skróciło czas szkolenia.
Jedno z najbardziej obiecujących zastosowań tej technologii dotyczy interakcji człowiek-komputer, gdzie można ją wykorzystać do sterowania urządzeniami i interfejsami za pomocą gestów dłoni zamiast tradycyjnych metod wprowadzania danych, takich jak klawiatury i myszy. Może to potencjalnie sprawić, że komputery i inne urządzenia będą bardziej dostępne i intuicyjne dla użytkowników niepełnosprawnych lub o ograniczonej sprawności ruchowej.