2021-01-07 08:56:06

저는 오늘 마스크 착용 유무 판별기를 만들어보려고 합니다. 객체 검출(object detection)의 방법으로 만들 수도 있겠지만, 그렇게 하면 데이터셋을 준비하는 데 꽤 큰 노력을 들여야합니다. 이미지들에서 마스크 쓴 얼굴과 마스크를 쓰지 않은 얼굴을 labelImg와 같은 라벨링 툴로 일일이 라벨링을 해줘야하기 때문입니다. 라벨링을 한다는 것은 이미지에서 마스크 쓴 얼굴과 마스크를 쓰지 얼굴을 모두 찾아서 거기에 바운딩 박스(bounding box)를 그려준 후에, 마스크를 쓰지 않은 얼굴에 0, 마스크를 쓴 얼굴에 1 이런식으로 라벨을 부여하는 것을 의미합니다. 이게 꽤 많은 시간을 들여야 하는 작업입니다. 그래서 좀 더 쉽고 간단하게 마스크 착용 유무 판별기를 만들 수 없나 고민하다가 택한 방법은 다음과 같습니다.

 

1. 먼저 이미 훈련된 얼굴 검출기로 얼굴들을 검출합니다.

이때 주의해야할 것은 마스크를 쓴 얼굴도 검출할 수 있는 얼굴 검출기여야 한다는 것입니다. cvlib 라이브러리의 얼굴 검출기는 마스크를 쓴 얼굴들도 잘 검출하더라고요. 그래서 저는 그것을 선택했습니다. 

 

2. 마스크를 쓴 얼굴인지, 마스크를 쓰지 않은 얼굴인지 판별할 수 있는 이미지 분류기를 만듭니다. 

이를 위해서는 마스크를 쓴 얼굴 이미지들과 마스크를 쓰지 않은 얼굴 이미지들이 필요합니다. 그래야 이미지 이진 분류기를 훈련시킬 수 있기 때문입니다. 저는 전이학습(transfer learning)을 이용해서 마스크 착용 유무 분류기를 훈련시키겠습니다. 

 

3. 1번째 단계에서 검출된 얼굴들을 마스크를 쓴 얼굴과 마스크를 쓰지 않은 얼굴로 분류합니다. 

 

 

일단 결과물을 보여드리고 한 단계씩 설명드리도록 하겠습니다. youtu.be/DvnIxNjC8sI

 

여러 명이 동시에 등장해도 잘 판별해냅니다. 프라이버시를 위해 촬영하진 않았습니다.

 

또한 마스크를 안 쓰고 손으로 입주변을 가리는 분들이 계실 수 있을 것 같아서, 손으로 입주변을 가릴 때는 어떻게 판별해내는지 테스트해봤습니다. 이때도 역시 노 마스크로 판별해냅니다. (2021년 1월 27일에 꼬장스카이비전 님께서 남겨주신 좋은 질문 감사합니다.)

 

 

 

 

자, 그럼 이제 마스크 착용 유무 판별기를 함께 만들어보겠습니다. 

 

 

 

마스크 착용 유무 판별기 만들기

1. 마스크 착용 얼굴, 마스크 미착용 얼굴 데이터 수집

웹캠을 이용해서 촬영되는 영상에 얼굴 검출기를 적용해서 얻은 얼굴들을 잘라서 저장합니다. 한 번은 마스크를 착용한 채 촬영하고, 한 번은 마스크를 미착용한 상태로 촬영합니다. 마스크를 착용한 상태에서 얻은 얼굴 이미지들은 ./mask 디렉토리에 저장하고, 마스크 미착용 얼굴 이미지들은 ./nomask 디렉토리에 저장합니다. 모든 프레임의 영상을 다루면, 얼굴 이미지에 큰 차이가 없기 때문에 저는 8장마다 한 장의 프레임을 선택해서 얼굴 이미지들을 저장했습니다. 

 

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
import cv2
import cvlib as cv
 
