Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

Team Logo

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

팀소개 및 프로잭트 설명 동영상

졸음운전 여부를 판단하여 알림을 통해 사고를 예방하는 기기를 제작하는 팀입니다.

졸음운전의 단계를 drowsing, sleeping 두 단계로 나누어 각각의 상황에 조치를 해주는 시스템입니다.

온라인해커톤으로의 전환에 따라 부대에서 잘못된 부품,전선 및 센서들을 구매하고 확인하지 못하여 라즈베리 파이 프로토타입 제작 과정 중 하드웨어 구성을 완전하게 하지 못했습니다. 아래 영상은 라즈베리가 아닌 우분투 os 노트북 환경에서 실행한 프로젝트입니다.

  • 라즈베리 파이 부저알림 --> 얼굴을 감싸는 앵커 우측 하단에 "drowsing" 또는 "SLEEPING"으로 표시 및 터미널 출력
  • 그 외 기능 동일

기능 설계

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji
Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

컴퓨터 구성 / 필수 조건 안내 (Prerequisites)

  • Debian 계열 os
  • openCV 4.X 버전
  • (권장)라즈베리파이 3B+
  • 피에조 부저 필요(GPIO 16번 연결)

기술 스택 (Technique Used)

Drowsing detection

  • Real-Time Eye Blink Detection using Facial Landmarks(Tereza Soukupova and Jan ´ Cech외, 2016)의 방법을 따름

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

사고예방

  • 100km/h 속도를 갖는 차량이 안전거리 100m를 유지하고 있다고 가정

고속도로를 달리는 차량을 가정하면 56m의 제동거리가 필요하므로 운전자는 44m의 여유가 있다.
1초에 27m를 이동하므로 시간으로 환산하면 운전자는 1.63초의 여유가 있는 셈이다. 반응속도 0.7초를 빼면 약 0.9초의 시간이 남는다.

눈을 감지않지 않은 상태를 not drowsing, 눈을 감은 상태를 drowsing 상태로 판단하고 opencv 객체 cap을 30fps로 설정했으므로 0.9초에 해당하는 27프레임동안 drowsing이 지속되면 sleeping으로 판단합니다. drowsing상태에서는 짧은 비프음, sleeping상태에서는 not drowsing상태가 될 때까지 비프음이 지속되게 합니다.

Raspberry GPIO control

사용자에게 가는 알림은 라즈베리파이 GPIO를 사용함.

설치 안내 (Installation Process)

dlib 라이브러리 필요:

git clone https://github.com/davisking/dlib.git
cd dlib
git clone https://github.com/osamhack2020/IoT_drowsinessCare_GyeongHoKim.git .

프로젝트 사용법 (Getting Started)

cmake 빌드

mkdir build&&cd build
cmake ..
cmake --build . --config Release

프로그램 실행 ./sleep_detection

라즈베리에서 실행할 경우
CMakeLists.txt 파일의 project name을 sleep_detectionForRaspberry로 고쳐야 함. 또, wiringPi 27번에 해당하는 GPIO 핀에 부저를 연결해야 함.

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

gpio readall //wiringPi 27번에 해당하는 BCM 넘버를 찾아야 함

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

팀 정보 (Team Information)

  • 김경호 (), Github Id: GyeongHoKim

References

  • sleep detection
  • drowsiness detection

저작권 및 사용권 정보 (Copyleft / End User License)

  • MIT

이 글은 기초적인 CNN 지식을 가지신 분, opencv, dlib을 사용하시거나 경험이 있으신 분, pytorch 기본 이상의 지식을 가지신 분들께 추천드립니다.

CNN: cs231n 또는 제가 리뷰한 https://ys-cs17.tistory.com/category/cs231n을 참고해주세요

opencv, dlib: 파이썬으로 만드는 opencv 프로젝트[이세우 저] 서적을 추천합니다.

pytorch: https://tutorials.pytorch.kr/beginner/deep_learning_60min_blitz.html 정도의 지식만 있으면 문제없습니다.

해당 프로젝트는 https://github.com/kairess/eye_blink_detector를 참고하였습니다.

kairess/eye_blink_detector

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

github.com

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

전체 코드는 https://github.com/yunseokddi/Pytorch_dev/tree/master/sleep_detect를 참고해주시기 바랍니다.

1. Overview

