设为首页收藏本站

[项目] 头戴式肌电鼠标

xijiajie123 发表于 2017-8-1 19:23:13 | 显示全部楼层 [复制链接]
13 1915
本帖最后由 xijiajie123 于 2017-8-1 20:12 编辑

为啥要做个头戴式肌电鼠标?先说说想法来源吧,去年参加了蘑菇云的创客大赛,作品就是给残疾人做的无障碍输入设备,超大号的键盘,还拿了一个一等奖,嘚瑟一下~~,详见如下链接:

然而觉得还是对于残疾人来说不够方便,于是在跟一些资深玩家们头脑激荡时,想到了可以用头部输入的方式,简单介绍一下实现方式:
技术实现:用该设备采用运动感应、肌电传感、语音识别等技术,可以实现:
1. 用陀螺仪将头部运动转化为鼠标运动,从而解放双手,帮助双手行动不便及单/双臂缺失的人。
2. 肌肉电传感器检测牙齿咀嚼肌的咬合,实现鼠标单击双击。
3. 语音可选控制/输入模式,控制模式可实现命令控制,如“复制”“粘贴”等;输入模式可将语音转换为文字。从而实现快速控制与输入。
4. 运动感应器,实现坐姿检测、颈椎病预防等功能;

硬件列表:
  
名称
  
购买链接
Arduino Leonardo
OYMotion肌电模块
即将在DF上开放购买
  
