https://google.github.io/mediapipe/

MediaPipe

MediaPipe란 구글에서 제공하는 AI 프레임워크로써, 비디오 형식 데이터를 이용한 다양한 비전 AI 기능을 파이프라인 형태로 손쉽게 사용할 수 있도록 제공됩니다. AI 모델 개발 및 수많은 데이터셋을 이용한 학습도 마친 상태로 제공되므로 라이브러리를 불러 사용하듯이 간편하게 호출하여 사용하기만 하면 되는 형태로, 비전 AI 기능을 개발할 수 있습니다.


제공되는 여러가지 모델

 

MediaPipe는 오픈소스 프로젝트로서 소스가 공개되기 때문에 원하는 부분을 수정하여 추가 개발할 수도 있습니다. 또한 솔루션 별로 상세한 기술자료 및 예제 등이 풍부하게 제공되고 있습니다. 

 

학습모델을 범위나 용도별에 따라 구분하여 사용할 수 있도록 Lite,full,Heavy 등으로 구분하여 제공되기 때문에 각자 환경이나 목적에 따라 적정한 모델을 골라 쓰기만 하면 됩니다. 

 

이렇게 MediaPipe에 여러가지 모델이 있는데 이번 프로젝트에서는 손가락과 손의 위치를 알려주는 모델을 이용했습니다.


MediaPipe Hands

 

손가락과 손의 위치를 알려주는 모델

 

 

 

그래서 MediaPipe로 손가락을 인식 한다는데, 그러면 도대체 어떻게 인식을 하는가 해서 알아보니,

손가락 뼈 마디마디를 인식을 한 다음에 손가락 마디의 각도를 계산하는 방법으로 인식을 한다고 합니다. 

손가락 마디의 점 마다의 각도가 있고, 이 각도를 가지고 데이터셋에 저장된 제스처를 인식해서 이 손가락이 어떤 제스처인지 인식을 하는 것입니다.

 

이렇게 빨간색 점으로 된 랜드마크로 손가락을 인식하고, 점 사이를 벡터로 하여 각각의 각도를 구해서, 이 손가락 모양이 어떤 모양인지 인식을 하는 원리라고 합니다.

 

이런저런 소개와 랜드마크에 대한 것은 MediaPipe document에 가면 각 joint의 랜드마크 번호를 확인할 수 있습니다.

 

MediaPipe Hands document : https://google.github.io/mediapipe/solutions/hands#python-solution-api


데이터셋 추가 코드

gather_dataset.py

import cv2
import mediapipe as mp
import numpy as np

max_num_hands = 1 # 인식할 수 있는 손 개수
gesture = {
    0:'fist', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five',
    6:'six', 7:'rock', 8:'spiderman', 9:'yeah', 10:'ok', 11:'fy'
} # 12가지의 제스처, 제스처 데이터는 손가락 관절의 각도와 각각의 라벨을 뜻한다.

# MediaPipe hands model
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(
    max_num_hands=max_num_hands,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5)

# Gesture recognition data
file = np.genfromtxt('data/gesture_train.csv', delimiter=',')
print(file.shape)

cap = cv2.VideoCapture(0)

def click(event, x, y, flags, param):
    global data, file
    if event == cv2.EVENT_LBUTTONDOWN:
        file = np.vstack((file, data))
        print(file.shape)

cv2.namedWindow('Dataset')
cv2.setMouseCallback('Dataset', click)

while cap.isOpened():
    ret, img = cap.read()
    if not ret:
        continue

    img = cv2.flip(img, 1)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    result = hands.process(img)

    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

    if result.multi_hand_landmarks is not None:
        for res in result.multi_hand_landmarks:
            joint = np.zeros((21, 3))
            for j, lm in enumerate(res.landmark):
                joint[j] = [lm.x, lm.y, lm.z]

            # Compute angles between joints
            v1 = joint[[0,1,2,3,0,5,6,7,0,9,10,11,0,13,14,15,0,17,18,19],:] # Parent joint
            v2 = joint[[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],:] # Child joint
            v = v2 - v1 # [20,3]
            # Normalize v
            v = v / np.linalg.norm(v, axis=1)[:, np.newaxis]

            # Get angle using arcos of dot product
            angle = np.arccos(np.einsum('nt,nt->n',
                v[[0,1,2,4,5,6,8,9,10,12,13,14,16,17,18],:], 
                v[[1,2,3,5,6,7,9,10,11,13,14,15,17,18,19],:])) # [15,]

            angle = np.degrees(angle) # Convert radian to degree

            data = np.array([angle], dtype=np.float32)
            
            data = np.append(data, 11) # 

            mp_drawing.draw_landmarks(img, res, mp_hands.HAND_CONNECTIONS)

    cv2.imshow('Dataset', img)
    if cv2.waitKey(1) == ord('q'):
        break

