ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OpenCV+DLIB를 활용한 눈 깜박임 감지 모델(Colab)
    카테고리 없음 2023. 11. 24. 21:22

    동영상 강의 시청시 집중도를 계산하고, 공부 시간 측정에 반영하고 다시 강의를 들어야 하는 부분을 알려주는 모델을 만들고자 한다. 집중도를 계산하기 위해 사용될 수 있는 데이터가 너무 많기 때문에 먼저 눈 깜박임 시간 데이터만 사용해 졸음 감지 모델을 만들어 볼 예정이다.

     

    DLIB는 OpenCV가 제공하지 않는 얼굴 랜드마크를 검출할 수 있는 라이브러리다. OpenCV의 face 모듈에도 몇가지 얼굴 랜드마크가 추가되어 있지만, 아직 구현과 지원이 미흡하기 때문에 얼굴 랜드마크 검출 분야에 널리 알려져 있는 DLIB 라이브러리를 이용해서 얼굴 랜드마크 검출을 하고자 한다. 


    [OpenCV+DLIB를 활용해 얼굴 랜드마크 검출하기]

    1. 필요한 라이브러리 import 해오기

    import numpy as np
    import imutils
    import dlib
    import cv2
    from google.colab.patches import cv2_imshow

    colab에서 imshow 함수를 사용하기 위해서는 from google.colab.patches import cv2_imshow 로 import 해야 사용할 수 있다. 



    2. OpenCV imread 함수로 이미지 파일 받아오기

    image_path = '/content/drive/MyDrive/project/data/img/face_img1.jpg' #=환경에 맞게 변경
    #이미지 파일을 Numpy array 형태로 읽어오기
    img = cv2.imread(image_path)
    img = imutils.resize(img, width=500) #resize
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) #BGR 채널 이미지를 단일 채널, Grayscale 로 변환

    원하는 얼굴 이미지 파일을 저장한 뒤, 드라이브에서 이미지 파일이 저장된 위치 경로를 image_path에 넣고 OpenCV imread 함수를 사용해 이미지 파일을 읽어온다. 이미지 파일을 Numpy array 형태로 숫자 값을 받아오고, 이 숫자는 해당 위치에서의 색을 나타낸다. imutils.resize 함수로 이미지 가로 사이즈를 500으로 resize한다. OpenCV 컬러변환 cvtColor 함수를 사용해 RGB(=BGR) 컬러를 Grayscale로 변환해 gray에 넣는다.


    3. 얼굴 검출기 + 랜드마크 검출기

    #얼굴 검출기
    detector = dlib.get_frontal_face_detector()
    #랜드마크 검출기
    predictor = dlib.shape_predictor('/content/drive/MyDrive/project/lib/landmark/shape_predictor_68_face_landmarks.dat') #=환경에 맞게 변경

    얼굴 랜드마크 검출기인 shape_predictor_68_face_landmarks.dat 파일은 http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2에서 다운받아서 경로를 넣어주면 된다.


    4. 얼굴 영역 + 랜드마크 검출 및 표시

    #얼굴 영역 검출
    faces = detector(gray)
    for rect in faces:
        #얼굴 영역을 좌표로 변환 후 사각형 표시
        x, y = rect.left(), rect.top()
        w, h = rect.right()-x, rect.bottom()-y
        cv2.rectangle(img, (x,y), (x+w, y+h), (0, 255, 0), 1)
    	cv2.putText(img, "Face #{}".format(t + 1), (x - 10, y - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    
        #얼굴 랜드마크 검출
        shape = predictor(gray, rect)
        for i in range(68):
            #부위별 좌표 추출 및 표시
            part = shape.part(i)
            cv2.circle(img, (part.x, part.y), 2, (0, 0, 255), -1) #(Blue , Green , Red)
            cv2.putText(img, str(i), (part.x, part.y), cv2.FONT_HERSHEY_PLAIN, 
            				0.5, (255, 255, 255), 1, cv2.LINE_AA)
    
    cv2_imshow(img)
    cv2.waitKey(0)

    위 코드 출력시 왼쪽 이미지가 출력된다.  detector로 얼굴 영역을 검출해 faces에 넣고, 검출한 영역을 좌표로 변환 후 사각형을 그려준다. 검출된 하나의 얼굴 영역에서 랜드마크를 검출해 shape에 넣고 부위별 좌표를 추출하고 표시해준다. 

     

    • detector = dlib.get_frontal_face_detector() : 얼굴 검출기 생성
    • predictor = dlib.shape_predictor(file) : 랜드마크 검출기 생성
    • rects = detector(img) : 얼굴 검출
      • rects : 얼굴 좌표 배열
      • rect.left() : 얼굴 영역 x 좌표
      • rect.right() : 얼굴 영역 x2 좌표
      • rect.top() : 얼굴 영역 y 좌표
      • rect.bottom() : 얼굴 영역 y2 좌표
    • shape = predictor(img, rect) : 랜드마크 검출
      • rect : 얼굴 영역
      • shape : 랜드마크 영역
        • mark = shape.part(id) : id(0~68)로 좌표 획득
        • mart.x , mark.y : id에 대응하는 좌표

     

    cv2.putText 함수를 사용해서 랜드마크 좌표를 표시해줄 수 있고, 필요없다면 삭제할 수 있다.

    cv2.putText(image, text, org, font, fontScale, color[, thickness[, lineType[, bottomLeftOrigin]]])

    5. Video(mp4) 파일에서 얼굴 랜드마크 검출 및 표시

    video_path = '/content/drive/MyDrive/project/data/img/video1.mp4' #=환경에 맞게 변경
    output_path = '/content/drive/MyDrive/project/data/img/video1_output.mp4'
    
    EYES = list(range(36,48)) #- 눈 영역의 랜드마크 좌표 저장
    # 영상 크기 저장
    img_h = 1080
    img_w = 1920
    
    #- vedio output 저장용
    w = round(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    h = round(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps = cap.get(cv2.CAP_PROP_FPS) # 카메라에 따라 값이 정상적, 비정상적
    # fourcc 값 받아오기, *는 문자를 풀어쓰는 방식, *'DIVX' == 'D', 'I', 'V', 'X'
    fourcc = cv2.VideoWriter_fourcc(*'DIVX')
    
    # 프레임과 다음 프레임 사이의 간격 설정
    delay = round(1000/fps)
    
    while cap.isOpened():
        ret, frame = cap.read()
    
        # cv2.VideoWriter 객체 생성, 기존에 받아온 속성값 입력
        out = cv2.VideoWriter(output_path,cv2.VideoWriter_fourcc(*'DIVX'), fps, (img_w,img_h))
    
        if not ret:
            print('Could not read frame')
            break
    
        cap.grab()
    
        #- detect face area
    
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = detector(gray, 1)
    
        for (t,rect) in enumerate(faces):
            #얼굴 영역을 좌표로 변환 후 사각형 표시
            x, y = rect.left(), rect.top()
            w, h = rect.right()-x, rect.bottom()-y
            cv2.rectangle(frame, (x,y), (x+w, y+h), (0, 255, 0), 1)
            cv2.putText(frame, "Face #{}".format(t + 1), (x - 10, y - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    
        #얼굴 랜드마크 검출
            shape = predictor(frame, rect)
            for i in range(68):
                #부위별 좌표 추출 및 표시
                part = shape.part(i)
                cv2.circle(frame, (part.x, part.y), 2, (0, 255, 255), -1)
                
        out.write(frame) #- vedio output 저장용
        #cv2_imshow(frame)
    
    #cap.release()
    out.release()
    #cv2.destroyAllWindows()

    주석을 풀면 출력창에서 캡처된 프레임별로 추출된 얼굴 랜드마크를 볼 수 있다. 원래 영상으로 출력되는 것 같은데 colab에서는 영상 출력이 안되는지 프레임 개수만큼 사진들이 왕창 출력된다. 위 코드 실행 시 output_path로 지정한 경로 파일로 영상이 저장돼야 하는데, 한 프레임밖에 저장되지 않는데 이건 좀 알아봐야 할 것 같다.

     


    [OpenCV+DLIB를 활용해 눈 깜박임 분류 훈련 및 예측]

    1. 필요한 라이브러리 import

    from torch.utils.data import Dataset
    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    from torchsummary import summary
    import numpy as np
    import matplotlib.pyplot as plt
    from torchvision.transforms import transforms
    import torch.optim as optim

    2. data load

    class eyes_dataset(Dataset):
        def __init__(self, x_file_paths, y_file_path, transform=None):
            self.x_files = x_file_paths
            self.y_files = y_file_path
            self.transform = transform
    
        def __getitem__(self, idx):
            x = self.x_files[idx]
            x = torch.from_numpy(x).float()
    
            y = self.y_files[idx]
            y = torch.from_numpy(y).float()
    
            return x, y
    
        def __len__(self):
            return len(self.x_files)
    x_train = np.load('/content/drive/MyDrive/project/data/dataset/x_train.npy').astype(np.float32)  #=환경에 맞게 변경
    y_train = np.load('/content/drive/MyDrive/project/data/dataset/y_train.npy').astype(np.float32)  #=환경에 맞게 변경
    
    train_transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.RandomRotation(10),
        transforms.RandomHorizontalFlip(),
    ])
    
    train_dataset = eyes_dataset(x_train, y_train, transform=train_transform)

     

    x_train.npy와 y_train.npy를 load해오고 train_dataset을 만든다. 관련 데이터셋은https://github.com/kairess/eye_blink_detector 에서 받을 수 있다.

       - train_dataset 출력

    fig = plt.figure(figsize=(12, 10))
    
    for i in range(16):
        x, y = train_dataset[i]
    
        plt.subplot(4, 4, i+1)
        plt.imshow(x_train[i].reshape((26, 34)), cmap='gray')
        plt.title(str(y_train[i]))
        
    
    plt.show()

    데이터 16개를 plot해서 x_train에 있는 이미지와 y_train에 있는 label을 확인해본다. 눈을 뜬 경우 1, 감은 경우 0으로 라벨링 되어 있는 것을 볼 수 있다.

     

     3. CNN 모델 구성과 학습

    class Net(nn.Module):
    	def __init__(self):
            super(Net, self).__init__()
    
            self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
            self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
            self.conv3 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
            self.fc1 = nn.Linear(1536, 512)
            self.fc2 = nn.Linear(512, 1)
    
        def forward(self, x):
            x = F.max_pool2d(F.relu(self.conv1(x)),2)
            x = F.max_pool2d(F.relu(self.conv2(x)), 2)
            x = F.max_pool2d(F.relu(self.conv3(x)), 2)
            x = x.reshape(-1, 1536)
            x = F.relu(self.fc1(x))
            x = self.fc2(x)
    
    
            return x
    
    model = Net().to('cuda')
    summary(model, (1,26,34))
    ----------------------------------------------------------------
            Layer (type)               Output Shape         Param #
    ================================================================
                Conv2d-1           [-1, 32, 26, 34]             320
                Conv2d-2           [-1, 64, 13, 17]          18,496
                Conv2d-3            [-1, 128, 6, 8]          73,856
                Linear-4                  [-1, 512]         786,944
                Linear-5                    [-1, 1]             513
    ================================================================
    Total params: 880,129
    Trainable params: 880,129
    Non-trainable params: 0
    ----------------------------------------------------------------
    Input size (MB): 0.00
    Forward/backward pass size (MB): 0.37
    Params size (MB): 3.36
    Estimated Total Size (MB): 3.74
    ----------------------------------------------------------------

    colab에서 model = Net().to('cuda')을 사용하려면 런타임 유형을 GPU로 변환해야 한다. 모델을 계속 실행하다가 문제가 발생했는데, 알 수 없는 이유로 런타임이 중지되고 초기화되는데, 해결 방법은 런타임(GPU) 연결, 드라이브 mount 후에 바로 위 코드를 실행하면 문제 없이 실행할 수 있다. 예상하기로는 그 전에 너무 많은 데이터가 저장되어 있어서 그 다음에 CNN을 구축하려 하면 문제가 생기는 것 같다.


    4. accuracy 함수 정의 및 모델 훈련, 예측 준비

    def accuracy(y_pred, y_test):
        y_pred_tag = torch.round(torch.sigmoid(y_pred))
    
        correct_results_sum = (y_pred_tag == y_test).sum().float()
        acc = correct_results_sum / y_test.shape[0]
        acc = torch.round(acc * 100)
    
        return acc
    
    # weights를 저장할 위치 지정
    PATH = '/content/drive/MyDrive/project/data/dataset/weights/trained.pth' #=환경에 맞게 변경
    
    train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
    
    model = Net()
    model.to('cuda')
    
    criterion = nn.BCEWithLogitsLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.0001)

    accuracy 함수는 예측값과 라벨값을 불러와 예측값을 sigmoid를 통해 확률화하고 round로 반올림해 0 또는 1로 저장해 y_pred_tag에 넣는다. 정확도를 계산해 출력해준다. 


    5. train 데이터로 모델 훈련

    for epoch in range(epochs):
        running_loss = 0.0
        running_acc = 0.0
    
        model.train()
    
        for i, data in enumerate(train_dataloader, 0):
            input_1, labels = data[0].to('cuda'), data[1].to('cuda')
    
            input = input_1.transpose(1, 3).transpose(2, 3)
    
            optimizer.zero_grad()
    
            outputs = model(input)
    
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
    
            running_loss += loss.item()
            running_acc += accuracy(outputs, labels)
    
            if i % 80 == 79:
                print('epoch: [%d/%d] train_loss: %.5f train_acc: %.5f' % (
                    epoch + 1, epochs, running_loss / 80, running_acc / 80))
                running_loss = 0.0
    
    print("learning finish")
    torch.save(model.state_dict(), PATH)
    epoch: [1/10] train_loss: 0.01442 train_acc: 99.51250
    epoch: [2/10] train_loss: 0.00634 train_acc: 99.88750
    epoch: [3/10] train_loss: 0.00463 train_acc: 99.96250
    epoch: [4/10] train_loss: 0.01392 train_acc: 99.55000
    epoch: [5/10] train_loss: 0.00488 train_acc: 99.96250
    epoch: [6/10] train_loss: 0.00300 train_acc: 100.00000
    epoch: [7/10] train_loss: 0.00259 train_acc: 100.00000
    epoch: [8/10] train_loss: 0.00220 train_acc: 100.00000
    epoch: [9/10] train_loss: 0.00121 train_acc: 100.00000
    epoch: [10/10] train_loss: 0.00115 train_acc: 100.00000
    learning finish

    epoch 10번 정도면 train 정확도가 100%가 나와 충분하다고 볼 수 있다.


    6. test 데이터로 예측

    #훈련 때 저장한 weight를 불러오기 위한 경로 저장
    PATH = '/content/drive/MyDrive/project/data/dataset/weights/trained.pth'
    
    x_test = np.load('/content/drive/MyDrive/project/data/dataset/x_val.npy').astype(np.float32)  #=환경에 맞게 변경
    y_test = np.load('/content/drive/MyDrive/project/data/dataset/y_val.npy').astype(np.float32)  #=환경에 맞게 변경
    
    test_transform = transforms.Compose([
        transforms.ToTensor()
    ])
    
    test_dataset = eyes_dataset(x_test, y_test, transform=test_transform)
    
    test_dataloader = DataLoader(test_dataset, batch_size=1, shuffle=False, num_workers=4)
    
    model = Net()
    model.to('cuda')
    model.load_state_dict(torch.load(PATH))
    model.eval()
    
    count = 0
    
    with torch.no_grad():
        total_acc = 0.0
        acc = 0.0
        for i, test_data in enumerate(test_dataloader, 0):
            data, labels = test_data[0].to('cuda'), test_data[1].to('cuda')
    
            data = data.transpose(1, 3).transpose(2, 3)
    
            outputs = model(data)
    
            acc = accuracy(outputs, labels)
            total_acc += acc
    
            count = i
    
        print('avarage acc: %.5f' % (total_acc/count),'%')
    
    print('test finish!')
    avarage acc: 99.65157 %
    test finish!

    훈련할 때 저장한 weight를 가져와 test 데이터셋으로 예측하고 정확도를 계산했을 때 99.65%가 나오는 것을 보면 모델이 잘 만들어졌음을 알 수 있다.


    7. 

    import cv2
    import dlib
    import numpy as np
    import torch
    from imutils import face_utils
    
    IMG_SIZE = (34,26)
    PATH = '/content/drive/MyDrive/project/data/dataset/weights/trained.pth'
    
    detector = dlib.get_frontal_face_detector()
    predictor = dlib.shape_predictor('/content/drive/MyDrive/project/lib/landmark/shape_predictor_68_face_landmarks.dat')
    
    model = Net()
    model.load_state_dict(torch.load(PATH))
    model.eval()
    
    n_count = 0

     

    모델이 잘 훈련된 것을 봤으니 직접 영상을 가져와 눈 깜박임을 감지해보도록 한다.

    필요한 weight와 랜드마크 검출 라이브러리, model을 가져온다. 

    def crop_eye(img, eye_points):
      x1, y1 = np.amin(eye_points, axis=0)
      x2, y2 = np.amax(eye_points, axis=0)
      cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
    
      w = (x2 - x1) * 1.2
      h = w * IMG_SIZE[1] / IMG_SIZE[0]
    
      margin_x, margin_y = w / 2, h / 2
    
      min_x, min_y = int(cx - margin_x), int(cy - margin_y)
      max_x, max_y = int(cx + margin_x), int(cy + margin_y)
    
      eye_rect = np.rint([min_x, min_y, max_x, max_y]).astype(np.int)
    
      eye_img = gray[eye_rect[1]:eye_rect[3], eye_rect[0]:eye_rect[2]]
    
      return eye_img, eye_rect
    
    def predict(pred):
      pred = pred.transpose(1, 3).transpose(2, 3)
    
      outputs = model(pred)
    
      pred_tag = torch.round(torch.sigmoid(outputs))
    
      return pred_tag

    눈 부분만 가져와 학습시킨 CNN 모델을 적용하기 위해 crop_eye 함수를 만든다.

     

    cap = cv2.VideoCapture(video_path)
    
    while cap.isOpened():
      ret, img_ori = cap.read()
    
      if not ret:
        break
    
      img_ori = cv2.resize(img_ori, dsize=(0, 0), fx=0.5, fy=0.5)
    
      img = img_ori.copy()
      gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
      faces = detector(gray)
    
      for face in faces:
        shapes = predictor(gray, face)
        shapes = face_utils.shape_to_np(shapes)
    
        eye_img_l, eye_rect_l = crop_eye(gray, eye_points=shapes[36:42])
        eye_img_r, eye_rect_r = crop_eye(gray, eye_points=shapes[42:48])
    
    
        eye_img_l = cv2.resize(eye_img_l, dsize=IMG_SIZE)
        eye_img_r = cv2.resize(eye_img_r, dsize=IMG_SIZE)
        eye_img_r = cv2.flip(eye_img_r, flipCode=1)
    
        eye_input_l = eye_img_l.copy().reshape((1, IMG_SIZE[1], IMG_SIZE[0], 1)).astype(np.float32)
        eye_input_r = eye_img_r.copy().reshape((1, IMG_SIZE[1], IMG_SIZE[0], 1)).astype(np.float32)
    
    
        eye_input_l = torch.from_numpy(eye_input_l)
        eye_input_r = torch.from_numpy(eye_input_r)
    
    
        pred_l = predict(eye_input_l)
        pred_r = predict(eye_input_r)
        if pred_l.item() == 0.0 and pred_r.item() == 0.0:
          n_count+=1
    
        else:
          n_count = 0
    
    
        if n_count > 100:
          cv2.putText(img,"Wake up", (120,160), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,255),2)
    
    
    
        # visualize
        state_l = 'O %.1f' if pred_l > 0.1 else '- %.1f'
        state_r = 'O %.1f' if pred_r > 0.1 else '- %.1f'
    
        state_l = state_l % pred_l
        state_r = state_r % pred_r
    
    
        cv2.rectangle(img, tuple(eye_rect_l[0:2]), tuple(eye_rect_l[2:4]), (0, 255, 0), 1)
        cv2.rectangle(img, tuple(eye_rect_r[0:2]), tuple(eye_rect_r[2:4]), (0, 255, 0), 1)
    
        cv2.putText(img, state_l, tuple(eye_rect_l[0:2]), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 1)
        cv2.putText(img, state_r, tuple(eye_rect_r[0:2]), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 1)
    
      cv2_imshow(img)
      cv2.waitKey(0)
    
      if cv2.waitKey(1) == ord('q'):
        break

    위에서 처럼 VideoCapture 함수를 사용해 영상을 frame으로 쪼개고 crop_eye 함수를 사용해 눈 부분만 가져와 눈을 감았는지 계산하고, cv2.rectangle 함수로 크롭해온 눈 부분을 시각화해준다.


     

     

    프로젝트를 위해 집중도 감지 모델을 만들어야 하는데, 눈 깜박임 외에 머리 숙임, 등을 감지해 집중도를 감지할 수 있도록 할 예정이다. 동영상 시청시 사용자가 시작 버튼을 누르면 노트북 카메라를 입력으로 받아 OpenCV와 DILB를 활용해 얼마나 집중하고 있는지 감지하고 실시간으로 집중도를 그래프로 그려주도록 할 예정이다. 

     

     


    전체 코드
    https://colab.research.google.com/drive/1GSVYcsoMucwyjqCvTLyRyQuyZpTV3Lcb?usp=sharing
    참고 자료
    • 이세우, "파이썬으로 만드는 OpenCV 프로젝트: 간단한 영상 입출력부터 머신러닝까지", 프로그래밍인사이트, 2019
    • https://github.com/kairess/eye_blink_detector
     

    GitHub - kairess/eye_blink_detector: Eye blink(Closeness-Openess) detection using CNN (Keras)

    Eye blink(Closeness-Openess) detection using CNN (Keras) - GitHub - kairess/eye_blink_detector: Eye blink(Closeness-Openess) detection using CNN (Keras)

    github.com

    • https://ys-cs17.tistory.com/24
Designed by Tistory.