iWPF:打开Kinect的大门
Kinect是微软XBOX360上的体感游戏控制器,早在去年11月份就发布了,由于微软没有公布SDK,我们就不能直接在PC上开发来控制这个超酷的装置。好消息是国外的黑客们已经破解了Kinect的驱动,包括pc,linux,mac等OS,这段时间我也花时间研究了一下kinect,现在已经可以在WPF里捕获Kinect来进行一些操作了,如隔空两手操作,控制鼠标,控制kinect马达等,我接下来把一些心得分享给大家。
在开发之前我们先做一些准备工作,当然你必须已经有了一台Kinect:
1.熟悉Kinect,如上图Kinect长的很像机器人瓦力的头,左边镜头为红外线发射器,中间的镜头是一般常见的RGB彩色摄像头,右边镜头是红外线CMOS摄像头所构成的3D深度传感器,同时具有追焦功能,底座马达可左右旋转27度。此外还有数组式麦克风。Kinect一次可捕获三种东西,分别是彩色影像、3D深度影像、以及声音讯号,主要就是靠3D深度传感器侦测玩家的动作。与电脑连接后,信号灯(默认绿色)会一闪闪的,别忘了检查电源是否插好。
2.安装驱动,SDK
为Kinect生产体感元器件的PrimeSense公司放出了Kinect开源驱动OpenNI。但我感觉对马达的控制不太好,所以我还安装了CL NUI Platform来驱动马达。顺序是先装OpenNI的驱动,然后更新马达的驱动时去CL NUI Platform的Dirver文件夹里找。
开发时需要用到SensorKinect,它提供了OpenNI的API开发库。
接下来需要安装PrimeSensor的NITE中间件,它提供了分析的人体骨骼资料。
安装时会提示你输入Key,0KOIk2JeIBYClPWVnMoRKn5cdY4=
驱动安装完设备管理器里的状态应该如下,
ps:有时间可以再了解一下FAAST,可以通过这个软件录入一些肢体动作然后控制键盘和鼠标,网上很多Kinect控制街霸4,魔兽世界等都是这么弄的。
以上所有的驱动我打包好了,可以在我的网盘里 下载
*
驱动安装好了就可以去PrimeSense和OpenNI根目录里Sample里运行例子文件来体验Kinect。
最简单的例子就是追踪手,在\Prime Sense\NITE\Samples\Bin\Debug\Sample-PointViewer.exe
然后就进入开发之旅吧,很遗憾你会发现例子里都是C++和Flash的程序和文档,没有C#的,所以对于WPF开发就比较苦涩了,首先我们先拿到开发用到的dll(\Prime Sense\NITE\Wrappers\C#\Bin\ManagedNite.dll),然后对应Prime Sense\NITE\Documentation下的C++ API文档来熟悉类库里的内容。
我的开发环境是Windows7 + VS2008SP1(WPF)
1.建好WPF工程后,把ManagedNite.dll和CLNUIDevice.dll(位于CL NUI Platform驱动文件夹下)放在执行目录下,并引用ManagedNite.dll;
在执行目录下新建名为data的文件夹,然后从OpenNI\Data下复制SamplesConfig.xml过来更名为openni.xml,打开xml文件释放<Licenses>节点,给Key赋值为"0KOIk2JeIBYClPWVnMoRKn5cdY4="
再从Prime Sense\NITE\Hands\Data下复制Nite.ini过来,把AllowMultipleHands和TrackAdditionalHands都设为2。至此,开发环境已经部署完。
2.创建KinectSession类,负责连接Kinect设备,识别手势更新等。
//建立手势识别上下文
XnMOpenNIContext context = new XnMOpenNIContext();
//初始化(连接Kinect,读取data下的配置文件)
context.Init();
//建立操作状态管理器,第二个参数是捕捉焦点手势,第三个参数是快速捕获焦点手势,多个手势可以用逗号隔开
XnMSessionManager sessionManager = new XnMSessionManager(context, "Wave,Click", "RaiseHand");
//开始追踪
sessionManager.SessionStarted += sessionManager_SessionStarted;
//创建点收集器
XnMPointFilter filter = new XnMPointFilter();
XnMPoint Position1;
filter.PrimaryPointCreate += filter_PrimaryPointCreate;
//在点更新的事件里可以进行记录时刻变换的坐标, 用Point1来记录
filter.PrimaryPointUpdate += filter_PrimaryPointUpdate;
filter.PrimaryPointDestroy += filter_PrimaryPointDestroy;
//监听点动作
sessionManager.AddListener(filter);
3.创建Kinect主类
主类负责管理Kinect的工作过程(初始化,跟踪,操作,结束),跟踪Kinect的捕捉的数据变化并根据变化来封装一组事件,还可以控制马达,控制鼠标等。
时刻捕捉数据变化,需要用while(ture)或DispatcherTimer的Tick来跟踪KinectSession类里的Position1的值的变化。
//根据Position1值的变化可以封装几组事件:
public event MoveEventHandler Move; //移动
public event SwipeEventHandler Swipe; //挥舞
public event ScaleEventHandler Scale;//放大缩小
public event RotateEventHandler Rotate;//旋转
public event PushEventHandler Push;//推
其中的放大缩小和旋转实现的是Kinect捕获两个点(手),感觉很像<<少数派报告4>>里的隔空多点操作,非常酷。下面描述一下实现原理,先回到KinectSession类里
//在KinectSession里需要添加处理多只手识别,并且确定data文件下的Nite.ini里参数值都大于2
XnMMultipleHands hands = new XnMMultipleHands();
sessionManager.ProcessPoints(hands);
XnMPointFilter类负责点收集,但是当两只手都识别后,XnMPointFilter得到的点集合是两只手交替的点集合,这样就没法确定哪个点是哪只手的,所以需要创建两个XnMPointFilter并用PrimaryPointCreate和PrimaryPointUpdate来创建更新点,前缀带一个Primary代表了主点,它只会追踪一个手作为主手势。可以通过一个点ID识别的算法来把两只手区分开,
//先创建两个ID 代表两只手
static int HandID = 0;
static int HandsID = 0;
static bool MoreHands = false;
XnMPoint Position2; //保存主手的更新
//主手更新时
PrimaryPointUpdate(object sender, HandPointContextEventArgs e){
Position = e.HPC.Position;
HandsID = e.HPC.ID;
TimeDuration = e.HPC.Time;//用来保存一个手位置变换所需的时间
//这个变量很重要,在主类里获得点位置更新时,如果需要调节更新速度就用这个变量来判断
}
//所有手更新时
PointUpdate(object sender, PointBasedEventArgs e)
{
if (HandID == 0)
{
HandID = e.Id;
}
else if (HandID != e.Id && !MoreHands)
{
MoreHands = true;
}
else if (MoreHands)
{
if (HandsID != e.Id)
{
HandID = e.Id;
Position2 = e.Position;
MoreHands = true;
}
else
{
Position2 = null;
//MoreHands = false;
}
}
}
有了Position1和Position2的值,在主类里就可以很容易的通过差值得到放大缩小比例以及角度
//放大缩小(两点间的距离变化)
double scale = Math.Sqrt(Math.Pow((KinectSession.Position1.X - KinectSession.Position2.X), 2)+ Math.Pow((KinectSession.Position1.Y - KinectSession.Position2.Y), 2)) / 300;
//旋转(差值的正切值)
double angle = -(Math.Atan2(KinectSession.Position2.Y - KinectSession.Position1.Y,KinectSession.Position2.X - KinectSession.Position1.X)) / Math.PI * 180.0;
其他的操作,
控制鼠标可以使用Win32 API提供的方法
[DllImport("user32.dll")]
public static extern UInt32 SendInput(UInt32 nInputs, Input[] pInputs, int cbSize);
由于Kinect识别的点是以屏幕中心为坐标远点,所以控制鼠标的时候,需要换算一下坐标,当然也可以用我写好的方法
ControlMouse(int type, double x, double y)
public void ControlMouseOnMove(Point p)
public event ControlMouseMoveEventHandler ControlMouseMove;
public void ControlMouseOnClick(Point p)
public event ControlMouseClickEventHandler ControlMouseClick;
控制Kinect马达和信号灯颜色什么的就是调用CL NUI Platform提供的方法了
[DllImport("CLNUIDevice.dll", EntryPoint = "SetNUIMotorPosition",CallingConvention = CallingConvention.Cdecl)]
public static extern bool SetMotorPosition(IntPtr motor, short position);
4.应用
Kinect kinect = new Kinect();
//移动,两手放大,缩小,旋转,推手势
kinect.Move += new Kinect.MoveEventHandler(_kinect_Move);
kinect.Scale += new Kinect.ScaleEventHandler(_kinect_Scale);
kinect.Rotate += new Kinect.RotateEventHandler(_kinect_Rotate);
kinect.Push += new Kinect.PushEventHandler(_kinect_Push);
//控制鼠标移动和单击(Z和M键来激活)
_kinect.ControlMouseMove += new Kinect.ControlMouseMoveEventHandler(_kinect_ControlMouseMove);
_kinect.ControlMouseClick += new Kinect.ControlMouseClickEventHandler(_kinect_ControlMouseClick);
隔空操作的时候不同于鼠标或触摸都有按下抬起相对应的事件,只有Push手势可以作为一个选中的状态相当于Click,如果想控制手的移动速度和间隔,我设置了几个变量来实现
private const double MIN_OFFSET = 5d; //最小移动间隔
private const double MAX_OFFSET = 10d; //最大移动间隔
public double MoveTimeDur = .333333d; //移动时间
当 MAX_OFFSET - MIN_OFFSET = 0 时移动最平滑
MoveTimeDur = 0d 时移动最快(实时),当然鼠标移动应该属于这种
运行程序后,观察控制台,当Trace到找到Kinect设备时,就可以举起手对着Kinect挥挥手,当焦点捕获到了,就可以随意的控制程序了。
这里是程序截图,左上角是数据更新区域,左小角是几组操作提示,右下角是控制Kinect的马达和LED的颜色
国际惯例: [源码下载]
OpenNI对C#提供的开发资料实在很少,我的代码实现有些粗糙,但做例子足够了,希望以后能继续研究深度算法,整体骨骼识别等。也期望大家在园子里能一起讨论关于kinect在WPF中的应用。