NotePad

[MediaPipe] Hand Landmark 적용 과정 정리

졸려질려 2023. 11. 17. 20:14
반응형

 이번 글에서는 Google MediaPipe 에서 개발한 Hand Landmark Detection 을 사용하여, 손이 나오는 영상에서 손을 추적해 스켈레톤화 하는 과정을 정리하고자 한다.

개발 환경
macOS Sonoma 14.1.1
Python 3.10.6

1. Google MediaPipe HandLandmark

 MediaPipe Hand Landmark 는 이미지에 보이는 손의 랜드마크(Landmark)를 인식하여, 시각적으로 가시화까지 해주는 기능들을 포함하고 있다. 여기서 랜드마크(Landmark) 라는 단어가 살짝 어색해 보일 수 있다. 쉽게 생각하자면, 랜드마크는 손의 중요 포인트를 뜻한다고 생각하면 된다. 중요 포인트는 다음 그림과 같다.

출처: MediaPipe - Hand Landmarks 공식문서

 위 그림을 보면 손의 형상을 0~20까지 21가지의 점으로 나타낸 것을 볼 수 있다. 아래에서 Hand Landmark 인식 결과 데이터와 연관지어 자세히 설명할 예정이므로, Hand Landmark 는 위 그림에 표시된 빨간점을 뜻한다고 생각하면 된다.

 본론으로 돌아와서, Google MediaPipe 팀에서 만든 Hand Landmark Detection 은 손이 보이는 단일 이미지나 비디오나 라이브 스트림의 프레임 데이터를 입력 받으면, 이미지 내에서의 좌표 기반의 Landmark 와 월드 좌표 기반의 Landmark, 왼손인지 오른손인지 나타내는 Handedness 값들을 출력한다.

 MediaPipe 에서 제공하는 HandLandmark 에 대한 가이드와 예시 코드들은 다음 링크를 통해 살펴볼 수 있다.

 HandLandmark 를 사용할 수 있는 플랫폼별 가이드와 예시 코드를 볼 수 있으며, 이미지, 비디오, 라이브 스트림의 입력값을 확인할 수 있다. 다만, Android 는 이미지, 비디오, 라이브 스트림 모두 확인 할 수 있지만, Python 은 이미지 입력만, Web 은 이미지와 웹캠을 통한 라이브 스트림 입력만 예시로 실행 해볼 수 있다.

 본 글에서는 Python 을 사용해 비디오 파일을 입력하여 랜드마크 값을 받고, 입력한 영상에 오버레이로 입혀서, 새로운 비디오 파일로 저장하는 과정을 정리한다.


2. Video -> Hand Landmark Video

 공식문서의 가이드 문서를 살펴보면, 이미지와 비디오, 라이브 스트림까지 사용할 수 있게 전반적인 가이드 내용이 잘 담겨있다. 이미지와 라이브 스트림은 가이드 문서대로 따라해도 쉽게 사용할 수 있었지만, 비디오 파일을 입력할 때는 원하는 결과를 얻을 수 없었다. 구글링을 해봐도 가이드 문서의 기본 설정을 기반으로는 비디오 파일에서 랜드마크를 추출할 수 없었고, 결국 가이드 문서와 다른 방식으로 원하는 결과를 얻을 수 있었다.


2.1) CV2 로 비디오 파일 입력

 우선, 비디오 파일을 입력 받을 수 있는 부분부터 구현한다. 비디오 파일을 입력 받는데에는 CV2 모듈을 사용했다.

pip3 install opencv-python

 터미널에서 위 명령어를 입력하여 opencv-python 을 설치하면, cv2 패키지를 사용할 수 있다. 필자의 맥북에는 pip3 가 기본적으로 설치되어 있어서 pip3 로 설치하였지만, 사용자의 컴퓨터에 맞게 opencv-python 을 설치해주면 된다.

 opencv-python 을 설치했다면, cv2 패키지를 임포트 해준다.

# landmark.py

import cv2

 cv2 패키지에서 VideoCapture 를 생성하고, 입력하고자 하는 비디오 파일의 경로를 생성자 인자로 넣어준다.

# landmark.py

import cv2

