OpenCV

[OpenCV] 손 인식 커스텀 INPUT 프로젝트 관련

선호하는 2024. 12. 25. 07:00

서론

OpenCV를 오랜만에 만지작거리다 보니, 예전에 하려고 했던 프로젝트 기획이 떠올랐다.
그때는 수화 번역기를 만들어 보려고 했는데, 그건 좀 많이 힘들 것 같아서 폐기했었더랬다.

아무튼 각설하고, 나는 카페에서 작업을 할 때가 잦은데, 가끔 마우스를 두고 다닌다. 트랙패드를 쓰면 되기는 하는데, 막상 쓰다 보면 익숙하지도 않고, 마우스가 있어도 책상이 작거나 낮아서 쓰기 불편할 때도 있다.
그래서 최근에 건드리던 게, 손가락을 인식해서 마우스 커서를 움직이는 그런 거였고, 이걸 직접 만들어서 쓰려고 하니, 어떤 기능이 더 있으면 좋겠다 하는 게 너무 많았다.
마우스도 되니까 키보드도 되면 좋을 것 같고, 키보드 전부에다가 단축키 입력까지 가능하게 하면 정말 좋겠다 해서 만들어 보려고 했다.

프로젝트 1차 목표

일단 내가 자주 하는 게임인 LOL이 가능하게 해 보려고 한다.
간단하게 q, w, e, r, d(점멸은 역시 D점멸), f, p(상점), b(귀환)키를 입력할 수 있게 해 보겠다.

현재까지 제작한 기능

  1. 마우스 기능(오른손 연동)
  • 어짜피 커서는 항상 움직일 테니, 특정 조건 없이 항상 검지와 연동되어 움직이게 했다.
  • 오른 클릭, 왼 클릭 기능은 일단, 엄지와 중지, 엄지와 약지를 붙이는 걸로 처리되게 했다.
  1. 키보드 기능(왼손 연동)
  • 손바닥과 손가락이 연결되는 지점을 기준으로 해서, 손 끝의 y축이 기준점보다 아래일 경우 인식하게 했다.
  • 지금은 손가락 한개 트리거만 인식할 수 있어서, q(검지), w(중지), e(약지), r(소지) 만 연동해 두었다.
  1. UX/UI
  • 중복 입력을 막기 위해, 한 번 발동한 제스처는, 발동 트리거가 해제되기 전까지 다시 작동하지 않게 했다.

      # 발동한 키 기능 확인용 변수
      press_ = {'q': True, 'w': True, 'e': True, 'r': True}
      click_ = {'left': True, 'right': True}
    # -----------------------------------------------------
                      # 발동 트리거
                      trigger_ = ring_finger_mcp_y < ring_finger_tip_y
                      # 트리거 + 작동 여부 조건 확인
                      if trigger_ and press_['e']:
                          pyautogui.press('e')
                          press_['e'] = False
                      elif not trigger_ and not press_['e']:
                          press_['e'] = True

2차 목표

  • 마우스 클릭에 연동할, 더 편한 트리거 찾기
  • 손가락 2개 이상 사용하는 트리거 만들기
  • 트리거에 사용하는 손가락 수에 따라서, 발동 우선 순위 적용하기
  • 코드 리팩토링. 제스처 인식 부분 모듈화 + 기능 작동 부분 분리

지금까지의 코드

