×

使用EMG的机械手控制

消耗积分:2 | 格式:zip | 大小:0.07 MB | 2022-11-04

386660

分享资料个

描述

我们的团队在机器人手方面有着悠久的历史。有一段时间我们试图制作一个可靠的假手,但对于这个项目,我使用了现有开源手的一个很好的例子:inMoov

我不会详细介绍手动组装 - 在项目网站上对此进行了很好的描述,并且非常复杂。我将在这里专注于控制,因为这是全新的 :)
另外,在下一个项目中查看这项技术如何随着时间的推移而发展

1.信号处理

控制基于 EMG - 肌肉的电活动。EMG 信号由三个 uECG 设备获得(我知道,它应该是 ECG 监视器,但由于它基于通用 ADC,它可以测量任何生物信号 - 包括 EMG)。对于 EMG 处理,uECG 有一个特殊的模式,它发出 32 段频谱数据和“肌肉窗口”平均值(平均频谱强度在 75 和 440 Hz 之间)。光谱图像如下所示:

 
pYYBAGNkdPqAYcO_AAEoqq6LtHs787.png
 

这里的频率在垂直轴上(在 3 个图的每一个上,底部的低频,顶部的高频率 - 从 0 到 488 Hz,步长约为 15 Hz),时间在水平轴上(左侧的旧数据总体在这里屏幕上大约 10 秒)。强度用颜色编码:蓝色 - 低,绿色 - 中,黄色 - 高,红色 - 甚至更高。为了可靠的手势识别,需要对这些图像进行适当的 PC 处理。但是对于机器人手指的简单激活,仅使用 3 个通道上的平均值就足够了 - uECG 可以方便地在某些数据包字节处提供它,以便 Arduino 草图可以解析它。这些值看起来要简单得多:

 
pYYBAGNkdP2AAC8VAABg2rR0G6M637.png
 

当我相应地挤压拇指、无名指和中指时,红色、绿色、蓝色图表是来自不同肌肉群的 uECG 设备的原始值。在我们看来,这些情况显然是不同的,但我们需要以某种方式将这些值转换为“手指分数”,以便程序可以将值输出到手动伺服系统。问题是,来自肌肉群的信号是“混合的”:在第一种和第三种情况下,蓝色信号强度大致相同 - 但红色和绿色不同。在第 2 和第 3 种情况下,绿色信号是相同的 - 但蓝色和红色是不同的。为了“解开”它们,我使用了一个相对简单的公式:

S0=V0^2 / ((V1 *a0 +b0)(V2 * c0+d0))

其中 S0 - 通道 0、V0、V1、V2 的得分 - 通道 0、1、2 和 a、b、c、d 的原始值 - 我手动调整的系数(a 和 c 从 0.3 到 2.0,b 和d 分别为 15 和 20,无论如何您都需要更改它们以适应您的特定传感器位置)。为通道 1 和 2 计算了相同的分数。在此之后,图表变得几乎完全分离:

 
poYBAGNkdQCAa3dpAABQFQpQbQs363.png
 

对于相同的手势(这次是无名指、中指和拇指),信号很清晰,只需与阈值比较即可轻松转换为伺服运动。

2. 原理图

 
poYBAGNkdQOAA9RNAAETk8qAb6Y619.png
 

原理图非常简单,您只需要 nRF24 模块、PCA9685 或类似的 I2C PWM 控制器,以及足以同时移动所有这些舵机的高安培 5V 电源(因此它需要至少 5A 的额定功率才能稳定运行)。

连接列表:
nRF24 pin 1 (GND) - Arduino 的 GND
nRF24 pin 2 (Vcc) - Arduino 的 3.3v
nRF24 pin 3 (Chip Enable) - Arduino's D9
nRF24 pin 4 (SPI:CS) - Arduino's D8
nRF24 pin 5 (SPI: SCK) - Arduino 的 D13
nRF24 引脚 6 (SPI:MOSI) - Arduino 的 D11
nRF24 引脚 7 (SPI:MISO) - Arduino 的 D12
PCA9685 SDA - Arduino 的 A4
PCA9685 SCL - Arduino 的 A5
PCA9685 Vcc - Arduino 的 5v
PCA9685 GND - Arduino 的 GND
PCA9685 V+ -高放大器 5V
PCA9685 GND - 高放大器 GND
手指伺服:到 PCA 通道 0-4,在我的符号中拇指 - 通道 0,食指 - 通道 1 等。

