[项目]头戴式肌电鼠标 精华

41312浏览
查看: 41312|回复: 33

[项目] 头戴式肌电鼠标

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


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

硬件列表:
  
名称
  
购买链接
Arduino Leonardo
OYMotion肌电模块
即将在DF上开放购买
  
(附OYMotion官方论坛 https://www.oymotion.com/)
6轴IMU
https://item.taobao.com/item.htm?spm=a1z09.2.0.0.1f30a53fr82qKo&id=43511(链接已失效)  
语音模块
蜂鸣器

好了,开工!!

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

好了,废话不多说,上图!这是原始模块

头戴式肌电鼠标图1

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

头戴式肌电鼠标图2

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

头戴式肌电鼠标图3

头戴式肌电鼠标图4

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

头戴式肌电鼠标图5

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

头戴式肌电鼠标图6

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

头戴式肌电鼠标图7

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

头戴式肌电鼠标图8

头戴式肌电鼠标图9

来来来,上视频!!
https://v.ku6.com/show/nHQQRFinso0eOYOBXCbszw...html
(此视频消失在了茫茫人海中)

再来!上代码!
[mw_shl_code=cpp,true]#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=(float)JY901.stcAngle.Angle/32768*180;
      //Angles=(float)JY901.stcGyro.w/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);
  }
  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=musle_data[i+1]-musle_data;
      musle_data=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;
    if(musle_data < MIN_VALUE || musle_data > MAX_VALUE){
      return STD_ABNORMAL;
    }
  }
  average = sum/DATA_SIZE;
  for(int i=0; i<DATA_SIZE; i++){
    std+=(diff_data-average)*(diff_data-average);
  }
  std=sqrt(std/(DATA_SIZE-1));
  return std;
}[/mw_shl_code]

tutorials

今天的小马同学  学徒

发表于 2020-11-17 23:03:15

xijiajie123 发表于 2020-2-3 14:26
不能直接copy,需要在Arduino环境下导入几个特定的模块的库,具体你可以在我链接里找一下 ...

请问,我试过了导入LD3320的库,然后删除了GY901的代码,还是编译不过。主要是296行和297行的diff_data和musle_data两个变量是在哪里定义的呢?看起来不是数组变量。
回复

使用道具 举报

想成为蟑螂恶霸  见习技师

发表于 2020-3-2 19:38:18

大神你好,想要问一下,这个肌电传感器可以用在STM32开发板子上吗,我没用过Arduino,因为要做毕设所以需要用到这个模块,看网上其他的模块都不咋样,但是我看了你的帖子之后还有模块的资料以后发现需要导入固定的库,所以来请教一下,多有打扰,请看到一定要回复一下我
回复

使用道具 举报

xijiajie123  初级技师
 楼主|

发表于 2020-2-3 14:26:47

DFB1M-nCHJB 发表于 2019-12-25 19:25
请问是用ide编程的吗,为什么我编译时出现各种不匹配的错误

不能直接copy,需要在Arduino环境下导入几个特定的模块的库,具体你可以在我链接里找一下
回复

使用道具 举报

Forgotten  版主

发表于 2017-8-2 18:30:08

厉害 控制精度有点难
回复

使用道具 举报

#嘉诚欧巴#  高级技师

发表于 2017-8-3 00:12:28

66666666666
回复

使用道具 举报

mickey  NPC

发表于 2017-8-3 14:17:40

本帖最后由 mickey 于 2017-8-3 14:53 编辑

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

使用道具 举报

xijiajie123  初级技师
 楼主|

发表于 2017-8-3 16:58:11

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

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

使用道具 举报

xijiajie123  初级技师
 楼主|

发表于 2017-8-3 16:59:15

Forgotten 发表于 2017-8-2 18:30
厉害 控制精度有点难

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

使用道具 举报

xiaoyanflora  学徒

发表于 2017-9-8 12:57:17

膜拜下大神,请问哪里还能买到您这个OYmotion呢?急求啊
回复

使用道具 举报

xijiajie123  初级技师
 楼主|

发表于 2017-9-13 15:53:27

xiaoyanflora 发表于 2017-9-8 12:57
膜拜下大神,请问哪里还能买到您这个OYmotion呢?急求啊

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

使用道具 举报

xiaoyanflora  学徒

发表于 2017-9-14 08:57:47

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

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

使用道具 举报

xijiajie123  初级技师
 楼主|

发表于 2017-9-21 10:16:25

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

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

使用道具 举报

kejixiaobai  学徒

发表于 2017-9-21 10:58:49

你好,请问楼主这个臂环拆开好不好接线???
回复

使用道具 举报

xijiajie123  初级技师
 楼主|

发表于 2017-9-21 16:19:22

kejixiaobai 发表于 2017-9-21 10:58
你好,请问楼主这个臂环拆开好不好接线???

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

使用道具 举报

RickyW  见习技师

发表于 2017-9-21 19:56:30

请问这个传感器现在上市了吗?有什么渠道可以购买到?
回复

使用道具 举报

xijiajie123  初级技师
 楼主|

发表于 2017-9-22 12:49:24

RickyW 发表于 2017-9-21 19:56
请问这个传感器现在上市了吗?有什么渠道可以购买到?

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

使用道具 举报

xiaoyanflora  学徒

发表于 2017-10-31 15:08:52

xijiajie123 发表于 2017-9-22 12:49
请看一下上面的留言,厂家那边说大概十月份会上线,DF官网上会有售卖的

等急了,还木有上线~呜呜
回复

使用道具 举报

rzyzzxw  版主

发表于 2017-11-5 23:17:31

厉害厉害厉害
回复

使用道具 举报

Ash  管理员

发表于 2017-11-7 18:03:13

回复

使用道具 举报

Ash  管理员

发表于 2017-11-7 18:03:32

RickyW 发表于 2017-9-21 19:56
请问这个传感器现在上市了吗?有什么渠道可以购买到?

上线了~ https://www.dfrobot.com.cn/goods-1503.html
回复

使用道具 举报

VaeGreen  学徒

发表于 2018-4-29 20:18:27

原始信号中的1.46~1.54V、120HZ左右的放松状态肌电信号特别多,而且无法滤除,这是什么原因导致的?头戴式肌电鼠标图1头戴式肌电鼠标图2


回复

使用道具 举报

pATAq  版主

发表于 2018-5-1 16:33:59

这个有点酷
回复

使用道具 举报

Oliver  学徒

发表于 2018-5-25 14:33:12

楼主,这个视频呢
回复

使用道具 举报

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

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail