AI 탐구노트

Mediapipe를 이용한 졸음감지 본문

DIY 테스트

Mediapipe를 이용한 졸음감지

42morrow 2024. 11. 2. 13:15

 

 

1.개요

1.1.졸음감지란?

현대의 바쁜 일상에서 장시간 운전은 불가피해졌습니다. 특히 장거리 운행이나 야간 운전 중에는 운전자의 졸음이 심각한 사고를 초래할 수 있습니다. 국내의 경우, 최근 5년간(2019~2023) 통계로는 졸음운전으로 인한 교통사고가 1만765건, 그 사고로 인한 사망자는 316명에 이른다고 합니다. 음주운전의 2배 수준으로 위험하다고 하네요.

 

이에 따라 운전자 졸음 감지 시스템이 주목받고 있으며, 여러 기술 기업들이 이를 개발해 도로 안전성을 높이고 있습니다. 최근 나오는 차량에는 자율주행 등급에 따라 다르긴 하지만, 카메라를 이용한 졸음감지 기능이 탑재되어 있는 것들도 있습니다. 예를 들어 Tesla의 FSD 최신버전은 실내 (in-cabin) 카메라를 통해 운전자의 상태를 체크하는 기능이 들어있죠.

 

1.2.졸음감지 기준

운전자의 졸음감지 기준으로 많이 사용되는 것은 다음과 같다고 합니다. 

 

1) 눈 깜빡임 빈도 및 속도

  • PERCLOS (Percentage of Eye Closure) : 일정 시간 동안 눈을 감고 있는 시간의 비율입니다. 눈을 감고 있는 시간이 길어질수록 졸음이 의심됩니다.
  • 눈 깜빡임 속도 : 깜빡이는 속도가 느려지거나 깜빡임 간 간격이 길어지면 졸음 상태로 간주할 수 있습니다.
  • 깜빡임 패턴 : 갑작스럽게 깜빡임이 늘어나거나 일정한 패턴 없이 불규칙해지는 것도 졸음의 징후일 수 있습니다.

2) 고개 움직임

  • 고개가 아래로 떨어지는 경우 : 졸음 상태에 빠질 때 흔히 발생하는 현상으로, 고개가 아래로 숙여졌다가 갑자기 올라오는 경우가 감지됩니다.
  • 고개 흔들림 : 고개가 일정 방향으로 계속 기울어지거나 일정하게 움직이지 않고 불안정할 때 졸음 가능성을 감지할 수 있습니다.

3. 안면 근육 및 표정 변화

  • 하품 빈도 : 졸음 상태에서는 하품 횟수가 증가하는 경향이 있습니다.
  • 표정 분석 : 피곤한 표정, 눈이 반쯤 감긴 상태, 무표정 등 졸음과 연관된 얼굴 표정을 분석하여 졸음 상태를 예측할 수 있습니다.

4. 운전 패턴

  • 차선 이탈: 졸음 상태에서는 차선을 넘어가거나 벗어나는 경향이 증가합니다.
  • 급격한 핸들 조작: 졸린 상태에서 갑자기 핸들을 꺾는 경우가 늘어납니다.
  • 주행 속도 변화: 졸음으로 인해 일정한 속도를 유지하지 못하거나, 갑자기 속도가 줄어드는 경우가 관찰될 수 있습니다.

5. 심박수 및 생체 신호

  • 심박수: 졸음 상태에 빠질 때 심박수 변화가 발생할 수 있어, 일정 수준 이하로 낮아질 경우 졸음을 의심할 수 있습니다.
  • 피부 전도도: 졸음과 관련된 스트레스 변화로 인해 피부 전도도가 낮아질 수 있습니다.

 

1.3.카메라를 이용한 테스트 대상 

이 글에서는 카메라 영상을 이용한 운전자 졸음감지에 사용되는 기술에 대해 알아보고, Mediapipe를 이용해 간단한 기능 테스트를 진행해 보겠습니다. 측정할 졸음 기준 항목은 위의 경우에서 카메라로 할 수 있는 몇 가지를 추렸는데 다음과 같습니다. 

  • 눈 감김 감지
  • 하품 탐지
  • 고개 움직임
  • 눈 깜박임 빈도

 