3. EMG 传感器放置

为了获得合理的读数,将记录肌肉活动的 uECG 设备放置在正确的位置非常重要。虽然这里有许多不同的选项,但每个选项都需要不同的信号处理方法 - 所以我分享我用过的东西:

 
pYYBAGNkdQqAM3HtAAqxomsPJeU308.jpg
 

这可能违反直觉,但拇指肌肉信号在手臂的另一侧更明显,因此将其中一个传感器放置在那里,并且所有传感器都放置在靠近肘部的位置(肌肉的大部分身体都在那个区域,但是您想检查您的确切位置-个体差异很大)

4. 代码

在运行主程序之前,您需要找出特定 uECG 设备的单元 ID(通过取消注释第 101 行并逐个打开设备来完成)并将它们填充到 unit_ids 数组中(第 37 行)。

#include 
#include 24.h>
#include 
#include 01.h>
#include 
#include 
#define SERVOMIN  150 // this is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX  600 // this is the 'maximum' pulse length count (out of 4096)
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
int rf_cen = 9; //nRF24 chip enable pin
int rf_cs = 8; //nRF24 CS pin
RF24 rf(rf_cen, rf_cs);
//pipe address - hardcoded on uECG side
uint8_t pipe_rx[8] = {0x0E, 0xE6, 0x0D, 0xA7, 0, 0, 0, 0};
uint8_t  swapbits(uint8_t a){ //uECG pipe address uses swapped bits order
// reverse the bit order in a single byte
uint8_t v = 0;
if(a & 0x80) v |= 0x01;
if(a & 0x40) v |= 0x02;
if(a & 0x20) v |= 0x04;
if(a & 0x10) v |= 0x08;
if(a & 0x08) v |= 0x10;
if(a & 0x04) v |= 0x20;
if(a & 0x02) v |= 0x40;
if(a & 0x01) v |= 0x80;
return v;
}
long last_servo_upd = 0; //time when we last updated servo values - don't want to do this too often
byte in_pack[32]; //array for incoming RF packet
unsigned long unit_ids[3] = {4294963881, 4294943100, 28358}; //array of known uECG IDs - need to fill with your own unit IDs
int unit_vals[3] = {0, 0, 0}; //array of uECG values with these IDs
float tgt_angles[5]; //target angles for 5 fingers
float cur_angles[5]; //current angles for 5 fingers
float angle_open = 30; //angle that corresponds to open finger
float angle_closed = 150; //angle that corresponds to closed finger
void setup() {
//nRF24 requires relatively slow SPI, probably would work at 2MHz too
SPI.begin();
SPI.setBitOrder(MSBFIRST);
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
for(int x = 0; x < 8; x++) //nRF24 and uECG have different bit order for pipe address
pipe_rx[x] = swapbits(pipe_rx[x]);
//configure radio parameters
rf.begin();
rf.setDataRate(RF24_1MBPS);
rf.setAddressWidth(4);
rf.setChannel(22);
rf.setRetries(0, 0);
rf.setAutoAck(0);
rf.disableDynamicPayloads();
rf.setPayloadSize(32);
rf.openReadingPipe(0, pipe_rx);
rf.setCRCLength(RF24_CRC_DISABLED);
rf.disableCRC();
rf.startListening(); //listen for uECG data
//Note that uECG should be switched into raw data mode (via long button press)
//in order to send compatible packets, by default it sends data in BLE mode
//which cannot be received by nRF24
Serial.begin(115200); //serial output - very useful for debugging
pwm.begin(); //start PWM driver
pwm.setPWMFreq(60);  // Analog servos run at ~60 Hz updates
for(int i = 0; i < 5; i++) //set initial finger positions
{
tgt_angles[i] = angle_open;
cur_angles[i] = angle_open;
}
}
void setAngle(int n, float angle){ //sends out angle value for given channel
pwm.setPWM(n, 0, SERVOMIN + angle * 0.005556 * (SERVOMAX - SERVOMIN));
}
float angle_speed = 15; //how fast fingers would move
float v0 = 0, v1 = 0, v2 = 0; //filtered muscle activity values per 3 channels
void loop()
{
if(rf.available())
{
rf.read(in_pack, 32); //processing packet
byte u1 = in_pack[3];//32-bit unit ID, unique for every uECG device
byte u2 = in_pack[4];
byte u3 = in_pack[5];
byte u4 = in_pack[6];
unsigned long id = (u1<<24) | (u2<<16) | (u3<<8) | u4;
//Serial.println(id); //uncomment this line to make list of your uECG IDs
if(in_pack[7] != 32) id = 0; //wrong pack type: in EMG mode this byte must be 32
int val = in_pack[10]; //muscle activity value
if(val != in_pack[11]) id = 0; //value is duplicated in 2 bytes because RF noise can corrupt packet, and we don't have CRC with nRF24
//find which ID corresponds to current ID and fill value
for(int n = 0; n < 3; n++)
if(id == unit_ids[n])
unit_vals[n] = val;
}
long ms = millis();
if(ms - last_servo_upd > 20) //don't update servos too often
{
last_servo_upd = ms;
for(int n = 0; n < 5; n++) //go through fingers, if target and current angles don't match - adjust them
{
if(cur_angles[n] < tgt_angles[n] - angle_speed/2) cur_angles[n] += angle_speed;
if(cur_angles[n] > tgt_angles[n] + angle_speed/2) cur_angles[n] -= angle_speed;
}
for(int n = 0; n < 5; n++) //apply angles to fingers
setAngle(n, cur_angles[n]);
//exponential averaging: prevents single peaks from affecting finger state
v0 = v0*0.7 + 0.3*(float)unit_vals[0];
v1 = v1*0.7 + 0.3*(float)unit_vals[1];
v2 = v2*0.7 + 0.3*(float)unit_vals[2];
//calcualting scores from raw values
float scor0 = 4.0*v0*v0/((v1*0.3 + 20)*(v2*1.3 + 15));
float scor1 = 4.0*v1*v1/((v0*2.0 + 20)*(v2*2.0 + 20));
float scor2 = 4.0*v2*v2/((v0*1.2 + 20)*(v1*0.5 + 15));
//print scores for debugging
Serial.print(scor0);
Serial.print(' ');
Serial.print(scor1);
Serial.print(' ');
Serial.println(scor2);
//compare each score with threshold and change finger states correspondingly
if(scor2 < 0.5) //weak signal - open finger
tgt_angles[0] = angle_open;
if(scor2 > 1.0) //strong signal - close finger
tgt_angles[0] = angle_closed;
if(scor1 < 0.5)
{
tgt_angles[1] = angle_open;
tgt_angles[2] = angle_open;
}
if(scor1 > 1.0)
{
tgt_angles[1] = angle_closed;
tgt_angles[2] = angle_closed;
}
if(scor0 < 0.5)
{
tgt_angles[3] = angle_open;
tgt_angles[4] = angle_open;
}
if(scor0 > 1.0)
{
tgt_angles[3] = angle_closed;
tgt_angles[4] = angle_closed;
}
}
}