우리의 최종적인 목표는 해당 객체 (사람)가 졸고 있는 상태인지 깨어 있는 상태인지를 파악하는 것입니다.

쉽게 말하면 눈이 몇 초 이상 감기면 졸고 있다고 판단하면 됩니다. 그러면 우선 눈의 위치를 파악하여 눈이 감긴 상태인지 떠있는 상태인지 판단하면 됩니다.

저는 우선 dlib를 사용하여 눈으로 판단되는 좌표 값을 얻어내고 (opencv+dlib) 그러고 나서 해당 눈의 상태를 파악하는 것으로 (pytorch) 구성하였습니다.

눈의 좌표값을 얻어내는 과정에서 dlib를 사용한 이유는 실험해본 결과 굳이 이 부분까지 한 번에 묶어 object detection으로 문제를 해결하면 좋지만 우리의 최종적인 목표는 mobile (android)에 적용시키는 것이고, 눈 같은 작은 부분은 적은 parameter로 될 것 같지 않다고 판단하여 이 부분은 deep learning을 사용하지 않았습니다.

눈이 감겼는지 판단하는 classification 부분은 다른 유명 네트워크를 사용해도 되지만 간단한 cnn으로 충분한 accuracy에 도달하여 custom network model을 사용하였습니다.

2. Development environment

OS: Ubuntu 18.04 LTS

Language: python 3.6.8

Interpreter: pip3

GPU: RTX2070 SUPER

CUDA: 10.2

Library

pytorch: 1.5.0

torchvision: 0.6.0

numpy: 1.18.5

opencv-python: 4.2.0.34

dlib: 19.20.0

matplotlib: 3.2.1

imutils: 0.5.3

Dataset: npy 파일 형식의 눈 데이터 (git 참고 바랍니다.)

3. Codes

A) data_loader.py

Deep learning project에서 가장 까다롭고 시간이 오래 걸리는 부분은 data 처리 과정입니다.

이번 프로젝트에서 사용되는 이미지 데이터는 png, jpg 형식이 아닌 npy라는 확장자를 가진 이미지를 가지고 진행합니다. npy는 numpy 자료형으로만 저장하지만 데이터의 용량이 매우 적다는 점에서 유용합니다.

data_loader는 간단한 custom dataset class로 구성되어있습니다.

pytorch의 custom dataset class에 대해 잘 모르시면

https://tutorials.pytorch.kr/beginner/data_loading_tutorial.html 문서를 참고해주세요.

from torch.utils.data import Dataset
import torch


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)

가장 먼저 우리가 가지고 있는 eyes_dataset에 대해 class를 구성해줍니다.

x_file의 shape는 (2586,26,34,1) 즉 2586개 데이터의 width, heigh, channel이 각각 26, 34, 1로 구성되어 있고, y_file의 shape는 (2586,1)로 2586개의 데이터 각각의 라벨링이 있습니다. 각 라벨링은 눈을 감으면 0, 뜨면 1로 구성되어 있습니다.

우리의 데이터는 앞서 말씀드린 바와 같이 numpy로 구성되어 있습니다. 하지만 우리는 tensor를 사용하여 GPU 연산을 해야 되기 때문에 torch.from_numpy()를 사용하여 자료형을 tensor로 변화시켜줍니다.

B) model.py

import torch.nn as nn
import torch.nn.functional as F
from torchsummary import summary

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은 binary classification이고, 해결하려는 문제가 그렇게 어렵지 않기 때문에 batch normalization, drop out과 같은 기법들은 사용하지 않았습니다.

conv, activation, pool의 3번의 과정을 거쳐 2번의 linear layer을 거치게 되면 최종적인 값이 나오게 되며 이 값을 통해 loss를 구하고, 추후에는 Probability를 구해 boundary를 0.5로 잡고 눈을 감았는지 떴는지 판단합니다.

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
----------------------------------------------------------------

summary를 통해 input이 (26, 34)일 때  모델 전체를 요약한 결과입니다.

전체적인 model의 parameter size는 3.7MB이므로 충분히 모바일 환경에서 적용시킬 수 있습니다. 또한 CPU 환경에서도 학습을 시킬 수 있습니다.

C) train.py

import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torchvision.transforms import transforms
from torch.utils.data import DataLoader
from data_loader import eyes_dataset
from model import Net
import torch.optim as optim

