Home 캡스톤일지 | ~ 10.13 미디어파이프 이용한 정적제스처 인식과 문제 발생
Post
Cancel

캡스톤일지 | ~ 10.13 미디어파이프 이용한 정적제스처 인식과 문제 발생

image

회의록 작성과 일정 관리, 공유 자료 정리 등등 팀 노션 관리는 나의 역할!
첨에 그냥 남들 다 쓰는 노션 나도 뽐새나게 써보고싶어서 시작한거였는데 재밌어서 내가 맡아서 하구있당
암튼! 수요일 회의 전까지 각자 정적 제스처 인식 기능과 시간이 되면 클릭하는 모션의 동적 제스처 인식 기능 개발을 해오기로 했다.


손 제스처 인식 대상

정적 제스처 

  • 확인 : 엄지를 제외한 손가락을 모두 접고 엄지만 위로 올린 모양 👍🏻
  • 취소 : 엄지를 제외한 손가락을 모두 접고 엄지만 아래로 내린 모양 👎🏻

원래 검지와 엄지를 맡대는 OK 싸인을 확인으로 하려 했으나 취소 제스처와 통일감을 위해 따봉으로 변경

 동적 제스처 

  • 화면(탭) 왼쪽으로 이동 : 손목은 고정한 채 손가락을 모두 편 상태로 왼쪽에서 오른쪽으로 움직이는 동작
  • 화면(탭) 오른쪽으로 이동 : 손목은 고정한 채 손가락을 모두 편 상태로 오른쪽에서 왼쪽으로 움직이는 동작
  • 화면(또는 스크롤) 아래로 이동 : 손목은 고정한 채 손가락을 모두 편 상태로 위에서 아래로 움직이는 동작
  • 화면(또는 스크롤) 위로 이동 : 손목은 고정한 채 손가락을 모두 편 상태로 아래에서 위로 움직이는 동작
  • 클릭 : 검지를 제외한 모든 손가락을 접은 상태에서 검지를 구부렸다가 펴는 동작

정적 제스처 인식 (주란 ver)

나는 이전에 포스팅하였던 미디어파이프를 통해 인식된 포인트를 이용하는 방식으로 제스처 인식 기능을 구현해보았다.
📄 관련 이전 포스팅 https://ijo0r98.github.io/posts/capstone1/

기능 구현에 앞서 그린 스케치 image

DOM & 미디어파이프 cdn 태그

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
</head>
<body>
  <div class="container">
    <video class="input_video"></video>
    <canvas class="output_canvas" width="1000px" height="500px"></canvas>
  </div>
</body>
</html>

나는 손목의 0번 포인트를 기준으로 0번의 y보다 4번의 y값이 크면 엄지를 올린 동작으로, 작으면 엄지를 내린 동작으로 인식하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<script type="module">
const videoElement = document.getElementsByClassName('input_video')[0];
const canvasElement = document.getElementsByClassName('output_canvas')[0];
const canvasCtx = canvasElement.getContext('2d');

// 손가락의 접힘 판단 포인트쌍
const compareIndex = [[18, 4], [6, 8], [10, 12], [14, 16], [18, 20]]
var open = [false, false, false, false, false]
const gesture = [true, false, false, false, false, "Thumb"] // 엄지만 펼쳐진 경우

function dist(x1, y1, x2, y2) { // 두 점 사이 거리 비교하여 손가락의 접힘 여부 판단
  return Math.sqrt(Math.pow(x1-x2, 2)) + Math.sqrt(Math.pow(y1-y2, 2));
}

function onResults(results) {
  canvasCtx.save();
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
  canvasCtx.drawImage(
      results.image, 0, 0, canvasElement.width, canvasElement.height);
      
  if (results.multiHandLandmarks) {
    for (const landmarks of results.multiHandLandmarks) { // landmarks.length == 21 (0-20)
      for (var i=0; i<5; i++) {
        // true 펼쳐짐, false 접힘
          open[i] = dist(landmarks[0]['x'], landmarks[0]['y'], landmarks[compareIndex[i][0]]['x'], landmarks[compareIndex[i][0]]['y']) < dist(landmarks[0]['x'], landmarks[0]['y'], landmarks[compareIndex[i][1]]['x'], landmarks[compareIndex[i][1]]['y'])
      }

      // 지정된 제스처와 비교
      var flag = true;
      for (var i=0; i<5; i++) {
        if (gesture[i] != open[i]) {
          flag = false
        }
      }
      
      if (flag == true) { // 지정된 제스처(thumb)와 동일할 때
        if(landmarks[0]['y'] > landmarks[4]['y']) {
          console.log('up!'); 
        } else {
          console.log('down!');
        }
      }

      drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color: '#4badb3', lineWidth: 5});
      drawLandmarks(canvasCtx, landmarks, {color: '#e04e04', lineWidth: 2});
    }
  }
  canvasCtx.restore();
}

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