cap = VideoCapture(FILE_PATH)

 비디오 파일의 경로를 넣어주는데까지 했다면, 다음 파트로 넘어가기 전에 확인을 해볼 방법이 있다. VideoCapture 의 isOpened() 메소드를 사용하여 VideoCapture 초기화가 잘 되었는지 확인할 수 있다.

출처: OpenCV Docs

# landmark.py

import cv2

cap = VideoCapture(FILE_PATH)

print(cap.isOpened())

cap.release()

 cap.isOpened() 메소드를 호출하여 true 가 반환되었다면, VideoCapture 가 정상적으로 초기화 된 것이다. 비디오 파일이 정상적으로 입력 되었다면, 이제 비디오의 프레임들을 얻어볼 차례이다.

 참고로, cap.release() 는 여기에서 미리 써두었지만, VideoCapture 를 사용한 로직이 모두 종료되면 호출해주는 것이 좋다.

2.2) 비디오 파일의 프레임 얻기

 비디오 파일의 프레임을 얻는데는 VideoCapture 의 read() 메소드를 사용한다.

출처: OpenCV Docs

 실제로 코드로 써보면 다음과 같이 사용할 수 있다.

# landmark.py

import cv2

cap = VideoCapture(FILE_PATH)

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        print("END: Empty Camera Frame.")
        break

cap.release()

 read() 메소드는 프레임을 읽는데 성공했는지 알려주는 boolean 값과 읽는데 성공했다면 프레임의 데이터를 반환한다. 따라서 ret 의 값이 false 일 때는 프레임이 더 이상 없는 영상의 끝지점에 도달했다는 뜻이므로 while() 문에서 벗어나도록 break 걸어준다.

2.3) MediaPipe HandLandmark 적용

 비디오 파일의 프레임까지 얻을 수 있게 되었으니, 프레임을 사용하여 Landmark 를 뽑아낼 수 있게 HandLandmark 패키지를 임포트시켜준다.

# landmark.py

import cv2
import mediapipe as mp

mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands

cap = VideoCapture(FILE_PATH)

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        print("END: Empty Camera Frame.")
        break

cap.release()

  mp_drawing 과 mp_drawing_styles 는 프레임 위에 Landmark 를 그릴 때 사용하는 패키지들이다. mp_hands 는 입력 받은 프레임에서 Landmark 를 출력해준다. 현재 MediaPipe 에 있는 공식 문서의 가이드와 다른 점은 HandLandmark Task 모델을 따로 경로 지정하여 선언할 필요가 없다. mp_hands 내부에 내장되어 있어 그대로 사용할 수 있기 때문이다.

 이제 임포트한 mp_hands 를 다음 코드처럼 사용할 수 있다.

# landmark.py

import cv2
import mediapipe as mp

mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands

cap = VideoCapture(FILE_PATH)

with mp_hands.Hands(model_complexity=0, min_detection_confidence=0.5, min_tracking_confidence=0.5) as hands:
    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            print("END: Empty Camera Frame.")
            break
            
        frame.flags.writeable = False
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        results = hands.process(frame)

        frame.flags.writeable = True
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)

cap.release()

 CV2 패키지를 사용해서 프레임의 색상을 BGR 에서 RGB 로 바꿔주고, process() 메소드를 통해 Landmark 인식 결과를 받는다. 그 후에 BGR 로 원상 복구를 한다. 원상 복구를 한 이유는 위 작업을 마친 후에 원본 영상 위에 Landmark 를 그려주기 위해서이다.

 이제 mp_drawing_styles 와 mp_drawing 을 사용하여 원본 프레임 위에 Landmark 를 그려서 출력한다.

# landmark.py

import cv2
import mediapipe as mp

mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands

cap = VideoCapture(FILE_PATH)

with mp_hands.Hands(model_complexity=0, min_detection_confidence=0.5, min_tracking_confidence=0.5) as hands:
    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            print("END: Empty Camera Frame.")
            break
            
        frame.flags.writeable = False
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        results = hands.process(frame)

        frame.flags.writeable = True
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
        
        if results.multi_hand_landmarks:
            for hand_landmarks in results.multi_hand_landmarks:
                mp_drawing.draw_landmarks(
                    frame,
                    hand_landmarks,
                    mp_hands.HAND_CONNECTIONS,
                    mp_drawing_styles.get_default_hand_landmarks_style(),
                    mp_drawing_styles.get_default_hand_connections_style()
                )
                
        cv2.imshow("Live", frame)

