新年好
大家好。己亥年到了,这意味着什么呢?这意味着我又拖更几个月了。
年前,数字媒体处理技术这门课作为最后出分的科目,有点惊到我。我都不知道为什么要给我rank 1。所以今天就接着写这门课的内容吧。
做了啥
要求
第四次实验围绕体感交互展开。发到大家手里的设备有Kinect(或Xtion)和Leap Motion两种,具体做什么自己来决定。Kinect就是XBOX上的那玩意儿大家都知道;Leap Motion是在比较近的距离做手势识别的。按道理来说,一个小组做什么东西还是要brainstorm一下搞个技术选型,但是我们没有。只因组长太优秀,在大家还在复习软件工程四大金刚的时候,东西都做完了。
制品
这个东西的功能就是给小人画衣服(大雾),我们组长老早就做好了的,应该说基本只差接入Leap Motion的控制就完工了。虽然这个东西感觉好像没有什么用的样子(小声),但是作为这次大作业还是可以的。之后不知道怎么回事搞Leap Motion的活就到了我身上。我们做的东西too simple,本来有点不好意思写来着。演示视频请看文末。
LM简介
是什么
我们使用的Leap Motion,内置了两个红外摄像头来模拟眼睛,从而构建出拍摄区域的3D场景。摄像头的数据实时传回电脑,由驱动处理之后生成手的识别信息。通过SDK接入驱动,就能获取到处理出来的信息,进行利用。Leap Motion坐标系以毫米为单位,两个摄像头的连线中点为原点,远离数据线接口的一侧是$x$轴正方向,右手系(坑,之后因为把$y$、$z$轴搞反调了很久)。
LM的坐标系如图所示(盗图不好,只能自己拍了)。
怎么用
首先你得把官方的驱动装上,这个有100多兆。之后就要看开发使用什么语言了,默认送了一个C的库。我们使用的是Unity,那么4.x版本的LM是有一个对应的Unity的SDK。不要以为这是好事,Unity的SDK就意味着开发、测试都必须在Unity里面完成,而甚至用Visual Studio建个简单的Console App来做技术验证都不能做到。为什么我这么说,那当然是因为我试过啊。把Unity SDK里面的dll提出来扔进VS,你会发现build不了——LM官方坏得很,竟然用了Mono的私货(Unity为了跨平台当然是用Mono),这就导致微软的CLR是没法跑它的。要知道4.x以前的版本是有.NET SDK提供的。所以在“怎么用”这个话题上面,我只能说必须在Unity的环境中用了。
接入LM控制
读书人的事能叫偷吗
我是这么想的:因为这个软件原始版本就是主要用鼠标操作的,那么我用LM来模拟鼠标就好了啊。所以我找到了这篇文章,虽然原作者用的是CoffeeScript的binding和老版本的SDK,但是我觉得问题不大,应该可以翻译到新版本的C#上(naïve!)。结果当然是失败了的……毕竟这个实现过于魔幻,而且技术栈差异有点大。
接着在GitHub逛,找到了Zeukkari/leapgim。这个repo还带了视频,看起来效果不错的样子。JavaScript的实现,我仍然认为可以抄一抄。相比上面的那个高端实现,这个原理好像简单得多,我看了一下代码,就是取数据出来根据稳定后的手掌位置结合interaction box来做归一化,映射到屏幕坐标。当我抄到
let iBox = frame.interactionBox;
let normalizedPoint = iBox.normalizePoint(leapPoint, false);
我傻眼了——VS都不出提示了。跳到dll里面看了一下,果然,根本没interaction box这个东西。垃圾LM的4.x SDK又砍功能了! 诸位请看这篇文章的The Interaction Box部分,这里介绍了SDK中可以使用interaction box来直接归一化坐标点。我想,既然它SDK砍掉了,那我就自己整一个啊?反正以前的C#版本里面肯定是有的。嗯,找到了源码,InteractionBox.NormalizePoint
方法确实是可以抄……那么问题来了,InteractionBox
自己是怎么构造出来的?我一路找到了这里,好的,果然是从C的API里面搞的。没问题,那我去看看C的API啊。你可能已经猜到了结局,没错,C的API里面也把interaction_box
砍了。
所以我根本没抄成。
自己动手丰衣足食
不怕话说得难听,4.x版本的Unity SDK的文档就是屎。可能是官方认为我们都用Unity了,那肯定是要做游戏要套模型要绑骨的,那么最原始的东西文档直接就不写了,只教你们绑骨就好了。我:???最后我在归档里面找到了3.2版本的C#文档,照着这个样子慢慢写慢慢改,终于在Unity环境里通联上LM的驱动了。其实你们看一下会发现直接用原始数据挺简单的,建一个Controller
之后把回调绑上,然后把回调写好就行了。
这些回调函数中最重要的是FrameReady
,该事件每帧触发一次,该帧内识别到的数据将会传回到回调函数中,数据包括手的信息,常用的有:手掌的位置,手掌平面的法向量,抓和捏动作的可能性概率等。我们就是通过这些数据完成对程序的操作。
Math...
其实就是各种花式归一化。
全域映射
你现在得到了手掌位置PalmPosition
在LM坐标系中的坐标,怎么把这个位置映射到屏幕的二维坐标系(左上角为原点)中来作为鼠标的位置?考虑将手掌位置的$x$和$z$坐标映射到屏幕的宽和高上。首先考虑以Leap Motion原点为屏幕中心的映射。规定一个Leap Motion的扫描范围为归一化区域的范围,将手掌位置坐标的$x$、$z$两个分量映射到$[0,\ 1]$,如果超出就clamp掉。将归一化的值分别乘以屏幕的宽和高,就得到了映射到屏幕上的坐标。
右侧映射
为了实现左右两侧识别区域能有不同功能划分,我们使用右手控制鼠标的移动。为避免与左手冲突,我们希望将映射范围修改为Leap Motion的右半边识别范围,因此当识别右手的$x$值小于0时,则将其值设为0;当右手$x$坐标值大于0时,则将其值除以新的识别范围,得到新的归一化值。而这时对于$z$坐标值的处理,先将$z$坐标加上一个识别半径,使之值为正,再除以两倍识别半径,得到一个$[0,\ 1]$的值,对于大于0.75和小于0.25的值分别设置为0.75和0.25,再将最后的结果减去0.25后乘以2,得到归一化值。这样的做法可以使得两个方向上的移动比较均匀,不缩小$z$方向上的范围就会显得移动非常迟缓(况且本来横屏状态$z$方向就是更短的)。
设置鼠标位置
C#对native C的互操作性很好。只需加入:
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern int SetCursorPos(int x, int y);
就可以在C#里调用GetSystemMetrics
和SetCursorPos
了。获取到屏幕分辨率后,再计算屏幕上的对应坐标,并设置鼠标位置。这样一来右手就可以在识别区域的右侧控制鼠标位置了。
控制点击
实验发现,为了避免对右手控制精度的不良影响,使用左手控制模拟鼠标的点击更好。实验还发现捏的动作比抓的动作更加轻松,所以从易用性的角度,我们选择捏,当左手保持捏的动作时,表示鼠标左键一直按下,左手摊平时就是放开。在每只手的信息中,有一个属性叫PinchStrength
,值介于$[0,\ 1]$,实验选择一个合适的阈值,来控制鼠标点击即可。
和上面一样,模拟点击也是调native,用到的是mouse_event
。不过要考虑一点,就是要支持“点击后拖拽”这样的一个场景。这样一来,点击这一方面相当于是做一个简单的状态机。维护“左键是否按下”这一状态,然后根据“左手捏”的条件进行状态转移就好。
控制模型旋转
我本来的打算是以模型为中心做上下左右的旋转的,不过因为大家懒只做了水平旋转(
这个功能也由左手控制,左手向左转模型就向左转,反之亦然。该功能通过判断左手手掌法向量来实现。这里一定要注意坐标系、符号和阈值的标定。
整合Unity工程
其实挺简单的,因为我负责的部分是完全不与原Unity项目中的元素发生交互的,最多只有旋转功能的那个法向量需要被其他地方的代码读取。而这个法向量又不是特别的critical,所以完全不需要考虑并行情况下的问题,那么锁也就不可能需要加。所以直接把我的那一坨代码扔到一个地方然后new
一个Bokjan.LeapMotionManager
就完事了。我的完整实现请看PaintManager.cs的Bokjan
命名空间部分。
演示
点开看吧:https://static-1251934385.file.myqcloud.com/2019/0213/demo_compressed.mp4