이 가운데 눈 감김 감지를 위해 EAR(Eye Aspect Ratio)가 가장 많이 사용되는 것 같은데, 이에 대한 사항은 다음과 같습니다. (출처)

 

 

 

 

2.환경구성

2.1.가상환경 생성 및 패키지 설치

# 가상환경 생성
$ conda create -n drowsy python=3.10
$ conda activate drowsy

# python 패키지 설치
$ pip install mediapipe

 

2.2. 테스트 코드

몇 차례 추가적인 요구사항을 제시했고 이 내용이 반영된 코드는 다음과 같습니다. 

  • 눈 감김 외에 하품, 고개 떨굼 등을 함께 반영
  • 경고 메시지 표시 : 감지되면 O, 그렇지 않으면 X로 한글 폰트 적용 표시
  • 경고 용 화면 깜박임  (붉은 색 마스크 적용)
  • 고개 기울인 경우에 하품 감지가 잘 안 되는 현상 처리 
import cv2
import mediapipe as mp
import time
import math
import numpy as np
from PIL import ImageFont, ImageDraw, Image

# MediaPipe 초기화
mp_face_mesh = mp.solutions.face_mesh
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles

# 졸음 감지를 위한 눈 비율 계산 함수
def calculate_ear(landmarks, eye_indices):
    left_point = landmarks[eye_indices[0]]
    right_point = landmarks[eye_indices[3]]
    top_mid = ((landmarks[eye_indices[1]].x + landmarks[eye_indices[2]].x) / 2,
               (landmarks[eye_indices[1]].y + landmarks[eye_indices[2]].y) / 2)
    bottom_mid = ((landmarks[eye_indices[4]].x + landmarks[eye_indices[5]].x) / 2,
                  (landmarks[eye_indices[4]].y + landmarks[eye_indices[5]].y) / 2)
    
    horizontal_length = ((left_point.x - right_point.x) ** 2 + (left_point.y - right_point.y) ** 2) ** 0.5
    vertical_length = ((top_mid[0] - bottom_mid[0]) ** 2 + (top_mid[1] - bottom_mid[1]) ** 2) ** 0.5
    return vertical_length / horizontal_length

# 하품 감지를 위한 입 비율 계산 함수
def calculate_mar(landmarks, mouth_indices):
    top_mid = ((landmarks[mouth_indices[0]].x + landmarks[mouth_indices[1]].x) / 2,
               (landmarks[mouth_indices[0]].y + landmarks[mouth_indices[1]].y) / 2)
    bottom_mid = ((landmarks[mouth_indices[2]].x + landmarks[mouth_indices[3]].x) / 2,
                  (landmarks[mouth_indices[2]].y + landmarks[mouth_indices[3]].y) / 2)
    left_point = landmarks[mouth_indices[4]]
    right_point = landmarks[mouth_indices[5]]
    
    horizontal_length = ((left_point.x - right_point.x) ** 2 + (left_point.y - right_point.y) ** 2) ** 0.5
    vertical_length = ((top_mid[0] - bottom_mid[0]) ** 2 + (top_mid[1] - bottom_mid[1]) ** 2) ** 0.5
    return vertical_length / horizontal_length

# 고개 기울기 계산 함수
def calculate_head_tilt(landmarks):
    left_shoulder = landmarks[234]
    right_shoulder = landmarks[454]
    
    dx = right_shoulder.x - left_shoulder.x
    dy = right_shoulder.y - left_shoulder.y
    angle = math.degrees(math.atan2(dy, dx))
    return abs(angle)

# 웹캠 열기
cap = cv2.VideoCapture(0)

