Mediapipe-前端机器学习之人体姿势检测

3,301 阅读6分钟

“之前写过一个项目是关于检测人体姿势:使用mediapipe检测仰卧起坐并记录个数。在此记录一下使用笔记。”

Mediapipe是Google推出的一个关于机器学习的开源免费的项目。是个挺好玩的项目,它可以对人脸、人体、人体姿势、手、头发等进行检测处理。具体使用方式请查看官网。它已运用于Google的多个项目中。Mediapipe可以集成到Android、IOS、Python、C++、JavaScript环境中,在这我介绍的是Mediapipe的人体检测在Vue项目中的使用方法。

GitHub地址:google/mediapipe。官网地址:Home Mediapipe

安装

npm i @mediapipe/camera_utils @mediapipe/drawing_utils @mediapipe/control_utils @mediapipe/pose -S

Mediapipe为JavaScript环境提供了相应的npm包, 有@mediapipe/camera_utils、@mediapipe/drawing_utils、@mediapipe/control_utils、@mediapipe/control_utils_3d和相对应的检测包,比如人体姿势的@mediapipe/pose,手势的@mediapipe/hands,你可以根据你的需求安装对应的包。 一般来讲@mediapipe/camera_utils、@mediapipe/drawing_utils这两个包是必须的。当你需要显示3D坐标系时@mediapipe/control_utils包是必要的。

注意:你的电脑一定要有摄像头。

人体姿势检测

因为我们是对人体姿势进行检测,根据官网提供的示例代码,我们新建个PoseMonitor.vue文件,然后把代码移植进去。代码如下:

<template>
  <div class="container">
    <video
      ref="input_video"
      class="input_video"
    ></video>
    <canvas
      class="output_canvas"
      ref="output_canvas"
      :width="boxWidth"
      :height="boxHeight"
    ></canvas>
  </div>
</template>
<script>
import { Camera } from "@mediapipe/camera_utils";
import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils";
import { Pose, POSE_CONNECTIONS } from "@mediapipe/pose";

export default {
  name: "PoseMonitorPage",
  data() {
    return {
      ctx: null,
      canvasElement: '',
    };
  },
  props: {
    boxWidth: {
      type: Number,
      require: true
    },
    boxHeight: {
      type: Number,
      require: true
    }
  },
  computed: {
    inputVideo() {
      return this.$refs.input_video;
    },
  },
  mounted() {
    this.$nextTick(() => {
      this.canvasElement = document.getElementsByClassName('output_canvas')[0];
      this.ctx = this.canvasElement.getContext("2d");
      this.init();
    })
  },
  beforeDestroy() {
    this.ctx = null;
    this.canvasElement = null;
  },
  methods: {
    init() {
      const pose = new Pose({locateFile: (file) => {
        // return `/poseFile/${file}`  加载本地文件
        return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;
      }});
      pose.setOptions({
        modelComplexity: 1,
        smoothLandmarks: true,
        enableSegmentation: false,
        smoothSegmentation: false,
        minDetectionConfidence: 0.5,
        minTrackingConfidence: 0.5
      });
      pose.onResults(this.onResults);

      const camera = new Camera(this.inputVideo, {
        onFrame: async () => {
          await pose.send({ image: this.inputVideo });
        },
        width: this.boxWidth,
        height: this.boxHeight
      });
      camera.start();
    },
    onResults(results) {
      if (!results.poseLandmarks) return;
      
      const canvasElement = this.canvasElement;
      const canvasCtx = this.ctx;

      canvasCtx.save();
      canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
      // canvasCtx.drawImage(results.poseLandmarks, 0, 0,
      //                     canvasElement.width, canvasElement.height);
    
      // Only overwrite existing pixels.
      canvasCtx.globalCompositeOperation = 'source-in';
      canvasCtx.fillStyle = '#00FF00';
      canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
    
      // Only overwrite missing pixels.
      canvasCtx.globalCompositeOperation = 'destination-atop';
      canvasCtx.drawImage(
          results.image, 0, 0, canvasElement.width, canvasElement.height);
    
      canvasCtx.globalCompositeOperation = 'source-over';
      drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS,
                     {color: '#00FF00', lineWidth: 4});
      drawLandmarks(canvasCtx, results.poseLandmarks,
                    {color: '#FF0000', lineWidth: 2});
      canvasCtx.restore();
    }
  },
};
</script>