train 코드를 작성하기 전 학습을 위한 pytorch, 데이터 시각화를 위한 matplotlib 라이브러리들과 전에 작성한 model, data 처리 클래스들을 import 해줍니다.

x_train = np.load('./dataset/x_train.npy').astype(np.float32)  # (2586, 26, 34, 1)
y_train = np.load('./dataset/y_train.npy').astype(np.float32)  # (2586, 1)

train_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.RandomRotation(10),
    transforms.RandomHorizontalFlip(),
])

train_dataset = eyes_dataset(x_train, y_train, transform=train_transform)

다음으로는 data loader에 관련된 코드를 작성하여봅시다.

우리가 사용할 데이터는 dataset 폴더 안에 있고, 파일 확장자는 앞에서 말씀드린 것처럼. npy입니다. 이를 불러오기 위해선 np.load() 함수를 사용합니다. 그러고 나서 train data는 transforms를 사용하여 이미지를 tensor로 변환시킨후 randomrotation, random horizontal flip을 사용하여 데이터를 변형시킵니다.

그리고 나서 이렇게 처리된 데이터들을 앞에서 정의하였던 eyes_dataset class의 parameter로 넣어줍니다.

이렇게 까지 해서 데이터 처리가 끝났습니다!

# --------데이터 출력----------
plt.style.use('dark_background')
fig = plt.figure()

for i in range(len(train_dataset)):
    x, y = train_dataset[i]

    plt.subplot(2, 1, 1)
    plt.title(str(y_train[i]))
    plt.imshow(x_train[i].reshape((26, 34)), cmap='gray')

    plt.show()

matplotlib를 사용하여 우리가 데이터를 잘 불러왔는지, 라벨링이 잘되었는지를 확인해봅시다.

(그대로 코드를 실행시키면 전체 이미지가 순차적으로 출력되니 range를 조절해 주세요)

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji
눈을 뜸 (1)
Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji
눈을 감음 (0)

이와 같이 눈을 떴을 때는 1, 감았을 때는 0으로 라벨링이 제대로 되었고,  (26,34) size로 npy 이미지도 잘 불러온 것을 볼 수 있습니다.

PATH = '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)

epochs = 50

다음은 trained parameter를 저장할 path를 설정해 주시고, train, val data loader를 작성해 줍니다. batch size는 본인의 하드웨어 환경에 따라 변경하여 주시고 윈도 환경에서는 num_workers를 0으로 설정해야 오류가 나지 않습니다. shuffle은 train시에는 true로 설정합니다. criterion은 우리는 이진 분류를 위한 코드니 BCE를 사용하고, optimizer는 무난한 adam을 사용합니다. sgd를 사용해도 문제는 없을 것 같습니다. epoch은 50입니다. 또한 우리의 model은 gpu를 사용하기 때문에 cuda를 선언해 줍니다.

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)

pytorch에서는 train전 model.train()으로 학습 모드를 선언을 해주는 것이 좋습니다. (안 해도 상관은 없습니다.)

두 번째 for 문을 보면 우선 data의 index 0은 이미지이고, 1은 라벨입니다. 이를 그냥 받아오면 안 되고 꼭 to('cuda')로 받아야지만 gpu를 사용할 수 있습니다. 그러고 나서 우리의 이미지를 transpose를 통해 형상을 변화시킨 후 model에 넣어줍니다. model의 output을 criterion을 통해 loss를 구하고, optimizer.step()을 통해 loss update를 해줍니다.

그러고 나서 running loss와 running acc를 구합니다. 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

accuracy 함수도 식을 이해하는데 크게 어려움이 없습니다. 우선 예측값 (model output)과 label값을 parameter로 불러옵니다. 그러고 예측값을 sigmoid를 통해 확률화 시키고, round를 이용하여 0또는 1로 변환시킵니다. (0.5이하는 0, 초과는 1) 그리고 나서 맞춘 개수를 count 한 뒤, 전체 개수많큼 나눈 후 100을 곱한 값을 return 해줍니다.