np.savetxt('data/gesture_train_fy.csv', file, delimiter=',')

 

코드 설명

코드는 먼저 데이터 셋을 추가하는 것부터 시작합니다.

gesture  코드의 작성자가 11가지의 제스처 데이터를 모아놨고, 프로젝트는 제스처의 클래스를 정의하는 부분에서 11 라벨인 뻐큐 제스처를 추가해줍니다. 

data = np.append(data, 11)

 

그리고 코드 마지막 부분에 정답 라벨에 뻐큐 제스처가 인식되게끔 11을 추가해주고, 데이터를 추가하기 위해서 click()이라는 함수를 만들어서 뻐큐를 인식할 수 있게끔 해주는데요

def click(event, x, y, flags, param):
    global data, file
    if event == cv2.EVENT_LBUTTONDOWN:
        file = np.vstack((file, data))
        print(file.shape)

이렇게 데이터를 모으기 위해, 클릭 했을 현재 각도 데이터가 추가되게끔 하는 click 함수를 만들어주고, 뻐큐제스처를 계속 클릭하면서 데이터를 추가해 줍니다.

 

그러면 이 데이터가 gesture_train_fy.csv 파일에 저장이 됩니다.

손가락 각도가 다 저장이 되어있는 것을 확인할 수 있고, 마지막에 뻐큐 라벨인 11이 저장되어 있는 데이터를 확인할 수 있습니다.

 


뻐큐 제스처 모자이크 코드 

프로세스는 아래와 같다.

  1. 손가락 인식
  2. 손가락 마디 각도 계산
  3. 뻐큐 제스처 인식
  4. 모자이크

 

fy_filter.py

import cv2
import mediapipe as mp
import numpy as np

max_num_hands = 1
gesture = {
    0:'fist', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five',
    6:'six', 7:'rock', 8:'spiderman', 9:'yeah', 10:'ok', 11:'fy'
}

# MediaPipe hands model
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(
    max_num_hands=max_num_hands,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5)

# Gesture recognition model
file = np.genfromtxt('data/gesture_train_fy.csv', delimiter=',')
angle = file[:,:-1].astype(np.float32)
label = file[:, -1].astype(np.float32)
knn = cv2.ml.KNearest_create()
knn.train(angle, cv2.ml.ROW_SAMPLE, label)

cap = cv2.VideoCapture(0)

while cap.isOpened():
    ret, img = cap.read()
    if not ret:
        continue

    img = cv2.flip(img, 1)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    result = hands.process(img)

    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

    if result.multi_hand_landmarks is not None:
        for res in result.multi_hand_landmarks:
            joint = np.zeros((21, 3))
            for j, lm in enumerate(res.landmark):
                joint[j] = [lm.x, lm.y, lm.z]

            # Compute angles between joints
            v1 = joint[[0,1,2,3,0,5,6,7,0,9,10,11,0,13,14,15,0,17,18,19],:] # Parent joint
            v2 = joint[[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],:] # Child joint
            v = v2 - v1 # [20,3]
            # Normalize v
            v = v / np.linalg.norm(v, axis=1)[:, np.newaxis]

            # Get angle using arcos of dot product
            angle = np.arccos(np.einsum('nt,nt->n',
                v[[0,1,2,4,5,6,8,9,10,12,13,14,16,17,18],:], 
                v[[1,2,3,5,6,7,9,10,11,13,14,15,17,18,19],:])) # [15,]

            angle = np.degrees(angle) # Convert radian to degree

            # Inference gesture
            data = np.array([angle], dtype=np.float32)
            ret, results, neighbours, dist = knn.findNearest(data, 3)
            idx = int(results[0][0])

            if idx == 11:
                x1, y1 = tuple((joint.min(axis=0)[:2] * [img.shape[1], img.shape[0]] * 0.95).astype(int))
                x2, y2 = tuple((joint.max(axis=0)[:2] * [img.shape[1], img.shape[0]] * 1.05).astype(int))

                fy_img = img[y1:y2, x1:x2].copy()
                fy_img = cv2.resize(fy_img, dsize=None, fx=0.05, fy=0.05, interpolation=cv2.INTER_NEAREST)
                fy_img = cv2.resize(fy_img, dsize=(x2 - x1, y2 - y1), interpolation=cv2.INTER_NEAREST)

                img[y1:y2, x1:x2] = fy_img

            # mp_drawing.draw_landmarks(img, res, mp_hands.HAND_CONNECTIONS)

    cv2.imshow('Filter', img)
    if cv2.waitKey(1) == ord('q'):
        break

   

코드 설명

이제 본코드인 fy_filter 코드를 보면,

 

max_num_hands = 1
gesture = {
    0:'fist', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five',
    6:'six', 7:'rock', 8:'spiderman', 9:'yeah', 10:'ok', 11:'fy'
}

먼저 손을 하나만 인식할 것이기 때문에 max_num_hands는 1로 설정을 해줍니다. 

이 코드는 최대 몇개의 손을 인식할 건지 정의하는 것입니다.

뻐큐 모자이크 프로젝트는 한 개의 손만 인식하면 되니까 1로 정의를 해주고, 만약 2개를 인식하고 싶다하면 2를 입력하면 됩니다.

 

현재 코드에서 11개의 제스처가 저장이 되어있는 상태이고, 총 11개의 제스처를 인식할 수 있는 데이터가 있습니다.

 

# MediaPipe hands model
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(
    max_num_hands=max_num_hands,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5)

미디어 파이프의 drawing_utils는 웹캠 영상에서 손가락의 뼈마디 부분, 연두색으로 표시되는 부분을 그릴 있게 도와주는 유틸리티라고 합니다.

 

file = np.genfromtxt('data/gesture_train.csv', delimiter=',')

그리고 나서 아까 모아놓은 뻐큐손가락 각도가 저장된 제스처 파일을 가져옵니다.

 

# Gesture recognition model
file = np.genfromtxt('data/gesture_train_fy.csv', delimiter=',')
angle = file[:,:-1].astype(np.float32)
label = file[:, -1].astype(np.float32)
knn = cv2.ml.KNearest_create()
knn.train(angle, cv2.ml.ROW_SAMPLE, label)

angle하고 label을 데이터로 모아주고

opencv의 k 최근접 알고리즘을 사용해서 학습을 시켜줍니다.

 

그러면 knn 모델에 데이터가 잘 학습이 돼서 들어가 있을거고

 

cap = cv2.VideoCapture(0)

while cap.isOpened():
    ret, img = cap.read()
    if not ret:
        continue

웹캠에서 저의 뻐큐 손가락 이미지를 읽어와서, 만약 읽어오는데 성공했다하면 위의 코드를 전부 실행하고, 성공하지 못했다면 다음 프레임으로 넘어가게 합니다.

 

 

    img = cv2.flip(img, 1)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    result = hands.process(img)

    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

 

그리고 이미지가 거울 형태로 뒤집어져 있기 때문에 좌우로 반전시켜주는 flip 사용합니다.

 

 

또한 opencv는 BGR 컬러 시스템을 이용하는데 MediaPipe RGB 컬러 시스템을 사용한다고 합니다.

cvtColor 사용해서 opencv 읽어온 프레임을 BGR에서 RGB 변경해준 다음에,

 

MediaPipe 모델에 넣어주기전에 전처리를 해줍니다.

hands.process 하면 전처리 이미지가 result 들어가게 됩니다.

그다음 이미지를 출력해야 하니까 다시 RGB BGR 바꿔줍니다.

 

 

 

그래서 전처리가 되고 모델추론까지 된 다음에 결과가 나오게 되면 

    if result.multi_hand_landmarks is not None:

multi_hand_landmarks 가 true가 됩니다. 

만약 손이 인식되지 않으면 결과가 false 됩니다.

 

 

    if result.multi_hand_landmarks is not None:
        for res in result.multi_hand_landmarks:
            joint = np.zeros((21, 3))
            for j, lm in enumerate(res.landmark):
                joint[j] = [lm.x, lm.y, lm.z]

 