可以看到以上代码虽然很简单,只需导入相对应的包,以及一个videocanvas标签即可。但有几个注意的点。

1、videocanvas是必不可少的,且video可以用css设置为不可见,video主要是采集图像,最后检测结果是通过canvas展示出来的。

2、在这我检测的是人体姿势,在new Pose时会进行文件下载,即这句代码return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;由于下载的文件多且模型文件比较大,所以第一次启动时会比较慢。你可以先把模型等文件下载到本地,放到public文件夹下,然后加载本地文件以加快初次启动速度。

3、传入Camera对象的是video元素,同时可以通过选项自定Camera的大小,这个就是我们看到检测结果区域面板的大小。

4、onResults函数会频繁调用,切记不要在里面做复杂的计算,同时不要在里面更新响应式的数据,避免卡顿严重。

5、可以使用drawLandmarksdrawConnectors控制绘制点和线的样式。

关于Pose配置参数的说明:

STATIC_IMAGE_MODE:如果设置为 false,该解决方案会将输入图像视为视频流。它将尝试在第一张图像中检测最突出的人,并在成功检测后进一步定位姿势地标。在随后的图像中,它只是简单地跟踪那些地标,而不会调用另一个检测,直到它失去跟踪,以减少计算和延迟。如果设置为 true,则人员检测会运行每个输入图像,非常适合处理一批静态的、可能不相关的图像。默认为false。

MODEL_COMPLEXITY:姿势地标模型的复杂度:0、1 或 2。地标准确度和推理延迟通常随着模型复杂度的增加而增加。默认为 1。

SMOOTH_LANDMARKS:如果设置为true,解决方案过滤不同的输入图像上的姿势地标以减少抖动,但如果static_image_mode也设置为true则忽略。默认为true。

UPPER_BODY_ONLY:是要追踪33个地标的全部姿势地标还是只有25个上半身的姿势地标。

ENABLE_SEGMENTATION:如果设置为 true,除了姿势地标之外,该解决方案还会生成分割掩码。默认为false。

SMOOTH_SEGMENTATION:如果设置为true,解决方案过滤不同的输入图像上的分割掩码以减少抖动,但如果 enable_segmentation设置为false或者 static_image_mode设置为true则忽略。默认为true。

MIN_DETECTION_CONFIDENCE:来自人员检测模型的最小置信值 ([0.0, 1.0]),用于将检测视为成功。默认为 0.5。

MIN_TRACKING_CONFIDENCE:来自地标跟踪模型的最小置信值 ([0.0, 1.0]),用于将被视为成功跟踪的姿势地标,否则将在下一个输入图像上自动调用人物检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 static_image_mode 为 true,则忽略,人员检测在每个图像上运行。默认为 0.5。

仰卧起坐个数统计

这里我们要对仰卧起坐的个数进行统计,那启动成功后要如何计算个数呢?

在你启动的后输出在canvas上的图像你会看到很多的点以及连线,这些点就是坐标点,我们可以拿这些点,通过3个点之间的夹角计算出人体是躺下还是坐起,以此计算仰卧起坐的个数。

那要如何获取对应的坐标点呢?官方提供了一个坐标图,详细的标明每个点对应的坐标索引。图如下:

pose_tracking_full_body_landmarks.png 比如我们要获取左肩的点,代码如下:

// 在 onResults 函数中
const [x, y, z, visibility] = results.poseLandmarks[12];

