javascript如何实现手势🤚🏻的检测与识别?🤨

1.前言

  • 手势识别与检测的介绍: 手势识别与检测是计算机视觉和人工智能领域的重要研究方向,通过算法让计算机理解人类手势的含义,手势检测(Gesture Detection)表示定位图像与视频中手部的位置,手势识别(GestureRecognition)表示将检测到的手势氛围特定的语言,例如张开手掌,握拳,点赞等手势
  • 前端实现手势的检测与识别的作用: 前端的本质就是用户与计算机的交互行为,手势的检测与识别可以拓展传统的点击行为交互方式

2.实现所使用的技术栈

  • Vue3+Vite+@mediapipe

@mediapipe是由 Google 开发的一个开源跨平台框架,专为​​实时多媒体处理​​(如手势识别、人脸检测、姿态估计等)设计,具有高效、轻量化和多硬件支持的特点,所以可以使用@mediapipe来在前端实现手势识别

3.源码讲解

  • 静态页面搭建
js 复制代码
<template>
  <div id="liveView" class="videoView">
    <video id="webcam" autoplay playsinline></video>
    <canvas class="output_canvas" id="output_canvas"></canvas>
  </div>
  <div class="imgInfo">
    <p>识别到的手势:{{ videoGestureInfo.categoryName }}</p>
    <p>相似度:{{ videoGestureInfo.categoryScore }}</p>
    <p>左右手:{{ videoGestureInfo.handedness }}</p>
  </div>
</template>

<style lang="scss" scoped>
video {
  clear: both;
  display: block;
  transform: rotateY(180deg);
  -webkit-transform: rotateY(180deg);
  -moz-transform: rotateY(180deg);
  width: 100vw;
  height: 100vh;
}

.imgInfo {
  position: fixed;
  top: 0;
  left: 0;
  color: #fff;
  p {
    margin: 10px 0;
  }
}

.videoView {
  display: flex;
  justify-content: center;
  width: 100vw;
  height: 100vh;
  background-color: #000;
  #webcam {
    margin: 0 auto;
    width: 1250px;
    height: 100%;
  }
}

.canvas {
  z-index: 1;
  position: absolute;
  pointer-events: none;
}

.output_canvas {
  position: absolute;
  top: 0;
  margin: 0 auto;
  transform: rotateY(180deg);
  -webkit-transform: rotateY(180deg);
  -moz-transform: rotateY(180deg);
}

.output {
  display: none;
  width: 100%;
  font-size: calc(8px + 1.2vw);
}
</style>
  • 辅助方法
js 复制代码
// 判断是否可以使用摄像头
function hasGetUserMedia() {
  return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}
  • 创建手势识别器
js 复制代码
// 创建手势识别器
const createGestureRecognizer = async () => {
  // 加载指定版本的MediaPipe视觉任务WebAssembly模块
  const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/wasm');

  // 创建了一个手势识别器实例(这个手势识别器实例使用的是指定版本的MediaPipe视觉任务WebAssembly模块)
  gestureRecognizer = await GestureRecognizer.createFromOptions(vision, {
    // 识别器配置
    baseOptions: {
      // 指向手势识别模型的路径
      modelAssetPath: './gesture_recognizer.task',
      // 设置为GPU以尝试利用图形处理单元进行加速,提高模型推理的速度
      delegate: 'GPU',
    },
    // 检测手掌的数量
    numHands: 2,
  });

  console.log('手势识别器加载完毕');
  // 识别视频中的手势
  predictWebcam();
};
  • 识别视频中的手势