# 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
 
    # 이미지 내 얼굴 검출
    face, confidence = cv.detect_face(frame)
    
    print(face)
    print(confidence)
 
    # loop through detected faces
    for idx, f in enumerate(face):
        
        (startX, startY) = f[0], f[1]
        (endX, endY) = f[2], f[3]
 
 
        if sample_num % 8  == 0:
            captured_num = captured_num + 1
            face_in_img = frame[startY:endY, startX:endX, :]
            cv2.imwrite('./mask/face'+str(captured_num)+'.jpg', face_in_img) # 마스크 미착용 데이터 수집시 주석처리
            # cv2.imwrite('./nomask/face'+str(captured_num)+'.jpg', face_in_img) # 마스크 미착용 데이터 수집시 주석해제
 
 
    # display output
    cv2.imshow("captured frames", frame)        
    
    # press "Q" to stop
    if cv2.waitKey(1& 0xFF == ord('q'):
        break
    
# release resources
webcam.release()
cv2.destroyAllWindows()   
cs

 

./mask 디렉토리를 확인해보면, 마스크를 착용한 얼굴들이 잘 저장되어 있는 것을 확인하실 수 있습니다. ./nomask 디렉토리에는 마찬가지로 마스크를 미착용한 얼굴들이 잘 저장되어 있습니다. 그렇게 많은 장수의 이미지가 필요하지 않습니다. mask 이미지, nomask 이미지 각각 200장 정도면 충분한 것 같습니다. 

 

 

그 다음에 제대로 검출되지 않은 이미지들이 있다면 삭제해줍니다. 별로 많진 않더라고요. 

 

2. 마스크 착용, 미착용 판별기 훈련하기

데이터를 수집했으니 이제 마스크 착용, 미착용 판별기를 훈련시킬 차례입니다. ImageNet에 훈련된 ResNet50을 가지고 와서 전이학습을 이용해서 커스터마이징 해줍니다. 

 

 

ResNet50의 마지막 층을 제거한 것에 flatten, fc 층, batch normalization, fc 층을 추가한 것을 마스크 착용 얼굴 이미지와 마스크 미착용 얼굴 이미지들과 그것에 해당하는 라벨값으로 훈련시켰습니다. 저는 마스크 착용 이미지에 1의 라벨을, 마스크 미착용 이미지에 0의 라벨을 부여했습니다. 

 

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
99
100
101
102
103
104
105
106
import os
import numpy as np
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Flatten, BatchNormalization
from tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input
from tensorflow.keras.preprocessing.image import load_img, img_to_array
 
 
path_dir1 = './nomask/'
path_dir2 = './mask/'
 
file_list1 = os.listdir(path_dir1) # path에 존재하는 파일 목록 가져오기
file_list2 = os.listdir(path_dir2)
 
file_list1_num = len(file_list1)
file_list2_num = len(file_list2)
 
file_num = file_list1_num + file_list2_num
 
 
#%% 이미지 전처리
num = 0;
all_img = np.float32(np.zeros((file_num, 2242243))) 
all_label = np.float64(np.zeros((file_num, 1)))
 
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)
    all_img[num, :, :, :] = x
    
    all_label[num] = 0 # nomask
    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)
    all_img[num, :, :, :] = x
    
    all_label[num] = 1 # mask
    num = num + 1
 
 
# 데이터셋 섞기(적절하게 훈련되게 하기 위함) 
n_elem = all_label.shape[0]
indices = np.random.choice(n_elem, size=n_elem, replace=False)
 
all_label = all_label[indices]
all_img = all_img[indices]
 
 
# 훈련셋 테스트셋 분할
num_train = int(np.round(all_label.shape[0]*0.8))
num_test = int(np.round(all_label.shape[0]*0.2))
 
train_img = all_img[0:num_train, :, :, :]
test_img = all_img[num_train:, :, :, :] 
 
train_label = all_label[0:num_train]
test_label = all_label[num_train:]
 
 
 
#%% 
# 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))
 
flatten_layer = Flatten()
dense_layer1 = Dense(128, activation='relu')
bn_layer1 = BatchNormalization()
dense_layer2 = Dense(1, activation=tf.nn.sigmoid)
 
