본문 바로가기
인공지능 기초 수업

Mediapipe 메쉬 모델 입크기를 서보모터 각도 값으로 변환하기.

by SwMaker_Jun 2025. 9. 3.
728x90
반응형

1. Math 라이브러리를 활용한 입술 랜드마크 거리 측정

math 라이브러리 활용에 대한 핸드모델 실습 예)

https://swmakerjun.tistory.com/59

 

math 라이브러리 활용

1. math 라이브러리 파이썬의 math 라이브러리는 수학적인 연산을 수행하는 함수와 상수들을 제공하는 표준 라이브러리 모듈 중 하나 이 라이브러리를 사용하면 다양한 수학적인 작업을 수행할 수

swmakerjun.tistory.com

 

 

제곱 → 합 → 제곱근 과정   [두점의 좌표]

import math
# frame_bgr, h, w, face_landmarks 가 준비되어 있다고 가정



# 얼굴이 인식된 코드 안에 아래 코드 적용하기

# 윗입술(13), 아랫입술(14) 좌표 구하기
x_1, y_1 = int(face_landmarks.landmark[13].x * w), int(face_landmarks.landmark[13].y * h)  # 윗입술
x_2, y_2 = int(face_landmarks.landmark[14].x * w), int(face_landmarks.landmark[14].y * h)  # 아랫입술

# 피타고라스 정리를 활용한 거리 계산
dist = int(math.sqrt((x_1 - x_2)**2 + (y_1 - y_2)**2))

# 결과를 영상에 출력
cv2.putText(frame_bgr, f"Mouth dist: {dist}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)

 

 

 

2. 입술의 거리를 출력하는 전체 코드

 

 

import cv2
import mediapipe as mp
import math

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

# 흰색 선 스타일 정의
white_line = mp_drawing.DrawingSpec(color=(255, 255, 255), thickness=1)

# 웹캠 열기
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:

    while cap.isOpened():
        success, frame = cap.read()
        if not success:
            print("웹캠을 불러올 수 없습니다.")
            break

        # 좌우 반전 (거울 모드)
        frame = cv2.flip(frame, 1)

        # OpenCV는 BGR, MediaPipe는 RGB
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        # 얼굴 메쉬 추론
        results = face_mesh.process(frame_rgb)

        # 출력용으로 다시 BGR 변환
        frame.flags.writeable = True
        frame_bgr = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)

        # 랜드마크 그리기 + 입술 거리 계산
        if results.multi_face_landmarks:
            for face_landmarks in results.multi_face_landmarks:
                # 얼굴 전체 메쉬 (흰색)
                mp_drawing.draw_landmarks(
                    image=frame_bgr,
                    landmark_list=face_landmarks,
                    connections=mp_face_mesh.FACEMESH_TESSELATION,
                    landmark_drawing_spec=None,
                    connection_drawing_spec=white_line
                )

                # 이미지 크기
                h, w, _ = frame_bgr.shape

                # 윗입술(13), 아랫입술(14) 좌표 구하기
                x_1, y_1 = int(face_landmarks.landmark[13].x * w), int(face_landmarks.landmark[13].y * h)  # 윗입술
                x_2, y_2 = int(face_landmarks.landmark[14].x * w), int(face_landmarks.landmark[14].y * h)  # 아랫입술

                # 피타고라스 정리를 활용한 거리 계산
                dist = int(math.sqrt((x_1 - x_2)**2 + (y_1 - y_2)**2))

                # 입술 중앙 좌표에 원 그리기
                cv2.circle(frame_bgr, (x_1, y_1), 3, (0, 255, 0), -1)
                cv2.circle(frame_bgr, (x_2, y_2), 3, (0, 255, 0), -1)

                # 결과를 영상에 출력
                cv2.putText(frame_bgr,
                            f"Mouth dist: {dist}",(10, 30),cv2.FONT_HERSHEY_SIMPLEX,0.7,(0, 255, 0),2)

        # 결과 출력
        cv2.imshow('Face Mesh White Lines', frame_bgr)

        # ESC 키로 종료
        if cv2.waitKey(1) & 0xFF == 27:
            break

cap.release()
cv2.destroyAllWindows()

 

  • 얼굴 메쉬가 흰색 선으로 표시
  • 윗입술(13)과 아랫입술(14) 위치에 초록색 점 표시
  • 두 점 사이 거리를 피타고라스 공식으로 계산해 "Mouth dist: N" 형태로 출력됩니다.

 

 

3. 선형 매핑

입술 거리 → 서보모터 각도(0~180°) 로 바꾸는 가장 깔끔한 방법 : 선형 매핑(linear mapping) 

 

  • 카메라로 윗입술(13번)과 아랫입술(14번) 사이 거리를 구한다.
  • 이 거리는 픽셀 단위로 나오며, 입을 닫으면 값이 작고, 크게 벌리면 값이 커진다.
  • 이 값을 서보모터의 제어 범위인 0° ~ 180° 사이 값으로 매핑하면,
    입술 움직임 → 모터 각도 변환이 가능하다.

 

  • 매핑 공식
    • D_MIN : 입을 닫았을 때의 거리 (픽셀)
    • D_MAX : 크게 벌렸을 때의 거리 (픽셀)
    • np.interp 함수를 이용하면, D_MIN,  D_MAX →  0,  180° 로 선형 매핑할 수 있다.
#상단에 변수 선언
D_MIN = 0    # 입을 다물었을 때 거리
D_MAX = 36   # 입을 벌렸을 때 거리


