“之前写过一个项目是关于检测人体姿势:使用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>
可以看到以上代码虽然很简单,只需导入相对应的包,以及一个video和canvas标签即可。但有几个注意的点。
1、video和canvas是必不可少的,且video可以用css设置为不可见,video主要是采集图像,最后检测结果是通过canvas展示出来的。
2、在这我检测的是人体姿势,在new Pose时会进行文件下载,即这句代码return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;由于下载的文件多且模型文件比较大,所以第一次启动时会比较慢。你可以先把模型等文件下载到本地,放到public文件夹下,然后加载本地文件以加快初次启动速度。
3、传入Camera对象的是video元素,同时可以通过选项自定Camera的大小,这个就是我们看到检测结果区域面板的大小。
4、onResults函数会频繁调用,切记不要在里面做复杂的计算,同时不要在里面更新响应式的数据,避免卡顿严重。
5、可以使用drawLandmarks和drawConnectors控制绘制点和线的样式。
关于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个点之间的夹角计算出人体是躺下还是坐起,以此计算仰卧起坐的个数。
那要如何获取对应的坐标点呢?官方提供了一个坐标图,详细的标明每个点对应的坐标索引。图如下:
比如我们要获取左肩的点,代码如下:
// 在 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中显示点线你可能会觉得不够好看,你可以通过drawLandmarks和drawConnectors修改样式,其中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例子