×

构建3D跟踪器开源分享

消耗积分:0 | 格式:zip | 大小:0.35 MB | 2022-11-11

万物死

分享资料个

描述

动机

数据分析是科学、技术、工程和数学 (STEM) 领域的一个关键问题。可视化数据的能力对于理解数据并得出结论至关重要。因此,视力受损的学生处于不利地位,可能会不鼓励从事科学事业。已经开发了许多工具来克服这一挑战,但是,它们通常依赖于口头描述或静态触觉图片,这可能会受到限制。据报道,触觉和听觉刺激的结合可以提高盲人学生对几何等视觉主题的学习[1] 很明显,需要让视障者更容易接触到 STEM 科目,因此在这个项目中,我们的目标是构建一个廉价的3D 跟踪器,用于动态听觉数据表示.

计划

这个想法是构建一个将数学函数(在真实的线、平面或 3D 空间上)转换为声音的设备。我们可以通过使用MGC3130芯片的 3D 跟踪传感器(例如FlickSkywriter )检测用户手的位置来导航空间。然后可以将记录的位置用作数学函数的输入,该数学函数的输出可以处理成声音信号。通过将 3D-tracker 连接到ArduinoRaspberryPi可以轻松完成数据采集,然后我们可以处理数据并将输出发送到声音处理软件,例如SupercolliderCSound

由于我们打算将其作为一种包容性技术,我们还将尝试包括视觉和触觉反馈。为了可视化数据,我们可以使用Processing ,这是一种专门为视觉艺术家开发的简单编程语言。触觉反馈可以通过不同的方式来实现。一方面,我们可以使用超声波换能器在半空中创建触觉反馈(Ultraino )。另一方面,我们可以使用振动马达来模拟不同的纹理(Hap2U )。

目前,我们仍然不确定哪种是包含触觉反馈的最佳方式,但我们会不断更新。

适用于 Chromebook 的 Arduino、Processing 和 Supercollider

我们项目所需的软件适用于 Windows、Mac 和 Linux 系统,但不适用于 Chrome OS。如果您使用的是 Chromebook,有一些选项可以解决它。第一个是使用ArduinoProcessing的网络编辑器,不幸的是,Supercollider 没有在线选项。另一种选择是CSound Web IDE ,但我们发现 Supercollider 更易于使用。第二种选择是安装 Linux,可以通过多种方式完成。最简单的一种是启用原生 Chrome OS Linux(测试版)虚拟机并安装 Linux 应用程序。这种方法的问题是虚拟机仍然存在一些问题,特别是设置音频不是那么容易,使用 Supercollider 几乎是不可能的。在 Chromebook 上安装 Linux 的另一种方法是使用Crouton ,它为我们提供了一个可以更轻松地配置声音的 Linux 发行版。让 Supercollider 服务器运行还有一个额外的步骤,将JACK 转发到 CRAS 请注意,此方法需要启用开发者模式,这会使您的 Chromebook 保修失效。

Arduino基础知识

  • 代码结构
  • 变量
  • 功能

超级对撞机基础

客户端-服务器架构

  • 评估线
  • 变量
  • 基本振荡器
  • 合成器

加工基础

  • 代码结构
  • 变量
  • 功能

轻弹/天空作家 (MGC3130)

第一步是获取位置数据。我们使用Pi Supply的 Flick Large ,但 Pimoroni 的Skywriter可以以相同的方式连接,实际上,两者都使用相同的 Arduino 库。Flick 有 8 个公针,两个用于电源(VCC、GND),两个用于数据通信(SDA、SCL)和四个数字针(TS、RESET、LED1、LED2)。前四个必须连接到Elegoo (Arduino) UNO R3中的相应引脚,其他四个可以连接到 Elegoo 中的任何数字引脚。下面我们展示了示意图连接,Skywriter 应该以类似方式连接,除了在这种情况下不存在的 LED 引脚。

pYYBAGNsZpuAOLmTAACNZKmH4E8743.jpg
Flick-Arduino 连接
 

为了控制 Flick,我们使用 Pimoroni 的Skywriter 库下载后,我们通过将 Skywriter 目录复制到 Arduino 库目录来安装库。我们现在可以启动 Arduino IDE 并开始编码。一个简单的例子是跟踪位置并将其显示在串行监视器上。为了启用串行端口上的通信,我们包括接下来的两个库

#include 
#include 

然后我们初始化Arduino板如下

