HeadTexter:使用 Intel Perceptual Computing SDK 将头部运动转换为文本






4.80/5 (7投票s)
使用 C# 中的感知计算 SDK 进行头部到文本的转换
引言
HeadTexter 是一款旨在将头部运动转换为英文文本的简单应用程序。它使用 Intel Perceptual Computing SDK 进行头部跟踪,并且是最近结束的感知计算挑战赛的参赛作品。
背景
在您阅读文章的其余部分之前,您必须了解将头部运动转换为文本的疯狂之举背后的原因。它旨在开展一项研究,为阿尔茨海默病患者提供沟通方向。
阿尔茨海默病是一种主要由中风引起的疾病。它会使患者瘫痪,除了头部之外,他们无法移动身体的任何其他部位。随着时间的推移,头部运动也会变得有限,他们唯一能传达信息的方式是通过眼睛。所以我最初想建立一个将眼球运动转化为文本的系统。但是,Beta SDK 在面部跟踪方面,尤其是在眼球跟踪方面存在局限性,这让我不得不稍微调整设计,转向面部跟踪。
对于阿尔茨海默病患者来说,这种交互可能不太现实,但总得有个开始。所以我决定先构建一个基于头部运动的基础框架(事实证明这比我想象的要复杂),并在 SDK 问题修复后将工作转移到眼球跟踪。这意味着在转换部分进行一些简单的更改,应该会比较容易。
现在,关于我决定撰写关于基于头部跟踪的系统的教程,而不是手势识别工作,原因很简单。它适用于任何普通的网络摄像头。是的,没错。你们都可以安装 SDK 并开始编程,而无需购买创意手势摄像头。因此,我向社区(特别是那些痴迷于 C# 的开发者)介绍 Intel Perceptual Computing SDK 的动机是,让大家都有机会并获得一个简单的 SDK 操作流程,以便他们可以继续探索。
那么,我们在这个教程中主要学习什么?
a) 使用 Perceptual Computing SDK 进行开发
b) 使用 Perceptual Computing SDK 构建头部跟踪系统
c) 利用跟踪到的数据做一些有趣(或有意义)的事情。
d) 学习如何有效地使用一个已有 175 年历史的计算机概念。
让我们不要再浪费任何数字字节来阐明我的动机或文章的目标,开始我们最擅长的事情吧!编码。
使用代码
首先,请从 Perceptual Computing SDK 下载页面 下载 SDK。
开始使用 SDK
正如你可能预料到的那样,由于速度限制,SDK 主要用 C++ 编写,而你得到的 C# 版本是一个托管 DLL。所以,开始一个项目,并添加对位于 sdk/bin/X64 或 sdk/bin/X86 文件夹中的 libpixclr.dll 的引用。如果你确实需要一个 64 位应用程序,那么你还必须将项目属性更改为 64 位。如果你选择 x86,不要忘记将项目属性更改为 x86。“Any CPU”将无法正常工作。与 Microsoft 的 Ink 技术不同,该技术只能在 x86 架构上运行,应用程序在 x64 上会失败,而这款 SDK 没有这种担忧。选择 x86,它在两种架构上都能很好地运行。
对于我们这些精通 OpenCV 和 C++ 编码风格的人来说,我们会使用无限循环来获取数据。然而,由于它是 C#,我希望解决方案基于 BackgroundWorker。所以,我们将跳出 OpenCV 的 for(;;) 语句,在 DoWork 中捕获帧,并在 ProgressChanged 中进行处理。UI 和 SDK 线程是完全不同的栈,所以我们将使用一个委托来用 SDK 线程的结果更新 UI。
使用任何 PerC SDK 的第一件事就是创建一个会话。所以你需要一个 PXCMSession 对象。让我们称之为 session;
任何与 SDK 的操作都会返回一个 PxcmStatus 类型的状态。
让我们称对象为 sts。这个对象与成功和错误代码相关联,本质上是一个枚举。创建会话后,您需要从摄像头捕获帧。捕获类名为 UtilMCapture。让我们有一个对象叫 capture。在继续编写代码之前,我想告诉一些关于这个 Capture 类的有趣事实。SDK 支持多种类型的数据捕获,包括深度图捕获、RGB 数据捕获、语音等。你可以通过查看 bin 文件夹中的 capture_viewer 演示来找出 SDK 支持的流类型。所以,一旦会话创建,你确实需要一个配置文件或管道来获取正确的流。
所以,我们以以下方式开始会话。
sts = session.CreateImpl(PXCMFaceAnalysis.CUID, out fanalysis);
这是 SDK 中任何会话初始化的通用代码。第一个参数必须指定您想要创建会话的算法。在我们的例子中是 PXCMFaceAnalysis。所以我们将传递 PXCMFaceAnalysis 的 CUID 作为第一个参数。在 C++ 实现中,它接受一个指针作为输入,并通过引用传递值。C# 采用了类似的方法。该方法返回一个 PXCMBase 对象。PXCMBase 是一个类,所有感知计算算法类,如 FaceAnalysis、手势类等都继承自它。所以 fanalysis 只是一个 PXCMBase 类型的对象。
如果会话创建成功(更准确地说,如果你的代码获得了所需摄像头的控制权),那么 sts 将是 NO_ERROR 类型。现在你需要将 fanalysis 对象动态转换为 PXCMFaceAnalysis 对象。
这很简单。
fa = (PXCMFaceAnalysis)fanalysis.DynamicCast(PXCMFaceAnalysis.CUID);
现在,可以感到高兴的是,这种方法在 SDK 的任何功能中都非常通用。所以,你可以通过传递所需类的 CUID 并使用显式类型转换,动态地将通过 SessionImplementation 创建的任何 PXCMBase 对象转换为相应的 Analysis 类。
现在,第一步是构建一个 ProfileInfo,它将保存 faceAnalysis 模块任何查询的结果。
PXCMFaceAnalysis.ProfileInfo pf = new PXCMFaceAnalysis.ProfileInfo();
fa.QueryProfile(0, out pf);
'0'对我来说仍然不清楚。然而,英特尔表示它将来可以支持多种不同的配置文件,而“0”仅代表默认配置文件。
现在,结果配置文件应作为引用传递给 FaceAnalysis 模块。
fa.SetProfile(ref pf);
FaceAnalysis 是基础模块,它反过来支持 FaceAttribute 和 FaceDetection。FaceDetection 负责识别面部位置。
FaceAttribute 负责从面部数据中获取姿势,如性别信息、微笑、眼睛睁开程度等。
虽然这在当前的比赛中不是一个严肃的要求,因为我们不试图找出面部表情,但我还是想介绍分析部分,如果你想从面部跟踪模式中制作一个真正的游戏,这应该会很有帮助。
detection = (PXCMFaceAnalysis.Detection)fa.DynamicCast(PXCMFaceAnalysis.Detection.CUID);
face_attribute = (PXCMFaceAnalysis.Attribute)fa.DynamicCast(PXCMFaceAnalysis.Attribute.CUID);
现在,我们的这个小程序已经准备好进行面部和眼睛位置的检测以及面部属性的获取了。
就像你查询 PXCMFaceAnalysis 的配置文件一样,你需要为检测和属性对象也这样做。
dinfo = new PXCMFaceAnalysis.Detection.ProfileInfo();
attribute_dinfo = new PXCMFaceAnalysis.Attribute.ProfileInfo();
detection.QueryProfile(0, out dinfo);
face_attribute.QueryProfile(PXCMFaceAnalysis.Attribute.Label.LABEL_EMOTION, out attribute_dinfo);
detection.SetProfile(ref dinfo);
face_attribute.SetProfile(PXCMFaceAnalysis.Attribute.Label.LABEL_EMOTION, ref attribute_dinfo);
face_attribute.QueryProfile(PXCMFaceAnalysis.Attribute.Label.LABEL_GENDER, out attribute_dinfo);
face_attribute.SetProfile(PXCMFaceAnalysis.Attribute.Label.LABEL_GENDER, ref attribute_dinfo);
face_attribute.QueryProfile(PXCMFaceAnalysis.Attribute.Label.LABEL_EYE_CLOSED, out attribute_dinfo);
face_attribute.SetProfile(PXCMFaceAnalysis.Attribute.Label.LABEL_EYE_CLOSED, ref attribute_dinfo);
还有一个名为 landmark 的模块。两个眼点、鼻孔点和嘴唇上的一个点构成 FaceLandmark。
在这里,我又一次使用了 landmark 数据来向你介绍它。
landmark = (PXCMFaceAnalysis.Landmark)fa.DynamicCast(PXCMFaceAnalysis.Landmark.CUID);
landmark.QueryProfile(1, out lpi);
landmark.SetProfile(ref lpi);
不要太纠结于这里使用的命名约定。我只有一个月的时间来研究 SDK 和我想要尝试的各种想法,而且 C# 样本部分构建得不够好,我不得不将 C++ 版本代码转换为 C#。它几乎与 SDK 样本一样(我试图让它们更有意义)。但就是这样。
随着每个模块到位,相机启动,现在我们开始我们的捕获代码。
所以,我们异步运行一个 BackgroundWorker,并在 DoWork 中捕获帧。
sts = capture.ReadStreamAsync(images, out sps[0]);
嗯,第一个参数是一个 PXCMImage 类型数组,它被声明为
PXCMImage[] images = new PXCMImage[1];
对于面部,它总是 1。你可能会想,SDK 为什么期望参数是数组类型?首先,它告诉底层代码它只期望一个数据流(在某些 SDK 模块中,这是 RGB、深度图等的组合),其次,捕获图像的引用可以直接传递给数组类型。
但是,你不能使用从 SDK 返回的任何流,除非你同步它们。好吧,老实说,我不知道这到底是怎么回事。我只是从 SDK 手册中抄了一句话。
sts = fa.ProcessImageAsync(new PXCMImage[] { images[0] }, out sps[1]);
PXCMScheduler.SyncPoint.SynchronizeEx(sps);
我们还想在处理基于网络摄像头的项目时,在窗口中看到我们的脸。不是吗?它增加了趣味性,并告诉我们摄像头正在按预期工作。在 Windows 窗体中显示图像?一个 PictureBox。它能显示什么?Image 或 Bitmap 类型。但我们的图像真的是 Bitmap 吗?不,不是。这正是你需要从 PXCMImage 类型转换为 Bitmap 类型的原因。
bmp = new Bitmap((int)images[0].imageInfo.width, (int)images[0].imageInfo.height);
images[0].QueryBitmap(session, out bmp);
是的。这是 Bitmap 代码,我们现在更多地使用 C# 了。但是等等!仍然有一些 SDK 处理工作需要完成。还记得吗,我们需要检测头部的位置?
为了显示和检测面部位置,我们直接进入 ProgressChanged 方法。
PXCMFaceAnalysis.Detection.Data face_data;
detection.QueryData(fid, out face_data);
if (face_data.rectangle.h > 0)
{
try
{
Bitmap localBitmap = (Bitmap)bmp.Clone();
Graphics g = Graphics.FromImage(bmp);
float variation = 0;
this.Invoke((MethodInvoker)delegate
{
g.DrawRectangle(new Pen(new SolidBrush(Color.Red), 6), new Rectangle(new Point((int)face_data.rectangle.x, (int)face_data.rectangle.y), new Size((int)face_data.rectangle.w, (int)face_data.rectangle.h)));
});
x = (int)face_data.rectangle.x;
y = (int)face_data.rectangle.y;
}
catch
{
}
}
face_data 现在应该包含检测结果,本质上是围绕面部的矩形。我们获得矩形的 x 和 y 坐标。
好的,现在我们要将 x 和 y 信息存储到临时变量中,并将其与最后的位置数据进行比较,以获取运动方向的信息。
在我们开始处理简单部分之前。这里快速回顾一下摩尔斯电码。嗯,你们都一定听说过它。它是一种编码,为所有字母和数字分配一组“.”和“_”。让我惊讶的是,它是在 1836 年左右开发的。它是曾经快速通信的生命线——电报——背后的支柱。可惜的是,它在战争通信中比在计算中用得更多。
摩尔斯电码的优点是,它是一种可变长度编码,最大代码长度为 4。与二进制流一样,它使用空格或更确切地说,序列的缺失来检测数字的结尾。然而,在二进制系统中,我们需要大约 8 位来表示一个字符。所以,如果你想通过头部运动来表示“a”,那就是 8 次头部运动,然后其产物就得称之为“头痛”了,而不是其他什么。
我们需要理解的是,与你可以握紧、张开、伸出一根手指或所有手指、挥动的手不同,头部运动主要有 4 种运动:上、下、左、右。头部的对角线运动很难模拟。因此,我们只能为文本输入使用两种符号。其他两种必须用于触发转换和删除误解的符号。
为了有效地使用系统,我们将使用一个字符到摩尔斯电码的转换例程(尽管在这里不会使用它)和一个摩尔斯电码到字符的转换例程。然后,我们将为上和下运动分配一个代码,并触发转换。这纯粹是查找表的工作。
#region Morse code related part
private Char[] Letters = new Char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', ' '};
private String[] MorseCode = new String[] {".-", "-...", "-.-.",
"-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..",
"--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-",
"...-", ".--", "-..-", "-.--", "--..", "-----", ".----", "..---",
"...--", "....-", ".....", "-....", "--...", "---..", "----.", " "};
public String ConvertMorseToText(String text)
{
text = "@" + text.Replace(" ", "@@") + "@";
int index = -1;
foreach (Char c in Letters)
{
index = Array.IndexOf(Letters, c);
text = text.Replace("@" + MorseCode[index] + "@", "@" + c.ToString() + "@");
}
return text.Replace("@@@@", " ").Replace("@", "");
}
public String ConvertTextToMorse(String text)
{
text = text.ToLower();
String result = "";
int index = -1;
for (int i = 0; i <= text.Length - 1; i++)
{
index = Array.IndexOf(Letters, text[i]);
if (index != -1)
result += MorseCode[index] + " ";
}
return result;
}
string MorseSymbol2Gesture(char symbol)
{
switch (symbol)
{
case '.':
return "UP ";
case '-':
return "DOWN ";
case ' ':
return "RIGHT";
default:
return "LEFT";
}
}
#endregion
如果你使用 SDK 的 HandGesture 和跟踪功能,你会得到一种事件处理程序委托。它会在任何有效的手势,如 THUMB_UP 时触发。你需要通过一个本地方法订阅事件。然而,在头部运动中没有这样的事件。所以我们需要通过原始代码创建这个事件。
在你敢想这很简单之前,我建议你仔细看看视频。SDK 的面部跟踪速度相当慢。因此,在计算上,你不能在不实际向左或向右移动的情况下低头,向上移动也是如此。我们人类不是机器人,不会简单地沿着一个轴低头和抬头。肩膀会试图平衡它,你也会立即有侧向移动。有时,侧向移动与上下移动一样重要。另一个重要方面是,每次向左移动后,头部必须回到中心,触发恰好相反的运动。
当应用程序几乎准备就绪时,这个问题几乎让应用程序崩溃了。
为了解决这个问题,我们将使用基于状态的编程。这是一种概率编程模型,如隐马尔可夫模型,它基于某个运动与其他运动相关时的概率。我首先根据用例创建了一个状态图,它有 5 个状态,我发现向上或向下运动的频率会远低于侧向运动。所以用户可以有效地使用左和右,但不能使用上和下。所以我们将首先检查垂直运动,如果有一点点,我们将首先对其进行转换并跳过水平转换。如果垂直运动不显著,我们将转换水平运动。一旦检测到特定运动,我们将使用休眠来允许用户将头部恢复到正常状态。但是任何 Thread.Sleep() 都会冻结捕获,你将丢失任何中间帧,因为我们在 ProgressChanged 方法中。因此,我们不是使用 Thread.Sleep,而是使用一个普通的变量。一旦检测到运动,我们将给变量赋值 15。主线程等待 50 毫秒来捕获一帧。所以,我每 50 毫秒都有一个 Sleep 调用。一旦变量被赋值且不为零,每次进入 ProgressChanged 时,我们都会将其递减。我们的正常处理将在变量恢复到零后开始。
由于我们不是机器人,我们的头部无法像蜡烛一样稳定。它会移动。因此,我们无法固定一个中心点。运动必须从当前位置和前一位置之间的差异派生。
variation = (float)Math.Sqrt((double)((x - x1) * (x - x1) + (y - y1) * (y - y1)));
//////////// Morse Logic///////////////////////////////////////////
if (!head.Equals(HeadState.NONE))
{
head = HeadState.NONE;
x1 = x;
y1 = y;
return;
}
if (nn != 0)
{
if(nn>0)
nn--;
}
if ((variation > 9) && (nn==0) )
{
int xvar = x - x1;
int yvar = y - y1;
if (Math.Abs(yvar) > Math.Abs(xvar))
{
if (Math.Abs(yvar) > 9)
{
if (yvar < 0)
{
head = HeadState.UP;
}
if (yvar > 0)
{
head = HeadState.DOWN;
}
}
}
else
{
if (Math.Abs(xvar) > Math.Abs(yvar))
{
if (Math.Abs(xvar) > 9)
{
if (xvar < 0)
{
head = HeadState.LEFT;
}
if (xvar > 0)
{
head = HeadState.RIGHT;
}
}
}
}
//................ Put it as Morse Code................../
this.Invoke((MethodInvoker)delegate
{
switch (head)
{
case HeadState.UP:
txtCurrentSequence.Text += ".";
nn = 8;
break;
case HeadState.DOWN:
txtCurrentSequence.Text += "-";
nn = 8;
break;
case HeadState.RIGHT:
txtRecognition.Text += ConvertMorseToText(txtCurrentSequence.Text + " ");
txtCurrentSequence.Text = "";
nn = 8;
break;
case HeadState.LEFT:
char[] m = txtCurrentSequence.Text.ToCharArray();
string s = "";
for (int i = 0; i < m.Length-1; i++)
{
s = s + m[i];
}
txtCurrentSequence.Text = s;
nn =8;;
break;
}
});
}
x1 = x;
y1 = y;
/////////////////////////////////////////////////////////////////
哇!这就是你所需要的一切。发挥你的创造力,把它应用到游戏中,用它来创建密码管理器,做所有你能用代码做到的好事。我希望 SDK 很快能得到改进,这样我才能实现我的目标,让它适合真正的患者。
关注点
我看到最近在一些比赛和新技术使用中有一个趋势,人们更倾向于视觉效果。XNA、Unity 和 3D 渲染占据了使用任何酷炫技术(无论是 Ultrabook 还是 Perceptual Computing)的应用的大部分。我的目标不是创建适合此类开发的应用程序,而是将功能集成到现有问题中,并尝试解决关键问题。微软近期对 Silverlight 和 WPF 等技术的模糊立场让生活变得更加糟糕。然而,C# 是一种任何当前语言都无法比拟的强大力量。行动必须比言语更有力。因此,我邀请我亲爱的社区探索 SDK 并构建令人惊叹的东西。让微软看看,如果你不断地把忠诚的硬核程序员抛弃给儿童语言,你就无法生存。