js 复制代码
// 识别视频中的手势
const predictWebcam = async () => {
  // 判断是否可以使用摄像头
  if (!hasGetUserMedia()) return alert('此设备不允许使用摄像头!');
  // 判断手势识别器是否加载完成
  if (!gestureRecognizer) return alert('手势识别器未加载完成');

  if (runningMode.value !== 'VIDEO') {
    // 设置识别器识别的类型为视频
    runningMode.value = 'VIDEO';
    await gestureRecognizer.setOptions({ runningMode: runningMode.value });
  }

  await gestureRecognizer.setOptions({ numHands: 2 });

  nextTick(() => {
    // 获取video元素
    const video = document.getElementById('webcam');
    // 获取视频手势节点绘制的canvas元素
    const canvasElement = document.getElementById('output_canvas');
    // 设置canvas的宽度和高度为video的宽度和高度
    canvasElement.width = video.clientWidth;
    canvasElement.height = video.clientHeight;
    // 获取canvas的上下文
    const canvasCtx = canvasElement.getContext('2d');

    // 设置上次识别视频手势的时间
    let lastVideoTime = -1;

    // 识别视频中的手势
    const predictWebcam = () => {
      // 获取当前视频的时间
      let nowInMs = Date.now();
      let results = {};

      // 如果视频的时间发生变化,则识别视频中的手势
      if (video.currentTime !== lastVideoTime) {
        // 替换上次识别视频手势的时间
        lastVideoTime = video.currentTime;
        results = gestureRecognizer.recognizeForVideo(video, nowInMs);
      }

      // 保存当前的canvas状态
      canvasCtx.save();
      // 清除canvas的内容
      canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);

      // 创建drawingUtils实例,用于可视化MediaPipeVision任务的结果
      const drawingUtils = new DrawingUtils(canvasCtx);
      // 判断是否识别到手势
      if (results.landmarks) {
        // 循环绘制手势的节点
        for (const landmarks of results.landmarks) {
          // 绘制手势连接线
          drawingUtils.drawConnectors(landmarks, GestureRecognizer.HAND_CONNECTIONS, {
            // 连接线的颜色
            color: '#00FF00',
            // 连接线的宽度
            lineWidth: 3,
          });
          // 绘制手势关节点
          drawingUtils.drawLandmarks(landmarks, {
            // 关节点的颜色
            color: '#FF0000',
            // 关节点的半径
            radius: 2.5,
          });
        }
      }
      // 恢复canvas的状态
      canvasCtx.restore();

      // 判断是否识别到手势数据
      if (results?.gestures?.length > 0) {
        videoGestureInfo.value.categoryName = enumGesture[results.gestures[0][0].categoryName];
        videoGestureInfo.value.categoryScore = parseFloat(results.gestures[0][0].score * 100).toFixed(2);
        videoGestureInfo.value.handedness = results.handednesses[0][0].displayName;
      } else {
        videoGestureInfo.value.categoryName = '';
        videoGestureInfo.value.categoryScore = '';
        videoGestureInfo.value.handedness = '';
      }

      // 递归调用,继续识别视频中的手势
      requestAnimationFrame(predictWebcam);
    };

    // 打开摄像头
    navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
      // 视频流添加到video元素中
      video.srcObject = stream;
      // 绑定视频加载完成事件,开始识别视频中的手势
      video.addEventListener('loadeddata', predictWebcam);
    });
  });
};
  • 整体js代码
js 复制代码
import { GestureRecognizer, FilesetResolver, DrawingUtils } from './[email protected]';

// 手势识别器实例
let gestureRecognizer;

// 识别器识别的类型(图片/视频)
const runningMode = ref();

// 视频手势信息
const videoGestureInfo = ref({});

// 手势枚举
const enumGesture = {
  Closed_Fist: '握紧拳头',
  Open_Palm: '张开手掌',
  Thumb_Up: '竖起大拇指',
  Thumb_Down: '拇指朝下',
  Pointing_Up: '指向上',
  Victory: '胜利',
  None: '未识别',
};

// 创建手势识别器
const createGestureRecognizer = async () => {
  // 加载指定版本的MediaPipe视觉任务WebAssembly模块
  const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/wasm');

  // 创建了一个手势识别器实例(这个手势识别器实例使用的是指定版本的MediaPipe视觉任务WebAssembly模块)
  gestureRecognizer = await GestureRecognizer.createFromOptions(vision, {
    // 识别器配置
    baseOptions: {
      // 指向手势识别模型的路径
      modelAssetPath: './gesture_recognizer.task',
      // 设置为GPU以尝试利用图形处理单元进行加速,提高模型推理的速度
      delegate: 'GPU',
    },
    // 检测手掌的数量
    numHands: 2,
  });

  console.log('手势识别器加载完毕');
  // 识别视频中的手势
  predictWebcam();
};