判断仰卧起坐即判断肩、腰、膝盖三个点的夹角,即点【12,24,26】或【11,23,25】。我们可以使用Math.atan2计算出3个点之间的夹角,由于计算出的夹角是弧度制的,所以还需转换为角度制

弧度和角度换算如下:

1度 = π/180

弧度= 角度 * Math.PI / 180;

角度 = 弧度 * 180 / Math.PI;

仰卧起坐计数代码如下:

let stage = 'DOWN';
let counter = 0;

// 获取角度
findAngle(poseLandmarks, point = [11, 23, 25]) {
  // 获取人体姿势的3个点
  const p1 = poseLandmarks[point[0]];
  const p2 = poseLandmarks[point[1]];
  const p3 = poseLandmarks[point[2]];

  // 获取3个点p1-p2-p3之间的角度,以p2为角的角度值,0-180度之间
  let angle = this.radiansToDegrees(
    Math.atan2(p1.y - p2.y, p1.x - p2.x) - Math.atan2(p3.y - p2.y, p3.x - p2.x)
  );
  angle = Number(angle);
  if (angle > 180) {
    angle = 360 - angle;
  }
  if (angle < 0) {
    angle += 30;
  }
  return angle;
}
// 检测动作,获取次数
findBehavior(poseLandmarks) {
  const angle = this.findAngle(poseLandmarks);
  // 角度大于120度默认为躺下
  if (angle >= 120) {
    stage = 'DOWN';
  }
  // 角度小于55度默认为坐起
  if (angle > 0 && angle <= 55 && stage == "DOWN") {
    stage = 'UP';
    counter += 1;
  }
  const canvasCtx = this.ctx;
  canvasCtx.save();
  canvasCtx.font = "26px Arial";
  canvasCtx.fillStyle = "red";
  canvasCtx.fillText("个数: " + counter.toString(), 20, 35);
  canvasCtx.restore();

  return counter;
}
// 弧度转角度
radiansToDegrees(radians) {
  var pi = Math.PI;
  return radians / (pi / 180);
}

在 onResults 函数中调用

onResults(results) {
  // ...省略代码
  this.findBehavior(results.poseLandmarks);
}

点,线条样式处理

canvas中显示点线你可能会觉得不够好看,你可以通过drawLandmarksdrawConnectors修改样式,其中drawLandmarks控制的是点的样式。比如在onResults加入如下代码看效果:

drawLandmarks(
    canvasCtx,
    Object.values(POSE_LANDMARKS_LEFT)
    .map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: zColor, fillColor: 'rgb(255,138,0)' });
drawLandmarks(
    canvasCtx,
    Object.values(POSE_LANDMARKS_RIGHT)
    .map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: zColor, fillColor: 'rgb(0,217,231)' });
drawLandmarks(
    canvasCtx,
    Object.values(POSE_LANDMARKS_NEUTRAL)
    .map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: zColor, fillColor: 'white' });

如果你想在对应的点上显示对应的文字,比如夹角等,可以这样做:

// 在指定的点坐标绘制
const p = poseLandmarks[0];
let c = canvasCtx.canvas;
let path = new Path2D();
path.arc(p.x * c.width, p.y * c.height, 20, 0, 2 * Math.PI);
canvasCtx.stroke(path);
canvasCtx.fillText('45°', p.x * c.width, p.y * c.height);

其它

如果你要关闭摄像头可以用一个字段保存Camera的实例,然后在调用stop函数。比如:

if (this.cameraInstance) {
    this.cameraInstance.stop();
}

开发时要保持程序的健壮性是很有必要的。这里摄像头检测是必须的,我们可以使用DetectRTC检测用户是否安装了摄像头。代码如下:

DetectRTC.load(function recLoaded() {
  if (DetectRTC.hasWebcam) {
    // TODO: you code
  } else {
    alert("你还没有安装摄像头,请安装摄像头");
  }
})

参考:

GitHub地址:google/mediapipe

官网地址:Home Mediapipe

姿势:Pose例子

CodePan