2020-04-20 15:41:50

안녕하세요. b스카이비전입니다. 오늘은 여러분과 함께 재밌는 것을 하나 만들어보려고 합니다. 예전에 혹시 뉴스에서 이런 영상을 보셨는지 모르겠습니다.

 

https://www.youtube.com/watch?v=aM-anUHTIxM

 

바로 가위바위보 로봇인데요. 이 로봇의 핵심 기술은 사람의 손동작을 보고 그것이 가위인지, 바위인지, 보인지를 분류해내는 것입니다. 그것도 아주 아주 빠르게요. 최대한 빠르게 분류해낼 수 있어야 빠르게 그것을 이길 수 있는 손동작을 로봇에게 명령해줄 수 있겠죠. "저 사람이 가위 냈으니까 너는 빨리 바위를 내!"

 

저는 오늘 전이학습(transfer learning)을 이용해서 가위, 바위, 보를 분류할 수 있는 분류기를 만들어보도록 하겠습니다. 전이학습은 미리 훈련된 CNN 모델을 가지고 와서 우리의 목적에 맞도록 모델 가중치들을 재보정해줘서 사용하는 것을 의미합니다.

 

가위바위보 분류기를 만드려면 우선 데이터셋이 필요합니다. 비교적 많은 수의 가위 이미지, 바위 이미지, 보 이미지가 필요한 것이죠. 가위바위보 데이터셋이 어딘가에 존재할 수도 있겠지만, 찾는 것이 귀찮기도 하고 직접 만드는 것이 그렇게 어려운 일도 아니기 때문에 제가 하나 만들었습니다. 이와 관련해서는 이전 포스팅, [Anaconda+python] 웹캠 영상 프레임 샘플링해서 저장하기(쉽게 이미지 데이터베이스 만들기)를 참고해주세요. 데이터베이스를 만들기 위해 사용한 코드는 다음과 같습니다. opencv-python 패키지 설치가 선행되어야 합니다. 

 

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
import cv2
 
# open webcam (웹캠 열기)
webcam = cv2.VideoCapture(0)
 
if not webcam.isOpened():
    print("Could not open webcam")
    exit()
    
 
sample_num = 0    
captured_num = 0
    
