olrlobt

[Pose Estimation] MediaPipe Pose / 미디어 파이프로 사람 포즈 감지하기 본문

Spring/Project

[Pose Estimation] MediaPipe Pose / 미디어 파이프로 사람 포즈 감지하기

olrlobt 2023. 3. 15. 02:57

 

Media pipe

Media pipe는 Google에서 제작한 Machine Lunning Solution으로 얼굴추적, 손추적, 객체 인식과 같은 다양한 기능들을 제공한다.

 

 

Media pipe에서 제공하는 기능들은 아래에서 확인 가능하며, 자세히는 공식 홈페이지를 참고하길 바란다.

 

 

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

 

Home

Cross-platform, customizable ML solutions for live and streaming media.

google.github.io

 

 

 

이 많은 기능들 중 내가 사용할 것은 Pose기능이다.

Pose 기능은 사람의 자세를 탐색하는 기술로 머신러닝으로 학습한 모델을 이용하여, 이미지, 동영상, 실시간 동영상 에서의 사람의 자세를 탐색한다. 반환 값은 좌표 값이며, 이를 통하여 canvas에 사람의 skeleton을 찍거나, grid를 이용하여 3D 화면에서의 렌더링이 가능하다.

 

 

 

 

자세한 Pose Estimation에 대하여 더 알고 싶다면, 이 전 포스팅을 참고하길 바라고,

간단한 Pose 테스트와 Hands 추적 테스트 도 가능하다.

 

https://olrlobt.tistory.com/49

 

[API] 다양한 Pose Estimation API 비교와 정리

Pose Estimation Pose estimation은 인공지능 및 컴퓨터 비전 기술을 사용하여 이미지나 비디오에서 인간의 포즈(자세)를 감지하고 추정하는 기술이다. 이미지 예 : 동영상 예 : 예를 들어, 얼마 전 올렸던

olrlobt.tistory.com

 


Media pipe JavaScript 구현

나는 현재, Spring 토이 프로젝트로 Pose estimation을 주제로 하고 있어서, JavaScript로 구현을 했다.

 

 

 

Import

<script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose/pose.js" crossorigin="anonymous"></script>

 

Media pipe의 Pose Detection 모델 호출

const pose = new Pose({
	locateFile: (file) => {
		return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;
	}
});

 

Pose 모델을 호출하여, 변수 pose로 선언한다.

간단하게, "https://cdn.jsdelivr.net/npm/@mediapipe/pose/" 패키지에서 Pose 객체를 가져왔으며, locateFile 함수를 통하여 경로에서 필요한 파일들을 가져오게 된다. 

 

 

 

Pose Detection 설정

pose.setOptions({
		upperBodyOnly: true,
		modelComplexity: 1,
		smoothLandmarks: true,
		enableSegmentation: false,
		minDetectionConfidence: 0.5,
		minTrackingConfidence: 0.5
});

 

upperBodyOnly: (기본값 false)

 - 상체만 탐색할지 여부를 설정한다. 

 - true로 설정하면, 화면에 상체만 나왔을 때는 상체만 탐색하지만, 전신이 다 나오면 전신을 다 탐색한다.

 - false로 설정하면, 화면에 상체만 나와도 전신을 다 탐색하려 하고, 전신이 다 나오면 전신을 다 탐색한다.

 - 결론적으로, 전신을 다 탐색할 필요가 없어서 성능적 이득을 보려면 true로 해주자.

 

modelComplexity: (기본값 1)

 - 모델의 복잡도 ( 0,1,2)의 값을 갖는다. 

 - 값이 높을수록 정확도는 높아지고 속도는 느려진다.

 

smoothLandmarks: (기본값 true)

 - 추적 결과를 부드럽게 만든다. 

 - false 설정이 결과는 더 정확하지만, 덜 부드러울 수 있다. 


enableSegmentation: (기본값 false)

 - 비디오 세그멘테이션 사용 여부. 

 - 이 기능을 이용하여 비디오에서 전경과 배경을 분리할 수 있다.


minDetectionConfidence: (기본값 0.5)

 - 물체를 감지하는 최소 점수이다. (0~1)의 값을 갖는다.

 - 값이 높을수록 정확도는 높아지지만, 더 많은 노이즈가 발생한다.


minTrackingConfidence: (기본값 0.5)

 - 추적할 물체의 신뢰도 (0~1)의 값을 갖는다.

 - 값이 높을수록 정확도는 높아지지만, 더 많은 추적 실패가 발생한다.

 

 

 

이 외에도 많은 옵션들이 있지만, 기본적으로는 이 정도만 알면 된다.

 

 

Pose Detection 콜백 함수 설정

pose.onResults((results) => {
	console.log(results);
});

onResults 메서드를 통하여 콜백 함수를 설정한다. onResults의 경우, Pose의 Detection이 발생할 때마다 실행된다.

 

간단한 코드의 경우 위와 같이 작성하고, 조금 더 복잡도가 올라갈 경우나 재호출 할 경우 따로 함수를 선언해 아래와 같이 선언해 주자.

 

pose.onResults(onPose);

function onPose(results) {
	console.log(results);
    }

 

 

Pose Detection 실행

pose.send({ 
	image: user_video 
});

기본적인 형식은 위와 같다.

send() 메서드의 image 속성에 분석할 대상 (이미지, 비디오, 실시간 영상)을 입력해 주면 된다.

 

입력 상세사항은 아래를 참고한다.

 

 

1. 이미지

pose.send({ image: user_image });

user_image 변수에 html의 img 태그를 연결해 주고 분석을 시작한다.

 

 

