이전 👉🏻 [캡스톤일지] 10.13 미디어파이프 이용한 정적제스처 인식과 문제 발생 https://ijo0r98.github.io/posts/capstone4/
위 포스팅 하단에서 잠깐 언급된 동적 제스처.. 저때 저분 깃허브 코드 보고 똑같이 했는데 성능이 잘 안나와서 그냥 미뤄뒀었다.
근데 매주 주간보고도 해야하고.. 문교수님 질문도 들었고.. 더 이상 미룰 수 없어 동적 제스처 모델학습에 몇주간 시간을 많이 쏟았다.
이전의 모델학습 방법
우리가 참고한 깃허브(유튜브도 같이 하심!!) https://github.com/kairess/gesture-recognition
우리랑 비슷하게 미디어파이프 이용해서 제스처 인식하는 팀이 있었는데 그 팀도 여기 참고해서 한 듯 하당 ㅋㅎ
위의 코드를 이용한 동적 제스처 모델 학습의 순서는 아래와 같다.
- opencv 웹캠을 통해 실시간으로 촬영된 영상에 미디어파이프 적용하여 각도정보 추출, 데이터셋 생성 > create_data.py
- 생성된 데이터셋으로 모델 학습 > train.ipynb
- 동적 제스처가 잘 인식되는지 테스트 > test.py
처음에는 클릭 모션을 3가지 정도 두어서 세 모션을 학습시켰는데 모두 비슷비슷하다보니 분류 성능이 매우 좋지 않았다.
수치상의 accuray는 0.95로 매우 높았지만 체감은 사실상 그의 반도 안되는걸 보아 데이터셋이 적어 과적합된거가 싶기도 하고..
영상 촬영 시간을 30초에서 60초까지 늘려봤지만 크게 달라지는 건 없었다ㅠㅠ
모델 성능 향상시키기 위한 노력
모션 3개가 비슷하기도 하고 클릭이 아닌 모션도 자꾸 모델을 통과시키면 세 라벨중 하나로 분류가 되어 라벨을 바꿔보았다.
(+ 동적 제스처를 하기 위한 트리거를 고안하는 방법도 있었음)
클릭과 클릭이 아닌 모션 이렇게 2개로 나눈 이진분류 모델로 만들었다.
이렇게 아무행동도 취하지 않고 검지손가락만 들고 있는 클릭 아닌 모션을 취하고 있는 영상과
검지손가락을 들고 클릭 모션을 취하는 영상을 이용하여 데이터셋을 생성하였고 똑같이 모델학습을 하였다.
클릭 모션을 크게 2가지로 정해 각각 학습해보았는디 첫번째로 클릭 모션을 검지손가락만 접는 행동을 클릭 모션으로 지정했을 때
검지손가락을 편 채 손목을 움직여 클릭을 하는 제스처를 취했을 때
이렇게 결과가 나왔다.. 둘 다 마찬가지로 그 전보다 체감 성능이 쪼금 더 높아졌지만 여전히 썩 좋지는 못했다 ㅠㅠ
위에서 사용한 데이터셋 생성 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import cv2
import mediapipe as mp
import numpy as np
import time, os
actions = ['none', 'click'] # 클릭과 클릭이 아닌 모션으로 분류
seq_length = 30
secs_for_action = 60 # 60초
# MediaPipe hands model
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(
max_num_hands=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
cap = cv2.VideoCapture(0) # webcam open
created_time = int(time.time())
os.makedirs('dataset', exist_ok=True)
while cap.isOpened():
for idx, action in enumerate(actions):
data = []
ret, img = cap.read()
img = cv2.flip(img, 1)
cv2.putText(img, f'Waiting for collecting {action.upper()} action...', org=(10, 30), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, color=(255, 255, 255), thickness=2)
cv2.imshow('img', img)
cv2.waitKey(3000)
start_time = time.time()
while time.time() - start_time < secs_for_action:
ret, img = cap.read()
img = cv2.flip(img, 1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
result = hands.process(img)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
if result.multi_hand_landmarks is not None:
for res in result.multi_hand_landmarks:
joint = np.zeros((21, 4))
for j, lm in enumerate(res.landmark):
joint[j] = [lm.x, lm.y, lm.z, lm.visibility]
# Compute angles between joints
v1 = joint[[0,1,2,3,0,5,6,7,0,9,10,11,0,13,14,15,0,17,18,19], :3] # Parent joint
v2 = joint[[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], :3] # Child joint
v = v2 - v1 # [20, 3]
# Normalize v
v = v / np.linalg.norm(v, axis=1)[:, np.newaxis]
# Get angle using arcos of dot product
angle = np.arccos(np.einsum('nt, nt->n',
v[[0,1,2,4,5,6,8,9,10,12,13,14,16,17,18], :],
v[[1,2,3,5,6,7,9,10,11,13,14,15,17,18,19], :])) # [15,]
angle = np.degrees(angle) # Convert radian to degree
angle_label = np.array([angle], dtype=np.float32)
angle_label = np.append(angle_label, idx)
d = np.concatenate([joint.flatten(), angle_label])
data.append(d)
mp_drawing.draw_landmarks(img, res, mp_hands.HAND_CONNECTIONS)
cv2.imshow('img', img)
if cv2.waitKey(1) == ord('q'):
break
data = np.array(data)
print(action, data.shape)
np.save(os.path.join('dataset', f'raw_{action}_{created_time}'), data)
# Create sequence data
full_seq_data = []
for seq in range(len(data) - seq_length):
full_seq_data.append(data[seq:seq + seq_length])
full_seq_data = np.array(full_seq_data)
print(action, full_seq_data.shape)
np.save(os.path.join('dataset', f'seq_{action}_{created_time}'), full_seq_data)
break
위의 코드를 어떻게 좀 수정해서 데이터셋 크기도 늘리고 정확도를 높히고 싶은데 도저히 코드가 이해가 되지 않았다.. 특히 numpy 메서드들이 하나하나 무슨 의미인지 몰라 코드를 수정하는게 쉽지 않았다ㅠㅠ
그래서 그냥 내가 다시 코드를 짜기로 결심함!!! 😎
위 코드에서는 미디어파이프 랜드마크의 x, y, z 좌표를 이용해 각 랜드마크마다의 각도를 구하는 것으로 보인다. 그리고 각도 구할 때 두 랜드마크 정보만 이용하는 것을 보아 벡터값을 어찌저찌 하는거 아닌가 하는 생각이 든다. 나도 ‘각도’정보를 이용한다는 것을 중점으로 두고 우리가 동적제스처에 이용할 검지손가락의 마디마다의 각도 정보를 저장해 데이터셋을 생성하기로 했당
새로운 데이터셋 생성
먼저 나는 미디어파이프 설치가 안되서 google colab을 이용해 미디어파이프를 이용해야한다.
콜랩에서는 웹캠을 사용할 수 없음으로! 직접 영상을 촬영한 뒤 콜랩에 올리고 웹캠 대신 비디오를 열어서 미디어파이프 적용해 데이터셋 생성 하였다.
📍 GIT https://github.com/ijo0r98/capstone/blob/master/mldl/modeling/juran/create_data_ver2.0.ipynb
1. 구글 콜랩 파일 업로드 코드 스니펫
1
2
3
4
5
6
7
from google.colab import files
uploaded = files.upload()
for fn in uploaded.keys():
print('User uploaded file "{name}" with length {length} bytes'.format(
name=fn, length=len(uploaded[fn])))
2. 콜랩에 미디어파이프 설치
1
!pip install mediapipe
3. 라이브러리 import와 미디어파이프 환경 설정
1
2
3
4
5
import cv2
import mediapipe as mp
import pandas as pd
import numpy as np
import time
1
2
3
4
5
6
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(
max_num_hands=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
4. opencv로 비디오 open 후 데이터셋 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
cap = cv2.VideoCapture('click.MOV') # video open
data_click = pd.DataFrame()
while cap.isOpened():
ret, img = cap.read()
if img is None:
break
img = cv2.flip(img, 1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
result = hands.process(img)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
index_point = [[8, 7, 6], [7, 6, 5], [6, 5, 0]]
point_info = np.zeros((21, 2))
if result.multi_hand_landmarks is not None:
for res in result.multi_hand_landmarks:
for j, lm in enumerate(res.landmark):
point_info[j] = [lm.x, lm.y]
angles = []
for point in index_point:
# 검지손가락 포인트 각도 게산
ba = point_info[point[0]] - point_info[point[1]]
bc = point_info[point[2]] - point_info[point[1]]
cosine_angle = np.dot(ba,bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
angle = np.arccos(cosine_angle)
pAngle = np.degrees(angle)
angles.append(pAngle)
# 라벨 추가
angles.append(1) # 1 - click
data_click = data_click.append([angles])
나는 3 점의 x, y좌표를 이용하여 각도를 구하였고 이 부분은 구글링을 참고하였다.
1은 click, 0은 none으로 none일때도 동일함
opencv
1 2 opencv.VideoCapture(0) # 디바이스의 웹캠 사용 opencv.VideoCapture({파일경로/영상명}) # 해당 동영상 사용
5. 데이터셋 csv파일 저장
1
data.to_csv("dataset_click.csv")
새로운 모델 학습
위에서 생성한 데이터셋을 이용하여 내 방식대로 모델학습을 해보았다.
나는 LSTM을 사용하지 않았고 그냥 간단하게 내가 갖고있는 tensorflow2.0 RNN 딥러닝 코드 중 하나에 넣어서 돌렸다.
1. 데이터셋 로드
1
2
df_click = pd.read_csv('dataset/dataset_click.csv')
del df_click['Unnamed: 0']
1
2
df_none = pd.read_csv('dataset/dataset_none.csv')
del df_none['Unnamed: 0']
2. 데이터셋 전처리
의외로 각도 차이가 별로 나지 않은 곳이 꽤 있는데 미디어파이프가 적용이 제대로 안됐거나 랜드마크가 모두 인식이 안되어었거나 그런듯 하다. 가장 차이가 많은 랜드마크를 골라 기준을 정해 기준에 부합하는 데이터셋만 이용하기로 했다.
1
2
3
4
5
print('--- click 120도 이하 각도 수 ---')
print('total: ', len(df_click))
print('7번 랜드마크 각도: ', len(df_click[df_click['0'] <= 120]))
print('6번 랜드마크 각도: ', len(df_click[df_click['1'] <= 120]))
print('6, 7번 랜드마크 각도: ', len(df_click[(df_click['1'] <= 120) & (df_click['0'] <= 120)]))
1
2
3
4
print('--- none 120도 이하 각도 수 ---')
print('total: ', len(df_none))
print('7번 랜드마크 각도: ', len(df_none[df_none['0'] <= 120])) # 90도 이하 데이터만 사용
print('6번 랜드마크 각도: ', len(df_none[df_none['1'] <= 120])) # 90도 이하 데이터만 사용
가장 많은 데이터셋을 살리고 싶어 기준 각도를 바꿔가며 계산해보니 120도 정도가 적당한 것 같다.
7번 랜드마크 각도가 차이가 가장 많이 남으로 7번 랜드마크 각도가 120도 이하인 데이터만 click 데이터셋으로 이용하기로 결정
1
df_click = df_click[df_click['0'] <= 120]
두 데이터프레임 합치기
1
df_data = pd.concat([df_click, df_none])
x 데이터와 y 데이터 나누기
1
2
df_target = df_data[['3']]
df_data = df_data[['0', '1', '2']]
3. train / test split
1
2
3
from sklearn import model_selection
x_train, x_test, y_train, y_test = model_selection.train_test_split(df_data, df_target, test_size=0.2,random_state=0)
one-hot encoding
1
2
3
4
5
6
7
from sklearn import preprocessing
# Apply 'One-hot encoding' on labels
enc = preprocessing.OneHotEncoder(categories='auto')
y_train = enc.fit_transform(y_train).toarray()
y_test = enc.fit_transform(y_test).toarray()
one-hot encoding을 거쳐 0 > [1, 0] 그리고 1 > [0, 1]이 되었다.
4. 모델 학습
1
2
3
import tensorflow as tf
from tensorflow.keras import models, layers, activations, initializers, losses, optimizers, metrics
모델 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
model.add(layers.Dense(input_dim=3, units=64, activation=None, kernel_initializer=initializers.he_uniform())) # he-uniform initialization
# model.add(layers.BatchNormalization()) # Use this line as if needed
model.add(layers.Activation('elu')) # elu, relu / layers.ELU, layers.LeakyReLU
model.add(layers.Dense(units=32, activation=None, kernel_initializer=initializers.he_uniform()))
model.add(layers.Activation('elu'))
model.add(layers.Dense(units=32, activation=None, kernel_initializer=initializers.he_uniform()))
model.add(layers.Activation('elu'))
# 마지막 hidden layer droup out
model.add(layers.Dropout(rate=0.5))
model.add(layers.Dense(units=2, activation='softmax')) # Apply softmax function on model's output
모델 생성
1
2
3
model.compile(optimizer=optimizers.Adam(),
loss=losses.categorical_crossentropy,
metrics=[metrics.categorical_accuracy])
학습
1
history = model.fit(x_train, y_train, batch_size=200, epochs=100, validation_split=0.3)
단순한 모델이라 그런지 학습하는데 오래걸리지는 않았다.
5. 모델 정확도 측정
1
2
3
4
result = model.evaluate(x_test, y_test)
print('loss (cross-entropy) :', result[0])
print('test accuracy :', result[1])
1
2
3
4
5
6
7
8
9
10
11
12
13
acc = history.history['categorical_accuracy']
val_acc = history.history['val_categorical_accuracy']
x_len = np.arange(len(acc))
plt.plot(x_len, acc, marker='.', c='blue', label="Train-set Acc.")
plt.plot(x_len, val_acc, marker='.', c='red', label="Validation-set Acc.")
plt.legend(loc='upper right')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('Accuracy')
plt.show()
6. 모델 저장
1
model.save('models/model_ver2.h5')
나중에 노드에서 스크립트로 사용해야하기 때문에 h5 > json으로도 변환해주었다. tensorflo.js도 설치가 안돼서 콜랩을 이용했다.
1
!pip install tensorflowjs
1
2
3
import tensorflowjs as tfjs
tfjs.converters.save_keras_model(model, 'model')
model.json
파일과 group1-shared1of1.bin
파일이 생기는데
model.json
은 모델 신경망 정보가 담겨있고group1-shared1of1.bin
은 각 레이어의 가중치 정보가 담겨있다고 한다.
어디서는 bin 파일 이름을 model.weights.json으로 바꾸라던데 다음 포스팅에 쓰겠지만 난 바꾸지 않았을때만 작동했다. 아마 생성되면서 bin 파일 이름과 가중치 명들도 model.json에 담기는 듯 하다.
7. 모델 테스트
바보같은 내 맥북에는 미디어파이프가 설치가 안됨으로.. 테스트 코드도 콜랩에서 돌렸다. 데이터셋 생성하는 것처럼 영상을 따로 찍어 올려서.. 근데 영상과 결과를 같이 볼 수가 없어 우리중 유일하게 테스트가 가능한 수민님께 부탁드렸다.
아래는 내가 작성한 테스트코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# Test ver2.0
# create_data_ver2.0 -> train_ver2.0 으로 생성된 모델 테스트 코드(python ver)
# @Juran
import cv2
import mediapipe as mp
import numpy as np
import tensorflow.keras
actions = ['none', 'click']
seq_length = 30
model = tensorflow.keras.models.load_model('models/model.h5')
# MediaPipe hands model
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(
max_num_hands=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
seq = []
action_seq = []
index_point = [[8, 7, 6], [7, 6, 5], [6, 5, 0]]
while cap.isOpened():
ret, img = cap.read()
img0 = img.copy()
img = cv2.flip(img, 1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
result = hands.process(img)
if result.multi_hand_landmarks is not None:
for res in result.multi_hand_landmarks:
point_info = np.zeros((21, 2))
for j, lm in enumerate(res.landmark):
point_info[j] = [lm.x, lm.y]
angles = []
for point in index_point:
# 검지손가락 포인트 각도 게산
ba = point_info[point[0]] - point_info[point[1]]
bc = point_info[point[2]] - point_info[point[1]]
cosine_angle = np.dot(ba,bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
angle = np.arccos(cosine_angle)
pAngle = np.degrees(angle)
angles.append(pAngle)
this_action = '?'
if model.predict([angles])[0, 0] != 1:
print('none')
this_action = 'none'
else:
print('click')
this_action = 'click'
mp_drawing.draw_landmarks(img, res, mp_hands.HAND_CONNECTIONS)
cv2.putText(img, f'{this_action.upper()}', org=(int(res.landmark[0].x * img.shape[1]), int(res.landmark[0].y * img.shape[0] + 20)), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, color=(255, 255, 255), thickness=2)
cv2.imshow('img', img)
if cv2.waitKey(1) == ord('q'):
break
블로그를 참고하였기에 각도 계산하는 부분 빼고 거의 다 유사하다.
성능은 그닥.. 뭐.. 딱히 좋지는 않았다………. 인공지능 넘 어려웡…….. 그래도 오랜만에 멋사 교육 들을 때 생각도 나고 강사님도 보고싶고.. 이래저래 뚝딱뚝딱 힘겨운 시간이였다.
이번주 가장 큰 업적은 python 코드 javascript로 바꿔서 텐서플로 모델 로드한거인데 다음 포스팅에 써야겠다
모델학습 아주 그냥 흥이야
LSTM(Long Short-Term Memory)이란 https://wikidocs.net/22888