100-app-challenge 第二期 GestureGo手势识别

我的《100-app-challenge》挑战开发100款应用开始啦!持续更新中,可以关注我的专栏了解更多。

之前看到 手势隔空控制,比划就行!| Lazyeat 实现了通过摄像头识别手势来操作电脑,非常有趣,一直想做一个类似的但是更符合我的需求的工具,正好趁这次100-app-challenge,实现了一个浏览器插件------GestureGo手势控制

介绍

GestureGo手势控制是一个能够通过手势操控网页的浏览器插件,在你不方便点击鼠标时,启用此插件,就可轻松使用手指手势 ,实现移动鼠标、滑动、点击网页等操作。

下载

开源地址:github.com/100-app-cha...

已上架edge浏览器扩展商店:microsoftedge.microsoft.com/addons/deta...

截图

移动鼠标

滚动页面

点击页面跳转

暂停/停止识别

实现原理

1、在前端的content-script中,实时收集摄像头内容,转换成图片序列 ,发送至backgorund后端

ts 复制代码
async captureFrame(video: HTMLVideoElement) {
    try {
      if (!(video.videoWidth > 0 && video.videoHeight > 0))
        return
      if (!this.ctx) {
        return
      }
      if (!this.port) {
        return
      }
      if (this.canvas.width !== video.videoWidth || this.canvas.height !== video.videoHeight) {
        this.canvas.width = video.videoWidth
        this.canvas.height = video.videoHeight
      }
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
      this.ctx!.drawImage(video, 0, 0)
      const imageData = this.ctx!.getImageData(0, 0, this.canvas.width, this.canvas.height)
      const hexArray = this.imageDataToHexArrayOptimized(imageData)
      if (this.isAllBlack(hexArray)) {
        console.warn('black frame detected. Skipping...')
        return
      }
      const currentFrame = hexArray
      let message: any

      if (this.frameCounter % 30 === 0 || !this.previousFrame) {
        this.previousFrame = currentFrame
        this.frameCounter = 1
        message = {
          type: 'frame-full',
          data: currentFrame,
          width: this.canvas.width,
          height: this.canvas.height,
          timestamp: performance.now(),
        }
      }
      else {
        const diffData = this.calculateFrameDiff(currentFrame)
        this.previousFrame = currentFrame
        if (diffData.length >= 320 * 240) {
          this.frameCounter = 1
          message = {
            type: 'frame-full',
            data: currentFrame,
            width: this.canvas.width,
            height: this.canvas.height,
            timestamp: performance.now(),
          }
        }
        else {
          this.frameCounter += 1
          message = {
            type: 'frame-diff',
            data: diffData,
            width: this.canvas.width,
            height: this.canvas.height,
          }
        }
      }
      this.port?.postMessage({
        type: 'frame',
        data: message,
      })
    }
    catch (error) {
      console.error('captureFrame error:', error)
    }
  }

2、后端background,接收数据,调用mediapipe识别,判断手势 ,将结果返回content-script