2. 비디오

function processVideo() {
	pose.send({ image: user_video });
	
	requestAnimationFrame(processVideo);
}

user_video 변수에 html의 video 태그를 연결해 준다.

 

send의 image 속성 명에서 알 수 있듯이 image는 video에서 한 프레임을 의미한다.

따라서 processVideo 함수를 재귀 호출하여 video를 프레임 별로 send 해 주어야 한다.

 

여기서 requestAnimationFrame 함수는 애니메이션에서 Frame을 60 FPS로 조절하는 역할을 한다.

requestAnimationFrame 없이 재귀 호출을 하게 된다면, 1초에 60보다 많은 Frame을 send 하게 되어, 호출 과부하가 발생할 수 있다. 

 

requestAnimationFrame(processVideo,{
	  maxFPS: 30,
	  skipFrames: 2
	})
}

만약, Frame 수를 줄이고 싶다면,  위와 같이 호출할 수 있고

여기서 maxFPS는 최대 FPS를,

skipFrames는 넘길 프레임 수를 조절한다.

 

예를 들어 maxFPS 30 , skipFrames 2 라면, 최대 FPS는 30을 넘기지 않고, Frame을 2개당 하나를 분석하게 된다.

 

 

3. 실시간 영상

<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
camera = new Camera(user_video, {
		onFrame: async () => {
			await pose.send({ image: user_video });
		},
		width: 1280,
		height: 720
	});
	camera.start();

실시간 영상의 경우, 비디오 분석과 비슷하다.

 

먼저 mediapipe의 카메라 객체를 생성해 준다.

user_video의 경우 html의 video 엘리먼트이고, onFrame은 camera 객체가 프레임을 가져올 때마다 실행된다. 즉, 비디오 분석 에서의 requestAnimationFrame()의 역할을 한다.

 

 

결과

 

 

총 33개의 키 포인트를

poseLandmarks에서는 image 기준에서의 x, y, z, 좌표와 visibility 점수를,

poseWorldLandmarks에서는 사람 기준에서의 x, y, z 좌표와 visibility 점수를 제공한다.

 

visibility점수는 0~1 사이의 값으로, 신뢰도를 의미하며, 1에 가까울수록 신뢰도가 높다.

 


Canvas로 Skeleton 그리기

 

콜백 함수 작성

pose.onResults(onPose);

onResult의 콜백 함수에 작성한다.

 

 

<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
const canvasElement = document.getElementsByClassName('output_canvas')[0];
const canvasCtx = canvasElement.getContext('2d');

function onPose(results) {
//	console.log(results);

    canvasCtx.save(); 	// 캔버스 설정 저장
    canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); // 캔버스 초기화
    
    // 캔버스에 이미지 넣기
    canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);

    drawLandmarks(canvasCtx, results.poseLandmarks, {	// 랜드마크 표시
        color: '#FF0000', lineWidth: 2
    });
    drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS,{	// 연결 선 표시
            color: '#0000FF', lineWidth: 3
        });
    canvasCtx.restore();	// 캔버스 설정 불러오기
}

 

clearRect()로 캔버스를 초기화한 후,

drawLandmarks()와 drawConnectors()로 스켈레톤을 그려준다.

 

여기서 save()와 restore()가 마지막과 처음에 있는 이유는, 캔버스의 초기 설정값을 저장하기 위해서다.

아무것도 작성되지 않은 상태에서 save()를 해 주지 않으면, 캔버스를 원래 설정으로 되돌릴 수 없다고 한다.

 

 

 

결과


Canvas Skeleton 좌, 우 구분하기

각 메서드를 잘 이해하고 있다면, 간단하게 좌, 우를 구분하여 나타낼 수 있다.

 

// 좌측 키 값 // 우측 키 값
const leftIndices = [1, 2, 3, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31];
const rightIndices = [4, 5, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32];
const leftConnections = [	// 좌측 연결선
  [11,13],[13,15],[15,21],[15,17],[15,19],[17,19],
  [11,23],[23,25],[25,27],[27,29],[27,31],[29,31]
];
const rightConnections = [	// 우측 연결선
  [12,14],[14,16],[16,22],[16,18],[16,20],[18,20],
  [12,24],[24,26],[26,28],[28,30],[28,32],[30,32]
];
const centerConnections = [	// 중앙 연결선
  [11,12],[23,24]
];

function onPose(results) {
	//console.log(results);
	const keyPoint = results.poseLandmarks;
	let leftKeyPoint = [];	// 좌측 키포인트
	let rightKeyPoint = [];	// 우측 키포인트
    
	if (keyPoint != null) {
		canvasCtx.save();
		canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
		canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
		for (let i = 0; i < keyPoint.length; i++) {	// 키포인트 구분
			if (leftIndices.includes(i)) {
				leftKeyPoint.push(keyPoint[i]);
			} else {
				rightKeyPoint.push(keyPoint[i]);
			}
		}

		drawLandmarks(canvasCtx, leftKeyPoint, {
			color: '#FF0000', lineWidth: 2
		});
		drawLandmarks(canvasCtx, rightKeyPoint, {
			color: '#0000FF', lineWidth: 2
		});
		drawConnectors(canvasCtx, keyPoint, leftConnections,{
				color: '#00FFFF', lineWidth: 3
			});
		drawConnectors(canvasCtx, keyPoint, rightConnections,{
				color: '#00FF00', lineWidth: 3
			});
		drawConnectors(canvasCtx, keyPoint, centerConnections,{
			color: '#EEEEEE', lineWidth: 3
		});
		canvasCtx.restore();
	}
}

 

결과

Comments