우리 프로젝트는 실시간 화상 채팅을 하며 다같이 운동을 하도록 제공하는 서비스를 제공한다.
운동 동작(스쿼트, 런지 등등)을 카운트 해주어야 하는데, 이를 위해서 웹캠 영상을 통해 실시간으로 자세를 분석하고 미리 학습시켜둔 데이터로 올바르게 수행하는지 체크한다.
운동 동작 학습을 위해서 구글의 teachale machine을 사용한다.
teachable machine 홈페이지에서 쉽게 동작 학습 시켜 모델을 만들 수 있다.
문제는 react에서 모델을 불러와서 영상의 포즈를 학습 시키는것
teachable machine에서 웹에서 쉽게 사용 가능하도록 구현해둔 라이브러리인 Teachable Machine Pose를 사용하면 웹캠 생성부터 쉽게 이용이 가능하다.
//웹캠을 생성,초기화,시작하는 코드
webcam = new tmPose.Webcam(200, 200, flip); // width, height, flip
await webcam.setup(); // request access to the webcam
webcam.play();
라이브러리에 구현된 Webcam 클래스로 웹캠을 하나 만들고 그 웹캠의 포즈를 분석한다.
예제 코드도 이렇게 되어있고 구글링한 결과 대부분의 사용 예제들이 이 Webcam 클래스를 이용하여 웹캠을 생성한다.
그러나 우리는 실시간 화상 채팅방에서 동작을 진행한다.
그 말인 즉슨, 화상 채팅방이 열리는 순간 이미 openvidu 자체에서 웹캠을 만들고 실행한다.
이 때 생성되는 웹캠을 불러와서 바로 동작을 인식하도록 만들고 싶었다.
tmPose.Webcam을 사용해서 웹캠을 하나 더 띄울수 있긴 하지만 굳이 웹캠을 두개나 돌려버릴 필요가 있을까 싶기도 했고
해당 라이브러리는 웹캠을 만들고 -> html canvas에 웹캠 프레임을 그린다
canvas에 웹캠을 띄우는 이유는 인식 되는 동작의 스켈레톤을 보여주기 위해서 canvas를 사용해야 하는데, 어차피 만들 canvas니까 아예 캔버스에 그려버리도록 구현한듯 하다.
// setup or setupWebcam
// 웹캠을 셋업할 때 캔버스를 생성
@autobind
public async setup(options: MediaTrackConstraints = {}) {
if (!this.webcam) {
this.webcam = await this.getWebcam(options);
if (!this.canvas) {
this.canvas = document.createElement('canvas');
this.canvas.width = this.width;
this.canvas.height = this.height;
}
}
}
...
// 웹캠의 프레임을 canvas에 실시간으로 로딩시켜버림.
public renderCameraToCanvas() {
if (this.canvas && this.webcam) {
const ctx = this.canvas.getContext('2d');
if (this.webcam.videoWidth !== 0) {
const croppedCanvas = cropTo(this.webcam, this.width, this.flip);
ctx.drawImage(croppedCanvas, 0, 0);
}
}
}
우리 서비스는 포즈를 따라가는 스켈레톤을 보여주진 않을것이지만 개발을 위해서는 보여주는것이 시각적으로 편리할것 같았다.
그런데 이 라이브러리를 사용하면 openvidu에서 보여주는 웹캠 화면 위를 Webcam 라이브러리로 별도 생성한 웹캠(canvas)를 덮어씌워야 한다.
이미 영상을 통해 분석한 포즈를 렌더링하는것부터 리소스를 많이 쓸 것 같은데.. 웹캠을 하나 더 만들어 송출하고 그걸 캔버스에서 매 프레임마다 캡쳐해서 그리고, 그 위에 스켈레톤을 그려버리게 한다면 이미 뜨거운 노트북이 감당할 수 있을까 의문이 들었다.
거기다 화상 채팅방의 웹캠을 새로 생성한 canvas로 덮어씌우면 비디오 조작 버튼들(마이크 on/off, vidoe on/off 등)을 가려버리게 된다.
또, 추후엔 스켈레톤을 안띄울거기 때문에 canvas를 안써도 될텐데 자체적으로 구현된 canvas만 따로 삭제하기도 애매하다. 라이브러리 코드를 수정하면 될것같긴 하나 그럼 굳이 라이브러리를 쓸 이유도 없어지는거 같고
결론은 tmPose의 Webcam라이브러리는 사용하지 않는것으로 결정!!
openVidu가 생성한 웹캠을 이용해 동작을 분석하고 결과를 내는 것으로 코드를 짜기로 했다.
여기서 개발하는동안 테스트를 위한 스켈레톤을 그리기 위해 canvas를 이용해 동작 스켈레톤을 입히기로.
openvidu로 화상채팅방을 생성하면 만들어지는 웹캠 화면은 html video element를 사용해 만드는데 Ref에 담아서 모델을 분류하는 코드에서 넘겨준다.
model.estimatePose(
sample: ImageData | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | tf.Tensor3D,
flipHorizontal = false
)
tmPose로 생성한 모델의 메소드인 estimatePose로 동작을 분석하는데, 이 때 인자로 HTML video 엘리먼트를 넘겨줄 수 있다.
ref에 저장해둔 웹캠 video를 여기로 넘겨준다.
const { pose, posenetOutput } = await model.estimatePose(openVidu가 만든 웹캠);
결과값으로 pose와 posenetOutput을 보내준다.
이 pose를 이용해서 동작 스켈레톤을 그릴 수 있다.
posenetOutput은 정수배열로 이루어진 posenet 아웃풋 데이터이다. 이 데이터를 이용해 동작이 일치하는지 확률값을 얻을 수 있다.
const prediction = await model.predict(posenetOutput);
model의 predict메소드를 실행하면 해당 동작이 각 클래스별로 얼마나 일치하는지 확률을 배열에 담아 리턴해준다.
이 확률을 이용해 동작 카운트 로직을 짜면 된다.
글로 적으니 정말 간단한 과정같아 보이지만 생각보다 시간이 많이 들었다.
tmPose를 npm 패키지로 추가할 수 있다길래 처음엔 npm에서 받아서 사용했는데 계속 텐서플로우의 posenet을 로딩하는데 오류가 난다고했다..추가적으로 텐서플로우의 posent도 설치해봤는데 달라지는건 없었다. 여기서 이런저런 씨름하다가 결국 npm 패키지 -> cdn방식으로 변경했다. 구글링 해보면 나오는 많은 사용기도 cdn방식을 사용하는데 많은 사람이 쓰는 이유는 다 그럴만한 이유가 있는거다..
tmPose의 웹캠 라이브러리를 쓰지 않고 openvidu가 생성한 비디오를 넣기 위해서 기존의 tmPose의 Webcam라이브러리는 어떻게 구현 되어있나 라이브러리 뜯어보고나서야 성공했다. 역시 많은 사람이 시도 안하는 방법도 다 이유가 있는거다2
openvidu 영상을 넣는건 성공했는데 posenet 결과에서 동작들의 keypoint들이 제대로 나오지 않아서 스켈레톤이 그려지지 않았다.
라이브러리가 로딩이 잘 안되나 싶어서 또 라이브러리 설치했다가 지웠다가 반복하다가 tmPose라이브러리를 뜯어볼 수 밖에 없었다.
내부적으로 텐서플로우의 posenet을 사용하는데 내가 비디오를 넘겨줄 때 사이즈를 지정해주지 않고 넘겨줘서 생겼던 문제였다... 역시 많은 사람들이 안쓰는 방법은 다 이유가 있다3
최종 서비스엔 넣지도 않을 기능이 모라고 오기가 생겨서 붙잡고있었는지,, 하지만 성공하면 뿌듯한거 다들 아시죠?
스켈레톤이 나오지 않아서 텐서플로우쪽 posnet도 보다가 keypoint들이 어디인지 알게 되었다.
17개의 키포인트들을 분석한다. 그림이 너무 웃기다.
분석하고 리턴하는 pose배열에 이렇게 각각의 keypoint의 좌표들이 담겨있으므로 이를 canvas에 그리면 된다.
끝!