5. 结果

通过一些大约 2 小时的实验,我能够获得相当可靠的运行(视频显示了一个典型案例):

 

它的行为并不完美,通过这种处理只能识别张开和闭合的手指(甚至不能识别 5 个手指中的每一个,它只检测到 3 个肌肉群:拇指、食指和中指,无名指和小指)。但是分析信号的“AI”在这里需要 3 行代码,并使用来自每个通道的单个值。我相信可以通过在 PC 或智能手机上分析 32-bin 光谱图像来完成更多工作。此外,此版本仅使用 3 个uECG设备(EMG 通道)。有了更多的通道,应该可以识别真正复杂的模式——但是,这就是项目的重点,为任何有兴趣的人提供一些起点:) 手控绝对不是这种系统的唯一应用。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !

'+ '

'+ '

'+ ''+ '
'+ ''+ ''+ '
'+ ''+ '' ); $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code ==5){ $(pop_this).attr('href',"/login/index.html"); return false } if(data.code == 2){ //跳转到VIP升级页面 window.location.href="//m.lene-v.com/vip/index?aid=" + webid return false } //是会员 if (data.code > 0) { $('body').append(htmlSetNormalDownload); var getWidth=$("#poplayer").width(); $("#poplayer").css("margin-left","-"+getWidth/2+"px"); $('#tips').html(data.msg) $('.download_confirm').click(function(){ $('#dialog').remove(); }) } else { var down_url = $('#vipdownload').attr('data-url'); isBindAnalysisForm(pop_this, down_url, 1) } }); }); //是否开通VIP $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code == 2 || data.code ==5){ //跳转到VIP升级页面 $('#vipdownload>span').text("开通VIP 免费下载") return false }else{ // 待续费 if(data.code == 3) { vipExpiredInfo.ifVipExpired = true vipExpiredInfo.vipExpiredDate = data.data.endoftime } $('#vipdownload .icon-vip-tips').remove() $('#vipdownload>span').text("VIP免积分下载") } }); }).on("click",".download_cancel",function(){ $('#dialog').remove(); }) var setWeixinShare={};//定义默认的微信分享信息,页面如果要自定义分享,直接更改此变量即可 if(window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ var d={ title:'使用EMG的机械手控制',//标题 desc:$('[name=description]').attr("content"), //描述 imgUrl:'https://'+location.host+'/static/images/ele-logo.png',// 分享图标,默认是logo link:'',//链接 type:'',// 分享类型,music、video或link,不填默认为link dataUrl:'',//如果type是music或video,则要提供数据链接,默认为空 success:'', // 用户确认分享后执行的回调函数 cancel:''// 用户取消分享后执行的回调函数 } setWeixinShare=$.extend(d,setWeixinShare); $.ajax({ url:"//www.lene-v.com/app/wechat/index.php?s=Home/ShareConfig/index", data:"share_url="+encodeURIComponent(location.href)+"&format=jsonp&domain=m", type:'get', dataType:'jsonp', success:function(res){ if(res.status!="successed"){ return false; } $.getScript('https://res.wx.qq.com/open/js/jweixin-1.0.0.js',function(result,status){ if(status!="success"){ return false; } var getWxCfg=res.data; wx.config({ //debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId:getWxCfg.appId, // 必填,公众号的唯一标识 timestamp:getWxCfg.timestamp, // 必填,生成签名的时间戳 nonceStr:getWxCfg.nonceStr, // 必填,生成签名的随机串 signature:getWxCfg.signature,// 必填,签名,见附录1 jsApiList:['onMenuShareTimeline','onMenuShareAppMessage','onMenuShareQQ','onMenuShareWeibo','onMenuShareQZone'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ //获取“分享到朋友圈”按钮点击状态及自定义分享内容接口 wx.onMenuShareTimeline({ title: setWeixinShare.title, // 分享标题 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享给朋友”按钮点击状态及自定义分享内容接口 wx.onMenuShareAppMessage({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 type: setWeixinShare.type, // 分享类型,music、video或link,不填默认为link dataUrl: setWeixinShare.dataUrl, // 如果type是music或video,则要提供数据链接,默认为空 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ”按钮点击状态及自定义分享内容接口 wx.onMenuShareQQ({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口 wx.onMenuShareWeibo({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ空间”按钮点击状态及自定义分享内容接口 wx.onMenuShareQZone({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); }); }); } }); } function openX_ad(posterid, htmlid, width, height) { if ($(htmlid).length > 0) { var randomnumber = Math.random(); var now_url = encodeURIComponent(window.location.href); var ga = document.createElement('iframe'); ga.src = 'https://www1.elecfans.com/www/delivery/myafr.php?target=_blank&cb=' + randomnumber + '&zoneid=' + posterid+'&prefer='+now_url; ga.width = width; ga.height = height; ga.frameBorder = 0; ga.scrolling = 'no'; var s = $(htmlid).append(ga); } } openX_ad(828, '#berry-300', 300, 250);