# 얼굴 메시 모델 초기화
with mp_face_mesh.FaceMesh(
        max_num_faces=1,
        refine_landmarks=True,
        min_detection_confidence=0.5,
        min_tracking_confidence=0.5) as face_mesh:
    
    closed_eyes_frame_count = 0
    open_mouth_frame_count = 0
    head_tilt_frame_count = 0
    EAR_THRESHOLD = 0.2  # 눈 비율 임계값, 눈이 닫힌 것으로 간주하는 기준
    CLOSED_EYES_FRAMES = 30  # 졸음으로 판단할 프레임 수
    MAR_THRESHOLD = 0.5  # 입 비율 임계값, 하품으로 간주하는 기준
    OPEN_MOUTH_FRAMES = 30  # 하품으로 판단할 프레임 수 (약 1초 기준)
    HEAD_TILT_THRESHOLD = 15  # 고개 기울기 각도 임계값
    HEAD_TILT_FRAMES = 30  # 고개 기울기 판단할 프레임 수 (약 1초 기준)

    show_landmarks = True

    while cap.isOpened():
        success, image = cap.read()
        if not success:
            print("웹캠에서 영상을 읽을 수 없습니다.")
            break

        # BGR 이미지를 RGB로 변환
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        image.flags.writeable = False

        # 얼굴 메시 추론
        results = face_mesh.process(image)

        # 이미지를 다시 BGR로 변환
        image.flags.writeable = True
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

        alert = False

        if results.multi_face_landmarks:
            for face_landmarks in results.multi_face_landmarks:
                landmarks = face_landmarks.landmark

                # 왼쪽 및 오른쪽 눈의 인덱스 (좌표는 MediaPipe의 얼굴 랜드마크 참조)
                left_eye_indices = [362, 385, 387, 263, 373, 380]
                right_eye_indices = [33, 160, 158, 133, 153, 144]

                # 입의 인덱스 (좌표는 MediaPipe의 얼굴 랜드마크 참조)
                mouth_indices = [13, 14, 17, 18, 78, 308]

                # 눈 비율 계산
                left_ear = calculate_ear(landmarks, left_eye_indices)
                right_ear = calculate_ear(landmarks, right_eye_indices)
                ear = (left_ear + right_ear) / 2.0

                # 입 비율 계산
                mar = calculate_mar(landmarks, mouth_indices)

                # 고개 기울기 각도 계산
                head_tilt_angle = calculate_head_tilt(landmarks)

                # 눈 비율에 따른 졸음 감지
                if ear < EAR_THRESHOLD:
                    closed_eyes_frame_count += 1
                else:
                    closed_eyes_frame_count = 0

                # 입 비율에 따른 하품 감지 (고개 기울기와 관계없이 동작하도록 수정)
                if mar > MAR_THRESHOLD:
                    open_mouth_frame_count += 1
                else:
                    open_mouth_frame_count = 0

                # 고개 기울기에 따른 기울기 감지
                if head_tilt_angle > HEAD_TILT_THRESHOLD:
                    head_tilt_frame_count += 1
                else:
                    head_tilt_frame_count = 0

                # 경고 조건 (개별 조건에 따라 별도로 알림 설정)
                if closed_eyes_frame_count >= CLOSED_EYES_FRAMES:
                    alert = True
                elif open_mouth_frame_count >= OPEN_MOUTH_FRAMES:
                    alert = True
                elif head_tilt_frame_count >= HEAD_TILT_FRAMES:
                    alert = True

                # 경고 메시지 O/X 표시
                pil_image = Image.fromarray(image)
                draw = ImageDraw.Draw(pil_image)
                font = ImageFont.truetype("NanumGothic.ttf", 32)  # 한글 폰트 지정
                draw.text((50, 50), f"졸음: {'O' if closed_eyes_frame_count >= CLOSED_EYES_FRAMES else 'X'}", font=font, fill=(0, 0, 255))
                draw.text((50, 100), f"하품: {'O' if open_mouth_frame_count >= OPEN_MOUTH_FRAMES else 'X'}", font=font, fill=(0, 0, 255))
                draw.text((50, 150), f"자세: {'O' if head_tilt_frame_count >= HEAD_TILT_FRAMES else 'X'}", font=font, fill=(0, 0, 255))
                image = np.array(pil_image)

                # 얼굴 랜드마크 그리기 (토글 가능)
                if show_landmarks:
                    mp_drawing.draw_landmarks(
                        image=image,
                        landmark_list=face_landmarks,
                        connections=mp_face_mesh.FACEMESH_TESSELATION,
                        landmark_drawing_spec=None,
                        connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style())

        # 경고 화면 깜빡임 (붉은색 마스크 적용)
        if alert:
            mask = np.zeros_like(image, dtype=np.uint8)
            mask[:] = (0, 0, 255)
            image = cv2.addWeighted(image, 0.7, mask, 0.3, 0)

        # 결과 영상 출력
        cv2.imshow('Drowsiness, Yawn, and Head Tilt Detection', image)

        key = cv2.waitKey(5) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('h'):
            show_landmarks = not show_landmarks