(附OYMotion官方论坛http://139.224.25.190:8890/forum.php
6轴IMU
语音模块
蜂鸣器

好了,开工!!

第一步:测试安装肌电传感器
拿到OYMotion的肌电板子,还是有些小激动的,因为这是自己寻找了许久的肌电模块。在这里,情不自禁要为OYMotion打个小广告,自己找遍了市场上的肌电传感器,都是要额外贴一层胶在电极上,完全不是消费级方案,OYMotion就是用的干电极,而且可以实现医疗级的精度。自己有幸认识他们团队,并优先拿到了他们的肌电模块 欧耶~~
好了,废话不多说,上图!这是原始模块

肌电传感器原样

肌电传感器原样

为了减小体积,我们将耳机线路部分移除,用飞线的方式进行传输

改装后的肌电模块

改装后的肌电模块

如何接线呢?很简单:VCC对3.3V, GND-GND, 数据线接A0进行AD转换。 只要一句就可以读到数据data = analogRead(A0);
但如何实现牙齿单击双击呢?首先要大致熟悉HID开发宝贝ArduinoLeonardo,这个部分就不多说了,大家自己看资料就好了。用Arduino采集数据后,通过波形分析,就可实现鼠标的单击双击!波形分析的算法,可以详见代码库的部分。先给大家看一张波形图吧~~,波形图均为咬牙一次+长咬的过程,第一张是原始数据,第二章是加了算法后处理出来的数据(红线代表鼠标按下的过程)。

肌电数据

肌电数据

处理后的肌电数据

处理后的肌电数据
第二步:安装陀螺仪
       用陀螺仪来感知头部运动,是个和取巧的方法。我选用的是市场上现有的陀螺模块JY901,可以直接输出陀螺的角度数据。可以通过角度来算鼠标的移动,算法详见代码。注意:模块一定要水平/竖直位置,头顶部是最佳,这是经验!

IMU模块

IMU模块


那我们用什么做为支架呢?既要有弹性,能夹住肌电传感器,又要佩戴舒适!那,只好牺牲我的头戴式耳机了~~ 三滴泪~~

耳机

耳机


第三步:语音模块
语音模块听上去会让人眉头一紧,其实并不复杂,看到官网给出的Arduino操作,你就恍然大悟了,其实就是通过拼音进行识别嘛~~
语音模块与Arduino的连接,是通过SPI,具体请见http://www.waveshare.net/study/article-11-1.html

语音模块

语音模块


第四步:其他小件的连接:
还需要蜂鸣器、LED等这类的小器件作为提示信息使用的,LED接线,一根接GND一根接IO 13口,蜂鸣器除了VCC GND之外,数据线接IO7
整体安装的具体方式:1 肌电模块在耳机耳蜗中,佩戴时尽量与脸部贴合;2.IMU模块放在头顶部,来感应头部运动;3. 主控模块leonardo放在耳侧;4.语音模块放在主控模块的前部探出来,可以与讲话者更近;5. 蜂鸣器藏在主控板中,LED灯放在语音模块前段,带上后眼睛也可以看到。
好了,效果图如下,怎么样,还不错吧?

效果图1

效果图1


效果图2

效果图2
来来来,上视频!!


再来!上代码!
[C++] 纯文本查看 复制代码
#include <Wire.h>
#include <JY901.h>
#include <Keyboard.h>
#include <Mouse.h>
#include <ld3320.h>

#define PRINT_RAW_DATA 0
#define PRINT_STD_DATA 0
bool mouse_disabled=false;

void setup()
{
  Serial.begin(9600);                        //配置9600               
  voice_init();
  mouse_move_init();
  musle_click_init();
  Mouse.begin();
  Keyboard.begin();
  Serial.print("Initialized\n");
}

void loop() 
{
  handle_voice();
  if(!mouse_disabled){
    mouse_move();
    handle_musle_click();
  }
}

/*---------------------------------------------------------------mouse movement-------------------------------------------------------------------------*/
float Angles[3];
float xLast,yLast;
bool right_clicked=false;
bool left_click=false;

float view_center;
float view_angle=30;

int shake_x,shake_y;
#define SHAKE_LIMIT 16

void mouse_move_init(){
    Serial1.begin(9600);
}
void mouse_move(){
  int x,y;
  float xDelta,yDelta;
  float xNow,yNow;
  float X_RATE=0.06;//0.05;
  float Y_RATE=0.04;//0.05;

  get_IMU_data();
  xNow=Angles[2];
  yNow=Angles[0];
  
  if(xLast==0){
    xLast=xNow;
  }
  if(yLast==0){
    yLast=yNow;
  }
  xDelta=xNow-xLast;
  yDelta=yNow-yLast;

  x=xDelta/X_RATE;
  y=yDelta/Y_RATE;

  if((x!=0 || y!=0)){
    if(xLast*xNow<0){
      x=xLast/abs(xLast)*(360/X_RATE-abs(x));
    }
    int out_x=x, out_y=y;
    if(left_click==true){         //do not let mouse move when left button pressed, in case it's during right pressing.
      shake_x+=x;
      shake_y+=y;
    }else{
      while(abs(out_x)>120){
        out_x=out_x/abs(out_x)*(abs(out_x)-120);
        Mouse.move(-(out_x/abs(out_x)*120), 0);
      }
      while(abs(out_y)>120){
        out_y=out_y/abs(out_y)*(abs(out_y)-120);
        Mouse.move(0,-(out_y/abs(out_y)*120));
      }
      Mouse.move(-out_x, -out_y);
      Serial.print("xNow=");Serial.print(xNow);Serial.print(" yNow=");Serial.print(yNow);
      Serial.print(" xDelta=");Serial.print(xDelta);Serial.print(" yDelta=");Serial.print(yDelta);Serial.print(" x=");Serial.print(x);Serial.print(" y=");Serial.println(y);
    }
  }
  
  xLast=xLast+((int)(xDelta/X_RATE))*X_RATE;
  yLast=yLast+((int)(yDelta/Y_RATE))*Y_RATE;
}

void get_IMU_data(){
  if(Serial1.available()) {
    JY901.CopeSerialData(Serial1.read()); //Call JY901 data cope function
    for(int i=0; i<3; i++){
      Angles[i]=(float)JY901.stcAngle.Angle[i]/32768*180;
      //Angles[i]=(float)JY901.stcGyro.w[i]/32768*2000;
    }
  }
}
/*-------------------------------------------------------voice detect--------------------------------------------------------------------*/

#define buzzerPin 7
#define VOICE_ARRAY   16
bool start_switch = false;
int current_mode=0;
VoiceRecognition Voice;                         //声明一个语音识别对象

char *voices[VOICE_ARRAY]=
{"mo gu yun",            //0
"shu ru mo shi",          //1
"ming ling mo shi",       //2
"xin jian",               //3 ctrl+n
"guan bi",                //4 alt+f4
"fu zhi",                 //5 ctr+c
"zhan tie",               //6 ctr+v
"tian qi hen hao",        //7
"zhong mei chuang ke da sai",//8
"tou kong shu ru she bei", //9
"ni hao",                 //10
"hen kai xin",            //11
"hen shun li",           //12
"da kai shu biao",        //13 鼠标打开
"guan bi shu biao",       //14 鼠标停止
"zheng chang mo shi"      //15 正常模式
};

void voice_init(){
  pinMode(buzzerPin, OUTPUT);
  Voice.init();                               //初始化VoiceRecognition模块   
  for(int i=0; i<VOICE_ARRAY; i++){
    Voice.addCommand(voices[i],i);
  }
  Voice.start();//开始识别
}

void handle_voice(){
  bool start_switch=false;
  int voice_id = voice_control();

  if(current_mode==1){    //输入模式
    if(voice_id>2){
      Keyboard.print(voices[voice_id]);
    }
  }
  if(current_mode==2){    //命令模式
      char ctrlKey = KEY_LEFT_CTRL;
      char altKey = KEY_LEFT_ALT;
      
      switch(voice_id){
        case 3:
          Keyboard.press(ctrlKey);
          Keyboard.press('n');
          delay(10);
          Keyboard.release('n');
          Keyboard.release(ctrlKey);
          break;
        case 4:
          Keyboard.press(altKey);
          Keyboard.press(KEY_F4);
          delay(10);
          Keyboard.release(KEY_F4);
          Keyboard.release(altKey);
          break;
        case 5:
          Keyboard.press(ctrlKey);
          Keyboard.press('c');
          delay(10);
          Keyboard.release('c');
          Keyboard.release(ctrlKey);
          break;
        case 6:
          Keyboard.press(ctrlKey);
          Keyboard.press('v');
          delay(10);
          Keyboard.release('v');
          Keyboard.release(ctrlKey);
          break;
      }
  }
}
int voice_control(){
  int id = Voice.read();
  if(id >= VOICE_ARRAY || id < 0){
    return -1;
  }
  Serial.println(voices[id]);
  switch(id)                          //判断识别
  {
    case 0:                                     //开始指令
        start_switch = true;
        Serial.print("Start command\n");
        tone(buzzerPin, 5000, 200);
        delay(350);
        tone(buzzerPin, 5000, 200);
        break;
    case 1:                                     //输入模式
        if(start_switch){
          current_mode=1;
          start_switch = false;
          Serial.print("input mode\n");
          tone(buzzerPin, 5000, 200);
        }
        break;   
    case 2:                                     //命令模式
        if(start_switch){
          current_mode=2;
          start_switch = false;
          Serial.print("Command mode\n");
          tone(buzzerPin, 5000, 200);
        }
        break;
    case 15:                                     //正常模式
        if(start_switch){
          current_mode=0;
          start_switch = false;
          Serial.print("normal mode\n");
          tone(buzzerPin, 5000, 200);
        }
        break;
    case 14:
        if(start_switch){                         //关闭鼠标
          mouse_disabled=true;
          start_switch = false;
          Serial.print("Mouse disabled\n");
          tone(buzzerPin, 5000, 200);
        }
        break;
    case 13:
        if(start_switch){                         //打开鼠标
          mouse_disabled=false;
          start_switch = false;
          Serial.print("Mouse turn on\n");
          tone(buzzerPin, 5000, 200);
        }
        break;
    default:
        //Serial.print("nothing...");
        start_switch = 0;
        break;
  }
  return id;
}
/*----------------------------------------------------------- musle click---------------------------------------------------------------------------*/

int sensorPin = A0;    // select the input pin for the potentiometer
int sensorData = 0;  // variable to store the value coming from the sensor
int errorPin = 13;
bool errorHappen = false;

int calib_count=0;
long calib_data=0;

unsigned long errorTime=0;
#define DATA_SIZE 15
int musle_data[DATA_SIZE];
int diff_data[DATA_SIZE];
#define MIN_VALUE 150
#define MAX_VALUE 600

#define STD_ABNORMAL 250
#define STD_VALUE 10
#define STD_EDGE 3

unsigned long press_timestamp=0;
unsigned long release_timestamp=0;
int show_press_real=0;
#define PRESS_DELAY 40
#define RELEASE_DELAY 15
#define RIGHT_BT_DETECT 600

#define SAMPLE_DELAY 10
unsigned long last_sample_time=millis();


void musle_click_init() {
  Serial.begin(9600);
  pinMode(errorPin, OUTPUT);
  digitalWrite(errorPin, HIGH);
  errorTime = millis();
  Mouse.begin();
}

void handle_musle_click() {
  if(millis()-last_sample_time > SAMPLE_DELAY){
    sensorData = analogRead(sensorPin);

#if PRINT_RAW_DATA
    Serial.println(sensorData);
#endif
    for(int i=0; i<DATA_SIZE-1; i++){
      diff_data[i]=musle_data[i+1]-musle_data[i];
      musle_data[i]=musle_data[i+1];
    }  diff_data[DATA_SIZE-1]=sensorData-musle_data[DATA_SIZE-1];
    musle_data[DATA_SIZE-1]=sensorData;
    
    handle_musle_data();
    last_sample_time=millis();
  }
}

void handle_musle_data(){
  int show_press=0;
  long int std_value=0;
  
  std_value=get_std();
  if(std_value>STD_VALUE){
    show_press=30;
  }
#if  PRINT_STD_DATA
  Serial.print(show_press);Serial.print(" ");
  Serial.print(show_press_real);Serial.print(" ");
  Serial.println(std_value);
#endif
  if(std_value>=STD_ABNORMAL || std_value<0){
    errorTime = millis();
    digitalWrite(errorPin, HIGH);
    errorHappen = true;
    calib_data=0;
    calib_count=0;
  }
  if(errorHappen){
    if(std_value<STD_VALUE && std_value>=0){   //when error happened, make sure musle std data is below STD_VALUE for 1s.
      calib_data+=std_value;
      calib_count++;
    }else{
      errorTime = millis();
    }

    if(millis()-errorTime > 1500){
      digitalWrite(errorPin, LOW);
      errorHappen = false;
      calib_data=calib_data/calib_count;  //re-calibrate the musle average data.
    }
  }

  if(errorHappen){
    release_timestamp=millis();
    press_timestamp=0;
    show_press_real=0;
    if(Mouse.isPressed(MOUSE_LEFT)){
      Mouse.release(MOUSE_LEFT);
    }
  }

  if(!errorHappen){    
    if(std_value>calib_data+STD_EDGE){
      if(press_timestamp==0){
        press_timestamp=millis();     
      }else{
        unsigned long current_time = millis();
        if (current_time-press_timestamp>PRESS_DELAY && current_time-press_timestamp<=RIGHT_BT_DETECT) {       //make sure left-click can be detected.
          left_click=true;
        }
        if(sqrt(shake_x*shake_x+shake_y*shake_y)>SHAKE_LIMIT){  //when mouse moved, let left button "press"
          if(!Mouse.isPressed(MOUSE_LEFT)){
            Mouse.press(MOUSE_LEFT);
          }
          left_click=false;
        }else{
          if(current_time-press_timestamp>RIGHT_BT_DETECT && right_clicked==false){ //mouse did not move, and is pressed bigger than threshold value.
            Mouse.click(MOUSE_RIGHT);
            right_clicked=true;
            left_click=false;
          }
        }
          release_timestamp=0; 
          show_press_real=calib_data+STD_EDGE;
      }
    }else{
      if(release_timestamp==0){
        release_timestamp=millis();
      }else if (millis()-release_timestamp>RELEASE_DELAY) {       //when musle released, clear all the flags.
          if(left_click==true){
            Mouse.click(MOUSE_LEFT);
          }
          if(Mouse.isPressed(MOUSE_LEFT)){
            Mouse.release(MOUSE_LEFT);
          }
          right_clicked=false;
          left_click = false;
          shake_x=0;
          shake_y=0;
          press_timestamp=0;
          show_press_real=calib_data;
      }
    }
  }
}

long int get_std(){
  int average = 0;
  int sum = 0;
  long int std = 0;
  for(int i=0; i<DATA_SIZE; i++){
    sum+=diff_data[i];
    if(musle_data[i] < MIN_VALUE || musle_data[i] > MAX_VALUE){
      return STD_ABNORMAL;
    }
  }
  average = sum/DATA_SIZE;
  for(int i=0; i<DATA_SIZE; i++){
    std+=(diff_data[i]-average)*(diff_data[i]-average);
  }
  std=sqrt(std/(DATA_SIZE-1));
  return std;
}


发表于 2017-8-2 18:30:08 | 显示全部楼层
厉害 控制精度有点难
回复

使用道具 举报

发表于 2017-8-3 00:12:28 | 显示全部楼层
66666666666
回复

使用道具 举报

发表于 2017-8-3 14:17:40 | 显示全部楼层
本帖最后由 mickey 于 2017-8-3 14:53 编辑

其实你可以用DF的ASR Board主板,直接是带语音识别的Arduino Leonardo主板,一体化,很小巧。
回复

使用道具 举报

发表于 2017-8-3 16:58:11 | 显示全部楼层
mickey 发表于 2017-8-3 14:17
其实你可以用DF的ASR Board主板,直接是带语音识别的Arduino Leonardo主板,一体化,很小巧。 ...

一看就是高手啊!有找到ASR这款板子,它正好集成了HID功能+语音识别。不过当时DF缺货,急着做,就用其他方式了啊~~
回复

使用道具 举报

发表于 2017-8-3 16:59:15 | 显示全部楼层
Forgotten 发表于 2017-8-2 18:30
厉害 控制精度有点难

精度还是可以的,复制粘贴都不成问题。第一次用都还算能适应,如果用久了,更没问题了
回复

使用道具 举报

发表于 2017-9-8 12:57:17 | 显示全部楼层
膜拜下大神,请问哪里还能买到您这个OYmotion呢?急求啊
回复

使用道具 举报

发表于 2017-9-13 15:53:27 | 显示全部楼层
xiaoyanflora 发表于 2017-9-8 12:57
膜拜下大神,请问哪里还能买到您这个OYmotion呢?急求啊

我是认识那家公司的人,所以优先拿到了肌电传感器。这款传感器DF上现在还没上线,不过应该快了~
回复

使用道具 举报

发表于 2017-9-14 08:57:47 | 显示全部楼层
xijiajie123 发表于 2017-9-13 15:53
我是认识那家公司的人,所以优先拿到了肌电传感器。这款传感器DF上现在还没上线,不过应该快了~ ...

我是完全零基础小白,要是不用这款肌电传感,买别家的没有你的案例示范肯定搞不定~~呜
回复

使用道具 举报

发表于 2017-9-21 10:16:25 | 显示全部楼层
xiaoyanflora 发表于 2017-9-14 08:57
我是完全零基础小白,要是不用这款肌电传感,买别家的没有你的案例示范肯定搞不定~~呜 ...

有问过他们,大概十月份会上线,可以期待一下  哈哈
回复

使用道具 举报

发表于 2017-9-21 10:58:49 | 显示全部楼层
你好,请问楼主这个臂环拆开好不好接线???
回复

使用道具 举报

发表于 2017-9-21 16:19:22 | 显示全部楼层
kejixiaobai 发表于 2017-9-21 10:58
你好,请问楼主这个臂环拆开好不好接线???

臂环?这是个耳机啊。如果你问的是肌电传感器拆开好不好接线,答案是:很容易。
回复

使用道具 举报

发表于 2017-9-21 19:56:30 | 显示全部楼层
请问这个传感器现在上市了吗?有什么渠道可以购买到?
回复

使用道具 举报

发表于 2017-9-22 12:49:24 | 显示全部楼层
RickyW 发表于 2017-9-21 19:56
请问这个传感器现在上市了吗?有什么渠道可以购买到?

请看一下上面的留言,厂家那边说大概十月份会上线,DF官网上会有售卖的
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册  

本版积分规则 允许回帖同步到新浪微博  

  • 见习技师
  • 178
  • 1

楼主的其它帖子

推荐阅读

精华导读




公司简介| 联系我们| 小黑屋| 加入我们| 微博| 优酷| 英文网站| DF创客社区 ( 沪ICP备09038501号-4  
友情链接| 硬创邦| 花生壳社区| 模友之吧| 电子发烧友社区| 创客星球| 云汉电子社区| 电子工程网| 与非网| Arduino中文社区| 南极熊3D打印网| OneNET|

上海智位机器人有限公司  沪ICP备09038501号-4   

Powered by Discuz! X3.1

Licensed Comsenz Inc.

返回顶部 返回列表