이번 글에서는 Google MediaPipe 에서 개발한 Hand Landmark Detection 을 사용하여, 손이 나오는 영상에서 손을 추적해 스켈레톤화 하는 과정을 정리하고자 한다.
개발 환경
macOS Sonoma 14.1.1
Python 3.10.6
1. Google MediaPipe HandLandmark
MediaPipe Hand Landmark 는 이미지에 보이는 손의 랜드마크(Landmark)를 인식하여, 시각적으로 가시화까지 해주는 기능들을 포함하고 있다. 여기서 랜드마크(Landmark) 라는 단어가 살짝 어색해 보일 수 있다. 쉽게 생각하자면, 랜드마크는 손의 중요 포인트를 뜻한다고 생각하면 된다. 중요 포인트는 다음 그림과 같다.
위 그림을 보면 손의 형상을 0~20까지 21가지의 점으로 나타낸 것을 볼 수 있다. 아래에서 Hand Landmark 인식 결과 데이터와 연관지어 자세히 설명할 예정이므로, Hand Landmark 는 위 그림에 표시된 빨간점을 뜻한다고 생각하면 된다.
본론으로 돌아와서, Google MediaPipe 팀에서 만든 Hand Landmark Detection 은 손이 보이는 단일 이미지나 비디오나 라이브 스트림의 프레임 데이터를 입력 받으면, 이미지 내에서의 좌표 기반의 Landmark 와 월드 좌표 기반의 Landmark, 왼손인지 오른손인지 나타내는 Handedness 값들을 출력한다.
MediaPipe 에서 제공하는 HandLandmark 에 대한 가이드와 예시 코드들은 다음 링크를 통해 살펴볼 수 있다.
- Android : Code Example / Guide
- Python : Code Example / Guide
- Web : Code Example / Guide
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 초기화가 잘 되었는지 확인할 수 있다.
# landmark.py
import cv2
cap = VideoCapture(FILE_PATH)
print(cap.isOpened())
cap.release()
cap.isOpened() 메소드를 호출하여 true 가 반환되었다면, VideoCapture 가 정상적으로 초기화 된 것이다. 비디오 파일이 정상적으로 입력 되었다면, 이제 비디오의 프레임들을 얻어볼 차례이다.
참고로, cap.release() 는 여기에서 미리 써두었지만, VideoCapture 를 사용한 로직이 모두 종료되면 호출해주는 것이 좋다.
2.2) 비디오 파일의 프레임 얻기
비디오 파일의 프레임을 얻는데는 VideoCapture 의 read() 메소드를 사용한다.
실제로 코드로 써보면 다음과 같이 사용할 수 있다.
# 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개씩 있다는 것이다. 이 점은 글 초반부에 첨부한 이미지와 연관지을 수 있다.
위 그림에서 표시한 0부터 20까지 21개의 중요 포인트 좌표가 Landmark #0 부터 Landmark #20 으로 결과 출력된 것이다. 추후에 Landmark 좌표값으로 애니메이션과 같이 역렌더링을 해야한다면 필요한 정보일 것으로 보인다.
4. 글을 마치며...
오랜만에 Python 을 사용해서 재밌는 라이브러리를 사용 해볼 수 있었다. 적용을 해보면서 MediaPipe 공식 가이드 문서대로 했는데도 잘 안되고, 구글링해도 잘 풀리지 않아서 어렵긴 했다. 하지만, 현재의 HandLandmark 패키지의 이전 버전과 같은 모습으로 사용을 해봤더니 원했던 결과를 얻을 수 있어서 좋았다. 1분 정도의 영상을 처리하는데에 비슷한 시간의 처리시간이 소요되며, 많은 영상을 처리해야한다면 Python 의 Multi-Process 를 사용해야할 것으로 보인다.
'NotePad' 카테고리의 다른 글
[Flutter] CocoaPods could not find compatible versions for pod "NMapsMap" (0) | 2024.08.28 |
---|---|
Kotlin 으로 JWT 를 Decode 하기 (0) | 2022.11.09 |
MusicXML 4.0 Tutorial - File Structure (0) | 2022.09.21 |
MusicXML 4.0 Tutorial - "Hello World" (0) | 2022.09.20 |
[음악] 음자리표 (Time Signature) (0) | 2022.09.20 |