# 웹캠 해제 및 모든 창 닫기
cap.release()
cv2.destroyAllWindows()

 

 

 

3.실행 및 결과확인

3.1.실행 결과 확인

전반적으로 생각보단 잘 작동하는 것 같습니다. mediapipe를 이용하다보니 CPU를 사용하더라도 전반적으로 처리 속도도 빨라 제 생각으론 라즈베리 같은 것에서도 무난하게 동작하지 않을까 싶습니다. 물론 스마트폰에서도 가능할 겁니다. 요새 스마트폰 성능을 생각하면 당연하겠지만요... 

 

아래는 처리 결과 화면입니다. '탈'을 씌운 것은... 개인정보를 위해... -_-; 다음 번에는 딥페이크나 가상 아바타를 해 보고 그걸 적용해 봐야겠습니다. 

 

 

3.2.추가적인 개선 필요 사항

몇 가지 개선을 했으면 하는 사항이 있습니다. 

  • 초기 캘리브레이션 적용 : 선천적으로 눈이 작거나, 평상 시 자세가 한쪽으로 기울어진 상태로 운전하시는 분도 계실 수 있으니 최초 1~2초는 정상 자세, 표정을 캐치하고 그걸 기준으로 졸음 감지 등을 하는 것이 필요할 것 같습니다. 
  • 경고 소리 적용 : 합성음으로 '야이 XX야, 잠 안 깨?' 같은 쎈 말로 잠을 깨우는 것을 상상해 봤습니다. 

 

4.후기

졸음감지 기능은 자동차 운전자만 대상으로 하진 않습니다. 독서실 혹은 집에서라도 공부하다가 졸면 깨워주는 용도로 활용할 수 있을 겁니다. 최근에는 시계 형태로 되어 생체정보를 취득하고 이를 바탕으로 자는지, 깨어있는지 등을 판별해서 알림을 주는 것도 가능하고, 앞서 언급된 졸음 감지는 스마트폰 앱으로도 나와 있는 것으로 알고 있습니다. 그러니 이미 공개되어 있는 기술을 어디에 써먹을 수 있을지 고민하는 것이 필요하겠습니다. 

 

이번 글에서는 졸음감지를 할 수 있는 기술들과 직접 코드로 테스트를 진행해 봤습니다. 

 

 


참고사항

 

1.Drowsiness-Detection-Mediapipe

동일하게 Mediapipe를 이용하는 다른 케이스입니다. EAR, MAR, PUC, MOE 값을 이용한 판정을 합니다. 

 

GitHub - Tandon-A/Drowsiness-Detection-Mediapipe

Contribute to Tandon-A/Drowsiness-Detection-Mediapipe development by creating an account on GitHub.

github.com

 

2.Driver Drowsniess Detection Using Mediapipe

EAR 값을 이용하며, Mediapipe를 이용한 landmark 추출, AV를 이용한 경고음, Streamlit을 이용한 화면 UI, 영상 스트림 전송을 위한 streamlit_webrtc 등의 패키지가 필요합니다. 

 

Driver Drowsiness Detection Using Mediapipe In Python

Driver drowsiness detection systems help reduce mishaps due to tired or sleepy drivers. Learn to build such a robust system using MediaPipe in Python.

learnopencv.com