void setup() {
  Serial.begin(9600); //Initialise serial communication at 9600 bds
  while(!Serial){}; //We wait for the data serial port to start
  Serial.println("Hello world!"); //The port is ready!

  Skywriter.begin(10,11); //Initialise the Flick with the pins TS=D10 and RESET=D11
  
  Skywriter.onXYZ(handle_xyz); //This method records the position on the Flick and the function handle_xyz manipulates the data.
  
//We can initialise the LED pins as follows
  //pinMode (9, OUTPUT); //Red LED
  //pinMode (8, OUTPUT); //Green LED

  //We can turn on the green LED to know when the Flick is ready
  //digitalWrite (8, HIGH);
}

在这种情况下,Arduino 代码的循环部分将只包含一行

void loop() {
  Skywriter.poll(); //Check if the status of the Flick has changed
}

此行检查 Flick 的状态是否已更改。如果没有发生任何事情,我们将观察到串行监视器上没有发生任何事情。如果状态发生变化,例如将我们的手放在棋盘上,那么我们将观察到一些动作。最简单的方法是将位置打印为串行监视器中的元组。以下功能可以解决问题

void handle_xyz(unsigned int x, unsigned int y, unsigned int z){
  char buf[17]; //An array of 17 characters 5 for each coordinate and two delimiters
  sprintf(buf, "%05u:%05u:%05u",x,y,z); //Record the position on the Flick
  Serial.println(buf); //Print the position to the serial port, one line at a time
}

编译代码并将其上传到 Arduino 板后,我们打开串口监视器,应该会看到该行Hello world!(确保监视器的速度与我们用于初始化串口的速度相匹配,本例中为 9600 bds)。如果我们将手放在 Flick 上并在它周围移动,那么我们应该会看到该位置一次打印一行。

Arduino 和超级对撞机

为了获得代表数据的声音,我们首先需要让 Arduino 和 Supercollider 相互交谈。我们通过串口通信来实现这一点,并通过一个简单的例子来演示。我们将在 Supercollider 中播放一个简单的 Synth,并使用 Flick 上的气轮手势控制它的频率。

首先是Arduino代码。我们像以前一样初始化电路板,用方法更改行Skywriter.onXYZ();

Skywriter.onAirwheel(handleAirwheel); //This method records an airwheel event and returns a positive/negative value if clockwise/counterclockwise rotation is detedted

我们定义一个变量来控制正弦振荡器的频率:

int freq=440;

当检测到顺时针或逆时针旋转时,该功能handleAirwheel会增加或减少频率的值:

void handleAirwheel (int delta){
  if(delta>0){ //Increase the frequency if clockwise rotation
    if(freq<1000){ //Upper cutoff
      freq=freq+1;
      }
  }else if(delta<0){//Decrease the frequency if counterclockwise rotation
    if(freq>1){ //Lower cutoff
      freq=freq-1;
      }
  }
}

我们在代码的循环部分将频率的值打印到串口:

void loop() {
  Skywriter.poll(); //Check if the status of the Flick has changed
  Serial.print(freq); //Print frequency to serial port
  Serial.print('a'); //Print a delimiter character
  delay(1);
}

我们打印一个分隔符a来轻松处理数据,并打印一个小的延迟以避免 Supercollider 中的服务器崩溃。一旦我们的 Arduino 代码在板上运行,我们关闭 IDE,因为只有一个设备可以同时与串行端口通信。然后我们打开 Supercollider IDE 并启动服务器。我们检查下一行可用的串行端口

SerialPort.devices; //Check the available ports

帖子窗口(右下角)上的输出应该类似于[/dev/ttyACM0]. 然后我们定义一个新的 Serial 变量运行下一行

~port = SerialPort.new("/dev/ttyACM0",9600);

第一个参数对应端口的名称,第二个参数是速度,确保这与 Arduino 代码中的速度相匹配。然后我们需要创建一个从 Flick 读取数据并将其存储在 Supercollider 变量中的函数

(
~charArray = []; //An array to store the characters printed to the serial port
~getValues = Routine.new({ //The code inside the routine loops indefinitely
	var ascii; //Supercollider read the characters in the serial port as ascii, so we need to convert them to numbers
	{
		ascii = ~port.read.asAscii; //We read the characters one by one and convert them to digits
		if(ascii.isDecDigit, {
			~charArray = ~charArray.add(ascii)
		});
		if(ascii == $a, { //We stop reading the characters when Supercollider finds the delimiter 'a'
			~val = ~charArray.collect(_.digit).convertDigits; //We collect and combine the digits into a number
			~charArray = []; //We empty the array
		})
	}.loop;
}).play
)

然后我们定义最简单的合成器,一个正弦波