epoch: [1/50] train_loss: 0.38541 train_acc: 85.15000
epoch: [2/50] train_loss: 0.14086 train_acc: 96.11250
epoch: [3/50] train_loss: 0.10041 train_acc: 97.37500
epoch: [4/50] train_loss: 0.07550 train_acc: 97.78750
epoch: [5/50] train_loss: 0.04663 train_acc: 98.53750
epoch: [6/50] train_loss: 0.03403 train_acc: 99.21250
epoch: [7/50] train_loss: 0.02533 train_acc: 99.55000
epoch: [8/50] train_loss: 0.02284 train_acc: 99.40000
epoch: [9/50] train_loss: 0.01691 train_acc: 99.58750
epoch: [10/50] train_loss: 0.01310 train_acc: 99.70000
epoch: [11/50] train_loss: 0.01021 train_acc: 99.73750
epoch: [12/50] train_loss: 0.00665 train_acc: 99.92500
epoch: [13/50] train_loss: 0.00629 train_acc: 99.88750
epoch: [14/50] train_loss: 0.00510 train_acc: 99.96250
epoch: [15/50] train_loss: 0.00474 train_acc: 99.92500
epoch: [16/50] train_loss: 0.00271 train_acc: 100.00000
epoch: [17/50] train_loss: 0.00216 train_acc: 100.00000
epoch: [18/50] train_loss: 0.00184 train_acc: 100.00000
epoch: [19/50] train_loss: 0.00203 train_acc: 100.00000
.
.
.
epoch: [43/50] train_loss: 0.00011 train_acc: 100.00000
epoch: [44/50] train_loss: 0.00009 train_acc: 100.00000
epoch: [45/50] train_loss: 0.00009 train_acc: 100.00000
epoch: [46/50] train_loss: 0.00008 train_acc: 100.00000
epoch: [47/50] train_loss: 0.00008 train_acc: 100.00000
epoch: [48/50] train_loss: 0.00007 train_acc: 100.00000
epoch: [49/50] train_loss: 0.00007 train_acc: 100.00000
epoch: [50/50] train_loss: 0.00006 train_acc: 100.00000

train 코드의 결과를 보게 되면 loss는 점점 0으로,  acc는 100으로 수렴하는 모습을 볼 수 있습니다.

학습을 돌리기 전 validation을 사용할 생각을 했었는데 굳이 할 필요가 없어 validation data set을 test data로 변경하기로 했습니다. 또한 같은 이유로 batch normalization, drop out과 같은 기법을 사용하지 않았습니다.

해당 결과가 나온 이유는 아마 이미지 자체가 size가 작고, 무엇보다 어렵지 않은 binary classification이기 때문인 것 같습니다.

그리고 train후 weights/trained.pth라는 학습된 weight가 생성되었을 것입니다. 이를 가지고 test code에 적용시켜봅시다.

D) test.py

PATH = './weights/trained.pth'

x_test = np.load('./dataset/x_val.npy').astype(np.float32)  # (288, 26, 34, 1)
y_test = np.load('./dataset/y_val.npy').astype(np.float32)  # (288, 1)

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

train 코드와 비슷한 부분은 설명을 생략하겠습니다.

우선 data를 불러오는 방법은 같습니다. 하지만 test data set은 transform을 적용시킬 필요가 없어 그냥 단순히 tensor로만 변형시켜줍니다.

그리고 train과는 달리 shuffle을 사용하지 않았고, batch size는 디버그 때문에 1로 설정하였습니다.

train과 마찬가지로 model을 cuda로 선언하고 무엇보다 중요한 것은 load_state_dict으로 해당 path에 있는 weight 값을 불러오고 model.eval()을 통해 테스트 모델이라는 것을 알려야 됩니다.

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!')

본격적인 test 부분을 보시게 되면 생각보다 매우 간단합니다.

train과 거의 같지만 다른 부분은 loss를 구할 필요가 없고, 그냥 단순히 아까 선언했던 accuracy를 통해 정답의 정확도를 더해주고 count로 나누어서 평균 acc를 구할 수 있습니다.

avarage acc: 99.65157 %
test finish!

무려 99.65%의 정확도를 가집니다.

E) detect.py

import cv2
import dlib
import numpy as np
from model import Net
import torch
from imutils import face_utils

detect에 사용되는 라이브러리들입니다. 코드는 거의 대부분 opencv로 구성되어 있습니다.

dlib, imutils 같은 경우에는 얼굴 랜드마크 인식 때문에 쓰입니다. (머신 러닝)

IMG_SIZE = (34,26)
PATH = './weights/trained.pth'

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')