ts 复制代码
browser.runtime.onConnect.addListener((port) => {
      if (port.name === 'handTracking') {
        this.activeConnections.add(port)
        const canvas = new OffscreenCanvas(320, 240)
        const ctx = canvas.getContext('2d', {
          willReadFrequently: true,
        })

        port.onMessage.addListener(async (message: any) => {
          if (message.type === 'frame' && this.detector) {
            const encodedData = message.data
            if (!this.currentFrame || encodedData.type === 'frame-full') {
            // 完整帧
              this.currentFrame = encodedData.data
              canvas.width = encodedData.width
              canvas.height = encodedData.height
            }
            else {
              // 差值帧 - 应用变化
              const diffData = encodedData.data
              for (let i = 0; i < diffData.length; i += 2) {
                this.currentFrame[diffData[i]] = diffData[i + 1]
              }
            }

            // 创建 ImageData 并填充数据
            let imageData = ctx!.createImageData(canvas.width, canvas.height)
            imageData = this.hexArrayToImageDataOptimized(imageData, this.currentFrame!)
            ctx!.putImageData(imageData, 0, 0)

            // 直接使用 canvas 作为识别输入,而不是 imageData
            const result = this.detector.recognize(canvas)
            const detection: DetectionResult = {
            }

            if (result.landmarks && result.handedness) {
              for (let i = 0; i < result.landmarks.length; i++) {
                const hand: HandInfo = {
                  landmarks: result.landmarks[i],
                  handedness: result.handedness[i][0].categoryName as 'Left' | 'Right',
                  score: result.handedness[i][0].score,
                }

                if (result.gestures.length > 0) {
                  hand.categoryName = result.gestures[0][0].categoryName
                }

                if (hand.handedness === 'Left') {
                  detection.leftHand = hand
                }
                else {
                  detection.rightHand = hand
                }
              }
            }
            if (useHorizontalMirror.data.value) {
              detection.leftHand?.landmarks.forEach((landmark) => {
                landmark.x = 1 - landmark.x
              })
              detection.rightHand?.landmarks.forEach((landmark) => {
                landmark.x = 1 - landmark.x
              })
            }
            port.postMessage({
              type: 'handResult',
              data: detection,
              success: true,
            })
            const hand = detection.leftHand || detection.rightHand
            if (hand) {
              const gesture = this.gestureDetector.processHandLandmarks(hand)
              port.postMessage({
                type: 'gesture',
                data: gesture,
                success: true,
              })
            }
          }
        })

        port.onDisconnect.addListener(() => {
          this.activeConnections.delete(port)
        })
      }
    }

3、contentscript根据返回手势,进行显示和对应的手势操作

ts 复制代码
watch(handlandMark, (newVal) => {
  if (pauseMe.data.value) {
    return
  }
  if (newVal && canvasElement.value) {
    const ctx = canvasElement.value.getContext('2d')
    if (!ctx) {
      return
    }
    if (!drawingUtils.value) {
      drawingUtils.value = new DrawingUtils(ctx)
    }

    ctx.clearRect(0, 0, 640, 480)
    const hand = newVal.rightHand ? newVal.rightHand : newVal.leftHand
    if (hand) {
      drawingUtils.value.drawConnectors(
        hand.landmarks,
        GestureRecognizer.HAND_CONNECTIONS,
        {
          color: '#00ffcc55',
          lineWidth: 2,
        },
      )
      drawingUtils.value.drawLandmarks(hand.landmarks, {
        color: '#00ffcc99',
        lineWidth: 1.2,
      })
      const w = canvasElement.value.width
      const h = canvasElement.value.height

      ctx.strokeStyle = '#00ffcc38' // 发光边框颜色
      ctx.lineWidth = 4
      ctx.strokeRect(w * 0.25, h * 0.25, w * 0.5, h * 0.5)
    }
  }
})
相关推荐
Irene19913 分钟前
ElementPlus 与成熟后台框架对比:vue-element-plus-admin、vue-pure-admin等
前端·ui·框架·vue3
尘中客4 小时前
放弃 Echarts?前端直接渲染后端高精度 SVG 矢量图流的踩坑记录
前端·javascript·echarts·前端开发·svg矢量图·echarts避坑
FreeBuf_4 小时前
Chrome 0Day漏洞遭野外利用
前端·chrome
小彭努力中5 小时前
199.Vue3 + OpenLayers 实现:点击 / 拖动地图播放音频
前端·vue.js·音视频·openlayers·animate
2501_916007475 小时前
网站爬虫原理,基于浏览器点击行为还原可接口请求
前端·javascript·爬虫·ios·小程序·uni-app·iphone
前端大波5 小时前
Sentry 每日错误巡检自动化:设计思路与上手实战
前端·自动化·sentry
ZC跨境爬虫6 小时前
使用Claude Code开发校园交友平台前端UI全记录(含架构、坑点、登录逻辑及算法)
前端·ui·架构
慧一居士6 小时前
Vue项目中,何时使用布局、子组件嵌套、插槽 对应的使用场景,和完整的使用示例
前端·vue.js
Можно6 小时前
uni.request 和 axios 的区别?前端请求库全面对比
前端·uni-app
M ? A7 小时前
解决 VuReact 中 ESLint 规则冲突的完整指南
前端·react.js·前端框架