hands.setOptions({
  maxNumHands: 1,
  minDetectionConfidence: 0.5,
  minTrackingConfidence: 0.5
});

hands.onResults(onResults);

const camera = new Camera(videoElement, {
  onFrame: async () => {
    await hands.send({image: videoElement});
  },
  width: 1280,
  height: 720
});

camera.start();
</script>

UP image

DOWN image

문제1. 엄지손가락 인식 문제

엄지의 경우 손목 0번과 엄지손가락 끝인 4번까지의 거리와 새끼손가락의 두번째 마디 포인트인 18번까지 거리를 비교하여 전자가 길면 엄지가 펴진 상태로 후자가 길면 엄지가 접힌 상태라고 인식한다.

image

하지만 이는 정확도가 조금 떨어지는 듯 하다. image

위의 사진은 엄지가 확연하게 올라간(혹은 내려간) 상태는 아니지만 코드상 엄지가 펼쳐진 것으로 인식해 UP이란 결과가 나온다.
엄지가 펼쳐진것은 맞지만 과연 따봉 제스처로 인식해도 될지는 고민이다 🤔
다른 손가락과의 길이를 비교하거나 엄지손가락의 기울기나 각도를 구하는 방법으로 하려 했는데 이는 결국 우리가 직접 기준을 정하기 나름인 것 같다.

문제2. 손목과 엄지의 위치가 애매할 때

사진처럼 엄지손가락과 손목의 위치가 애매할 때 특히 일직선상에 위치할 때 어느 동작으로 인식되지 않는다. image 저기서 조금만 위치가 달라지면 up이나 down으로 인식되는데 이걸 손가락을 올리거나 내린 포즈로 인식되도 되는지가 의문이다..
이 또한 우리가 직접 기준을 정해야할 것 같다.

엄청난 문제 발견 🤯

마우스 커서 조작과 관련하여 문제가 발생했다ㅠㅠ 
계획은 검지손가락 끝 포인트인 8번의 좌표를 이용하여 마우스 커서를 이동시키려했는데.. 자바스크립트에서는 그런 기능을 제공하지 않는다고 한다. (참고: https://www.clien.net/service/board/kin/6099730)

생각해보면 그럴법도… 악용될 가능성도 있고 보안상 위험한 기능일 것 같긴 하다. 특히나 웹에서는.. 혼자 개발하던중 멘붕와서 멍때리다가 정신차리고 고민하면서 생각해본 대체 방법들로는

  • 가짜 마우스 커서 생성
  • 가상키보드(방향키)
  • 다시 파이썬으로 기능 개발
    파이썬에 마우스 조작 라이브러리가 있음. 미디어파이프 설치가 안됨으로 opencv 이용해서 손을 인식해야할듯

해결1. 가짜 마우스 커서 생성

검지 손가락이 마우스 커서처럼 보이도록 (웹캠 영상에 미디어파이프 결과가 그려지는) canvas 위에 작은 사각형을 하나 만들어 8번 포인트에 연결해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 다른 부분은 동일
function onResults(results) {
  canvasCtx.save();
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
  canvasCtx.drawImage(
      results.image, 0, 0, canvasElement.width, canvasElement.height);
  if (results.multiHandLandmarks) {
    for (const landmarks of results.multiHandLandmarks) {
      drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color: '#4badb3', lineWidth: 5});
      drawLandmarks(canvasCtx, landmarks, {color: '#e04e04', lineWidth: 2});
      var x = landmarks[8]['x'];
      var y = landmarks[8]['y'];
      canvasCtx.fillStyle = "black";
      canvasCtx.fillRect(x*700, y*400, 10, 10);
    }
  }
  canvasCtx.restore();
}

image

캔버스위에 도형이 늘어날때마다 조금 버벅이는 듯한 느낌..

주의해야할 점은 미디어파이프 포인트들의 x, y값이 좌표가 아니라 전체 캔버스에 대한 비율로 나온다.
따라서 절대적인 좌표를 구하기 위해서는 x, y값에 각각 가로, 세로 길이를 구해주어야한다.
이 방법의 아쉬운점은 canvas를 벗어나면, 즉 웹캠을 벗어나면 미디어파이프가 적용되지 않음으로 검지 포인트도 사라진다는점

해결2. 가상 키보드