model = Net()
model.load_state_dict(torch.load(PATH))
model.eval()

n_count = 0

이미지 사이즈는 전체 이미지 사이즈가 아니라 얼굴 이미지에서 눈으로 인식하여 crop 한 2개 이미지에 대한 사이즈입니다.

dlib의 얼굴을 인식할 수 있는 detector와 predictor를 선언해주고 눈을 감았는지 떴는지 판단하는 model을 불러옵니다. n_count는 프레임을 저장하는 변수입니다.

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

해당 함수는 이미지에서 눈 부분을 자르는 함수입니다. eye_points라는 parameter는 아래 사진 37~42, 43~48까지입니다.

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji
face landmark 68

이 점을 통해 x1, x2, y1, y2의 좌표를 뽑습니다. opencv에서는 (0,0)에 해당하는 점은 왼쪽 맨 위입니다.

x의 중점은 cx,  y의 중점인 cy를 구하고, width와 height를 구한 뒤 마진 값까지 구해줍니다.

그다음 마진 값을 고려하여 x, y좌표를 구해주고 이를 통해 eye_rect 좌표를 만듭니다. 그리고 좌표에 해당하는 이미지를 grayscale로 변경하여 이 이미지와 방금 구한 eye_rect 좌표를 return 해줍니다.

def predict(pred):
  pred = pred.transpose(1, 3).transpose(2, 3)

  outputs = model(pred)

  pred_tag = torch.round(torch.sigmoid(outputs))

  return pred_tag

다음은 model을 통해 예측을 하는 함수입니다. 이 함수도 단순하게 들어온 눈 이미지를 transpose 하여 model input과 같은 size로 변경해주고 model을 통해 나온 output을 test code와 마찬가지로 round를 통해 값을 return 해줍니다.

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)

    # cv2.imshow('l', eye_img_l)
    # cv2.imshow('r', eye_img_r)

    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, pt1=tuple(eye_rect_l[0:2]), pt2=tuple(eye_rect_l[2:4]), color=(255,255,255), thickness=2)
    # cv2.rectangle(img, pt1=tuple(eye_rect_r[0:2]), pt2=tuple(eye_rect_r[2:4]), color=(255,255,255), thickness=2)
    #
    # cv2.putText(img, state_l, tuple(eye_rect_l[0:2]), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)
    # cv2.putText(img, state_r, tuple(eye_rect_r[0:2]), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)


  cv2.imshow('result', img)
  if cv2.waitKey(1) == ord('q'):
    break

다음은 main에 해당하는 부분입니다. 우선 우리는 웹캠 기준으로 detect code를 실행시킬 것이기 때문에 웹캠에 해당하는 device number인 0으로 설정합니다. 그러고 이를 통해 읽은 비디오 프레임을 ret과 img_iri로 읽습니다.

cv2.resize를 통해 사이즈를 조절해주고 rgb image를 gray scale로 전환시킵니다.

detector를 통해 gray scale image에서 얼굴을 인식하고 predictor를 통해 face의 좌표에서 landmark를 추정합니다.

그리고 아까 선언한 crop_eye 함수를 통해 눈 부분만 crop을 하여 각각 eye_img_l, eye_rect_l, eye_img_r, eye_rect_r에 이미지와 눈 좌표를 저장합니다.

그리고 나서 눈이 인식된 이미지를 우리의 model input size에 맞게 조절합니다.

만약 눈만 인식한 결과를 보고 싶으면 cv2.imshow('l', eye_img_l), cv2.imshow('r', eye_img_r) 부분의 주석을 해제해주세요.

그다음은 우리의 이미지는 numpy 배열이니 torch.from_numpy를 통해 tensor로 변환해줍니다. 그 다음 우리의 model의 해당하는 predict를 통해 눈을 감았는지 판단합니다.

그 아래 if문은 만약 100 프레임 동안 눈을 감고 있다면 wake up이라는 문구가 나오도록 코드를 작성하였습니다.

맨 마지막으로 'q'를 입력하면 프로그램이 종료되게 합니다.

4. Result

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

원본 유튜브 동영상:https://www.youtube.com/watch?v=ruPaOWgA-cM&t=33s

Opencv 졸음운전 방지 - opencv jol-eum-unjeon bangji

다음 시간에는 오늘 제작한 코드를 안드로이드에 넣어봅시다.