```
def main():
mp_hands = mediapipe.solutions.hands
hands = mp_hands.Hands(min_detection_confidence=0.7, min_tracking_confidence=0.7)
mp_draw = mediapipe.solutions.drawing_utils

screen_w, screen_h = pyautogui.size()

press_ = {'q': True, 'w': True, 'e': True, 'r': True}
click_ = {'left': True, 'right': True}

cap = cv2.VideoCapture(0)

while cap.isOpened():
    ret, frame = cap.read()

    if not ret:
        break
    frame_h, frame_w, _ = frame.shape
    frame = cv2.flip(frame, 1)
    rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    results = hands.process(rgb_frame)
    if results.multi_hand_landmarks:
        for hand_landmarks, handedness in zip(results.multi_hand_landmarks, results.multi_handedness):
            # 엄지
            thumb_tip = hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_TIP]
            # 검지
            index_finger_tip = hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP]
            # 중지
            middle_finger_tip = hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP]
            # 약지
            ring_finger_tip = hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_TIP]
            # 소지
            pinky_tip = hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_TIP]

            # 오른손(마우스)
            if handedness.classification[0].label == 'Right':

                # 마우스 이동을 위한 검지 좌표 정규화 값
                index_finger_tip_x = int(index_finger_tip.x * screen_w)
                index_finger_tip_y = int(index_finger_tip.y * screen_h)

                # 마우스 이동 이벤트(상시 작동)
                pyautogui.moveTo(index_finger_tip_x, index_finger_tip_y)

                # 좌클릭 이벤트를 위한 엄지 좌표 값 + 중지 좌표 값
                thumb_tip_x = int(thumb_tip.x * screen_w)
                thumb_tip_y = int(thumb_tip.y * screen_h)
                middle_finger_tip_x = int(middle_finger_tip.x * screen_w)
                middle_finger_tip_y = int(middle_finger_tip.y * screen_h)

                distance = ((thumb_tip_x - middle_finger_tip_x) ** 2 + (
                        thumb_tip_y - middle_finger_tip_y) ** 2) ** 0.5
                trigger_ = distance < 50
                if trigger_ and click_['left']:
                    pyautogui.leftClick()
                    click_['left'] = False
                elif not trigger_ and not click_['left']:
                    click_['left'] = True

                # 우클릭 이벤트를 위한 약지 좌표 값
                ring_finger_tip_x = int(ring_finger_tip.x * screen_w)
                ring_finger_tip_y = int(ring_finger_tip.y * screen_h)

                distance = ((thumb_tip_x - ring_finger_tip_x) ** 2 + (thumb_tip_y - ring_finger_tip_y) ** 2) ** 0.5

                trigger_ = distance < 50
                if trigger_ and click_['right']:
                    pyautogui.rightClick()
                    click_['right'] = False
                elif not trigger_ and not click_['right']:
                    click_['right'] = True

            # 왼손(키보드)
            elif handedness.classification[0].label == 'Left':
                # 검지
                index_finger_mcp = hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP]
                # 중지
                middle_finger_mcp = hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP]
                # 약지
                ring_finger_mcp = hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_MCP]
                # 소지
                pinky_mcp = hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_MCP]

                index_finger_mcp_y = int(index_finger_mcp.y * screen_h)
                middle_finger_mcp_y = int(middle_finger_mcp.y * screen_h)
                ring_finger_mcp_y = int(ring_finger_mcp.y * screen_h)
                pinky_mcp_y = int(pinky_mcp.y * screen_h)

                index_finger_tip_y = int(index_finger_tip.y * screen_h)
                middle_finger_tip_y = int(middle_finger_tip.y * screen_h)
                ring_finger_tip_y = int(ring_finger_tip.y * screen_h)
                pinky_tip_y = int(pinky_tip.y * screen_h)

                trigger_ = index_finger_mcp_y < index_finger_tip_y
                if trigger_ and press_['q']:
                    pyautogui.press('q')
                    press_['q'] = False
                elif not trigger_ and not press_['q']:
                    press_['q'] = True

                trigger_ = middle_finger_mcp_y < middle_finger_tip_y
                if trigger_ and press_['w']:
                    pyautogui.press('w')
                    press_['w'] = False
                elif not trigger_ and not press_['w']:
                    press_['w'] = True

                trigger_ = ring_finger_mcp_y < ring_finger_tip_y
                if trigger_ and press_['e']:
                    pyautogui.press('e')
                    press_['e'] = False
                elif not trigger_ and not press_['e']:
                    press_['e'] = True

                trigger_ = pinky_mcp_y < pinky_tip_y
                if trigger_ and press_['r']:
                    pyautogui.press('r')
                    press_['r'] = False
                elif not trigger_ and not press_['r']:
                    press_['r'] = True

                '''
                type: 사용하는 손가락 수에 따른 타입
                point_1: 기준점이 되는 포인트
                point_2: 이동하는 포인트
                input: 입력해야 하는 이벤트
                output: 출력해야 하는 이벤트
                detail: 출력하는 이벤트의 상세 정보

                type: '1 finger'|'2 finger'|'3 finger'|'4 finger'|'5 finger'
                point_1: [str]
                point_2: [str]
                input: ['cross' | 'over' | 'under']
                output: 'press' | 'click' | 'hold'
                detail: 'right' | 'left' | keyboard_key
                '''

            mp_draw.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)

    cv2.imshow('camera', frame)
    if cv2.waitKey(1) & 0xFF == 27:
        break

cv2.destroyAllWindows()


if __name__ == '__main__':  
__main__()  

시연 영상 링크