[체온] 원격 체육수업 도우미 프로젝트 : keypoint를 통한 운동 동작 확인 및 운동 횟수 측정

40423 단어 projectproject

프로젝트 배경

초중고 학생들의 건강은 갈수록 안 좋아지고 있습니다. 스마트폰과 컴퓨터 기술이 발달하며 SNS, 온라인 게임 등에 시간을 쏟아, 활동량이 줄어들었기 때문입니다.

이런 문제는 COVID-19 사태의 발생으로 더욱 심화되었습니다. 체육 수업까지 원격으로 진행되며, 사실상 이론 위주의 수업이 진행 되고 있습니다.

이런 문제에서 착안하여, 원격 체육 수업 시에는 원활한 수업 진행을 돕고, 나아가 대면 수업 시에도 과제를 통해 학생들의 운동량을 늘리는 것을 돕기 위한 서비스를 개발하였습니다.

프로젝트 소개

프로젝트 이름은 체온(체육수업 온라인)입니다.
이름처럼 원격 체육 수업을 돕는 서비스입니다.


교사가 운동 동작들을 선택해서 과제를 내면, 학생들이 해당 운동을 수행하고, 수행 여부를 교사가 확인할 수 있습니다.

이를 통해

  1. 운동을 정확하게 수행하는지 확인
  2. 운동 횟수 측정
  3. 학생 관리

등의 기능을 자동으로 수행할 수 있습니다.

역할 & 개발 과정

해당 프로젝트에서 저는 운동 동작 확인과 횟수&시간 측정을 주로 담당하였습니다.
해당 내용의 수행 과정을 아래와 같습니다.

1) input preprocess

우선 주어진 input으로 사람의 신체 부위들에 대한 x, y 위치 정보가 있습니다.

keypoint를 검출하기까지의 과정은 https://crong-dev.tistory.com/71 (웹 담당 팀원 블로그)를 참고하십시오.
본 포스트는 "동작 계산"과 "횟수 & 시간 측정" 방법만 다루고 있습니다.

이때 위와 같은 input인 pose는 아래와 같은 형태로 전달됩니다.

[
    {
        "score": 0.8972646594047546,
        "part": "nose",
        "position": {
            "x": 193.2494088826053,
            "y": 371.1051601481289
        }
    },
    {
        "score": 0.8827599883079529,
        "part": "leftEye",
        "position": {
            "x": 213.5912971675117,
            "y": 327.83480257601354
        }
    },
 ...
     {
        "score": 0.00852131750434637,
        "part": "rightAnkle",
        "position": {
            "x": 476.34969773790954,
            "y": 391.47699494867464
        }
    }
]

이와 같은 리스트 형태로는 "코의 위치"와 같이 원하는 부위의 위치를 직관적으로 판단하기 어렵습니다.
매번 특정 포인트가 리스트의 몇 번재 인덱스인지 세어서 확인해야 하기 때문입니다.
따라서 아래와 같은 코드로 형식을 바꿨습니다.

    export function getKeypointsObject(pose) {
      return pose.keypoints.reduce((acc, { part, position }) => {
        acc[part] = position;
        return acc;
      }, {});
    }
    
    const {                          # 여기 있는 point들만 추출
      leftShoulder,
      rightShoulder,
      leftElbow,
      rightElbow,
      leftWrist,
      rightWrist,
    } = getKeypointsObject(pose);

그 결과 아래와 같은 형태로 나타납니다.

{
    "leftShoulder": {
        "x": 414.6123915269111,
        "y": 502.4310518451144
    },
    "rightShoulder": {
        "x": 179.63503071372855,
        "y": 485.2505055385915
    },
    "leftElbow": {
        "x": 193.71017575078002,
        "y": 483.08390722453225
    },
    "rightElbow": {
        "x": 178.42100051189547,
        "y": 486.4092852618244
    },
    "leftWrist": {
        "x": 201.69752766673167,
        "y": 490.2000328904626
    },
    "rightWrist": {
        "x": 178.80662843701248,
        "y": 495.6861011402027
    }
}

2) 각도 계산

이후 이 keypoint들을 통해, 이용자의 동작을 계산하였습니다.

동작을 확인하기 위해선 point 간의 각도를 활용하였습니다.
예를 들어, 위로 만세를 하는 동작을 할 경우, 어깨를 기준으로 손목의 각도가 약 180도 이어야 합니다.

팔 동작을 인식하기 위한 각도 계산은 아래와 같습니다.

    export function getAngle(x1, y1, x2, y2) {
      const rad = Math.atan2(y2 - y1, x2 - x1);
      return 1 * ((rad * 180) / Math.PI);
    }

    const anglesArms = {
      rightHigh: getAngle(
        rightShoulder.x,
        rightShoulder.y,
        rightElbow.x,
        rightElbow.y
      ),
      rightLow: getAngle(
        rightElbow.x,
        rightElbow.y,
        rightWrist.x,
        rightWrist.y
      ),
      leftHigh: getAngle(
        leftShoulder.x,
        leftShoulder.y,
        leftElbow.x,
        leftElbow.y
      ),
      leftLow: getAngle(leftElbow.x, leftElbow.y, leftWrist.x, leftWrist.y),
    };

3) 동작 인식

이후 이 각도들을 이용하여 원하는 동작을 확인합니다.
현제 제공된 keypoint들이 원하는 이미지이면 True, 아니면 False를 반환하는 함수를 만드는 것입니다.