영상위에 방향키 모양의 가상 키보드를 보여줘서 해당 자판(rect)위에서 클릭 모션을 하면 키보드가 선택되는 것처럼 동작하도록 하고싶었다. image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 나머지 동일
function onResults(results) {
  canvasCtx.save(); 
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
  canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
  // fillStyle로 색상을 정해주면 다음 그리는 도형의 색상이 지정한 색으로 바뀐다.
  canvasCtx.fillStyle = "green"; 
  canvasCtx.fillRect(110, 10, 100, 100);
  canvasCtx.fillStyle = "blue";
  canvasCtx.fillRect(10, 110, 100, 100);
  canvasCtx.fillStyle = "white";
  canvasCtx.fillRect(110, 110, 100, 100);
  canvasCtx.fillStyle = "yellow";
  canvasCtx.fillRect(210, 110, 100, 100);
  if (results.multiHandLandmarks) {
    for (const landmarks of results.multiHandLandmarks) {
      drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color: '#4badb3', lineWidth: 5});
      drawLandmarks(canvasCtx, landmarks, {color: '#e04e04', lineWidth: 2});
      var x = landmarks[8]['x'];
      var y = landmarks[8]['y'];
      canvasCtx.fillStyle = "black";
      canvasCtx.fillRect(x*700, y*400, 10, 10);
    }
  }
  canvasCtx.restore();
}

아까 마우스 커서 만들어준것처럼 각 사각형을 그릴 위치와 가로, 세로 길이를 정해주면 사각형이 그려진다. image

아직 클릭 모션 연결과 클릭했을 때의 동작은 구현하지 못했다.
바둑판 모양의 메뉴를 상하좌우 한칸씩 움직이는 것은 가로 세로 전체 길이가 정해졌을경우 쉬울 것 같긴 한데…
가장 첫번째 메뉴에서 마지막에 있는 메뉴까지 이동을 원할 경우 계속 키보드를 클릭해 한개씩 이동해야한다는 점이 사용자 입장에서 매우 번거로울 것 같다.
뿐만 아니라 클릭 모션은 어떻게 할것인지, 사각형들과 포인트들 사이 비교 & 모션 연결을 동시에 어떻게 해야할지도 고민이다 😱

현지가 회의 후 canvas에서 영상 부분을 지우고 미디어파이프 결과만 남겨둔 채 그 위에 div를 생성하여 메뉴판처럼 보이게 하는 작업을 진행중이다.

웹캠 영상 좌우반전

입력되는 영상과 반환하는 영상 모두 css 처리를 통해 좌우반전 시켜주었다.(현지 땡큐)

1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
    video{
      transform: rotateY(180deg);
      -webkit-transform:rotateY(180deg); /* Safari and Chrome */
      -moz-transform:rotateY(180deg); /* Firefox */
    }
    
    canvas{
      transform: rotateY(180deg);
      -webkit-transform:rotateY(180deg); /* Safari and Chrome */
      -moz-transform:rotateY(180deg); /* Firefox */
    }
</style>

정적 제스처 인식 (참고 ver)

✏️ https://github.com/kairess/gesture-recognition

📹 https://www.youtube.com/watch?v=eHxDWhtbRCk&t=218s

우리가 하고 싶은 것과 관련된 것들을 많아 이 분의 깃허브와 유튜브를 열심히 보고있다

나는 실시간 영상에 미디어파이프 적용하여 학습 데이터셋 만드는 것이 안돼서 찍어둔 손 제스처 영상을 이용하는 방식으로 데이터셋을 만들고자 하였고 그렇게 만들어진 데이터셋을 콜랩에서 학습시키려고 하였다. 그리고 학습된 모델을 다시 js파일로 만들어 스크립트로 실행하려 했었다. 하지만 코드를 수정하며 뭔가 잘못되었는지 다 해보지 못하였지만 미디어파이프 설치에 성공하신 수민님께서 파이썬으로 모두 해주셨다!
손가락 사이 각도를 x데이터로 학습해서 그런지(👍🏻와 👎🏻가 방향성을 빼고 봤을 때 각도가 동일한 동작이기 때문에) 아니면 우리는 지금 정적 제스처를 학습시켜서 그런건지는 잘 모르겠지만 정확도가 너어무 안나와서 포기해따 ..
그래도 동적 제스처를 학습시키는데에는 괜찮은 방법이지 않을까 싶다.

그리고 또 수민님이 알려주신 모델학습 시켜주는 서비스 https://teachablemachine.withgoogle.com/

아무래도 손가락이나 이런 것이 아닌 이미지 자체의 유사도를 판별하기 때문에 배경 등에 영향을 많이 받고 정확도가 많이 떨어진다.



수요일 회의는 각자 개발하며 찾은 문제점들을 공유하고 고민해보는 시간을 가졌다.
이번주는 설계서 작성은 잠시 보류하고 손 인식 기능 개발에 힘쏟기로 하여 금요일까지 다시 각자 맡은 부분을 더 고민해보기로 했다.
나는 해결방법 3으로 나온 파이썬으로 마우스 커서 조작을 해볼 예정인데.. 꼭 됐으면 좋겠다ㅠㅠ

이제 다시 내 캡스톤 과제하러..

This post is licensed under CC BY 4.0 by the author.

캡스톤일지 | ~ 10.05 NodeJS TDD 웹개발 훑어보기(1)

캡스톤일지 | ~ 10.16 robotjs 이용한 마우스 커서 이동