使用Microsoft Kinect点云和语音识别进行家庭自动化
使用Microsoft Kinect控制房屋中的家庭自动化。灯光可以通过语音识别打开和关闭,或者通过指向它们并挥动手来打开和关闭,一个方向打开,另一个方向关闭。
引言
我喜欢在我的家庭自动化项目中使用Microsoft Kinect。Microsoft Kinect for Windows API非常强大,可以用于跟踪我们在物理世界中的移动,以独特而创造性的方式,超越传统的游戏控制器。传统的计算机视觉系统太慢,无法跟踪正常的人类运动,但Kinect能够以每秒30次的速度提供20个关节的坐标。Kinect能够通过创建所谓的“点云”来简化计算机视觉问题,点云由红外光构成。这种红外光类似于可见光,但波长比我们能看到的要长。点云可以通过特殊相机或夜视镜看到,如下图所示。
Kinect有一个特殊的镜头,它发出已知模式的红外光线。光线会在接触到的物体上形成点,从而创建一个点云。Kinect有一个特殊的相机来查看红外点。Kinect的视觉系统测量点之间的距离,并分析图案中的位移,以确定物体有多远。请看下图,近距离的物体上的点更密集,远距离的物体上的点间隔更远。Kinect能够分析红外点的间距来构建深度图,并快速看到人类轮廓,因为人类位于其他物体的前面。
使用Kinect创建自然的用户界面
Kinect有一些很棒的用户界面,但大多数都需要你看着电脑屏幕。我构建了一个系统,不需要你看着电脑就能选择一个设备并打开或关闭它。你可以简单地用一只手指向一个设备,将另一只手举过头顶,朝一个方向挥动即可打开,朝另一个方向挥动即可关闭。除了使用手势,我还使用Kinect语音识别引擎来打开或关闭设备。
向量
向量是数学和物理学的基石,它们代表方向和大小(也称为长度)。它们是3D编程的基础,并广泛用于构建计算机游戏或工程应用程序的3D模型。
Vector3D
是System.Windows.Media.Media3D
命名空间中的一个结构。此命名空间包含支持Windows Presentation Foundation (WPF)应用程序中3D呈现的类型和结构。Vector3D
结构是为WPF应用程序构建的,但也非常有助于处理其他3D向量数据,包括来自Kinect的向量数据。Microsoft.Kinect
命名空间有一个Vector4
结构。这与WPF的Vector3D
结构类似,但除了Vector3D
标准的X、Y和Z属性外,还有一个名为W的属性。附加的W属性是Vector4
中的第四个维度,用于在称为四元数的数字系统中围绕向量定义的轴进行3D空间旋转。我使用WPF库中的Vector3D
而不是Kinect库中的Vector4
,因为对于这个项目,我不需要在3D空间中旋转任何东西,而且Vector3D
内置了计算点积和叉积的有用方法,并且还有一个长度属性。
我进一步扩展了Vector3D
的功能,创建了我的Line
类。在数学中,一条直线可以由一个点和一个通过该点的向量表示。定义一条直线需要2个点,因此构造函数具有来自Kinect的2个SkeletonPoints
的参数。
public Line(SkeletonPoint Point1, SkeletonPoint Point2)
{
_point.X = Point1.X;
_point.Y = Point1.Y;
_point.Z = Point1.Z;
_vector.X = Point2.X - Point1.X;
_vector.Y = Point2.Y - Point1.Y;
_vector.Z = Point2.Z - Point1.Z;
}
下面的方法位于KinectLiving
类中。它根据另一只手的手势确定哪只手指向设备,并利用肘部和手的位置返回一条用户指向设备的直线。
internal static Line GetLinePointingToDevice(Skeleton skeleton, Gesture gesture)
{
if (IsRightHandThePointer(gesture,skeleton))
return new Line(skeleton.Joints[JointType.ElbowRight].Position, skeleton.Joints[JointType.HandRight].Position);
else
return new Line(skeleton.Joints[JointType.ElbowLeft].Position, skeleton.Joints[JointType.HandLeft].Position);
}
查找物体坐标
Kinect API为您提供人体20个关节的X、Y和Z坐标。我使用肘部和手部的坐标来创建指向物体的直线。程序然后要求您从不同位置再次指向,并创建另一条指向物体的直线。这些直线是异面直线,因为它们不平行,并且在三维空间中它们也不相交。这些直线在您指向的物体坐标处非常接近。我使用Vector3D
库进行一些3D数学计算,包括点积和叉积,以获得同时垂直于两条直线的线段的中点。我最早是在大学三年级的微积分课上学习如何解决三维数学问题,但那已经是20年前的事了,我重新学习它的时候非常开心!
public Point3D Intersection(Line secondLine)
{
Vector3D vectorPerpendicularBothLinesFromLine1ToLine2 = Vector3D.CrossProduct
(secondLine.Vector, Vector3D.CrossProduct(this.Vector, secondLine.Vector));
//The skew lines are Parallel so return MaxValue for the coordinates
if (vectorPerpendicularBothLinesFromLine1ToLine2 == new Vector3D(0, 0, 0))
{
return new Point3D(double.MaxValue, double.MaxValue, double.MaxValue);
}
Vector3D vectorQP = new Vector3D(secondLine.Point.X - this.Point.X,
secondLine.Point.Y - this.Point.Y, secondLine.Point.Z - this.Point.Z);
double t1 = Vector3D.DotProduct(vectorPerpendicularBothLinesFromLine1ToLine2,
vectorQP) / Vector3D.DotProduct(vectorPerpendicularBothLinesFromLine1ToLine2, this.Vector);
Point3D firstPoint = this.Position(t1);
Vector3D vectorPerpendicularBothLinesFromLine2ToLine1 = Vector3D.CrossProduct
(this.Vector, Vector3D.CrossProduct(this.Vector, secondLine.Vector));
Vector3D vectorPQ = new Vector3D(this.Point.X - secondLine.Point.X,
this.Point.Y - secondLine.Point.Y, this.Point.Z - secondLine.Point.Z);
double t2 = Vector3D.DotProduct(vectorPerpendicularBothLinesFromLine2ToLine1,
vectorPQ) / Vector3D.DotProduct(vectorPerpendicularBothLinesFromLine2ToLine1, secondLine.Vector);
Point3D secondPoint = secondLine.Position(t2);
double midX = (firstPoint.X + secondPoint.X) /2;
double midY = (firstPoint.Y + secondPoint.Y) /2;
double midZ = (firstPoint.Z + secondPoint.Z) /2;
Point3D midPoint = new Point3D(midX,midY,midZ);
return midPoint;
}
确定您指向哪个物体
一旦您知道了物体在3D空间中的坐标,就可以计算角度来确定您指向哪个物体。角度的顶点是您的肘部。从肘部到手的向量用作参考,它指示了您指向的方向。计算从肘部到每个物体坐标的向量。参考向量与您的手之间的夹角越小,表明您指向的物体就是那个物体。
下面的代码展示了计算到对象角度的算法。点积的几何定义用于计算向量之间的角度。
求解角度的方程并将其转换为下面的代码。角度从弧度转换为度数,只是因为我更喜欢以度数而不是弧度来思考。结果还进行四舍五入,以消除小数近似误差,这样我在单元测试中就能得到预期的值,而不是一个非常接近但有舍入误差的值。
public double AngleToPoint(SkeletonPoint point)
{
Vector3D vectorToPoint = new Vector3D(point.X -
this.Point.X, point.Y - this.Point.Y, point.Z - this.Point.Z);
double cosOfAngle = Vector3D.DotProduct(vectorToPoint,
this.Vector) / (this.Vector.Length * vectorToPoint.Length);
double angleInDegrees = Math.Round( Math.Acos(cosOfAngle) * 180 / Math.PI,3);
return angleInDegrees;
}
绝对值最小的角度所对应的设备就是您所指向的设备。
public KinectDevice PointingToDevice(Skeleton skeleton, Gesture gesture)
{
Line line = GetLinePointingToDevice(skeleton, gesture);
KinectDevice pointingToDevice = null;
double shortestAngle=180;
foreach (KinectDevice kinectDevice in _devices)
{
double angleToPoint = Math.Abs(line.AngleToPoint(kinectDevice.Point));
if (angleToPoint <= shortestAngle || pointingToDevice == null)
{
shortestAngle = angleToPoint;
pointingToDevice = kinectDevice;
}
}
return pointingToDevice;
}
以下方法根据另一只手检测到的手势来打开或关闭您指向的设备。
private void UpdateDeviceBasedOnGesture(Skeleton skeleton, Gesture gesture)
{
KinectDevice closestDevice = KinectLiving.GetInstance().
ClosestDevicePointedAt(skeleton, gesture);
textBlockMessage.Text = closestDevice.Name;
textBlockLearnDevicePointMessage.Text = "";
switch (gesture)
{
case Gesture.TurnOnWithLeftHandPointingToDevice:
case Gesture.TurnOnWithRightHandPointingToDevice:
{
closestDevice.TurnOn();
break;
}
case Gesture.TurnOffWithLeftHandPointingToDevice:
case Gesture.TurnOffWithRightHandPointingToDevice:
{
closestDevice.TurnOff();
break;
}
}
}
KinectDevice TurnOn
和TurnOff
方法调用LogicalLiving.Web
项目中的Web服务方法。LogicalLiving.Web
项目是一个MVC4项目,其用户界面基于jQuery Mobile构建。所有设备都可以通过Web控制,意图是作为手机应用程序运行,但也可以在任何浏览器中运行。请阅读我的CodeProject jQuery Mobile文章以获取更多信息。KinectDevice
代码利用了jQuery Mobile网站用来打开或关闭设备的同一MVC控制器方法。
internal void TurnOn()
{
if (this.NetduinoMessageOn.Length!=0)
{
InvokeMvcControllerMethod.SendMessageToNetduino(this.NetduinoMessageOn, this.VoiceOn);
}
if (this.DeviceNode != DeviceNode.None)
{
InvokeMvcControllerMethod.SendZWaveMessage(this.DeviceNode, DeviceState.On);
}
}
Kinect手势
我编写了一个名为KinectGestures
的类,用于检测应用程序的手势。检测到的手势是以下之一:TurnOnWithRightHandPointingToDevice
(右手指向设备时打开)、TurnOnWithLeftHandPointingToDevice
(左手指向设备时打开)、TurnOffWithRightHandPointingToDevice
(右手指向设备时关闭)、TurnOffWithLeftHandPointingToDevice
(左手指向设备时关闭)、None
(无)。
了解哪只手正在执行手势很重要,因为另一只手正在指向设备。代码会跟踪左右手根据以下枚举定义的状况。
namespace LogicalLiving.KinectLiving
{
public enum Gesture { TurnOnWithRightHandPointingToDevice,
TurnOnWithLeftHandPointingToDevice, TurnOffWithRightHandPointingToDevice,
TurnOffWithLeftHandPointingToDevice, None }
public enum RightHandState { AboveHeadRight, AboveHeadSweepRightToLeft,
AboveHeadLeft, AboveShoulderSweepLeftToRight, BelowHead, Reset };
public enum LeftHandState { AboveHeadLeft, AboveHeadSweepLeftToRight,
AboveHeadRight, AboveHeadSweepRightToLeft, BelowHead, Reset };
}
打开设备的手势是举起一只手到头顶,然后向头部挥动。朝相反方向挥动可以关闭设备。不执行手势的另一只手指向您想要控制的设备。
右手状态的计算方法是
public static RightHandState GetRightHandState(Skeleton skeleton)
{
RightHandState rightHandState = RightHandState.Reset;
if (skeleton.TrackingState != SkeletonTrackingState.Tracked)
{
rightHandState = RightHandState.Reset;
}
else if (skeleton.Joints[JointType.HandRight].Position.Y
< skeleton.Joints[JointType.Head].Position.Y)
{
rightHandState= RightHandState.BelowHead;
}
else if (skeleton.Joints[JointType.HandRight].Position.X
>= skeleton.Joints[JointType.ShoulderRight].Position.X)
{
rightHandState = _previousRightHandState == RightHandState.AboveHeadLeft ||
_previousRightHandState == RightHandState.AboveShoulderSweepLeftToRight ?
RightHandState.AboveShoulderSweepLeftToRight : RightHandState.AboveHeadRight;
}
else if (skeleton.Joints[JointType.HandRight].Position.X
< skeleton.Joints[JointType.ShoulderRight].Position.X)
{
rightHandState = _previousRightHandState == RightHandState.AboveHeadRight ||
_previousRightHandState == RightHandState.AboveHeadSweepRightToLeft ?
RightHandState.AboveHeadSweepRightToLeft : RightHandState.AboveHeadLeft;
}
_previousRightHandState = rightHandState;
return rightHandState;
}
有一个类似的方法来计算左手状态。然后,通过查看右手状态和左手状态来检测手势,将检测到的手势计算为状态机。
public static Gesture DetectGesture(Skeleton skeleton)
{
Gesture gesture = Gesture.None;
RightHandState rightHandState = GetRightHandState(skeleton);
LeftHandState leftHandState = GetLeftHandState(skeleton);
if (rightHandState == RightHandState.AboveHeadSweepRightToLeft)
{
gesture = Gesture.TurnOnWithLeftHandPointingToDevice;
}
else if (leftHandState == LeftHandState.AboveHeadSweepLeftToRight)
{
gesture = Gesture.TurnOnWithRightHandPointingToDevice;
}
else if (rightHandState == RightHandState.AboveShoulderSweepLeftToRight)
{
gesture = Gesture.TurnOffWithLeftHandPointingToDevice;
}
else if (leftHandState == LeftHandState.AboveHeadSweepRightToLeft)
{
gesture = Gesture.TurnOffWithRightHandPointingToDevice;
}
if (gesture != Gesture.None)
{
_previousRightHandState = RightHandState.Reset;
_previousLeftHandState = LeftHandState.Reset;
}
return gesture;
}
语音识别
我编写的语音识别类基于Kinect for Windows资源与示例中的“Speech Basics – WPF”示例代码。Initialize
方法调用BuildAllChoices()
方法来返回所有语音命令选项。
private static Choices BuildAllChoices()
{
Choices voiceCommandChoices = new Choices();
foreach (KinectDevice kinectDevice in KinectLiving.GetInstance().Devices)
{
string[] voiceOn = kinectDevice.VoiceOn.Split('|');
string[] voiceOff = kinectDevice.VoiceOff.Split('|');
List<string> voiceCommandList = new List<string>();
voiceCommandList.AddRange(voiceOn);
voiceCommandList.AddRange(voiceOff);
foreach (string ignoreCommand in voiceCommandList)
{
//Commands to ignore - you must say Alice first
voiceCommandChoices.Add(new SemanticResultValue(ignoreCommand, ignoreCommand));
//Commands
string validCommand = string.Format("Alice {0}", ignoreCommand);
voiceCommandChoices.Add(new SemanticResultValue(validCommand, validCommand));
}
}
return voiceCommandChoices;
}
每个设备可以有无限数量的命令来打开或关闭它。例如,以下任何命令都可以用来打开客厅灯:light|light on|main light|main light on|living room|living room light|living room on|living room light on。
voiceCommandList
的创建包含
- 所有用于打开设备的语音命令
- 所有用于关闭设备的语音命令
string[] voiceOn = kinectDevice.VoiceOn.Split('|');
string[] voiceOff = kinectDevice.VoiceOff.Split('|');
List<string> voiceCommandList = new List<string>();
voiceCommandList.AddRange(voiceOn);
voiceCommandList.AddRange(voiceOff);
我选择将家里的计算机助手命名为Alice,灵感来自“The Brady Bunch”电视节目中的管家。我们的家庭生活与“The Brady Bunch”相似,因为我有两个前妻的孩子,我的妻子也有两个女儿。我们希望语音识别系统忽略不以“Alice”开头的命令。对于voiceCommandList
中的每一项,我都会添加一个没有“Alice”前缀的无效选项和一个包含“Alice”前缀的有效选项。将Choice加载没有“Alice”前缀的无效命令很重要,因为它能防止语音识别引擎在您不先说“Alice”时,从正常语音中选择您不想要的命令。
SpeechRecognized
方法是识别语音事件的处理程序,如下面的代码所示。SpeechRecognizedEventArgs
包含一个置信度值,表示Kinect确定它检测到正确选项的把握程度。该值在0到1之间,值越大,Kinect就越有信心它检测到了正确的选项。下面的代码会忽略不以“Alice”开头的选项,并且当Confidence
低于设备的MinSpeechConfidence
时也会忽略结果。每个设备都有自己的置信度级别,因为您希望在某些设备(如壁炉)上确保在更改其状态之前,而对灯的更改则不那么在意,如果您错误地更改了灯的状态。较低的置信度级别使得无需用户重复命令即可轻松控制设备,因为背景噪音或第一次没有说清楚。
private void SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
{
string phrase = e.Result.Semantics.Value.ToString();
if (phrase.StartsWith("Alice "))
{
phrase = phrase.Replace("Alice ", "");
KinectDevice kinectDeviceOn = KinectLiving.GetInstance().
Devices.FirstOrDefault(d => d.VoiceOn.Split('|').Contains(phrase));
if (kinectDeviceOn != null &&
e.Result.Confidence >= kinectDeviceOn.MinSpeechConfidence)
kinectDeviceOn.TurnOn();
KinectDevice kinectDeviceOff = KinectLiving.GetInstance().
Devices.FirstOrDefault(d => d.VoiceOff.Split('|').Contains(phrase));
if (kinectDeviceOff != null &&
e.Result.Confidence >= kinectDeviceOff.MinSpeechConfidence)
kinectDeviceOff.TurnOff();
}
}
下面的代码行返回包含在管道分隔列表的短语中的KinectDevice
,这些短语用于VoiceOn
属性。如果没有设备有匹配的短语,则返回null
。
KinectDevice kinectDeviceOn = KinectLiving.GetInstance().
Devices.FirstOrDefault(d => d.VoiceOn.Split('|').Contains(phrase));
如果找到匹配项,则调用TurnOn
方法。这是手势检测例程用于打开设备的相同方法。对于VoiceOff
短语,也有匹配的代码来关闭设备。KinectDevice TurnOn
和TurnOff
方法调用LogicalLiving.Web
项目中的Web服务方法。
Z-Wave
Z-Wave是一种专为家庭自动化设计的无线通信协议。市面上有大量的Z-Wave设备。LogicalLiving.ZWave
项目包含一个类库,用于通过Areon Z-Stick Z-Wave USB适配器控制Z-Wave设备。我在线购买了USB适配器,花费不到50美元,我所有的Z-Wave设备也都不到50美元。我通过关闭房屋电源来安装Z-Wave设备,然后将标准的电灯开关和电源插座替换为Z-Wave设备。我编写了LogicalLiving.Zwave.DesktopMessenger
项目,作为一个示例Windows Forms UI来控制LogicalLiving.ZWave
类库。使用LogicalLiving.Zwave.DesktopMessenger
来确定Z-WaveDeviceNode
的值非常有用。每个Z-Wave设备都有其唯一的DeviceNode
,这在Z-Wave消息中是必需的,以更改其状态。
Netduino
Netduino是一个基于.NET Micro Framework的出色的开源电子原型平台。我使用netduino plus 2和一个我构建的定制电路来控制家中的许多设备,包括打开壁炉、瞄准泳池中的喷水枪、浇灌花园和打开车库门。我使用Kinect.Living
的手势和音频命令来打开壁炉。我们家新来了一只小猫,对壁炉非常感兴趣。我很快就担心小猫会在有人执行打开壁炉的手势或音频命令时爬进壁炉。为了安全起见,我安装了一个她无法钻进去的网状屏幕帘!请阅读我之前关于netduino和jQuery Mobile的文章。
- 文章1 - 使用Netduino和Kinect进行家庭自动化
- 文章2 - 将jQuery Mobile与MVC和Netduino结合用于家庭自动化
- 文章3 - 面向家庭自动化的物联网
Kinect for Windows v2
本文最初是为Kinect for Windows v1撰写的。我在我的面向家庭自动化的物联网文章中升级了Kinect for Windows v2的代码。
摘要
使用Microsoft Kinect for Windows API进行家庭自动化项目非常有趣。本项目为控制家中的设备提供了一个更加自然的UI。无需遥控器即可控制设备,这一点非常好。在我们家,遥控器总是弄丢在沙发某个地方,但现在不用担心了。有了Microsoft Kinect和这个项目,您就是控制整个家庭设备的遥控器。
本文中的想法可以远远超出家庭自动化。对于能够知道您正在指向哪个物体的计算机来说,还有许多其他有用的应用。我们生活在一个激动人心的时代,视觉系统被封装成廉价的设备,并且像Microsoft Kinect一样容易获得。Kinect和Kinect for Windows SDK使我们能够以最小的努力构建令人难以置信的应用程序,这些应用程序在10年前可能被视为科幻小说。