(
SynthDef.new(\sineWave, { //Name of the Synth
	arg freq = 440; //Frequency
	var sig; //Output
	sig = SinOsc.ar(freq,0,1); //Sine oscillator with frequency freq, phase 0 and amplitude 1
	Out.ar(0,sig); //send output signal to the left speaker
}).add;
)

我们播放运行下一行的合成器

~synth = Synth(\sineWave, [\freq, 440]);

然后我们创建一个例程来使用来自 Flick 的数据修改振荡器的频率

(
~control = Routine.new({
	{
		~synth.set(\freq, ~val.linexp(1,1000,20,2000)); //exponential map from (1;1000) to (20,2000)
		0.01.wait;
	}.loop;
}).play;
)

该例程~control以指数方式将打印到串行端口的值范围映射到一个范围(maximum frequency, minimum frequency)当人类以对数方式感知频率时,我们使用指数图。

如果一切正常,我们应该听到这样的声音

 

当我们玩完合成器后,我们停止控制例程并释放服务器并停止串行通信,评估以下行

~control.stop;
~synth.free;
~port.stop;

Arduino 和处理

将 Arduino 板与 Processing 进行通信使我们能够可视化我们的数据。通讯也是通过串口实现的。我们将通过一个使用手势检测的简单示例来展示如何做到这一点。Arduino代码和之前一样,这次将Skywriter.onAirwheel();方法替换为

Skywriter.onGesture(handleGesture); //This method records a gesture event up/down/right/left

其中函数handleGesture定义如下

void handleGesture(unsigned char type){
  Serial.println(type,DEC);                 //Prints 2 left-right, 3 right-left, 4 bottom-top, 5 top-bottom swipe
}

现在要读取处理中的数据,我们打开 IDE,加载serial库,并设置草图窗口

import processing.serial.*;//load serial library
Serial myPort;//define serial variable

void setup(){
  size(400,400);//size of the sketch in pixels
  background(255);//white background
  myPort = new Serial(this, "/dev/ttyACM0",9600);//Use the same serial port and communication speed used in the Arduino code 
  myPort.bufferUntil('\n');//Wait for the port to be ready
}

该变量mySerial必须定义为从我们在 Arduino 代码中使用的相同端口读取,并以相同的速度读取数据。一旦我们建立通信,我们就可以使用串口中的数据。我们将创建一个带有移动粒子的简单草图,该粒子可以在 +/- x 和 y 方向上加速,具体取决于我们在 Flick 上滑动的方向。要读取mySerial端口中的数据,我们使用函数serialEvent(),该函数会在串行端口中有新信息可用时进行注册并对其进行处理,例如

void serialEvent (Serial myPort){
  direction=int(float(myPort.readStringUntil('\n')));//Read the data in the serial port as a string one line at a time, and converts it into a integer
  gravity();//Changes the direction of the acceleration depending on the value stored in direction. 2=right, 3=left, 4=up, 5 =down.
}

在这种情况下,我们一次读取mySerial端口中的一行数据(这是'\n'字符所指示的)并将其作为整数存储在变量中direction该函数gravity()会根据 o 中存储的值更改加速度,direction例如,如果我们向右滑动,粒子应该开始向右“下落”,如下面的视频所示。您可以在代码部分找到草图的详细信息。

 

处理和超级对撞机

为了听到和可视化我们的数据,我们需要让 Processing 和 Supercollider 相互交谈。与前面两个例子不同的是,两个平台之间的通信不是通过串口来实现的,而是通过OSC 消息来实现的,它是专门为音乐和表演控制而设计的。我们将展示如何让这两个程序与一个简单的示波器草图进行对话。让我们从处理草图开始。我们首先需要下载oscP5库并安装它。在 IDE 上转到 Sketch->Import Library->Add Library 并查找 oscP5 文件。我们草图的标题和设置部分应如下所示

import netP5.*;
import oscP5.*;

//Declare osc and supercollider ip address
OscP5 osc;
NetAddress supercollider; 

void setup(){
  
  size(800,400);
  
  osc = new OscP5(this,12000); //construct object osc, "this" references the Processing sketch and 12000 is the port at which it talks, this can be any number
  supercollider = new NetAddress("127.0.0.1", 57120); //construct object supercollider, 127.0.0.1 is the local IP address and 57120 the port
}