#얼굴이 인식되고 있을때의 문법안에 코드 작성
angle = np.interp(dist, [D_MIN, D_MAX], [0, 180])  # 거리 → 각도 변환 (비례식)
angle = int(np.clip(angle, 0, 180))                # 값이 0~180 범위에 들어오게 제한 + 정수 변환

 

 

   np.interp(dist, [D_MIN, D_MAX], [0, 180])

  • np.interp는 선형 보간 함수예요.
  • 문법 : np.interp(값, [입력최소, 입력최대], [출력최소, 출력최대])
  • 여기서는
    • 입력 범위: [D_MIN, D_MAX] → 입술 거리의 최소·최대값 (픽셀 단위)
    • 출력 범위: [0, 180] → 서보모터 각도 범위
  • 즉,
    • dist == D_MIN → angle = 0
    • dist == D_MAX → angle = 180
    • 그 사이 값은 비례해서 변환됩니다.
  • 위 사진에서 보면  D_MIN은 0  / D_MAX는 36 인것을 확인 할 수 있다 

        ※ 각자 값이 상이하므로 그전 실습으로 입술의 거리를 확인을 해야함.

 

비례식 " 0픽셀일 때 0°, 30픽셀일 때 180° → 그 사이 값은 선형으로 계산" 으로 풀어서 설명

중학교 수학식 비례식 풀이 

 

4. 각도매핑 출력 코드

import cv2
import mediapipe as mp
import math
import numpy as np

# =========================
# 상단에 변수 선언
# =========================
D_MIN = 0    # 입을 다물었을 때 거리(픽셀)
D_MAX = 36   # 입을 벌렸을 때 거리(픽셀)

# MediaPipe FaceMesh 초기화
mp_face_mesh = mp.solutions.face_mesh
mp_drawing = mp.solutions.drawing_utils
white_line = mp_drawing.DrawingSpec(color=(255, 255, 255), thickness=1)

# 웹캠 열기
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:

    while cap.isOpened():
        success, frame = cap.read()
        if not success:
            print("웹캠을 불러올 수 없습니다.")
            break

        # 거울 모드(좌우 반전)
        frame = cv2.flip(frame, 1)

        # 추론용 RGB
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        results = face_mesh.process(rgb)

        if results.multi_face_landmarks:
            h, w, _ = frame.shape
            for face_landmarks in results.multi_face_landmarks:
                # (선택) 얼굴 메쉬 흰색으로 그리기
                mp_drawing.draw_landmarks(
                    image=frame,
                    landmark_list=face_landmarks,
                    connections=mp_face_mesh.FACEMESH_TESSELATION,
                    landmark_drawing_spec=None,
                    connection_drawing_spec=white_line
                )

                # 윗입술(13)·아랫입술(14) 좌표
                x1 = int(face_landmarks.landmark[13].x * w)
                y1 = int(face_landmarks.landmark[13].y * h)
                x2 = int(face_landmarks.landmark[14].x * w)
                y2 = int(face_landmarks.landmark[14].y * h)

                # 피타고라스 정리로 거리 계산
                dist = int(math.sqrt((x1 - x2)**2 + (y1 - y2)**2))

                # 얼굴이 인식되고 있을 때의 코드 작성 (요청하신 매핑 2줄)
                angle = np.interp(dist, [D_MIN, D_MAX], [0, 180])  # 거리 → 각도 변환 (비례식)
                angle = int(np.clip(angle, 0, 180))                # 값이 0~180 범위에 들어오게 제한 + 정수 변환

                # 시각화
                cv2.circle(frame, (x1, y1), 3, (0, 255, 0), -1)
                cv2.circle(frame, (x2, y2), 3, (0, 255, 0), -1)
                cv2.putText(frame, f"Dist: {dist}", (10, 30),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2)
                cv2.putText(frame, f"Servo angle: {angle}", (10, 60),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,255), 2)

                # (선택) 시리얼로 아두이노 전송 예시
                # ser.write(f"{angle}\n".encode())

        cv2.imshow("Lip Distance -> Servo Angle (0~180)", frame)
        if cv2.waitKey(1) & 0xFF == 27:  # ESC
            break

cap.release()
cv2.destroyAllWindows()

 

 

  • 실행 후 화면 왼쪽 상단에 Dist(픽셀)와 Servo angle이 표시됩니다.
  • 환경에 따라 D_MAX가 더 클/작 수 있으니, 학생들이 직접 닫힘/최대 벌림 값을 관찰하고 D_MIN/D_MAX를 조정해보게 하면 학습 효과가 좋아요.

 

 

5. 각도를 시리얼통신으로 보내보자

import serial
import time

# 아두이노와 연결 (포트 이름과 보레이트 확인 필요)
# Windows 예: 'COM3', mac/Linux 예: '/dev/ttyUSB0'
ser = serial.Serial('COM3', 9600)  
time.sleep(2)  # 아두이노 초기화 대기 (필수)

while True:
    # angle 값은 얼굴 인식 코드에서 계산된 결과라고 가정
    angle = 90   # 예시: 90도로 고정
    data = f"{angle}\n"            # 문자열로 변환 후 줄바꿈 추가
    ser.write(data.encode())       # 바이트로 인코딩해 전송
    print("보낸 값:", angle)
    time.sleep(1)  # 1초마다 전송

 

 

 

 

 

 

728x90
반응형