# loop through frames
while webcam.isOpened():
    
    # read frame from webcam 
    status, frame = webcam.read()
    sample_num = sample_num + 1
    
    if not status:
        break
 
    # display output
    cv2.imshow("captured frames", frame)
    
    if sample_num == 4:
        captured_num = captured_num + 1
        cv2.imwrite('./rock/img'+str(captured_num)+'.jpg', frame) # 바위 이미지 수집시
        # cv2.imwrite('./paper/img'+str(captured_num)+'.jpg', frame) # 보 이미지 수집시
        # cv2.imwrite('./scissors/img'+str(captured_num)+'.jpg', frame) # 가위 이미지 수집시
        sample_num = 0
        
    
    # press "Q" to stop
    if cv2.waitKey(1& 0xFF == ord('q'):
        break
    
# release resources
webcam.release()
cv2.destroyAllWindows()   
cs

 

위 코드를 실행해서 얻은 이미지들 중에 만족스럽지 않은 이미지들은 삭제했고, 결과적으로 저는 394장의 바위 이미지, 461장의 가위 이미지, 413장의 보 이미지를 가진 가위바위보 데이터셋을 만들었습니다.

 

데이터셋 준비 완료!

 

데이터셋이 준비되었으니 이제 분류기를 만들어서 훈련시키도록 하겠습니다. 대개 훈련셋, 검증셋, 훈련셋으로 나눠서 제대로 훈련이 되었는지 평가하지만, 저는 편의상 모든 이미지를 훈련에 투입시키도록 하겠습니다. 이미지넷에서 미리 훈련된 ResNet50 모델을 가져와서 최종 출력 레이어를 제거한 후에 global average pooling 레이어, 3개의 뉴런으로 구성된 FC 레이어를 추가시키겠습니다. 가위 바위 보 세 가지를 분류하면 되기 때문에 3개의 뉴런을 갖도록 설정했습니다. 그 다음에 가위 바위 보 이미지들과 그에 맞는 레이블로 모델을 훈련시킵니다. 이때 중요한 것은 바위 이미지 폴더, 보 이미지 폴더, 가위 이미지 폴더에서 이미지를 불러온 후에 이미지들을 한번 섞어줘야합니다. 안 그러면 훈련시 바위 이미지만 쭉 사용되다가, 보 이미지가 쭉 나오고, 그 다음에 가위 이미지가 연달아서 나오기 때문에 모델의 가중치를 제대로 훈련시킬 수 없습니다. 훈련이 완료되면 훈련된 모델을 model.h5로 저장해주겠습니다. 이 일련의 과정에 대한 코드는 다음과 같습니다. tensorflow, numpy 패키지가 설치되어 있지 않는 경우 설치해주셔야 해요.

 

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
92
93
94
95
96
97
98
import os
import numpy as np
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D
from tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input
from tensorflow.keras.preprocessing.image import load_img, img_to_array
 
 
path_dir1 = './rock/'
path_dir2 = './paper/'
path_dir3 = './scissors/'
 
file_list1 = os.listdir(path_dir1) # path에 존재하는 파일 목록 가져오기
file_list2 = os.listdir(path_dir2)
file_list3 = os.listdir(path_dir3)
 
#%% train용 이미지 준비
num = 0;
train_img = np.float32(np.zeros((12682242243))) # 394+413+461
train_label = np.float64(np.zeros((12681)))
 
for img_name in file_list1:
    img_path = path_dir1+img_name
    img = load_img(img_path, target_size=(224224))
    
    x = img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    train_img[num, :, :, :] = x
    
    train_label[num] = 0 # rock
    num = num + 1
 
for img_name in file_list2:
    img_path = path_dir2+img_name
    img = load_img(img_path, target_size=(224224))
    
    x = img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    train_img[num, :, :, :] = x
    
    train_label[num] = 1 # paper
    num = num + 1
 
for img_name in file_list3:
    img_path = path_dir3+img_name
    img = load_img(img_path, target_size=(224224))
    
    x = img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    train_img[num, :, :, :] = x
    
    train_label[num] = 2 # scissors
    num = num + 1
 
 
# 이미지 섞기
     
n_elem = train_label.shape[0]
indices = np.random.choice(n_elem, size=n_elem, replace=False)
 
train_label = train_label[indices]
train_img = train_img[indices]
 
#%% 
# create the base pre-trained model
IMG_SHAPE = (2242243)
 
base_model = ResNet50(input_shape=IMG_SHAPE, weights='imagenet', include_top=False)
base_model.trainable = False
base_model.summary()
print("Number of layers in the base model: "len(base_model.layers))
 
GAP_layer = GlobalAveragePooling2D()
dense_layer = Dense(3, activation=tf.nn.softmax)
 
model = Sequential([
        base_model,
        GAP_layer,
        dense_layer
        ])
 
base_learning_rate = 0.001
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
model.summary()
 
model.fit(train_img, train_label, epochs=5)
 
# save model
model.save("model.h5")
 
print("Saved model to disk")  
 
cs

 

위 코드를 실행해서 가위바위보 분류기를 훈련시켜줍니다.

 

훈련 진행 중 ... 훈련 완료! 

 

자, 이제 제대로 가위 바위 보를 분류해내는지를 테스트할 차례입니다. 훈련된 모델을 불러온 후 웹캠을 통해 실시간으로 입력받은 영상이 가위인지, 바위인지, 보인지를 분류해서 영상 위에 한글 텍스트로 써주는 코드를 작성해봤습니다. 추가로 pillow 패키지가 필요합니다.  

 

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
import cv2
import numpy as np
from tensorflow.keras.models import load_model
from tensorflow.keras.applications.resnet50 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from PIL import ImageFont, ImageDraw, Image
 
#%%
model = load_model('model.h5')
model.summary()
 
# open webcam (웹캠 열기)
webcam = cv2.VideoCapture(0)
 
if not webcam.isOpened():
    print("Could not open webcam")
    exit()
      
# loop through frames
while webcam.isOpened():
    
    # read frame from webcam 
    status, frame = webcam.read()
    
    if not status:
        break
    
    img = cv2.resize(frame, (224224), interpolation = cv2.INTER_AREA)
    x = img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
 
    prediction = model.predict(x)
    predicted_class = np.argmax(prediction[0]) # 예측된 클래스 0, 1, 2
    print(prediction[0])
    print(predicted_class)
 
    
    if predicted_class == 0:
        me = "바위"
    elif predicted_class == 1:
        me = "보"        
    elif predicted_class == 2:
        me = "가위"
                
    # display
    fontpath = "font/gulim.ttc"
    font1 = ImageFont.truetype(fontpath, 100)
    frame_pil = Image.fromarray(frame)
    draw = ImageDraw.Draw(frame_pil)
    draw.text((5050), me, font=font1, fill=(002553))
    frame = np.array(frame_pil)
    cv2.imshow('RPS', frame)
        
    # press "Q" to stop
    if cv2.waitKey(1& 0xFF == ord('q'):
        break
    
# release resources
webcam.release()
cv2.destroyAllWindows()   
cs

 

마지막으로 위 코드를 실행해서 제가 가위 바위 보 중 하나를 웹캠을 향해 내는 것을 얼마나 정확히, 또 얼마나 빨리 분류해내는지 테스트해보도록 하겠습니다. 

 

https://www.youtube.com/watch?v=3ivhYy19Ppc

가위 바위 보 분류기 시연 영상

 

꽤 잘 분류해내죠?ㅎㅎ 거의 제가 냄과 동시에 가위, 바위, 보를 인식해냄을 알 수 있습니다. 배경에 다른 물체가 많거나 너무 패턴이 복잡하다면 인식 정확도가 별로 안 좋을 수 있습니다. 더 정교하게 만드려면, 다양한 배경에서 가위 바위 보 이미지를 수집하는 것이 좋습니다. 또한 더 많은 갯수의 이미지를 수집하는 것이 도움이 될 것입니다. 

 

지금까지 가위바위보 분류기 만들어봤는데 그렇게 어렵지 않았죠? 꼭 한번 직접 위 과정을 따라서 만들어보신다면 수확이 있으실 것입니다. 만약 제가 만든 가위바위보 데이터셋이 필요하신 분 또는 훈련된 모델 model.h5가 필요하신 분은 아래 링크를 통해 다운받으시기 바랍니다. 

 

1. model.h5

drive.google.com/file/d/1NLvpOgcuugzHOCyCVG2jEReyHpOpbN55/view?usp=sharing  

 

2. rock 바위 이미지 데이터셋

drive.google.com/file/d/11Gg19oB-tymoC8ji0habEPHnkkoXn9xJ/view?usp=sharing

 

3. paper 보 이미지 데이터셋

drive.google.com/file/d/10ASDF3x011xkMXh8tsDAN2nTNvKHP5Ae/view?usp=sharing

 

4. scissors 가위 이미지 데이터셋

drive.google.com/file/d/1GPe3SXCBEcc1Cu5Uue3pkw28Z7wCYiIO/view?usp=sharing