// 识别视频中的手势
const predictWebcam = async () => {
  // 判断是否可以使用摄像头
  if (!hasGetUserMedia()) return alert('此设备不允许使用摄像头!');
  // 判断手势识别器是否加载完成
  if (!gestureRecognizer) return alert('手势识别器未加载完成');

  if (runningMode.value !== 'VIDEO') {
    // 设置识别器识别的类型为视频
    runningMode.value = 'VIDEO';
    await gestureRecognizer.setOptions({ runningMode: runningMode.value });
  }

  await gestureRecognizer.setOptions({ numHands: 2 });

  nextTick(() => {
    // 获取video元素
    const video = document.getElementById('webcam');
    // 获取视频手势节点绘制的canvas元素
    const canvasElement = document.getElementById('output_canvas');
    // 设置canvas的宽度和高度为video的宽度和高度
    canvasElement.width = video.clientWidth;
    canvasElement.height = video.clientHeight;
    // 获取canvas的上下文
    const canvasCtx = canvasElement.getContext('2d');

    // 设置上次识别视频手势的时间
    let lastVideoTime = -1;

    // 识别视频中的手势
    const predictWebcam = () => {
      // 获取当前视频的时间
      let nowInMs = Date.now();
      let results = {};

      // 如果视频的时间发生变化,则识别视频中的手势
      if (video.currentTime !== lastVideoTime) {
        // 替换上次识别视频手势的时间
        lastVideoTime = video.currentTime;
        results = gestureRecognizer.recognizeForVideo(video, nowInMs);
      }

      // 保存当前的canvas状态
      canvasCtx.save();
      // 清除canvas的内容
      canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);

      // 创建drawingUtils实例,用于可视化MediaPipeVision任务的结果
      const drawingUtils = new DrawingUtils(canvasCtx);
      // 判断是否识别到手势
      if (results.landmarks) {
        // 循环绘制手势的节点
        for (const landmarks of results.landmarks) {
          // 绘制手势连接线
          drawingUtils.drawConnectors(landmarks, GestureRecognizer.HAND_CONNECTIONS, {
            // 连接线的颜色
            color: '#00FF00',
            // 连接线的宽度
            lineWidth: 3,
          });
          // 绘制手势关节点
          drawingUtils.drawLandmarks(landmarks, {
            // 关节点的颜色
            color: '#FF0000',
            // 关节点的半径
            radius: 2.5,
          });
        }
      }
      // 恢复canvas的状态
      canvasCtx.restore();

      // 判断是否识别到手势数据
      if (results?.gestures?.length > 0) {
        videoGestureInfo.value.categoryName = enumGesture[results.gestures[0][0].categoryName];
        videoGestureInfo.value.categoryScore = parseFloat(results.gestures[0][0].score * 100).toFixed(2);
        videoGestureInfo.value.handedness = results.handednesses[0][0].displayName;
      } else {
        videoGestureInfo.value.categoryName = '';
        videoGestureInfo.value.categoryScore = '';
        videoGestureInfo.value.handedness = '';
      }

      // 递归调用,继续识别视频中的手势
      requestAnimationFrame(predictWebcam);
    };

    // 打开摄像头
    navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
      // 视频流添加到video元素中
      video.srcObject = stream;
      // 绑定视频加载完成事件,开始识别视频中的手势
      video.addEventListener('loadeddata', predictWebcam);
    });
  });
};

// 判断是否可以使用摄像头
function hasGetUserMedia() {
  return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}

onMounted(() => {
  // 加载手势识别器
  createGestureRecognizer();
});

4.效果图展示

5.总结

本文简单介绍了手势检测与识别的概念,以及如何在前端中实现手势的识别与检测,大致流程就是通过Google@mediapipe开源框架实现,先创建手势识别器,之后再将视频的图像传入识别器,得到识别的结果即可

相关推荐
西哥写代码7 分钟前
基于cornerstone3D的dicom影像浏览器 第三十一章 从PACS服务加载图像
javascript·pacs·dicom
风吹头皮凉2 小时前
vue实现气泡词云图
前端·javascript·vue.js
南玖i2 小时前
vue3 + ant 实现 tree默认展开,筛选对应数据打开,简单~直接cv
开发语言·前端·javascript
南枝异客2 小时前
三数之和-力扣
开发语言·javascript·数据结构·算法·leetcode·排序算法
小钻风33662 小时前
深入浅出掌握 Axios(持续更新)
前端·javascript·axios
方圆工作室3 小时前
HTML实现的2048游戏
javascript·游戏·html
vvilkim3 小时前
Flutter 核心概念:深入理解 StatelessWidget 与 StatefulWidget
开发语言·javascript·flutter
sunly_3 小时前
Flutter:导航背景固定在顶部,下拉分页布局
开发语言·javascript·flutter
charlee443 小时前
给Markdown渲染网页增加一个目录组件(Vite+Vditor+Handlebars)(上)
javascript·vite·markdown·vditor·handlebars
BillKu3 小时前
Vue3 + Vite 中使用 Lodash-es 的防抖 debounce 详解
前端·javascript·vue.js