카메라 프레임에서 계속해서 손을 감지하므로 for문으로 처리합니다.

그리고 빨간 점들을 joint라고 부르고 이것의 x,y,z,좌표를 저장하도록 합니다.

joint 0부터 20까지의 랜드마크로 21개가 있고 x,y,z 좌표 3개를 저장을 해서 21 × 3개의 점을 만들어주고,

 

각 joint마다 랜드마크를 저장하는데, 각 랜드마크의 x,y,z 좌표를 joint 저장합니다.

 

 

            # Compute angles between joints
            v1 = joint[[0,1,2,3,0,5,6,7,0,9,10,11,0,13,14,15,0,17,18,19],:] # Parent joint
            v2 = joint[[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],:] # Child joint
            v = v2 - v1 # [20,3]
            # Normalize v
            v = v / np.linalg.norm(v, axis=1)[:, np.newaxis]

 

여기서 각 joint로 벡터를 계산해서 각도를 계산합니다.

도큐먼트에 나와있는 것처럼 조인트의 번호의 인덱스가 나와있어서

 

v1 v2  인덱스를 넣어주고

v2에서 v1 빼줍니다.

빼주면서 각각의 벡터의 각도를 계산 하게 됩니다.

이것이 관절의 벡터를 구해주는 과정입니다.

 

            # Normalize v
            v = v / np.linalg.norm(v, axis=1)[:, np.newaxis]

            # Get angle using arcos of dot product
            angle = np.arccos(np.einsum('nt,nt->n',
                v[[0,1,2,4,5,6,8,9,10,12,13,14,16,17,18],:], 
                v[[1,2,3,5,6,7,9,10,11,13,14,15,17,18,19],:])) # [15,]

            angle = np.degrees(angle) # Convert radian to degree

그리고 normalize를 합니다. 각 벡터의 길이를 유클리디안 거리로 구해주고 나눠주면 normalize 되고,

v1 벡터와 v2 벡터 를 내적(dot product)하면 

[v1벡터의 크기] × [v2벡터의 크기] × [두 벡터가 이루는 각의 cos값] 이 되는데,

바로 위에서 벡터들의 크기를 모두 1로 normalize 해줬으므로 

두 벡터의 내적값은 곧 [두 벡터가 이루는 각의 cos값]이 됩니다.

따라서 이것을 코사인 역함수인 arccos 대입하면 벡터가 이루는 각이 나오게 됩니다.

이렇게 15개의 각도를 구해서 angle이라는 변수에 저장을 하게 됩니다.

 

angle이 라디안으로 나오니까 degree 값으로 변환을 해줍니다.

 

 

            # Inference gesture
            data = np.array([angle], dtype=np.float32)
            ret, results, neighbours, dist = knn.findNearest(data, 3)
            idx = int(results[0][0])

            if idx == 11:
                x1, y1 = tuple((joint.min(axis=0)[:2] * [img.shape[1], img.shape[0]] * 0.95).astype(int))
                x2, y2 = tuple((joint.max(axis=0)[:2] * [img.shape[1], img.shape[0]] * 1.05).astype(int))

                fy_img = img[y1:y2, x1:x2].copy()
                fy_img = cv2.resize(fy_img, dsize=None, fx=0.05, fy=0.05, interpolation=cv2.INTER_NEAREST)
                fy_img = cv2.resize(fy_img, dsize=(x2 - x1, y2 - y1), interpolation=cv2.INTER_NEAREST)

                img[y1:y2, x1:x2] = fy_img

아까 위에서 뻐큐모양 제스처를 학습시킨 knn 모델로 inference를 하고 넘파이 array형태로 바꿔줍니다.

 

k가 3일때의 값을 구하고 결과는 result의 인덱스에 저장이되고

 

만약 인덱스가 뻐큐라면 모자이크 처리를 해줍니다.

모자이크는 단순히 이미지를 키웠다 줄이는 과정을 통해 모자이크 처리를 해줍니다.

 

결과는 아래와 같습니다.

 

 

 

 

Ref.

빵형의 개발도상국 유튜브 : https://www.youtube.com/watch?v=tQeuPrX821w&t=1s 

복사했습니다!