我们应该知道我们正在与之交谈的程序的IP地址,在这种情况下,我们正在向supercollider同一台计算机上运行的程序发送数据,因此我们使用本地IP地址127.0.0.1和端口号57120。可以获取此信息通过评估NetAddr.localAddr;Supercollider 中的线。我们现在必须在 Supercollider 中建立通信。打开 IDE,我们要做的第一件事是使用本地 IP 和我们用于 OSC 的端口号构造一个 Net Address 对象

~processing = NetAddr.new("127.0.0.1",12000); //Construct object processing at the local address 127.0.0.1 and port number 12000

现在我们将产生一个频率的正弦波,freq并将这个值发送到处理。我们使用与 Arduino-Supercollider 示例中相同的 Synth,但我们将控制例程更改如下

(
//Change the frequency at random
~fr = rrand(220,3520);
~synth.set(\freq, ~fr);

//Send OSC message to Processing, '/frequency' is the name of the message and ~fr its contents
~processing.sendMsg(
	'/frequency', ~fr
);
~fr; //Print the value of the frequency as a sanity check
)

每次我们评估这些线时,我们都会改变正弦波的频率并将其值发送到处理。要接收消息,我们使用 oscEvent() 函数

void oscEvent(OscMessage theOscMessage){
  omega=TWO_PI*float(theOscMessage.get(0).intValue())/(440*width); //convert the frequency value sent as a string from supercollider into an integer and then calculate the angular frequency 
}

在这种情况下,我们使用 OSC 消息中的数据来绘制一个角频率为 的正弦波omega,这样 440 Hz 波的一个周期就适合屏幕。我们还想从 Processing 向 Supercollider 发送消息,为此我们使用一个OscMessage对象。在这种情况下,我们将使用键盘上的向上和向下箭头来增加正弦波的幅度/音量

void keyPressed(){//Registers when a key is pressed and stores it value on the variable key
  if(keyCode==UP){//UP,DOWN,RIGHT and LEFT are coded keys
    if(amplitude<200){//Increase amplitude
      amplitude+=10;
    }
  }else if(keyCode==DOWN){//Decrease amplitude
    if(amplitude>0){
      amplitude-=10;
    }
  }
  OscMessage msg = new OscMessage("/amplitude"); //Construct OscMessage object with name /amplitude
  msg.add(map(amplitude,0,200,0,1)); //map the amplitude to the [0,1] range and add it to the OSC message
  osc.send(msg,supercollider); //send the message
}

我们捕捉到消息并通过评估以下几行来使用它来增加 Supercollider 中的音量

(
//This routine reads the message /amplitude from processing and uses it to control the volume of the sine wave
OSCdef('volume',{
	arg msg;//This variable stores the message
	~synth.set(\amp,msg[1]);//component [0] contains the OSC address, [1], [2],... contain the values added to the message
	},"/amplitude"); //the OSC message we are listening to
)

为了让这个示例运行,我们首先在 Processing 中播放草图,然后评估 Supercollider 代码。特别是,我们必须运行,OSCdef 以便 Supercollider 监听来自 Processing 的消息。在下面的视频中,您可以看到此示例的工作原理,您可以下载代码以查看详细信息。

 

数学之声

我们现在准备把它们放在一起并听一些数据。例如,我们可以根据输出信号的音量来推断分布的形状

 

 

添加触觉反馈

我们可以通过使用跟踪数据产生振动来增加另一个层次的感觉。为此,我们为传感器构建了一个外壳,它允许我们安装一个显示器,我们可以使用微型振动电机使其振动由于这需要定制零件,我们将对外壳进行建模并进行 3D 打印。我们可以使用几个免费软件工具来解决这个问题,例如适用于 Linux 的FreeCAD或在线选项Fusion 360OnShape 每一个都有几个在线教程和资源,但在我看来,OnShape 是最容易使用和学习最快的. 您可以在附件部分找到 skywriter 原型案例的 stl 文件。这种情况应该允许在传感器顶部安装一块透明的亚克力板,作为“触觉显示器”。我们可以将振动电机粘附在亚克力板上,使用跟踪数据激活。例如在上面的例子中,我们可以根据每个分布中点的局部密度来增加振动强度。

不幸的是,在使用振动反馈对传感器进行测试后,我们发现运动跟踪器在封装时的准确性并不是那么好。因此,跟踪数据是嘈杂的,并没有给出公平的表示。也许,使用不同类型的传感器可能会很方便,例如 Sparkfun 的ZXgesture传感器。

参考

[1] Cryer, H. (2013)。“向盲人和弱视学生教授 STEM 科目:文献回顾和资源”。RNIB 无障碍信息中心,伯明翰:文献综述 #6。

 


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

评论(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:'构建3D跟踪器开源分享',//标题 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);