cap.release()

 cv2.imshow() 메소드를 통해 프레임이 작업되는 대로 확인할 수 있다. 하지만, 원본 영상과 다르게 배속이 걸린듯이 빠르게 지나가는 것처럼 보일 것이다. 왜냐하면 영상으로 만들어서 재생하는 것이 아니라, 프레임이 읽히는 대로 Landmark 처리하고 출력하기 때문이다.

2.4) Landmark 가 적용된 비디오 파일로 저장

 이제 다른 비디오 파일로 저장하는 코드까지 추가하여 마무리 해준다.

# landmark.py

....

input_video_fps = cap.get(cv2.CAP_PROP_FPS)
input_video_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
input_video_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

# 비디오 라이터 객체 생성
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
vWriter = cv2.VideoWriter(OUTPUT_FILE_PATH, fourcc, input_video_fps, (input_video_width, input_video_height))  # 적절한 해상도로 변경하세요

with mp_hands.Hands(model_complexity=0, min_detection_confidence=0.5, min_tracking_confidence=0.5) as hands:
    ....
    vWriter.write(frame)

....

 입력 받은 비디오 파일의 FPS, Frame Width, Frame Height 를 얻어서 새로 만들 비디오의 기본 정보로 설정한다. fourcc 는 쉽게 말해서 어떤 코덱을 사용할지 설정하는 부분인데, mp4 파일로 받기위해 위와 같이 입력해주었다.


3. Hand Landmark 의 Process 결과

 hands.process() 메소드에 frame 을 넣었을 때, 어떤 결과 값이 나오는지 궁금할 수 있다. 글 초반부에 참고 이미지로 올렸던 Hand Landmark 이미지와 연관하여 결과 값을 어떻게 읽는지 살펴본다.

 결과값은 JSON 의 형식으로 다음과 같이 출력된다.

HandLandmarkerResult:
  Handedness:
    Categories #0:
      index        : 0
      score        : 0.98396
      categoryName : Left
  Landmarks:
    Landmark #0:
      x            : 0.638852
      y            : 0.671197
      z            : -3.41E-7
    Landmark #1:
      x            : 0.634599
      y            : 0.536441
      z            : -0.06984
    ... (21 landmarks for a hand)
  WorldLandmarks:
    Landmark #0:
      x            : 0.067485
      y            : 0.031084
      z            : 0.055223
    Landmark #1:
      x            : 0.063209
      y            : -0.00382
      z            : 0.020920
    ... (21 world landmarks for a hand)

 Handedness 는 인식한 손이 왼손인지 오른손인지 score 값과 함께 알려준다. 만약 양손이 인식된다면, 왼손 따로 오른손 따로 결과값이 연속적으로 출력된다. Landmarks 와 WorldLandmarks 는 인식한 손에서 중요 포인트들의 좌표값을 x,y,z 로 출력한 것이다. 두 항목의 공통점은 각각 Landmark 가 21개씩 있다는 것이다. 이 점은 글 초반부에 첨부한 이미지와 연관지을 수 있다.

출처 : Google MediaPipe HandLandmark

 위 그림에서 표시한 0부터 20까지 21개의 중요 포인트 좌표가 Landmark #0 부터 Landmark #20 으로 결과 출력된 것이다. 추후에 Landmark 좌표값으로 애니메이션과 같이 역렌더링을 해야한다면 필요한 정보일 것으로 보인다.


4. 글을 마치며...

 오랜만에 Python 을 사용해서 재밌는 라이브러리를 사용 해볼 수 있었다. 적용을 해보면서 MediaPipe 공식 가이드 문서대로 했는데도 잘 안되고, 구글링해도 잘 풀리지 않아서 어렵긴 했다. 하지만, 현재의 HandLandmark 패키지의 이전 버전과 같은 모습으로 사용을 해봤더니 원했던 결과를 얻을 수 있어서 좋았다. 1분 정도의 영상을 처리하는데에 비슷한 시간의 처리시간이 소요되며, 많은 영상을 처리해야한다면 Python 의 Multi-Process 를 사용해야할 것으로 보인다.

반응형