model = Sequential([
        base_model,
        flatten_layer,
        dense_layer1,
        bn_layer1,
        dense_layer2,
        ])
 
base_learning_rate = 0.001
model.compile(optimizer=tf.keras.optimizers.Adam(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=['accuracy'])
model.summary()
 
model.fit(train_img, train_label, epochs=10, batch_size=16, validation_data = (test_img, test_label))
 
 
# save model
model.save("model.h5")
 
print("Saved model to disk")  
cs

 

위 코드를 실행하면 model.h5를 얻게 되는데, 여기에 바로 마스크 착용 유무 판별기가 들어가 있습니다. 

 

3. 영상에서 얼굴 영역 검출 후 마스크 썼는지 안 썼는지 판별

이제 모든 준비를 마쳤습니다. 촬영되는 영상에 나오는 인물들이 마스크를 썼는지 안 썼는지를 잘 판별해내는지 확인해보겠습니다. 마스크를 썼을 때는 초록색 바운딩 박스를 얼굴 주위에 그려줬고, 마스크를 쓰지 않았을 때는 빨간색 바운딩 박스를 그려줬습니다. 

 

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
# import necessary packages
import cvlib as cv
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:
        print("Could not read frame")
        exit()
 
    # apply face detection
    face, confidence = cv.detect_face(frame)
 
    # loop through detected faces
    for idx, f in enumerate(face):
        
        (startX, startY) = f[0], f[1]
        (endX, endY) = f[2], f[3]
        
        if 0 <= startX <= frame.shape[1and 0 <= endX <= frame.shape[1and 0 <= startY <= frame.shape[0and 0 <= endY <= frame.shape[0]:
            
            face_region = frame[startY:endY, startX:endX]
            
            face_region1 = cv2.resize(face_region, (224224), interpolation = cv2.INTER_AREA)
            
            x = img_to_array(face_region1)
            x = np.expand_dims(x, axis=0)
            x = preprocess_input(x)
            
            prediction = model.predict(x)
 
            if prediction < 0.5# 마스크 미착용으로 판별되면,
                cv2.rectangle(frame, (startX,startY), (endX,endY), (0,0,255), 2)
                Y = startY - 10 if startY - 10 > 10 else startY + 10
                text = "No Mask ({:.2f}%)".format((1 - prediction[0][0])*100)
                cv2.putText(frame, text, (startX,Y), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)
                
            else# 마스크 착용으로 판별되면
                cv2.rectangle(frame, (startX,startY), (endX,endY), (0,255,0), 2)
                Y = startY - 10 if startY - 10 > 10 else startY + 10
                text = "Mask ({:.2f}%)".format(prediction[0][0]*100)
                cv2.putText(frame, text, (startX,Y), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2)
                
    # display output
    cv2.imshow("mask nomask classify", frame)
 
    # press "Q" to stop
    if cv2.waitKey(1& 0xFF == ord('q'):
        break
    
# release resources
webcam.release()
cv2.destroyAllWindows() 
cs

 

위 코드를 실행한 결과를 캡쳐한 것이 바로 맨 위에 보여드린 영상입니다. 

 

마무리 인사

오랜만에 영상처리 관련된 글을 쓰는 것 같네요. 앞으로는 다시 영상처리 관련된 글 작성 비중을 높이려고 합니다. 아무래도 그것이 제 블로그를 구독해주시는 분들, 관심을 가져주신 분들에 대한 예의가 아닐까 싶네요. ㅎㅎ 내용과 관련된 질의는 언제든 환영하니, 댓글로 남겨주세요.^^

 

관련 글

[1] [Anaconda+python] 전이학습 이용해서 가위, 바위, 보 분류기 만들기

[2] 전이학습(transfer learning) 재밌고 쉽게 이해하기

[3] [ubuntu+python] 얼굴 검출

[4] 이미지 분류(image classification)와 물체 검출(object detection)의 차이는?  

 

 

(이 글은 2022-5-20에 최종적으로 수정되었습니다.)