前言

昨晚看了以下许久没有访问的博客,发现最近一篇博客居然是今年3月份的一份AGC题解。还记得大一的时候,写博客的热情高涨,恨不得日更三篇;上了大二之后,一方面是各方面的压力变大了,另一方面是自己在这方面越来越懒,于是出现了四个月没写过一篇文章的情况。这学期上多媒体课程,期末大作业和刘总一起做了一个基于手势识别的人机交互小程序,就拿来水一水

介绍

看标题就知道,这是一个通过识别手势来完成一些电脑操作的东西。因为是一个桌面应用,同时为了界面好看(好看是第一生产力.jpg),我们选择了electron。首先是用python+Tensorflow训练一个CNN用于手势识别,并导出该模型为json格式;然后,我们在electron中使用opencv.js处理图像,并把上一步训练得到的json格式模型导入到Tensorflow.js中做手势识别;最后,借助robot.js来实现操控电脑。

放一下结构图

之所以要分离出前端和服务端,是因为robot.js要塞进electron里面比较麻烦,尝试了好几次之后都没有成功,于是索性不把他放到electron里面。这样一来,避免了配置上的麻烦,而且结构更清晰,代码写起来也更方便。

目前仅支持在windows上使用,可以完成的操作有:

  • 切换窗口
  • 隐藏窗口
  • 静音/恢复音量
  • 关闭当前窗口
  • 上、下、左、右四个方向键

模型搭建

这一步主要就是采集训练样本,然后使用Opencv处理图像,最后扔进Tensorflow里训练。

先说一下采集训练样本以及处理图像这两部分。通过Opencv调用摄像头,每0.1秒截取一次图像,然后用Opencv处理这一图像。所做的处理包括:

完成上面这些步骤后,就可以得到原始的训练样本了。

接着还要处理得到测试集与训练集。这里做的事情包括:

  • 改变原始图片尺寸

    将尺寸从\(300\times 300\)改成\(128\times 128\)

  • 归一化

    将图片中每个像素的值除以255。

  • 划分测试集与训练集

最后搭建CNN并训练了。CNN的结构如下:

Layer Width Height Filter Kernel Size
Input 128 128 - -
Convolution 128 128 32 3×3
Max Pooling 64 64 32 2×2
Convolution 64 64 64 3×3
Max Pooling 32 32 64 2×2
Convolution 32 32 128 3×3
Max Pooling 16 16 128 2×2
Flatten 32768 - - -
Dense 64 - - -
Dropout 64 - - -
Dense 9 - - -

最终训练结果:

效果还不错(至少目前来说)

前端

前端部分做的事情除了展示界面,还包括以下这些:

服务端

这一部分通过TCP协议获取前端的手势识别结果,并根据这一结果,通过调用robot.js来完成相应操作。

服务端代码:

const net = require('net')
const robot = require('robotjs')
let controlable = false//解锁
let directable = false//开启上下左右键
let key_set = []
function clear_key() {
  for (let index = 0; index < key_set.length; index++) {
    robot.keyToggle(key_set[index], 'up');
  }
  key_set = []
}

function minimize() {
  console.log('minimize')
  pre_gesture = 7;
  robot.keyToggle('command', 'down')
  robot.keyTap('D')
  robot.keyToggle('command', 'up')

}
function closeWindow() {
  console.log('closeWin')
  pre_gesture = 4;
  robot.keyToggle('alt', 'down')
  robot.keyTap('f4')
  robot.keyToggle('alt', 'up')
  
}
function altTab() {
  console.log('altTab')
  pre_gesture = 8;
  robot.keyToggle('alt', 'down')
  key_set.push('alt')
  robot.keyTap('tab')
}
function re_altTab() {
  console.log('re_altTab')
  pre_gesture = 9;
  robot.keyToggle('alt', 'down')
  key_set.push('alt')
  robot.keyToggle('shift', 'down')
  key_set.push('shift')
  robot.keyTap('tab')
}
function audioMute() {
  console.log('audio_mute')
  robot.keyTap('audio_mute');
}

let pre_gesture;
const server = net.createServer(function (sock) {
  sock.on('close', function () {
    console.log('close socket')
    server.close()
  })
  sock.on('data', function (data) {
    console.log('ok!')
    console.log(data.toString())
    let stringifyData = data.toString()
    if (stringifyData === '5') {
      directable = false;
      if (controlable) {
        controlable = false;
        clear_key();
      } else {
        controlable = true;
      }
      pre_gesture = stringifyData;
    }
    if (controlable) {
      if (directable) {
        if (stringifyData === '1') {
          console.log('up')
          robot.keyTap('up')
        } else if (stringifyData === '7') {
          console.log('down')
          robot.keyTap('down')
        } else if (stringifyData === '8') {
          console.log('right')
          robot.keyTap('right')
        } else if (stringifyData === '9') {
          console.log('left')
          robot.keyTap('left')
        }
      } else {
        if (stringifyData != pre_gesture) {
          clear_key()
        }
        if (stringifyData === '6') {
          audioMute()
        } else if (stringifyData === '7') {
          minimize()
        } else if (stringifyData === '4') {
          directable = true;
        } else if (stringifyData === '2') {
          closeWindow()
        } else if (stringifyData === '9') {
          re_altTab()
        } else if (stringifyData === '8') {
          altTab()
        }
      }
    }
  })
})

server.on('listening', function () {
  console.log('start listening')
})

server.on('error', function () {
  console.log('listen error')
})

server.on('close', function () {
  console.log('stop listening')
})

server.listen({
  port: 6080,
  host: '127.0.0.1',
  exclusive: true
})

electron的主进程中连接服务端并发送手势识别结果

最终实现效果

为了避免误操作,一开始是上锁状态,无法操作

手势5解锁

无法识别手势,需要调整手势