일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 생성형 AI
- 오픈AI
- ubuntu
- TRANSFORMER
- Stable Diffusion
- 딥러닝
- 오블완
- AI
- LLM
- 오픈소스
- PYTHON
- 확산 모델
- 시간적 일관성
- OpenAI
- 메타
- OpenCV
- 티스토리챌린지
- 우분투
- 인공지능
- 트랜스포머
- ChatGPT
- tts
- LORA
- 멀티모달
- AI 기술
- 아두이노
- 휴머노이드 로봇
- 다국어 지원
- 일론 머스크
- 강화 학습
- Today
- Total
AI 탐구노트
머리(Head) 자세 기반의 화면 주시 여부 감지 본문
1.서론 : 광고 주시
이전 글에서 광고 효과를 측정하기 위해 광고판 또는 디지털 사이니지를 주시하고 있는 사람에 대한 정보를 이용하며 이미 옥외 디지털사이니지나 광고판에 부착된 카메라를 이용해 광고 시청자의 속성 정보를 수집하는 실제 사례도 소개 드렸습니다.
AI 기반 디지털 사이니지: 광고 효과 측정의 새로운 시대
1. 서론 광고 산업은 오래전부터 사람들의 관심을 끌고 유지하는 데 초점을 맞춰왔습니다. 과거에는 광고의 효과를 단순히 도달 범위나 판매 수치로만 평가했지만, 오늘날의 마케팅 환경은 더
42morrow.tistory.com
이런 기술은 비단 광고만 해당한다기 보다는 온라인 시험에서의 부정행위 감지나 독서실에서 공부할 때 주의력이 흐트러지면 알림을 준다거나 하는 용도나 단체 사진 등을 찍으려 할 때 딴 곳을 보고 있는 사람을 감지해서 알려주는 기능 등으로도 활용할 수 있을 겁니다.
2.본론 : AI를 이용한 화면 주시 여부 및 주시 시간 측정
이번 글에서는 디지털 사이니지가 있다고 가정하고 카메라로 바로 앞에서 보고 있는 사람이 화면을 얼마나 오랫동안 주시하고 있는지 여부를 판별하는 간단한 테스트를 진행해 보겠습니다.
2.1.구현 시나리오
우선 카메라 혹은 영상 파일을 입력으로 받아 사람의 머리가 정면과 이루는 각도를 파악하고 일정 각도 이상을 벗어나면 주시를 하지 않는 것으로 간주하는 방식으로 구현합니다. 이를 위해 OpenCV와 DLib만 사용합니다.
2.2.필요 소프트웨어 또는 기술
필요한 패키지는 다음과 같습니다.
- 개발언어 : Python
- 패키지 : OpenCV (영상 입출력, 처리 용도), DLib (안면감지), deepface (성별, 연령 추정)
테스트 용 영상 데이터는 제가 좋아하는 슈카님의 아래 Youtube 영상 중 일부를 이용했습니다.
2.3.시스템 환경 구성
# 필수 패키지 설치
$ pip install dlib, numpy, opencv-python
2.4.코드 생성
ChatGPT에게 전달한 코드 생성 요구사항은 다음과 같습니다.
- python, Opencv 이용해서 개발하며 입력은 video 영상 (파일)입니다.
- 영상 속의 사람의 머리 각도를 계산해서 화면을 주시하고 있는지 판별합니다.
- 사람의 머리 각도가 정면(화면)을 0도로 잡을 때, 상하좌우 일정 각도(head_direction_threshold 변수로 설정, 예: 30도) 이내에 있으면 화면을 주시하고 있다고 판정합니다.
- 주시가 지속되는 시간은 보고 있는 시간을 누적한 값입니다. 다른 곳을 보면 주시시간은 다시 reset되어 0부터 시작합니다.
- 감지 결과는 화면 상의 사람 머리 위에 표시됩니다. 각도와 누적된 주시시간입니다.
- 복수의 사람이 나오면 개별적으로 누적시간을 표시해야 합니다.
- 동일인 여부 판별은 tracker를 이용하거나 혹은 근접 좌표 방식을 활용해도 됩니다.
위의 요구사항에 대해 몇번의 시행착오를 거쳐 생성된 코드는 다음과 같습니다.
import cv2
import time
import numpy as np
from math import atan2, degrees
from deepface import DeepFace
import mediapipe as mp
from concurrent.futures import ThreadPoolExecutor
import os
# GPU 사용 비활성화 (필요한 경우)
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# Mediapipe 초기화
mp_face_detection = mp.solutions.face_detection
mp_drawing = mp.solutions.drawing_utils
# DeepFace 분석 주기 설정
analyze_interval = 10 # 매 10 프레임마다 DeepFace 분석
frame_count = 0
# 초기화
cap = cv2.VideoCapture("interview_video.mp4")
head_direction_threshold = 40
people_data = {} # {person_id: {"age": None, "gender": None, "bbox": (x1, y1, x2, y2), "attention_start_time": None, "attention_time": 0}}
person_id_counter = 0 # 새로운 사람 ID 생성용 카운터
executor = ThreadPoolExecutor(max_workers=2) # 멀티스레딩 설정
def calculate_gaze_direction(left_eye, right_eye, nose_tip):
"""눈과 코의 좌표로 고개 각도를 계산"""
eye_center = ((left_eye[0] + right_eye[0]) / 2, (left_eye[1] + right_eye[1]) / 2)
dx = nose_tip[0] - eye_center[0]
dy = nose_tip[1] - eye_center[1]
angle_y = degrees(atan2(dx, dy))
return angle_y
def analyze_face_async(cropped_face):
"""DeepFace 분석을 비동기로 수행"""
return DeepFace.analyze(cropped_face, actions=["age", "gender"], enforce_detection=False)
def match_face(current_bbox, people_data, iou_threshold=0.5):
"""현재 얼굴과 이전에 추적된 얼굴을 매칭"""
x1, y1, x2, y2 = current_bbox
max_iou = 0
matched_id = None
for person_id, data in people_data.items():
prev_bbox = data['bbox']
iou = compute_iou(current_bbox, prev_bbox)
if iou > max_iou and iou > iou_threshold:
max_iou = iou
matched_id = person_id
return matched_id
def compute_iou(bbox1, bbox2):
"""두 바운딩 박스 간의 IoU 계산"""
x1, y1, x2, y2 = bbox1
x1_p, y1_p, x2_p, y2_p = bbox2
xi1 = max(x1, x1_p)
yi1 = max(y1, y1_p)
xi2 = min(x2, x2_p)
yi2 = min(y2, y2_p)
inter_area = max(0, xi2 - xi1) * max(0, yi2 - yi1)
bbox1_area = (x2 - x1) * (y2 - y1)
bbox2_area = (x2_p - x1_p) * (y2_p - y1_p)
union_area = bbox1_area + bbox2_area - inter_area
if union_area == 0:
return 0
iou = inter_area / union_area
return iou
try:
with mp_face_detection.FaceDetection(min_detection_confidence=0.5) as face_detection:
while True:
ret, frame = cap.read()
if not ret:
print("비디오 종료 또는 프레임 읽기 실패.")
break
original_frame = frame.copy()
results = face_detection.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
current_faces = []
if results.detections:
for detection in results.detections:
bboxC = detection.location_data.relative_bounding_box
ih, iw, _ = frame.shape
x1, y1, w, h = int(bboxC.xmin * iw), int(bboxC.ymin * ih), int(bboxC.width * iw), int(bboxC.height * ih)
x2, y2 = x1 + w, y1 + h
# 현재 얼굴 바운딩 박스
current_bbox = (x1, y1, x2, y2)
# 얼굴 Crop
cropped_face = frame[max(0, y1):min(y2, ih), max(0, x1):min(x2, iw)]
# 이전 얼굴과 매칭
matched_id = match_face(current_bbox, people_data)
if matched_id is None:
# 새로운 사람
person_id_counter += 1
person_id = person_id_counter
people_data[person_id] = {
"age": "Not Known",
"gender": "Not Known",
"bbox": current_bbox,
"attention_start_time": None,
"attention_time": 0
}
else:
person_id = matched_id
people_data[person_id]['bbox'] = current_bbox # 바운딩 박스 업데이트
person = people_data[person_id]
# DeepFace 분석 (매 analyze_interval 프레임마다 수행)
if frame_count % analyze_interval == 0:
future = executor.submit(analyze_face_async, cropped_face)
try:
analysis = future.result()
person["age"] = analysis[0]["age"]
person["gender"] = max(analysis[0]['gender'], key=analysis[0]['gender'].get)
except Exception as e:
print(f"DeepFace 분석 오류: {e}")
# Mediapipe 랜드마크로 고개 각도 계산
keypoints = detection.location_data.relative_keypoints
left_eye = (int(keypoints[0].x * iw), int(keypoints[0].y * ih))
right_eye = (int(keypoints[1].x * iw), int(keypoints[1].y * ih))
nose_tip = (int(keypoints[2].x * iw), int(keypoints[2].y * ih))
angle_y = calculate_gaze_direction(left_eye, right_eye, nose_tip)
# 주시 시간 계산
is_looking = -head_direction_threshold <= angle_y <= head_direction_threshold
if is_looking:
if person["attention_start_time"] is None:
person["attention_start_time"] = time.time()
else:
person["attention_time"] = time.time() - person["attention_start_time"]
else:
person["attention_start_time"] = None
person["attention_time"] = 0
# 텍스트 및 박스 출력
text_color = (0, 255, 0) if is_looking else (0, 0, 255)
cv2.putText(original_frame, f"ID: {person_id}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, text_color, 2)
cv2.putText(original_frame, f"Gender: {person['gender']} Age: {person['age']}", (x1, y1 + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, text_color, 2)
cv2.putText(original_frame, f"Angle: {angle_y:.1f} Attention: {int(person['attention_time'])}s", (x1, y1 + 40), cv2.FONT_HERSHEY_SIMPLEX, 0.6, text_color, 2)
# cv2.rectangle(original_frame, (x1, y1), (x2, y2), text_color, 2)
frame_count += 1
cv2.imshow("Gaze Tracker", original_frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == 32: # Spacebar to pause
cv2.waitKey(0)
finally:
cap.release()
cv2.destroyAllWindows()
executor.shutdown()
2.5.결과 확인
테스트 결과는 다음과 같습니다. 좌우 30도를 기준으로 했는데 이 각도값은 실제 환경에서는 카메라와 객체 간의 거리를 고려해서 조정하는 로직이 더 들어가야 정확한 결과가 나올 것으로 생각됩니다.
또 다른 영상을 이용해서 테스트 해 봤습니다. 최근 흑백요리사에서 심사위원으로 나왔던 안성재 세프와의 뉴스 인터뷰 영상 (원본)입니다.
위 영상에서는 방송 자체가 정면을 보고 하는 것이 아니다보니, 계속 전면 주시를 하지 않는 것으로 판정하고 있습니다. 그런데 Deepface에서 안성재 세프를 엄청 젊다고 판정을 해 주네요. 뉴스 앵커의 의문의 1패 입니다. ^^;
2.6.시행착오
처음에는 dlib을 이용해 감지도 하고 개인 별 id를 부여하는 방식으로 했는데 생각보다 많이 느려서 방식을 mediapipe를 이용하는 것으로 변경했습니다. 저는 mediapipe를 이용하면 기본은 한명 밖에 처리가 안 되는 줄 알았는데 ChatGPT는 그렇지 않다며 코드를 만들더군요. 역시 저보다 훨~씬 똑똑한 것 같습니다. -_-;
2.7.제약사항
Deepface의 연령 예측값이 정확치 않습니다. 아무래도 학습 데이터 차이 때문일 수도 있겠습니다. 그리고, 현재는 임의로 주시각도의 threshold값을 지정해 두었는데 영상에서의 depth를 예측하는 모델을 함께 걸어 두면 영상 자체에서 주시각도에 대한 기준을 계산해 내는 것이 가능하지 않을까 싶습니다. 하지만, 그건 다음 번으로...
3.결론
이와 비슷하게 온라인에서도 사용자가 현재 화면의 어떤 곳을 보고 있는지, 어떤 것에 관심이 있는지를 알아내고자 피 튀기는 기술 경쟁을 벌이고 있습니다. 사람들의 시선이 머무는 곳, 즉 관심을 가지는 곳에 비즈니스 거리가 있기 때문이죠.
사실 영상에 나온 것이 어떤 상황인지는 최근 나오고 있는 VLM(Visual Large Language Model)를 통해 화면에 대한 해석을 시키면 알 수 있습니다. 하지만 현재의 기술로는 매번 이런 과정을 거치는 것은 많은 비용이 들기 때문에 그 허들이 해소되기 전까지는 위와 같은 비전 전문 기술들을 응용하는 사례가 많이 나올 것을 생각합니다.
꼭 머리가 아니더라도 스마트폰이나 PC웹캠을 통해 확인할 수 있는 눈동자의 움직임을 추적해서 관심 영역이 어디인지 알아내는 기술도 같은 연장 선상에서 연구되고 실 생활에 활용되고 있습니다. 시선추적 기술이 적용되는 또 다른 분야는 몸이 불편하신 분들이 디지털기기를 다룰 때 사용하는 안구마우스 같은 것이 있구요. 이와 관련된 솔루션에 대해서는 다음 번에 소개를 드리도록 하겠습니다.
오늘도 즐거운 하루 보내세요~
'DIY 테스트' 카테고리의 다른 글
웹캠을 이용한 rPPG (원격 광혈류측정) 기반의 심박동수 측정 (2) | 2024.12.01 |
---|---|
'픽셀 넘버' 게임 : 예능 속 퍼즐을 직접 만들어보자 (1) | 2024.11.25 |
Yolov11-pose를 이용한 폭력 행위 감지 (Violence Detection) (29) | 2024.11.20 |
교통 CCTV 영상 기반의 자동차 속도 측정 (0) | 2024.11.19 |
한글 십자말풀이 게임 도구 만들기 (2) | 2024.11.16 |