// 왼쪽 어깨 스트레칭
// 왼팔은 오른쪽 앞으로 넘기고, 오른팔로 당기기
function checkLeftShoulderStretching(anglesArms) {
  if (-170 < anglesArms.leftHigh && anglesArms.leftHigh < 145) return false;
  else if (-70 < anglesArms.rightLow || anglesArms.rightLow < -110)
    return false;
  else return true;
}

// 오른쪽 어깨 스트레칭
// 위와 반대
function checkRightShoulderStretching(anglesArms) {
  if (-10 > anglesArms.rightHigh || anglesArms.rightHigh > 45) return false;
  else if (-70 < anglesArms.leftLow || anglesArms.leftLow < -110) return false;
  else return true;
}
const left = checkLeftShoulderStretching(anglesArms);
const right = checkRightShoulderStretching(anglesArms);

4) 횟수 측정

위 코드는 0.1초 마다 새로 호출되고, 맞는 동작이 인식되면 count를 증가시킵니다.
그리고 해당 동작을 원하는 횟수만큼 수행하면, step을 증가시켜 다음 동작으로 넘어갑니다.

  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  if (step == 0 && left) setCount((count) => count + 1);
  else if (step == 1 && right) setCount((count) => count + 1);
  useEffect(() => {
    if (step == 0 && count > 20) {
      setStep((step) => 1);
      setCount((count) => 0);
    }
    if (step == 1 && count > 20) {
      setStep((step) => 2);
      setCount((count) => 0);
    }
  }, [step, count]);

총정리

위 코드를 하나로 묶으면 아래와 같습니다.(어깨 스트레칭 예)

import { useState, useCallback, useEffect } from "react";
import { getKeypointsObject, getAngle } from "../estimate-pose";

// 추후 함수명은 동작 이름으로 변경. 대문자로 시작.
// 어깨 스트레칭
export default function ShoulderStretching() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);

  const checkPoses = useCallback((pose) => {
    const {
      leftShoulder,
      rightShoulder,
      leftElbow,
      rightElbow,
      leftWrist,
      rightWrist,
    } = getKeypointsObject(pose);

    const anglesArms = {
      rightHigh: getAngle(
        rightShoulder.x,
        rightShoulder.y,
        rightElbow.x,
        rightElbow.y
      ),
      rightLow: getAngle(
        rightElbow.x,
        rightElbow.y,
        rightWrist.x,
        rightWrist.y
      ),
      leftHigh: getAngle(
        leftShoulder.x,
        leftShoulder.y,
        leftElbow.x,
        leftElbow.y
      ),
      leftLow: getAngle(leftElbow.x, leftElbow.y, leftWrist.x, leftWrist.y),
    };

    const left = checkLeftShoulderStretching(anglesArms);
    const right = checkRightShoulderStretching(anglesArms);

    if (step == 0 && left) setCount((count) => count + 1);
    else if (step == 1 && right) setCount((count) => count + 1);
  });

  useEffect(() => {
    if (step == 0 && count > 20) {
      setStep((step) => 1);
      setCount((count) => 0);
    }
    if (step == 1 && count > 20) {
      setStep((step) => 2);
      setCount((count) => 0);
    }
  }, [step, count]);

  return [count, step, checkPoses];
}

// 왼쪽 어깨 스트레칭
// 왼팔은 오른쪽 앞으로 넘기고, 오른팔로 당기기
function checkLeftShoulderStretching(anglesArms) {
  if (-170 < anglesArms.leftHigh && anglesArms.leftHigh < 145) return false;
  else if (-70 < anglesArms.rightLow || anglesArms.rightLow < -110)
    return false;
  else return true;
}

// 오른쪽 어깨 스트레칭
// 위와 반대
function checkRightShoulderStretching(anglesArms) {
  if (-10 > anglesArms.rightHigh || anglesArms.rightHigh > 45) return false;
  else if (-70 < anglesArms.leftLow || anglesArms.leftLow < -110) return false;
  else return true;
}


## estimate-pose.js 파일
export function getKeypointsObject(pose) {
  return pose.keypoints.reduce((acc, { part, position }) => {
    acc[part] = position;
    return acc;
  }, {});
}

export function getAngle(x1, y1, x2, y2) {
  const rad = Math.atan2(y2 - y1, x2 - x1);
  return 1 * ((rad * 180) / Math.PI);
}

5) 동작 추가

위와 같은 방식으로 여러 운동 동작들을 인식할 수 있도록 만들고, 원하는 동작을 선택해 인식할 수 있게 하였습니다.

import hajung from "./detect-pose/hajung";
import shoulderStretching from "./detect-pose/shoulderStretching";
import waistStretching from "./detect-pose/waistStretching";
import legStretching from "./detect-pose/legStretching";
import legStretching2 from "./detect-pose/legStretching2";

export default function EstimatePose(action) {
  switch (action) {
    case "ShoulderStretching":
      return shoulderStretching();
    case "WaistStretching":
      return waistStretching();
    case "LegStretching":
      return legStretching();
    case "LegStretching2":
      return legStretching2();
  }
}

결과 예

위와 같은 동작 인식 기능을 통해, 아래와 같이

특정 동작을 정확히 수행하는지
몇 번 수행했는지

등을 확인 할 수 있습니다.

프로토타입

아래는 현재 이 프로젝트의 웹 프로토타입의 url입니다.

https://pe-assistant.vercel.app/

현제 개발 중이며, 고도화 과정 중에 있습니다.

코드 실행

아래 링크를 통해 github에서 코드를 확인할 수 있습니다.

git clone https://github.com/chorom-ham/pe-assistant

실행해보고 싶을 경우, 아래 명령어를 사용하시면 됩니다.

npm install
npm run dev

좋은 웹페이지 즐겨찾기