(转载请注明出处)
使用SDK: Kinect for Windows SDK v2.0 public preview
这次说说这骨骼帧的获取。嗯,Kinect买来就为这个啊。不然其他数据,买其他产品就行了,Kinect的卖点也是这个。
先看看这次支持的骨骼关节:
enum _JointType
{
JointType_SpineBase = 0,
JointType_SpineMid = 1,
JointType_Neck = 2,
JointType_Head = 3,
JointType_ShoulderLeft = 4,
JointType_ElbowLeft = 5,
JointType_WristLeft = 6,
JointType_HandLeft = 7,
JointType_ShoulderRight = 8,
JointType_ElbowRight = 9,
JointType_WristRight = 10,
JointType_HandRight = 11,
JointType_HipLeft = 12,
JointType_KneeLeft = 13,
JointType_AnkleLeft = 14,
JointType_FootLeft = 15,
JointType_HipRight = 16,
JointType_KneeRight = 17,
JointType_AnkleRight = 18,
JointType_FootRight = 19,
JointType_SpineShoulder = 20,
JointType_HandTipLeft = 21,
JointType_ThumbLeft = 22,
JointType_HandTipRight = 23,
JointType_ThumbRight = 24,
JointType_Count = ( JointType_ThumbRight + 1 )
} ;
支持这25个关节点,不排除会增加的可能,毕竟近景可以分辨十指。
每个关节的状态用这个结构体描述:
typedef struct _Joint
{
JointType JointType;
CameraSpacePoint Position;
TrackingState TrackingState;
} Joint;
JointType就是之前的关节编号,Position是Kinect的相机空间坐标,是三维的。TrackingState是目前关节的追踪状态,
有: 未追踪(0),位置是推测的(1),位置是追踪的(2)
值得说明的是这次C++的SDK也提供了判断手的状态:
enum _HandState
{
HandState_Unknown = 0,
HandState_NotTracked = 1,
HandState_Open = 2,
HandState_Closed = 3,
HandState_Lasso = 4
} ;
使用方法和之前的差不多,说说不同的:
IBody* ppBodies[BODY_COUNT] = {0};
if (SUCCEEDED(hr))
{
hr = pBodyFrame->GetAndRefreshBodyData(BODY_COUNT, ppBodies);
}
每个接口使用类似下面的代码获取数据:
for (int i = 0; i < nBodyCount; ++i)
{
IBody* pBody = ppBodies[i];
if (pBody)
{
BOOLEAN bTracked = false;
hr = pBody->get_IsTracked(&bTracked);
if (SUCCEEDED(hr) && bTracked)
{
Joint joints[JointType_Count];
HandState leftHandState = HandState_Unknown;
HandState rightHandState = HandState_Unknown;
pBody->get_HandLeftState(&leftHandState);
pBody->get_HandRightState(&rightHandState);
hr = pBody->GetJoints(_countof(joints), joints);
if (SUCCEEDED(hr))
{
// XXXXXXXX
}
}
}
代码很简单,从方法名字就能看出来。这里的可视化方法是用微软提供SDK例子里面的方法,
大概就是相连的关节使用直线连起来之类的,这部分代码相当无聊,有Direct2D基础的同学可以无视,详细请看范例。
效果如下
好了,其实之前的例子SDK提供的例子多少有,这里自然要给个原创的东西(当然创意不是原创的)。
看到标题的同学大概也能猜到了,那就是《攻壳机动队 S.A.C》(Ghost In The Shell: Stand Alone Complex)里面出现的一个人物,这个人物出场使用一个图标挡住了脸:
这里,我们的那个文字也要像原作一样旋转,为了保证流程,所以我们这次图像API选择的D2D 1.1(能够等在垂直同步)。
D2D 1.1的初始化,说实话,我都不记得,
那么怎么实现那个图标呢,您可是使用图片。但是作为程序猿,使用代码即时生成是一个不错的选择。
Direct2D提供了一个硬件加速的几何体渲染接口,非常方便。旋转的文字渲染需要DirectWrite + Direct2D,学习的范围不在这里,
详细请看范例。
至于这个图标的几何形状怎么表示,当然用目测。。。这是不现实的,我在其他地方找到了一个笑面男的SVG图像,代码如下:
// 创建笑面男相关
HRESULT ImageRenderer::CreateLaughingMan(){
// 基本半径
const FLOAT BASE_RADIUS = 135.f;
HRESULT hr = S_OK;
IDWriteTextFormat* pImpactFormat = nullptr;
// 创建Impact文本格式
hr = m_pDWriteFactory->CreateTextFormat(
L"Impact",
nullptr,
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_CONDENSED,
41.f/96.f*72.f,
L"",
&pImpactFormat
);
// 创建文本布局
if (SUCCEEDED(hr)){
pImpactFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);
WCHAR* text = L"I thought what I'd do was, I'd pretend I was one of those deaf-mutes";
auto length = wcslen(text);
hr = m_pDWriteFactory->CreateTextLayout(text, length, pImpactFormat, BASE_RADIUS, BASE_RADIUS, &m_pTextLayoutLaughingMan);
}
// 创建文本几何路径: 一个圆
if (SUCCEEDED(hr)){
D2D1_ELLIPSE ellipse;
ellipse.point.x = 0.f;
ellipse.point.y = 0.f;
ellipse.radiusX = BASE_RADIUS;
ellipse.radiusY = BASE_RADIUS;
hr = m_pD2DFactory->CreateEllipseGeometry(&ellipse, &m_pTextAnimationPath);
}
// 笑面男路径
if (SUCCEEDED(hr)){
hr = m_pD2DFactory->CreatePathGeometry(&m_pLaughingManGeometryBlue);
// 画线
ID2D1GeometrySink* pSink = nullptr;
if (SUCCEEDED(hr)){
hr = m_pLaughingManGeometryBlue->Open(&pSink);
}
if (SUCCEEDED(hr)){
auto nowPoint = D2D1::Point2F();
pSink->SetFillMode(D2D1_FILL_MODE_WINDING);
D2D1_ARC_SEGMENT arc;
D2D1_BEZIER_SEGMENT bezier;
arc.rotationAngle = 0.f;
//
nowPoint.x = -8.f; nowPoint.y = -124.f;
pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
nowPoint.x += 16.f;
pSink->AddLine(nowPoint);
nowPoint.x += 2.f; nowPoint.y += 5.f;
pSink->AddLine(nowPoint);
nowPoint.x -= 20.f;
pSink->AddLine(nowPoint);
pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
//
nowPoint.x = -105.f; nowPoint.y = -20.f;
pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
nowPoint.y -= 20.f;
pSink->AddLine(nowPoint);
nowPoint.x += 270.f;
pSink->AddLine(nowPoint);
nowPoint.y += 80.f;
arc.size.height = 40.f;
arc.size.width = 40.f;
arc.sweepDirection = D2D1_SWEEP_DIRECTION_CLOCKWISE;
arc.point = nowPoint;
arc.arcSize = D2D1_ARC_SIZE_SMALL;
pSink->AddArc(&arc);
nowPoint.x -= 55.f;
pSink->AddLine(nowPoint);
nowPoint.y -= 20.f;
pSink->AddLine(nowPoint);
nowPoint.x += 55.f;
pSink->AddLine(nowPoint);
nowPoint.y -= 40.f;
arc.size.height = 20.f;
arc.size.width = 20.f;
arc.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE;
arc.point = nowPoint;
pSink->AddArc(&arc);
pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
//
nowPoint.x = -85.f; nowPoint.y= 20.f;
pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
nowPoint.x += 170.f;
arc.size.height = 90.f;
arc.size.width = 90.f;
arc.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE;
arc.arcSize = D2D1_ARC_SIZE_SMALL;
arc.point = nowPoint;
pSink->AddArc(&arc);
nowPoint.x -= 20.f;
pSink->AddLine(nowPoint);
nowPoint.x -= 130.f;
arc.size.height = 70.f;
arc.size.width = 70.f;
arc.sweepDirection = D2D1_SWEEP_DIRECTION_CLOCKWISE;
arc.point = nowPoint;
pSink->AddArc(&arc);
pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
//
nowPoint.x = -65.f; nowPoint.y = 20.f;
pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
nowPoint.y += 20.f;
pSink->AddLine(nowPoint);
nowPoint.x += 130.f;
pSink->AddLine(nowPoint);
nowPoint.y -= 20.f;
pSink->AddLine(nowPoint);
pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
//pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
//
nowPoint.x = -20.f; nowPoint.y = 10.f;
pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
bezier.point1.x = nowPoint.x - 17.f;
bezier.point1.y = nowPoint.y - 14.f;
bezier.point2.x = nowPoint.x - 27.f;
bezier.point2.y = nowPoint.y - 14.f;
nowPoint.x -= 44.f;
bezier.point3 = nowPoint;
pSink->AddBezier(&bezier);
bezier.point1.x = nowPoint.x + 6.f;
bezier.point1.y = nowPoint.y - 25.f;
bezier.point2.x = nowPoint.x + 37.f;
bezier.point2.y = nowPoint.y - 25.f;
nowPoint.x += 44.f;
bezier.point3 = nowPoint;
pSink->AddBezier(&bezier);
pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
//
nowPoint.x = 60.f; nowPoint.y = 10.f;
pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
bezier.point1.x = nowPoint.x - 17.f;
bezier.point1.y = nowPoint.y - 14.f;
bezier.point2.x = nowPoint.x - 27.f;
bezier.point2.y = nowPoint.y - 14.f;
nowPoint.x -= 44.f;
bezier.point3 = nowPoint;
pSink->AddBezier(&bezier);
bezier.point1.x = nowPoint.x + 6.f;
bezier.point1.y = nowPoint.y - 25.f;
bezier.point2.x = nowPoint.x + 37.f;
bezier.point2.y = nowPoint.y - 25.f;
nowPoint.x += 44.f;
bezier.point3 = nowPoint;
pSink->AddBezier(&bezier);
pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
hr = pSink->Close();
}
SafeRelease(pSink);
}
// 笑面男白色部分
if (SUCCEEDED(hr)){
hr = m_pD2DFactory->CreatePathGeometry(&m_pLaughingManGeometryWhite);
// 画线
ID2D1GeometrySink* pSink = nullptr;
if (SUCCEEDED(hr)){
hr = m_pLaughingManGeometryWhite->Open(&pSink);
}
if (SUCCEEDED(hr)){
auto nowPoint = D2D1::Point2F();
//
nowPoint.x = -125.f; nowPoint.y = -20.f;
pSink->BeginFigure(nowPoint, D2D1_FIGURE_BEGIN_FILLED);
nowPoint.y += 10.f;
pSink->AddLine(nowPoint);
nowPoint.x += 35.f;
pSink->AddLine(nowPoint);
nowPoint.y += 30.f;
pSink->AddLine(nowPoint);
nowPoint.x += 260.f;
pSink->AddLine(nowPoint);
nowPoint.y -= 40.f;
D2D1_ARC_SEGMENT arc = { nowPoint, D2D1::SizeF(20.f, 20.f), 0.f, D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE, D2D1_ARC_SIZE_SMALL };
pSink->AddArc(&arc);
pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
hr = pSink->Close();
}
}
// 笑面蓝
if (SUCCEEDED(hr)){
hr = m_pD2DDeviceContext->CreateSolidColorBrush(D2D1::ColorF(0x005577), &m_pLaughingBlueBrush);
}
// 笑面白
if (SUCCEEDED(hr)){
hr = m_pD2DDeviceContext->CreateSolidColorBrush(D2D1::ColorF(0xFFFFFF), &m_pLaughingWhiteBrush);
}
SafeRelease(pImpactFormat);
return hr;
}
是的,我们这次使用的是D2D 1.1。之前说过,所以这里就使用轮询模式。
这里要使用彩色数据流 + 骨骼数据流,那么是否需要使用复源帧?
使用复源帧是为了保证数据同步,轮询模式下异步的效果很及时,所以这里不使用复源帧。
大致过程如下:
刷新:
获得彩色数据 -> 复制到位图
获得骨骼数据 -> 检查并更新头部位置,根据远近设置相对缩放率(实现近大远小,大致即可,不用精确)
渲染:
渲染彩色帧
渲染笑面男
Kinect2支持6人骨骼追踪,所以数据要准备6份。这样6人同时出现也能一起打码
效果如下:Kinect2获得的骨骼数据默认就已经平滑过了,不需要像1代那样设置平滑参数,也说明精度的提高。
为了模拟原作的抖动,只能自己手动模拟了。
范例下载地址:点击这里