[Mediapipe] 프레임별 인물의 관절 좌표 추출하기
음성을 수어 영상으로 번역하는 기능 (Speech2Sign) 개발 중 DB에 저장된 수어 영상을 프레임별 키포인트로 변환하는 작업이 필요하여 파이썬의 Mediapipe 기능을 활용하여 이를 구현해보고자 한다.
📌 MediaPipe란?
MediaPipe는 Google에서 개발한 오픈소스 멀티모달(Multimodal) 머신러닝 프레임워크이다.
Hand Tracking, Face Mesh, Pose Estimation, Object Detection과 같은 기능을 제공하여 수어 번역에 필수적인 라이브러리고 생각하여 도입하게 되었다.
우선 인물의 수어 영상을 보았을 때 필요한 것은 얼굴에서의 특징 좌표, 몸의 관절 좌표, 손가락 하나하나의 관절 좌표이며, 이를 위해 Mediapipe 기능을 분석 후 개발을 진행하였다.
수어 번역에 연관있는 좌표들만을 추출하여 사용 예정이기에 인물의 상반신과 얼굴, 손가락 좌표만 추출을 진행하였다.
상반신과 얼굴 : 15개 좌표. (pose의 얼굴 및 어깨, 팔꿈치 좌표)
왼손 : 21개 좌표 (0~20번 모두 사용)
오른손 : 21개 좌표 (0~20번 모두 사용)
이를 기반으로 json 파일을 만들면 다음과 같은 포멧으로 나타낼 수 있으며, 데이터베이스에 저장된 모든 수어 영상을 해당 json 파일 포멧으로 변환 작업을 수행하였다.
{
"frames": [
{
"keypoints": [
[x1, y1, z1], // (pose의 첫 번째 랜드마크)
[x2, y2, z2], // (pose의 두 번째 랜드마크)
...
[x15, y15, z15], // (pose의 열다섯 번째 랜드마크)
[x16, y16, z16], // (left_hand의 첫 번째 랜드마크)
...
[x36, y36, z36], // (left_hand의 스물한 번째 랜드마크)
[x37, y37, z37], // (right_hand의 첫 번째 랜드마크)
...
[x57, y57, z57] // right_hand의 스물한 번째 랜드마크)
]
},
...
]
}
앞선 방식과 같은 포멧으로 구현을 위해 다음과 같은 순서로 개발을 진행하였다.
📄 DB에 저장된 수어 영상을 불러와 Mediapipe 기능을 통해 프레임별 인물의 좌표값을 추출.
로직의 핵심 코드만 보면 다음과 같다.
def process_videos_and_upload_keypoints(db: Session, s3_bucket_name: str):
signs = db.query(Sign).filter(
Sign.id.between(7464, 9000),
Sign.keypoint.is_(None)
).all()
mp_holistic = mp.solutions.holistic
mp_drawing = mp.solutions.drawing_utils
for sign in signs:
video_path = sign.url
cap = cv2.VideoCapture(video_path)
animation_data = {"frames": []}
previous_keypoints = None
# Mediapipe Holistic 초기화
with mp_holistic.Holistic(
static_image_mode=False,
model_complexity=2,
enable_segmentation=False,
refine_face_landmarks=False) as holistic:
# 각 프레임에서 키포인트 추출 및 정규화
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
image_height, image_width, _ = frame.shape
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image.flags.writeable = False
results = holistic.process(image)
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
frame_data = {"keypoints": []}
upper_body_indices = list(range(15)) # 0부터 14까지의 인덱스 사용
current_keypoints = []
if results.pose_landmarks:
selected_keypoints = np.array([[results.pose_landmarks.landmark[i].x,
1.0 - results.pose_landmarks.landmark[i].y,
results.pose_landmarks.landmark[i].z]
for i in upper_body_indices])
current_keypoints.extend(selected_keypoints.tolist())
else:
if previous_keypoints:
current_keypoints.extend(previous_keypoints[:15])
else:
current_keypoints.extend([[0, 0, 0]] * 15)
if results.left_hand_landmarks:
left_hand_keypoints = np.array([[landmark.x,
1.0 - landmark.y,
landmark.z]
for landmark in results.left_hand_landmarks.landmark])
current_keypoints.extend(left_hand_keypoints.tolist())
else:
if previous_keypoints:
current_keypoints.extend(previous_keypoints[15:36])
else:
current_keypoints.extend([[0, 0, 0]] * 21)
if results.right_hand_landmarks:
right_hand_keypoints = np.array([[landmark.x,
1.0 - landmark.y,
landmark.z]
for landmark in results.right_hand_landmarks.landmark])
current_keypoints.extend(right_hand_keypoints.tolist())
else:
if previous_keypoints:
current_keypoints.extend(previous_keypoints[36:])
else:
current_keypoints.extend([[0, 0, 0]] * 21)
frame_data["keypoints"].extend(current_keypoints)
animation_data["frames"].append(frame_data)
previous_keypoints = current_keypoints
cap.release()
cv2.destroyAllWindows()
DB에 저장된 수어 영상을 불러와 mediapipe를 통해 좌표로 추출하는 로직이다.
위에서 언급한 좌표들을 pose, left hand, right hand 순으로 list에 삽입 후 json으로 추출하는 방식으로 구현을 완료하였다.
특이한 점은 y좌표로 변환하는 과정에서 y좌표계가 반대 방향이기 때문에 1.0에서 y좌표값을 빼는 작업을 수행하여 상하 반전 시켜주었다.
우선 수어 영상은 평균적으로 6~7초 분량이 가장 많았으며, 초당 프레임은 30으로써 하나의 영상에서 180~210 프레임이 추출되었다.
즉 180~210 프레임에 각 관절별 좌표값 57을 곱한 값이 하나의 json 파일로 완성됨으로 변환 시간이 다소 소모되었다.
-> (총 200 프레임으로만 개산해도 하나의 영상당 11,400의 좌표값이 하나의 json 파일로 저장된다. 😱)
영상 하나 당 변환 시간을 측정해보니 약 15초 정도가 소모되었으며, DB에 저장된 15,600개의 수어 영상을 변환하기 위해서는 234,000초. 즉 65시간이 소모되는 문제가 있었다...
시간이 부족한 관계로 이를 빠르게 수행하기 위해 4명의 팀원들 노트북 및 코랩을 활용해서 동시에 병렬적으로 작업을 수행하였다.
본인 포함 4명에서 각 노트북 + 코랩까지 동원하여 총 8개의 병렬로 데이터를 처리하였다.
(코드 상단을 보면 각자 수행해야 하는 DB 내 데이터의 id값을 정하고 작업을 수행한 것을 볼 수 있다. 😥)
이후에 프레임별로 추출된 좌표값들은 json 파일 형태로 AWS S3에 저장되고, 저장된 json파일의 url을 한국어 의미에 해당하는 데이터 컬럼에 삽입하는 로직을 통해 구현을 완료하였다.
🔍 최종적으로 json 변환 후 저장 완료된 데이터베이스
gloss는 수어 영상에 대한 한국어 의미, url은 원본 수어 영상의 링크, keypoint는 json 파일의 경로, gloss_vector는 gloss의 단어를 임베딩한 필드이다.
이로써 음성이 들어오면 gloss 필드를 통해 가장 유사도가 높은 데이터가 조회되며, 매핑된 keypoint를 반환할 수 있는 로직이 완성되었다.
이를 통해 최종적으로 json 파일을 읽어 수어 영상을 생성하는 로직을 개발을 진행해주면 Speech2Sign 기능 개발은 완료될 것이다.