感知计算:从深度数据生成 3D
在使用感知计算硬件时,您很快就会发现,要突破 Intel® Perceptual Computing SDK 的限制,就需要深入了解相机提供的深度数据以及如何对其进行操作。本文将介绍获取此数据的基础知识,以及它如何...
引言
在使用感知计算硬件时,您很快就会发现,要突破 Intel® Perceptual Computing SDK 的限制,就需要深入了解相机提供的深度数据以及如何对其进行操作。本文将介绍获取此数据的基础知识,它与相机的关系,以及如何将其转换为纹理化的 3D 模型以进行渲染。
作为最近参加的两次 Intel Ultimate Coder 挑战赛的参与者,我在过去两个月里有机会接触这项技术,而我设计和开发 3D 游戏引擎的背景帮助我克服了您在自己的旅程中肯定会遇到的许多障碍。
作为先决条件,您应该对向量、顶点格式等 3D 概念有广泛的了解,并具备 C 语言的基础知识。
为什么深度生成 3D 很重要
如果您是感知计算新手,您可能会从 Intel Perceptual Computing SDK 开始,并探索文档和代码示例中描述的方法。当您想探索这些方法之外的功能时,就需要直接利用原始数据。通过将深度数据转换为 3D 格式(如网格或点云),您可以获得额外的技术和创造机会。
获取相机前方世界 3D 表示的主要原因是为了进行简单的渲染,或者用于更高级的控制系统,如手指和身体跟踪。
本文将使用 C++ API 探讨此技术,但无论您是 Unity* 还是 C# 开发者,其概念都是相同的。Unity 开发者需要创建自己的第三方模块来实现其应用中的此功能。
技术详解
该方法分为两个主要步骤。第一步是从相机读取深度数据并将值存储在数组中。第二步是过滤此数据并将其转换为三维坐标,然后从中创建网格。
该技术背后的理念非常简单。在此过程中,没有任何数据被负面更改;它只是从一个上下文转换到另一个上下文。深度数据存在为一个二维的 16 位整数数组。目标格式存在为一个适合渲染或 3D 交互的一维类型化数据数组。
原始深度数据
分辨率会因设备而异,但第一代 Creative* Interactive Gesture 相机提供 320x240 的原始深度数据,使用 16 位整数存储表示每个像素处物体到相机的距离的值。通过初始化相机硬件并请求深度数据流,您可以以每秒 30 帧的默认速度将此数据拉入您的应用程序。
一旦数据流与您的应用程序同步,就可以访问原始数据,而提取深度值的最简单、最快捷的方法是使用嵌套的 for 循环,并扫描数据行,直到读取每个像素。
目标数组
为了在您的应用程序中保持高性能,您需要避免重复扫描深度数据,因此最好在开始扫描深度数据之前准备好目标数组。为了进一步提高性能,我们还将确保我们的数组可以直接用于渲染,因此数据结构将足够大,可以存储顶点位置、顶点法线和顶点 UV 坐标。
在 DirectX* 中,术语 FVF(柔性顶点格式)用于描述 3D 空间中点的结构,它包含比特殊信息更多的内容。它还存储顶点的朝向以及顶点可能拥有的颜色。由于本主题超出本文范围,因此假定您熟悉 3D 模型如何存储和渲染。
对于我们的技术,我们的 FVF 结构包括三个 32 位浮点数用于位置 (XYZ),三个 32 位浮点数用于法线/方向 (NXNYNX),以及两个 32 位浮点数用于纹理坐标 (UV)。请注意 U 和 V 值的位置,因为我们稍后将处理这个问题。每个顶点占用 8 个浮点数,然后我们将其乘以深度数据的分辨率,以生成目标数组的最终大小。
流程
一旦源和目标准备就绪,过程本身就相对简单。每个 16 位整数在深度分辨率的特定 X 和 Y 坐标处读取。深度值本身代表距离,在这种情况下,我们将将其转换为一个新值,我们称之为 Z。现在,我们可以使用代表 3D 空间中位置的 X、Y 和 Z 值来填充我们的目标数组。通过对深度数据中的每个像素执行此操作,我们将生成一个 320 个顶点宽、240 个顶点高,沿深度轴范围任意的 XYZ 值 3D 矩阵。
此时,您拥有未纹理化的 3D 网格数据,作为相机深度数据流的实时表示。
您可以选择两条路。您可以处理现有形式的 3D 数据,也许将其转换为点云以进行进一步的 3D 分析。第二条路,也是我们将要走的,是使用数据直接渲染到屏幕。
纹理注意事项除了将深度信息转换为 X、Y、Z 值之外,您还需要将 X 和 Y 值再次转换为 UV 坐标。由于我们要对 3D 模型进行纹理化,因此每个顶点都需要知道它在使用的纹理图像上的哪个点。
计算仅涉及将 X 值除以深度数据的整体宽度,并将结果存储为 U 值。同样,将 Y 值除以深度数据的高度将生成顶点格式所需的 V 值。每个顶点内的 UV 坐标现在应该是 0 到 1 之间的浮点值。
法线注意事项
如果在此时渲染 3D 网格,您会惊恐地发现,尽管可以看到深度数据以 3D 形式显示,并且纹理图像均匀地分布在网格上,但光照却无法正常工作。为了使网格正确光照,还需要填充法线值 (NXNYNX)。
作为回顾,法线向量需要远离位置顶点,以便在指定光照位置时可以确定正确的光照衰减,而计算此法线向量的唯一方法是知道顶点邻居的位置。由于在主要过程中此信息未知,因此需要在写入所有位置后将其应用于网格。
在第二个通道中,我们遍历网格中的每个顶点,并基于邻近顶点的位置,计算该顶点的理想平滑法线。完成后,3D 网格将可以针对光源进行渲染。
此时,我们的 3D 网格已填充了位置、纹理和光照信息,并代表了来自相机的深度数据的实时形状。可以应用进一步的技术来改进所需的渲染,可以应用于提取循环或法线处理循环。一些概念包括将数据处理成单独的网格,或从网格中删除背景像素。
技巧和窍门
做
尽量减少对数据的遍历次数。考虑到深度数据产生的数据量和 3D 网格数据生成的数据量,多次遍历这些数据会影响您应用程序的性能。
请记住在应用程序的适当阶段释放所有内存使用和接口。确保这一点的好方法是在添加创建部分后立即添加终止或释放代码。如果您的应用程序只是终止,那么后果并不算太糟糕,但感知计算应用程序通常会在应用程序的生命周期中多次激活然后重新激活相机,确保您的程序没有内存泄漏将减少后续的支持事件。
捕获深度数据时,请确保在驱动程序中启用深度平滑,这将有助于稳定您在 3D 表示的边缘可能观察到的不规则值。这是由于相机发出的红外光束散射,导致深度数据在物体是近还是远时出现混淆。尝试进行边缘检测可能也值得,以帮助澄清前景对象的真实边缘。
不要做的事
不要尝试将彩色流和深度流的帧率属性设置为不同的值,因为驱动程序不允许这样做。如果您只需要每 30 帧彩色数据(具有更大的 640 像素宽度分辨率),但需要每秒 60 帧更新 3D 模型,那么您需要创建设备的两个实例,以便它们可以以不同的速率同步。
渲染 320x240 3D 模型时,不要使用 16 位索引缓冲区,因为它会放不下。使用 32 位索引缓冲区或仅顶点缓冲区,以便您可以渲染整个网格。
如果您稍后想从 3D 网格中分离出各种特征,请不要共享顶点位置。您的 3D 网格必须由真正独立的多个多边形组成,以便您可以实时从任何背景网格中分离出来。
代码概述
以下代码片段重点介绍了该技术的关键要素。
pxcStatus sts=PXCSession_Create(&session);
UtilCmdLine cmdl(session);
此时我们已经创建了一个会话,在开始捕获相机数据之前需要此会话。
pcapture = new UtilCapture(session);
for (std::list<PXCSizeU32>::iterator itr=cmdl.m_csize.begin();itr!=cmdl.m_csize.end();itr++)
pcapture->SetFilter(PXCImage::IMAGE_TYPE_COLOR,*itr);
if (cmdl.m_sdname) pcapture->SetFilter(cmdl.m_sdname);
我们现在已经创建了一个 Capture 接口,稍后将使用它。
memset(&request, 0, sizeof(request));
request.streams[0].format=PXCImage::COLOR_FORMAT_RGB32;
request.streams[1].format=PXCImage::COLOR_FORMAT_DEPTH;
sts = pcapture->LocateStreams (&request);
我们从 Capture 接口请求彩色流和深度流。
pcapture->QueryDevice()->SetProperty(PXCCapture::Device::PROPERTY_DEPTH_SMOOTHING,1);
pcapture->QueryVideoStream(0)->QueryProfile(&pcolor);
pcapture->QueryVideoStream(1)->QueryProfile(&pdepth);
swprintf_s(line,sizeof(line)/sizeof(pxcCHAR),L"Depth %dx%d", pdepth.imageInfo.width, pdepth.imageInfo.height);
pdepth_render = new UtilRender(line);
swprintf_s(line,sizeof(line)/sizeof(pxcCHAR),L"UV %dx%d", pcolor.imageInfo.width, pcolor.imageInfo.height);
puv_render = new UtilRender(line);
sts=pcapture->QueryDevice()->QueryPropertyAsUID(PXCCapture::Device::PROPERTY_PROJECTION_SERIALIZABLE,&prj_value);
pcapture->QueryDevice()->QueryProperty(PXCCapture::Device::PROPERTY_DEPTH_LOW_CONFIDENCE_VALUE,&dvalues[0]);
pcapture->QueryDevice()->QueryProperty(PXCCapture::Device::PROPERTY_DEPTH_SATURATION_VALUE,&dvalues[1]);
session->DynamicCast<PXCMetadata>()->CreateSerializable<PXCProjection>(prj_value, &projection);
上面的代码准备了 pdepth_render
和 puv_render
,它们稍后用于读取流。此代码直接取自工作原型,为便于阅读,仅删除了错误处理。遵循上述顺序,您应该能够顺利地初始化您的相机设备。
上述示例可在 Intel Perceptual Computing SDK 的“camera-uvmap”示例中找到,这是一个非常好的精简示例,展示了如何快速入门。
让我们看一下我们目标数组的数据结构
struct sVertexType
{
float fX;
float fY;
float fZ;
float fNX;
float fNY;
float fNZ;
float fU;
float fV;
}
sVertexType * pVertexMem = new sVertexType[320*240];
在这里,我们看到了用于 3D 网格数据的结构,以便我们可以渲染、纹理化和光照最终的 3D 模型。
现在让我们看看处理循环
Int n=0;
for ( int y=0; y<240; y++ )
{
for ( int x=0; x<320; x++ )
{
float fX = (float)x;
float fY = (float)y;
float fZ = 0.0f;
float fU = 0.0f;
float fV = 0.0f;
pxcU16 depthvalue = ((pxcU16*)ddepth.planes[0])[y*pdepth.imageInfo.width+x];
if ( depthvalue>10 && depthvalue<1500 )
{
fZ = (depthvalue-10) /10.0f;
fU = fX / 320.0f;
fV = fY / 240.0f;
pVertexMem[n].fX = fX;
pVertexMem[n].fY = fY;
pVertexMem[n].fZ = fZ;
pVertexMem[n].fU = fU;
pVertexMem[n].fV = fV;
n++;
}
}
}
正如您所看到的,可以使用简单的嵌套循环来遍历所有深度数据像素并将其转换为 3D XYZ 坐标。我们还将 XY 转换为 UV 坐标,以便我们的 3D 模型在渲染时可以具有纹理。
最后,对于那些希望将相机彩色流导入 DirectX 以便您的 3D 模型具有纹理的人
LPDIRECT3DTEXTURE9 lpTexture = pMesh->pTextures[0].pTexturesRef;
if ( lpTexture )
{
D3DLOCKED_RECT d3dlock;
DWORD bitdepth = 32/8;
RECT rc = { 0, 0, 320, 240 };
if(SUCCEEDED(lpTexture->LockRect ( 0, &d3dlock, &rc, 0 ) ) )
{
// copy from surface
LPSTR pDst = (LPSTR)d3dlock.pBits;
for ( int y=0; y<239; y++ )
{
int colx, coly;
LPSTR pDstBase = pDst;
for ( int x=0; x<320; x++ )
{
colx = (int)(uvmap[(y*dwidth2+x)*2+0]*pcolor.imageInfo.width+0.5f);
coly = (int)(uvmap[(y*dwidth2+x)*2+1]*pcolor.imageInfo.height+0.5f);
pxcU32 colorvalue = ((pxcU32*)dcolor.planes[0])[coly*320+colx];
colorvalue = colorvalue + (255<<24);
*(DWORD*)(pDst)=colorvalue;
pDst+=bitdepth;
}
pDst = pDstBase + d3dlock.Pitch;
}
lpTexture->UnlockRect(0);
}
}
这段代码示例可能有很多内容需要消化,但它很容易分解。前几行只是定位您在应用程序初始化过程中早些时候创建的纹理表面。然后,您锁定表面以便可以安全地写入,然后创建一个嵌套循环,该循环将遍历深度数据的每个像素。请注意,我们没有说彩色数据,并且注意嵌套循环迭代是 320x240,而不是 640x480,这是彩色数据的分辨率。
一旦进入内部嵌套,就会发生三件事。第一步是 COLX 和 COLY 被填充为颜色数据中属于由 X 和 Y 循环变量指定的深度数据坐标的像素的参考坐标。请记住,我们正在为 320 个顶点宽的网格进行纹理化,因此我们不需要相机的所有 640 个彩色像素!我们通过使用 UVMAP 数组来实现这一点,该数组将在流完成同步时在代码早期读取。有关更多信息,请查看 Intel SDK 中的“camera_unmap”示例。
使用颜色数据中的参考坐标,我们可以读取像素颜色,应用纯白色 alpha 颜色(使用 (255<<24)),然后将最终颜色写入锁定的纹理。最后一步是推进写入指针并等待嵌套完成。我们通过解锁纹理表面来释放它,下次使用纹理表面时,您将发现最新的相机彩色数据已准备就绪,可以进行渲染。
技术画廊
以下是一些在开发过程中从原型中截取的屏幕截图,该原型在视频会议应用程序中采用了此技术。
关于作者
在撰写文章之余,Lee Bamber 是 The Game Creators(http://www.thegamecreators.com)的首席执行官。这是一家英国公司,专门从事游戏创作工具的开发和分发。公司成立已有 13 年多,公司及其周围的游戏制作者社区负责了许多流行品牌,包括 Dark Basic、FPS Creator,以及最近的 App Game Kit (AGK)。
启发本文的应用程序及其为期七周的开发过程的博客可以在这里找到:http://ultimatecoderchallenge.blogspot.co.uk/2013/02/lee-going-perceptual-part-one.html Lee 也在这里记录他作为一名程序员的日常生活,包括屏幕截图和偶尔的视频:http://fpscreloaded.blogspot.co.uk
Intel 和 Intel 徽标是 Intel Corporation 在美国和/或其他国家/地区的商标。版权所有 © 2013 Intel Corporation。保留所有权利。*其他名称和品牌可能被声称为他人财产。