SKT FLY AI

[Mediapipe] 프레임별 인물의 관절 좌표 추출하기

48965 2025. 1. 13. 20:02

음성을 수어 영상으로 번역하는 기능 (Speech2Sign) 개발 중 DB에 저장된 수어 영상을 프레임별 키포인트로 변환하는 작업이 필요하여 파이썬의 Mediapipe 기능을 활용하여 이를 구현해보고자 한다.

 


📌 MediaPipe란?

MediaPipe는 Google에서 개발한 오픈소스 멀티모달(Multimodal) 머신러닝 프레임워크이다.

Hand Tracking, Face Mesh, Pose Estimation, Object Detection과 같은 기능을 제공하여 수어 번역에 필수적인 라이브러리고 생각하여 도입하게 되었다.

 

우선 인물의 수어 영상을 보았을 때 필요한 것은 얼굴에서의 특징 좌표, 몸의 관절 좌표, 손가락 하나하나의 관절 좌표이며, 이를 위해 Mediapipe 기능을 분석 후 개발을 진행하였다.

pose landmarks

 

hand landmarks

 

수어 번역에 연관있는 좌표들만을 추출하여 사용 예정이기에 인물의 상반신과 얼굴, 손가락 좌표만 추출을 진행하였다.

상반신과 얼굴 : 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 기능 개발은 완료될 것이다.