diff --git a/yolov8n/.gitignore b/yolov8n/.gitignore new file mode 100644 index 0000000..95f41c5 --- /dev/null +++ b/yolov8n/.gitignore @@ -0,0 +1 @@ +yolo_face_env/ \ No newline at end of file diff --git a/yolov8n/README.md b/yolov8n/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/yolov8n/README.md @@ -0,0 +1 @@ + diff --git a/yolov8n/requirements.txt b/yolov8n/requirements.txt new file mode 100644 index 0000000..56553bd --- /dev/null +++ b/yolov8n/requirements.txt @@ -0,0 +1,64 @@ +appnope==0.1.4 +asttokens==3.0.0 +certifi==2025.8.3 +charset-normalizer==3.4.3 +comm==0.2.2 +contourpy==1.3.3 +cycler==0.12.1 +debugpy==1.8.13 +decorator==5.2.1 +executing==2.2.0 +filelock==3.19.1 +fonttools==4.59.2 +fsspec==2025.9.0 +idna==3.10 +ipykernel==6.29.5 +ipython==9.1.0 +ipython_pygments_lexers==1.1.1 +jedi==0.19.2 +Jinja2==3.1.6 +jupyter_client==8.6.3 +jupyter_core==5.7.2 +kiwisolver==1.4.9 +MarkupSafe==3.0.2 +matplotlib==3.10.6 +matplotlib-inline==0.1.7 +mpmath==1.3.0 +nest-asyncio==1.6.0 +networkx==3.5 +numpy==2.2.5 +opencv-python==4.12.0.88 +packaging==24.2 +pandas==2.2.3 +parso==0.8.4 +pexpect==4.9.0 +pillow==11.3.0 +platformdirs==4.3.7 +polars==1.33.1 +prompt_toolkit==3.0.50 +psutil==7.0.0 +ptyprocess==0.7.0 +pure_eval==0.2.3 +Pygments==2.19.1 +pyparsing==3.2.3 +python-dateutil==2.9.0.post0 +pytz==2025.2 +PyYAML==6.0.2 +pyzmq==26.4.0 +requests==2.32.5 +scipy==1.16.2 +setuptools==80.9.0 +six==1.17.0 +stack-data==0.6.3 +sympy==1.14.0 +torch==2.8.0 +torchvision==0.23.0 +tornado==6.4.2 +traitlets==5.14.3 +typing_extensions==4.15.0 +tzdata==2025.2 +ultralytics==8.3.199 +ultralytics-thop==2.0.17 +urllib3==2.5.0 +wcwidth==0.2.13 +wheel @ file:///opt/homebrew/Cellar/python%403.13/3.13.1/libexec/wheel-0.45.1-py3-none-any.whl#sha256=da46333d5dcbde6e20cf7e2f8fff9e9ce76e8c94dc4afd6fb95fc4bc2745fb5e diff --git a/yolov8n/yolo_mediapipe_face.py b/yolov8n/yolo_mediapipe_face.py new file mode 100644 index 0000000..968bb4e --- /dev/null +++ b/yolov8n/yolo_mediapipe_face.py @@ -0,0 +1,200 @@ +import argparse +import time +from pathlib import Path + +import cv2 +import numpy as np +import torch +from ultralytics import YOLO +import mediapipe as mp + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="YOLO + MediaPipe 얼굴 검출 및 랜드마크") + parser.add_argument( + "--model", + type=str, + default="face_yolov8n.pt", + help="YOLO face 모델 경로(.pt). 기본값: face_yolov8n.pt", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + choices=["cpu", "cuda"], + help="추론 디바이스 선택", + ) + parser.add_argument( + "--conf", + type=float, + default=0.5, + help="신뢰도 임계값 (0~1)", + ) + parser.add_argument( + "--camera", + type=int, + default=0, + help="웹캠 인덱스 (기본 0)", + ) + parser.add_argument( + "--show-fps", + action="store_true", + help="화면에 FPS 표기", + ) + return parser.parse_args() + + +class YOLOMediaPipeFaceDetector: + def __init__(self, yolo_model_path): + # YOLO 모델 로딩 + self.yolo_model = YOLO(yolo_model_path) + + # MediaPipe Face Mesh 초기화 + self.mp_face_mesh = mp.solutions.face_mesh + self.face_mesh = self.mp_face_mesh.FaceMesh( + static_image_mode=False, + max_num_faces=5, + refine_landmarks=True, + min_detection_confidence=0.5, + min_tracking_confidence=0.5 + ) + self.mp_drawing = mp.solutions.drawing_utils + self.mp_drawing_styles = mp.solutions.drawing_styles + + def detect_faces_and_landmarks(self, frame, conf_threshold=0.5): + # YOLO로 얼굴 바운딩 박스 검출 + yolo_results = self.yolo_model.predict( + source=frame, + conf=conf_threshold, + verbose=False, + ) + + boxes = [] + confs = [] + + if yolo_results and len(yolo_results) > 0: + r = yolo_results[0] + if r.boxes is not None and len(r.boxes) > 0: + xyxy = r.boxes.xyxy.cpu().numpy() + scores = r.boxes.conf.cpu().numpy() + boxes = xyxy.tolist() + confs = scores.tolist() + + # MediaPipe로 얼굴 랜드마크 검출 + rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + mp_results = self.face_mesh.process(rgb_frame) + + landmarks = [] + if mp_results.multi_face_landmarks: + for face_landmarks in mp_results.multi_face_landmarks: + landmarks.append(face_landmarks) + + return boxes, confs, landmarks + + def draw_results(self, frame, boxes, confs, landmarks): + output = frame.copy() + h, w = frame.shape[:2] + + # YOLO 바운딩 박스 그리기 + for (x1, y1, x2, y2), conf in zip(boxes, confs): + x1, y1, x2, y2 = map(int, [x1, y1, x2, y2]) + cv2.rectangle(output, (x1, y1), (x2, y2), (0, 255, 0), 2) + label = f"face {conf:.2f}" + (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2) + cv2.rectangle(output, (x1, max(0, y1 - th - 6)), (x1 + tw + 6, y1), (0, 255, 0), -1) + cv2.putText(output, label, (x1 + 3, y1 - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2) + + # MediaPipe 랜드마크 그리기 + for face_landmarks in landmarks: + # 주요 랜드마크 인덱스 (MediaPipe Face Mesh 기준) + # 코끝: 1, 좌안: 33, 우안: 362, 입 중앙: 13 + key_points = { + 'nose_tip': 1, + 'left_eye': 33, + 'right_eye': 362, + 'mouth_center': 13, + 'left_mouth': 61, + 'right_mouth': 291 + } + + colors = { + 'nose_tip': (0, 0, 255), # 빨간색 + 'left_eye': (255, 0, 0), # 파란색 + 'right_eye': (0, 255, 0), # 초록색 + 'mouth_center': (255, 255, 0), # 노란색 + 'left_mouth': (255, 0, 255), # 자주색 + 'right_mouth': (0, 255, 255) # 청록색 + } + + for name, idx in key_points.items(): + if idx < len(face_landmarks.landmark): + landmark = face_landmarks.landmark[idx] + x = int(landmark.x * w) + y = int(landmark.y * h) + + cv2.circle(output, (x, y), 3, colors[name], -1) + + # 코 좌표 텍스트 표시 + if name == 'nose_tip': + cv2.putText(output, f"nose({x},{y})", (x + 5, y - 5), + cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1) + + return output + + +def main(): + args = parse_args() + + model_path = Path(args.model) + if not model_path.exists(): + print("[오류] YOLO 모델(.pt) 파일이 없습니다.") + print("- 현재 경로:", Path.cwd()) + print("- 찾은 경로:", model_path.resolve()) + return + + print("YOLO + MediaPipe 얼굴 검출기 초기화 중...") + detector = YOLOMediaPipeFaceDetector(str(model_path)) + + print("웹캠 오픈 중... (종료: q)") + cap = cv2.VideoCapture(args.camera) + if not cap.isOpened(): + print("[오류] 웹캠을 열 수 없습니다.") + return + + prev_time = time.time() + try: + while True: + ok, frame = cap.read() + if not ok: + print("[오류] 프레임을 읽을 수 없습니다.") + break + + # 얼굴 및 랜드마크 검출 + boxes, confs, landmarks = detector.detect_faces_and_landmarks(frame, args.conf) + + # 결과 그리기 + drawn = detector.draw_results(frame, boxes, confs, landmarks) + + # FPS 표시 + if args.show_fps: + now = time.time() + fps = 1.0 / max(1e-6, (now - prev_time)) + prev_time = now + cv2.putText(drawn, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2) + + # 얼굴 수 표시 + cv2.putText(drawn, f"faces: {len(boxes)}", (10, 65), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) + cv2.putText(drawn, f"landmarks: {len(landmarks)}", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) + + cv2.imshow("YOLO + MediaPipe Face Detection", drawn) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + finally: + cap.release() + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/yolov8n/yolov8n-face.pt b/yolov8n/yolov8n-face.pt new file mode 100644 index 0000000..bc96a2f Binary files /dev/null and b/yolov8n/yolov8n-face.pt differ diff --git a/yolov8n/yolov8n_face_cam.py b/yolov8n/yolov8n_face_cam.py new file mode 100644 index 0000000..e0e63e2 --- /dev/null +++ b/yolov8n/yolov8n_face_cam.py @@ -0,0 +1,159 @@ +import argparse +import time +from pathlib import Path + +import cv2 +import numpy as np +import torch +from ultralytics import YOLO + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="YOLOv8n-face 실시간 얼굴 검출 (웹캠)") + parser.add_argument( + "--model", + type=str, + default="yolov8n-face.pt", + help="YOLOv8 face 모델 경로(.pt). 기본값: yolov8n-face.pt", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + choices=["cpu", "cuda"], + help="추론 디바이스 선택", + ) + parser.add_argument( + "--conf", + type=float, + default=0.5, + help="신뢰도 임계값 (0~1)", + ) + parser.add_argument( + "--imgsz", + type=int, + default=640, + help="추론 입력 이미지 크기 (정사각)", + ) + parser.add_argument( + "--camera", + type=int, + default=0, + help="웹캠 인덱스 (기본 0)", + ) + parser.add_argument( + "--show-fps", + action="store_true", + help="화면에 FPS 표기", + ) + return parser.parse_args() + + +def draw_boxes_with_center(frame: np.ndarray, boxes, confidences) -> np.ndarray: + output = frame.copy() + + for (x1, y1, x2, y2), conf in zip(boxes, confidences): + x1, y1, x2, y2 = map(int, [x1, y1, x2, y2]) + + # 바운딩 박스 그리기 + cv2.rectangle(output, (x1, y1), (x2, y2), (0, 255, 0), 2) + + # 바운딩 박스 중앙 좌표 계산 + center_x = (x1 + x2) // 2 + center_y = (y1 + y2) // 2 + + # 중앙에 빨간 점 그리기 + cv2.circle(output, (center_x, center_y), 5, (0, 0, 255), -1) # 빨간색 원 + + # 중앙 좌표 텍스트 표시 + coord_text = f"center({center_x},{center_y})" + cv2.putText(output, coord_text, (center_x + 10, center_y - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) + + # 신뢰도 라벨 + label = f"face {conf:.2f}" + (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2) + cv2.rectangle(output, (x1, max(0, y1 - th - 6)), (x1 + tw + 6, y1), (0, 255, 0), -1) + cv2.putText(output, label, (x1 + 3, y1 - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2) + + return output + + +def main(): + args = parse_args() + + model_path = Path(args.model) + if not model_path.exists(): + print("[오류] yolov8n-face 모델(.pt) 파일이 없습니다.") + print("- 현재 경로:", Path.cwd()) + print("- 찾은 경로:", model_path.resolve()) + print("- 해결: yolov8n-face.pt 파일을 프로젝트 폴더에 넣거나 --model 경로를 지정하세요.") + return + + print("YOLOv8n-face 모델 로딩 중...", model_path) + try: + # Torch 2.6+ weights_only 정책으로 인한 호환 이슈는 Ultralytics 내부에서 처리됩니다. + model = YOLO(str(model_path)) + except Exception as e: + print("[오류] 모델 로딩 실패:", e) + return + + print(f"디바이스: {args.device}") + print("웹캠 오픈 중... (종료: q)") + + cap = cv2.VideoCapture(args.camera) + if not cap.isOpened(): + print("[오류] 웹캠을 열 수 없습니다. 다른 인덱스를 시도하거나 권한을 확인하세요.") + return + + prev_time = time.time() + try: + while True: + ok, frame = cap.read() + if not ok: + print("[오류] 프레임을 읽을 수 없습니다.") + break + + # 추론 + results = model.predict( + source=frame, + conf=args.conf, + imgsz=args.imgsz, + device=args.device, + verbose=False, + ) + + boxes = [] + confs = [] + + if results and len(results) > 0: + r = results[0] + + # 바운딩 박스 정보 + if r.boxes is not None and len(r.boxes) > 0: + xyxy = r.boxes.xyxy.cpu().numpy() + scores = r.boxes.conf.cpu().numpy() + boxes = xyxy.tolist() + confs = scores.tolist() + + drawn = draw_boxes_with_center(frame, boxes, confs) + + if args.show_fps: + now = time.time() + fps = 1.0 / max(1e-6, (now - prev_time)) + prev_time = now + cv2.putText(drawn, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2) + + cv2.putText(drawn, f"faces: {len(boxes)}", (10, 65), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) + cv2.imshow("YOLOv8n-face Webcam", drawn) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + finally: + cap.release() + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() +