转自:http://www.sjsjw.com/kf_other/article/41_23129_23822.asp

Kinect SDK 读取彩色、深度、骨骼信息并用OpenCV显示

一、原理说明



对于原理相信大家都明白大致的情况,因此,在此只说比较特别的部分。

1.1 深度流数据:

深度数据流所提供的图像帧中,每一个像素点代表的是在深度感应器的视野中,该特定的(x, y)坐标处物体到离摄像头平面最近的物体到该平面的距离,

注意是平面到平面距离,并不是到摄像机的斜线距离(以毫米为单位)。



Kinect中深度值最大为4096mm,0值通常表示深度值不能确定,一般应该将0值过滤掉。微软建议在开发中使用1220mm~3810mm范围内的值。

在进行其他深度图像处理之前,应该使用阈值方法过滤深度数据至1220mm-3810mm这一范围内。

下图显示了Kinect Sensor的感知范围,其中的default range对Xbox 360和Kinect for Windows都适用,而near range仅对后者适用:







深度数据的存储:

Kinect的深度图像数据含有两种格式,两种格式都是用两个字节来保存一个像素的深度值,而两方式的差别在于:

(1)唯一表示深度值:那么像素的低12位表示一个深度值,高4位未使用;

(2)既表示深度值又含有游戏者ID:Kinect SDK具有分析深度数据和探测人体或者游戏者轮廓的功能,它一次能够识别多达6个游戏者。SDK为每一个追踪到的游戏者编号作为索引。

而这个方式中,像素值的高13位保存了深度值,低三位保存用户序号,7 (0000 0111)这个位掩码能够帮助我们从深度数据中获取到游戏者索引值。对于这种情况的处理如下:

USHORTrealDepth = (depthID & 0xfff8) >> 3; //提取距离信息,高13位

USHORTplayer =  depthID & 0x07 ;  //提取ID信息,低3位

SDK提供了专门的函数:

SHORT realDepth= NuiDepthPixelToDepth(depth);
USHORT playerIndex         = NuiDepthPixelToPlayerIndex(depth);//获得深度图该点像素位置对应的 UserID;


在对OpenCV进行赋值时需要将其转化到[0,255]

BYTE b = 255 - static_cast<BYTE>(256 * realDepth / 0x0fff);



1.2 Skeleton信息





玩家的各关节点位置用(x, y, z)坐标表示。与深度图像空间坐标不同的是,这些坐标单位是米。坐标轴x,y, z是深度感应器实体的空间x, y, z坐标轴。这个坐标系是右手螺旋的,Kinect感应器处于原点上,z坐标轴则与Kinect感应的朝向一致。y轴正半轴向上延伸,x轴正半轴(从Kinect感应器的视角来看)向左延伸,如下图所示。



Kinect放置的位置会影响生成的图像。例如,Kinect可能被放置在非水平的表面上或者有可能在垂直方向上进行了旋转调整来优化视野范围。在这种情况下,y轴就往往不是相对地面垂直的,或者不与重力方向平行。最终得到的图像中,尽管人笔直地站立,在图像中也会显示出事倾斜的。



Kinect最多可以跟踪两个骨骼,可以最多检测六个人。站立模式可以跟踪20个关节点,坐姿模式的话,可以跟踪10个关节点。

NUI骨骼跟踪分主动和被动两种模式,提供最多两副完整的骨骼跟踪数据。主动模式下需要调用相关帧读取函数获得用户骨骼数据,而被动模式下还支持额外最多四人的骨骼跟踪,但是在该模式下仅包含了用户的位置信息,不包括详细的骨骼数据。也就是说,假如Kinect面前站着六个人,Kinect能告诉你这六个人具体站在什么位置,但只能提供其中两个人的关节点的数据(这两个人属于主动模式),也就是他们的手啊,头啊等等的位置都能告诉你,而其他的人,Kinect只能提供位置信息,也就是你站在哪,Kinect告诉你,但是你的手啊,头啊等具体在什么位置,它就没法告诉你了(这四个人属于被动模式)。

对于所有获取的骨骼数据,其至少包含以下信息:

1)、相关骨骼的跟踪状态,被动模式时仅包括位置数据(用户所在位置),主动模式包括完整的骨骼数据(用户20个关节点的空间位置信息)。

2)、唯一的骨骼跟踪ID,用于分配给视野中的每个用户(和之前说的深度数据中的ID是一个东西,用以区分现在这个骨骼数据是哪个用户的)。

3)、用户质心位置,该值仅在被动模式下可用(就是标示用户所在位置的,当无法检测出Skeleton数据时,就应该显示用户质心位置,因为质心是根据深度图的UserID计算出来的,并不受骨骼信息影响)。




二、流程解析



作为一名KINECT程序员,你需要记得的是,微软SDK中提供的运行环境在处理KINECT传输数据时,是遵循一条3步骤的运行管线的。
第一阶段:只处理彩色和深度数据
第二阶段:处理用户索引并根据用户索引将颜色信息追加到深度图中。(参考 NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX)
第三阶段:处理骨骼追踪数据





1. 基本设置

1.1 在vs2010项目中,需要设置C++目录
包含目录中加入 $(KINECTSDK10_DIR)\inc;


库目录中加入    $(KINECTSDK10_DIR)\lib\x86

(注意安装Kinect SDK以后,路径不一样了,环境变量名变成KINECTSDK10_DIR)

MSRKINECTSDK是环境变量,正确安装MS KINECT FRO WINDOWS SDK 后,会在计算机中的环境变量中看到。
1.2 添加特定库
   除了指定目录外,你还需要在链接器中设置附加依赖项,填入KinectNUI.lib
1.3 头文件
    为了使用NUI中的API,首先我们要包含 NuiApi.h,切记,在这之前,要保证你已经包含了windows.h


#include <Windows.h>

#include "NuiApi.h"

切记,在这之前,要保证你已经包含了windows.h否则 Nuiapi中很多根据windows平台定义的数据类型及宏都不生效。

1.4 其他设置

如果要使用Kinect SDK 1.7+中的Inteaction部分控件,除了以上提到的基本设置需要额外加入Kinect toolkit文件夹下的相应的.h、.lib以及dll文件。

PS:

其实最简单的方法就是把Kinect Toolkits中的类似sample Install到目录,一看就知道怎么做的了、





2. Nui初始化

接下来,任何想使用微软提供的API来操作KINECT,都必须在所有操作之前,调用NUI的初始化函数

HRESULT NuiInitialize(DWORD dwFlags);

DWORD dwFlags参数:
NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX    使用NUI中的带用户信息的深度图数据
NUI_INITIALIZE_FLAG_USES_COLOR                                        使用NUI中的彩色图数据
NUI_INITIALIZE_FLAG_USES_SKELETON                                  使用NUI中的骨骼追踪数据
NUI_INITIALIZE_FLAG_USES_DEPTH                                        仅仅使用深度图数据(如果你自己有良好的场景分析或物体识别算法,那么你应该用这个)


以上4个标志位,你可以使用一个,也可以用 | 操作符将它们组合在一起。例如:
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);//只使用彩色图
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX | NUI_INITIALIZE_FLAG_USES_SKELETON | NUI_INITIALIZE_FLAG_USES_COLOR);
//使用带用户信息的深度图/使用用户骨骼框架/使用彩色图



NuiInitialize就是应用程序用通过传递给dwFlags参数具体值,来初始化Kinect SDK处理管线中必须的阶段。因此,我们总是先在标志位中指定图像类型,才可以在接下来的环节中去调用NuiImageStreamOpen之类的函数。如果你初始化的时候没指定NUI_INITIALIZE_FLAG_USES_COLOR,那你以后就别指望NuiImageStreamOpen能打开彩色数据了,它肯定会调用失败,因为没初始化嘛。



初始化以后,在我们继续其他深入获取NUI设备的数据之前,先了解一下如何关闭你的程序与NUI之间的联系。
VOID NuiShutdown();
关于这个函数,没什么可说的,你的程序退出时,都应该调用一下。甚至于,你的程序暂时不使用KINECT了,就放开对设备的控制权,好让其他程序可以访问KINECT。




友情提示使用OpenGL的程序员们,如果你们是在使用glut库,那么不要在glMainLoop()后面调用NuiShutdown(),因为它不会执行,你应该在窗口关闭以及任意你执行了退出代码的时刻调用它。




注意:

一个应用程序对一个KINECT设备,必须要调用此函数一次,并且也只能调用一次。如果在这之后又调用一次初始化,势必会引起逻辑错误(即使是2个不同程序)。比如你运行一个SDK的例子,在没关闭它的前提下,再运行一个,那么后运行的就无法初始化成功,但不会影响之前的程序继续运行。

如果你的程序想使用多台KINECT,那么请使用INuiInstance接口来初始化你的设备。

3. CreateNextFrame Event(是否读取下一帧的控制信号)

一个用来手动重置信号是否可用的事件句柄(event),该信号用来控制KINECT是否可以开始读取下一帧数据。

HANDLE m_hNextVideoFrameEvent   = CreateEvent( NULL, TRUE, FALSE, NULL );
HANDLE m_hNextDepthFrameEvent  = CreateEvent( NULL, TRUE, FALSE, NULL );
HANDLE m_hNextSkeletonEvent        = CreateEvent( NULL, TRUE, FALSE, NULL ); 
HANDLE m_hEvNuiProcessStop
= CreateEvent(NULL,TRUE,FALSE,NULL);//用于结束的事件对象;

也就是说在这里指定一个句柄后,随着程序往后继续推进,当你在任何时候想要控制kinect读取下一帧数据时,

都应该先使用WaitForSingleObject(handleEvent, 0)判断一下该句柄,是否有数据可拿。

CreateEvent()创建一个windows事件对象,创建成功则返回事件的句柄。事件有两个状态,有信号和没有信号!上面说到了。就是拿来等待新数据的。

CreateEvent函数需要4个参数:

设定为NULL的安全描述符;

一个设定为true的布尔值,因为应用程序将重置事件消息;

一个未指定的事件消息初始状态的布尔值;

一个空字符串,因为事件未命名

4. Open Stream(For color & depth stream)

打开对NUI设备的访问通道,只针对彩色数据流和深度数据流。

使用这个函数来打开kinect彩色或者深度图的访问通道,当然,其内部原理是通过"流"来实现的,因此,你也可以把这个函数理解为,创建一个访问彩色或者深度图的数据流。似乎从很久远的时候开始,微软就在windows中开始使用流来访问所有硬件设备了。

HRESULT NuiImageStreamOpen ( NUI_IMAGE_TYPE eImageType, 

NUI_IMAGE_RESOLUTION eResolution,

DWORD dwImageFrameFlags_NotUsed,

DWORD dwFrameLimit,

HANDLE hNextFrameEvent,

HANDLE *phStreamHandle);



参数:

NUI_IMAGE_TYPE eImageType [in]

这是一个NUI_IMAGE_TYPE 枚举类型的值(对应NuiInitialize中的标志位),用来详细指定你要创建的流类型。比如你要打开彩色图,就使用NUI_IMAGE_TYPE_COLOR

要打开深度图,就使用NUI_IMAGE_TYPE_DEPTH。能打开的图像类型,必须是你在初始化的时候指定过的。



NUI_IMAGE_RESOLUTION eResolution[in]

这是一个NUI_IMAGE_RESOLUTION 枚举类型的值,用来指定你要以什么分辨率来打开 eImageType (参数1)中指定的图像类别。

彩色图NUI_IMAGE_TYPE_COLOR,支持2种分辨率: NUI_IMAGE_RESOLUTION_1280x1024,NUI_IMAGE_RESOLUTION_640x480

深度图NUI_IMAGE_TYPE_DEPTH,支持3种分辨率:NUI_IMAGE_RESOLUTION_640x480, NUI_IMAGE_RESOLUTION_320x240, NUI_IMAGE_RESOLUTION_80x60



DWORD dwImageFrameFlags_NotUsed [in]

一点用没有,你随便给个整数就行了。以后的版本里不知道它会不会有意义。




DWORD dwFrameLimit[in]

指定NUI运行时环境将要为你所打开的图像类型建立几个缓冲。最大值是NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM(当前版本为 4).对于大多数啊程序来说,2就足够了。



HANDLE hNextFrameEvent [in, optional]

就是之前建立的一个用来手动重置信号是否可用的事件句柄(event),该信号用来控制KINECT是否可以开始读取下一帧数据。

也就是说在这里指定一个句柄后,随着程序往后继续推进,当你在任何时候想要控制kinect读取下一帧数据时,都应该先使用WaitForSingleObject判断一下该句柄。



HANDLE phStreamHandle[out] (就是通过这个读取数据)

指定一个句柄的地址。函数成功执行后,将会创建对应的数据访问通道(流),并且让该句柄保存这个通道的地址。也就是说,如果现在创建成功了。

那么以后你想读取数据,就要通过这个句柄了。

5. Skeleton Flag Enable(For skeleton stream)

设置Skeleton Stream跟踪标志位

HRESULTNuiSkeletonTrackingEnable( m_hNextSkeletonEvent, Flag );

Flag:

近景: NUI_SKELETON_TRACKING_FLAG_ENABLE_IN_NEAR_RANGE

坐姿: NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT;        只有头肩手臂10个节点;

站姿:flag&~(NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT);  有标准的20个节点;

默认值0:站姿和非近景数据位分布如上图。

6.等待新的数据,等到后触发

WaitForSingleObject(nextColorFrameEvent, INFINITE)==0    

WAIT_OBJECT_0 == WaitForSingleObject(m_hEvNuiProcessStop, 0)

WAIT_OBJECT_0 == WaitForSingleObject(m_hNextVideoFrameEvent, 0)

WAIT_OBJECT_0 == WaitForSingleObject(m_hNextDepthFrameEvent, 0)

WAIT_OBJECT_0 == WaitForSingleObject(m_hNextSkeletonEvent, 0)

程序运行堵塞在这里,这个事件有信号,就是说有数据,那么程序往下执行,如果没有数据,就会等待。函数第二个参数表示你愿意等多久,具体的数据的话就表示你愿意等多少毫秒,还不来,我就不要了,继续往下走。如果是INFINITE的话,就表示无限等待新数据,直到海枯石烂,一定等到为止。等到有信号后就返回0 。

7. 从流数据获得Frame数据



7.1 For Color & Depth Sream

7.1.1 NuiImageStreamGetNextFrame

HRESULT NuiImageStreamGetNextFrame (HANDLE hStream,

DWORD dwMillisecondsToWait,

CONST NUI_IMAGE_FRAME **ppcImageFrame);

eg:

const NUI_IMAGE_FRAME * pImageFrame = NULL;

HRESULT hr = NuiImageStreamGetNextFrame( h, 0, &pImageFrame );

参数:

HANDLE hStream [in]

前面NuiImageStreamOpen打开数据流时的输出参数,就是流句柄,彩色数据对应彩色流句柄,深度数据对应深度流句柄。

DWORD dwMillisecondsToWait [in]

延迟时间,以微秒为单位的整数。当运行环境在读取之前,会先等待这个时间。

CONST NUI_IMAGE_FRAME **ppcImageFrame [out] 出参,

指定一个 NUI_IMAGE_FRAME 结构的指针,当读取成功后,该函数会将读取到的数据地址返回,保存在此参数中。pImageFrame包含了很多有用信息,包括:图像类型,分辨率,图像缓冲区,时间戳等等。

返回值

同样是S_OK表示成功

7.1.2 INuiFrameTexture接口



INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;

一个容纳图像帧数据的对象,类似于Direct3D纹理,但是只有一层(不支持mip-maping)。

其公有方法包含以下:

AddRef---增加一个对象上接口的引用数目;该方法在每复制一个指向该对象上接口的指针时都要调用一次;

BufferLen---获得缓冲区的字节长度;

GetLevelDesc---获得缓冲区的描述;

LockRect---给缓冲区上锁;

Pitch---返回一行的字节数;

QueryInterface---获取指向对象所支持的接口的指针,该方法对其所返回的指针调用AddRef函数;

Release---减少一个对象上接口的引用计数;

UnlockRect---对缓冲区解锁;

7.1.3 提取数据帧到LockedRect并锁定数据

pTexture->LockRect(0, &LockedRect, NULL, 0);

提取数据帧到LockedRect,它包括两个数据对象:pitch每行字节数,pBits第一个字节地址。另外,其还锁定数据,这样当我们读数据的时候,kinect就不会去修改它。

好了,现在真正保存图像的对象LockedRect我们已经有了,并且也将图像信息写入这个对象了。



7.1.4 将数据转换为OpenCV的Mat格式

然后将其保存图像的对象LockedRect的格式,转化为OpenCV的Mat格式

BYTE* pBuffer = (BYTE*) LockedRect.pBits;

Mat colorImg(COLOR_HIGHT,COLOR_WIDTH,CV_8UC4,pBuffer);

Mat depthImg(DEPTH_HIGHT,DEPTH_WIDTH,CV_16U,pBuffer);

注意:

对于INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;获得的数据彩色数据和深度数据的规格是不一样的!!!

彩色数据:单位数据是32位,对应BGRA

深度数据:单位数据是16位。

深度数据的取值还有另一种方式:


INuiFrameTexture* pDepthImagePixelFrame;
BOOL nearMode = TRUE;
m_pNuiSensor->NuiImageFrameGetDepthImagePixelFrameTexture(m_pDepthStreamHandle, &pImageFrame, &nearMode, &pDepthImagePixelFrame);
INuiFrameTexture * pTexture = pDepthImagePixelFrame;
NUI_LOCKED_RECT LockedRect;
pTexture->LockRect(0, &LockedRect, NULL, 0 );



通过这种方式获得的深度数据跟彩色数据的规格一致都是32bit一组,但是16~35位为空,不包含数据,数据依然保存在低16位,只不过加了两个通道。



7.2 For Skeleton Stream



HRESULT NuiSkeletonGetNextFrame(

DWORD dwMillisecondsToWait,

NUI_SKELETON_FRAME  *SkeletonFrame);

eg:

NUI_SKELETON_FRAME SkeletonFrame;   

HRESULT hr = NuiSkeletonGetNextFrame( 0, &SkeletonFrame );

在获得了 Depth Sream的Frame的数据之后,才能获得skeleton数据。这属于三步走管线的第三步。



骨骼帧数据保存在NUI_SKELETON_FRAME结构体中,我们首先来分析下这个最重要的结构体:

typedef struct _NUI_SKELETON_FRAME {

   LARGE_INTEGER liTimeStamp;

   DWORD dwFrameNumber;

   DWORD dwFlags; //没用到

   Vector4 vFloorClipPlane;

   Vector4 vNormalToGravity;

   NUI_SKELETON_DATA SkeletonData[NUI_SKELETON_COUNT];

} NUI_SKELETON_FRAME;




LARGE_INTEGER liTimeStamp &  DWORD dwFrameNumber ( 时间标记字段):

SkeletonFrame的dwFrameNumber和liTimestamp字段表示当前记录中的帧序列信息。FrameNumber是深度数据帧中的用来产生骨骼数据帧的帧编号。帧编号通常是不连续的,但是之后的帧编号一定比之前的要大。骨骼追踪引擎在追踪过程中可能会忽略某一帧深度数据,这跟应用程序的性能和每秒产生的帧数有关。例如,在基于事件获取骨骼帧信息中,如果事件中处理帧数据的时间过长就会导致这一帧数据还没有处理完就产生了新的数据,那么这些新的数据就有可能被忽略了。如果采用查询模型获取帧数据,那么取决于应用程序设置的骨骼引擎产生数据的频率,即取决于深度影像数据产生骨骼数据的频率。

Timestap字段记录自Kinect传感器初始化(调用NuiInitialize函数)以来经过的累计毫秒时间。不用担心FrameNumber或者Timestamp字段会超出上限。FrameNumber是一个32位的整型,Timestamp是64位整型。如果应用程序以每秒30帧的速度产生数据,应用程序需要运行2.25年才会达到FrameNumber的限,此时Timestamp离上限还很远。另外在Kinect传感器每一次初始化时,这两个字段都会初始化为0。可以认为FrameNumber和Timestamp这两个值是唯一的。

这两个字段在分析处理帧序列数据时很重要,比如进行关节点值的平滑,手势识别操作等。在多数情况下,我们通常会处理帧时间序列数据,这两个字段就显得很有用。目前SDK中并没有包含手势识别引擎。在未来SDK中加入手势引擎之前,我们需要自己编写算法来对帧时间序列进行处理来识别手势,这样就会大量依赖这两个字段。



NUI_SKELETON_DATA SkeletonData[NUI_SKELETON_COUNT] 骨骼数据段:

最重要的要数这个成员了。首先,Kinect可以检测到6个骨骼,在NuiSensor.h中有宏定义:#define    NUI_SKELETON_COUNT  ( 6 )。所以SkeletonData[NUI_SKELETON_COUNT]定义了六个骨骼的数据。骨骼数据是通过一个NUI_SKELETON_DATA类型的结构体保存的:

typedef struct _NUI_SKELETON_DATA {

   NUI_SKELETON_TRACKING_STATE eTrackingState;

   DWORD dwTrackingID;

   DWORD dwEnrollmentIndex;

    DWORDdwUserIndex;

   Vector4 Position;

   Vector4 SkeletonPositions[20];

   NUI_SKELETON_POSITION_TRACKING_STATE eSkeletonPositionTrackingState[20];

   DWORD dwQualityFlags;

} NUI_SKELETON_DATA;

其中:

eTrackingState:

eTrackingState字段表示当前的骨骼数据的状态,是一个枚举类型。

typedef enum NUI_SKELETON_TRACKING_STATE

{

   NUI_SKELETON_NOT_TRACKED = 0,

   NUI_SKELETON_POSITION_ONLY,

   NUI_SKELETON_TRACKED

} NUI_SKELETON_TRACKING_STATE;

下表展示了SkeletonTrackingState枚举的可能值的含义:

NUI_SKELETON_NOT_TRACKED   表示骨架没有被跟踪到,这个状态下,骨骼数据的Position字段和相关的关节点数据中的每一个位置点值都是0 。

NUI_SKELETON_POSITION_ONLY   检测到了骨骼对象,但是跟踪没有激活,也就是说骨骼数据的Position字段有值,但是相关的关节点数据中的每一个位置点值都是0(对应被动模式,被动模式只提供骨骼的位置,不提供关节点的位置)。

NUI_SKELETON_TRACKED   所有骨骼点的位置都被跟踪,骨骼数据的Position字段和相关的关节点数据中的每一个位置点值都非零(对应主动模式,骨骼位置和关节点位置都提供)。



dwTrackingID:

骨骼追踪引擎对于每一个追踪到的游戏者的骨骼信息都有一个唯一编号。这个值是整型,他会随着新的追踪到的游戏者的产生添加增长。另外,这个编号的产生是不确定的。如果骨骼追踪引擎失去了对游戏者的追踪,比如说游戏者离开了Kinect的视野,那么这个对应的唯一编号就会过期。当Kinect追踪到了一个新的游戏者,他会为其分配一个新的唯一编号。编号值为0表示这个骨骼信息不是游戏者的。



Position:

Position是一个Vector4类型的字段,代表所有骨骼的中间点。身体的中间点和脊柱关节的位置相当。该字段提供了一个最快且最简单的所有视野范围内的游戏者位置的信息,而不管其是否在追踪状态中。在一些应用中,如果不用关心骨骼中具体的关节点的位置信息,那么该字段对于确定游戏者的位置状态已经足够。该字段对于手动选择要追踪的游戏者也是一个参考。例如,应用程序可能需要追踪距离Kinect最近的且处于追踪状态的游戏者,那么该字段就可以用来过滤掉其他的游戏者。

Vector4类型是一个空间坐标的类型:

Vector4typedef struct _Vector4 {

FLOAT x;  //X coordinate

FLOAT y;  //Y coordinate

FLOAT z;  //Z coordinate

FLOAT w;  //W coordinate

} Vector4;



SkeletonPositions[20]:

这个数组记录的是主动模式下骨骼的20个关节点对应的空间位置信息。每一个关节点都有类型为Vector4的位置属性,他通过X,Y,Z三个值来描述关节点的位置。X,Y值是相对于骨骼平面空间的位置,他和深度影像,彩色影像的空间坐标系不一样。KinectSnesor对象有一系列的坐标转换方法,可以将骨骼坐标点转换到对应的深度数据影像中去。



SkeletonPositionTrackingState[20]:

上面说到骨骼有跟踪的好与不好,那么关节点也是跟踪和分析的,那也有好与不好之分吧。而SkeletonPositionTrackingState属性就是标示对应的每一个关节点的跟踪状态的:

typedef enum _NUI_SKELETON_POSITION_TRACKING_STATE

{

   NUI_SKELETON_POSITION_NOT_TRACKED = 0,

   NUI_SKELETON_POSITION_INFERRED,

   NUI_SKELETON_POSITION_TRACKED

} NUI_SKELETON_POSITION_TRACKING_STATE;

各个状态表示的含义:

NUI_SKELETON_POSITION_NOT_TRACKED   关节点位置没有跟踪到,而且也不能通过一些信息推断出来,所以关节点的Position值都为0 。

NUI_SKELETON_POSITION_INFERRED   骨骼的关节点位置可以由上一帧数据、已经跟踪到的其他点位置和骨架的几何假设这三个信息推测出来。

NUI_SKELETON_POSITION_TRACKED   关节点被成功跟踪,它的位置是基于当前帧的计算得到的。



NUI_SKELETON_POSITION_INDEX:

另外,SkeletonPositions[20]这个数组是保存20个关节点的位置的,那么哪个数组元素对应哪个关节点呢?实际上,这个数组的保存是有顺序的,然后,我们可以通过下面的枚举值做为这个数组的下标来访问相应的关节点位置信息:

typedef enum _NUI_SKELETON_POSITION_INDEX

{

   NUI_SKELETON_POSITION_HIP_CENTER = 0,

   NUI_SKELETON_POSITION_SPINE,

   NUI_SKELETON_POSITION_SHOULDER_CENTER,

   NUI_SKELETON_POSITION_HEAD,

   NUI_SKELETON_POSITION_SHOULDER_LEFT,

   NUI_SKELETON_POSITION_ELBOW_LEFT,

   NUI_SKELETON_POSITION_WRIST_LEFT,

   NUI_SKELETON_POSITION_HAND_LEFT,

   NUI_SKELETON_POSITION_SHOULDER_RIGHT,

   NUI_SKELETON_POSITION_ELBOW_RIGHT,

    NUI_SKELETON_POSITION_WRIST_RIGHT,

   NUI_SKELETON_POSITION_HAND_RIGHT,

   NUI_SKELETON_POSITION_HIP_LEFT,

   NUI_SKELETON_POSITION_KNEE_LEFT,

   NUI_SKELETON_POSITION_ANKLE_LEFT,

   NUI_SKELETON_POSITION_FOOT_LEFT,

   NUI_SKELETON_POSITION_HIP_RIGHT,

   NUI_SKELETON_POSITION_KNEE_RIGHT,

   NUI_SKELETON_POSITION_ANKLE_RIGHT,

   NUI_SKELETON_POSITION_FOOT_RIGHT,

   NUI_SKELETON_POSITION_COUNT

} NUI_SKELETON_POSITION_INDEX;



好了,感觉把这些说完,代码里面的东西就很容易读懂了。所以也没必要赘述了。但是还需要提到的几点是:



平滑化:

NuiTransformSmooth(&skeletonFrame,NULL);

在骨骼跟踪过程中,有些情况会导致骨骼运动呈现出跳跃式的变化。例如游戏者的动作不够连贯,Kinect硬件的性能等等。骨骼关节点的相对位置可能在帧与帧之间变动很大,这回对应用程序产生一些负面的影响。例如会影响用户体验和给控制造成意外等。

而这个函数就是解决这个问题的,它对骨骼数据进行平滑,通过将骨骼关节点的坐标标准化来减少帧与帧之间的关节点位置差异。

HRESULT NuiTransformSmooth(

        NUI_SKELETON_FRAME *pSkeletonFrame,

        const NUI_TRANSFORM_SMOOTH_PARAMETERS *pSmoothingParams

)

这个函数可以传入一个NUI_TRANSFORM_SMOOTH_PARAMETERS类型的参数:

typedef struct_NUI_TRANSFORM_SMOOTH_PARAMETERS {

   FLOAT fSmoothing;

   FLOAT fCorrection;

   FLOAT fPrediction;

   FLOAT fJitterRadius;

   FLOAT fMaxDeviationRadius;

} NUI_TRANSFORM_SMOOTH_PARAMETERS;

NUI_TRANSFORM_SMOOTH_PARAMETERS这个结构定义了一些属性:

fSmoothing:平滑值(Smoothing)属性,设置处理骨骼数据帧时的平滑量,接受一个0-1的浮点值,值越大,平滑的越多。0表示不进行平滑

fCorrection:修正值(Correction)属性,接受一个从0-1的浮点型。值越小,修正越多。

fJitterRadius:抖动半径(JitterRadius)属性,设置修正的半径,如果关节点“抖动”超过了设置的这个半径,将会被纠正到这个半径之内。该属性为浮点型,单位为米。

fMaxDeviationRadius:最大偏离半径(MaxDeviationRadius)属性,用来和抖动半径一起来设置抖动半径的最大边界。任何超过这一半径的点都不会认为是抖动产生的,而被认定为是一个新的点。该属性为浮点型,单位为米。

fPrediction:预测帧大小(Prediction)属性,返回用来进行平滑需要的骨骼帧的数目。



空间坐标转换:

NuiTransformSkeletonToDepthImage(pSkel->SkeletonPositions[i], &fx, &fy );

由于深度图像数据和彩色图像数据来自于不同的摄像头,而这两个摄像头所在的位置不一样,而且视场角等也不也一样,所以产生的图像也会有差别(就好像你两个眼睛看到的东西都不一样,这样大脑才能通过他们合成三维场景理解),也就是说这两幅图像上的像素点并不严格一一对应。例如深度图像中,你在图像的位置可能是(x1,y1),但在彩色图像中,你在图像的位置是(x2,y2),而两个坐标一般都是不相等的。另外,骨骼数据的坐标又和深度图像和彩色图像的不一样。所以就存在了彩色坐标空间、深度坐标空间、骨骼坐标空间和你的UI坐标空间四个不同的坐标空间了。那么他们各个空间之前就需要交互,例如我在骨骼空间找到这个人在(x1,y1)坐标上,那么对应的深度图像,这个人在什么坐标呢?另外,我们需要把骨骼关节点的位置等画在我们的UI窗口上,那么它们的对应关系又是什么呢?

微软SDK提供了一系列方法来帮助我们进行这几个空间坐标系的转换。例如:

voidNuiTransformSkeletonToDepthImage(

Vector4 vPoint,  //骨骼空间中某点坐标

FLOAT *pfDepthX, //对应的深度空间中的坐标

FLOAT *pfDepthY

)//将骨骼坐标转换到深度图像坐标上去。



HRESULTNuiImageGetColorPixelCoordinatesFromDepthPixel(

NUI_IMAGE_RESOLUTION eColorResolution,

const NUI_IMAGE_VIEW_AREA *pcViewArea,

LONG lDepthX,

LONG lDepthY,

USHORT usDepthValue,

LONG *plColorX,

LONG *plColorY

)//获取在深度图中具体坐标位置的像素在相应彩色空间中的像素的坐标。



Vector4NuiTransformDepthImageToSkeleton(

LONG lDepthX,

LONG lDepthY,

USHORT usDepthValue

)//传入深度图像坐标,返回骨骼空间坐标


三、代码及注释


#include <iostream>
#include <opencv/cv.h>
#include <opencv/highgui.h>using namespace std;
using namespace cv;//KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
#include "windows.h"
#include "NuiApi.h"
//KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK//----各种内核事件和句柄-----------------------------------------------------------------
HANDLE       m_hNextVideoFrameEvent;
HANDLE       m_hNextDepthFrameEvent;
HANDLE       m_hNextSkeletonEvent;
HANDLE       m_pVideoStreamHandle;
HANDLE       m_pDepthStreamHandle;
HANDLE       m_hEvNuiProcessStop;//用于结束的事件对象; //---图像大小等参数--------------------------------------------
#define COLOR_WIDTH                 640
#define COLOR_HIGHT                 480
#define DEPTH_WIDTH                 640
#define DEPTH_HIGHT                 480
#define SKELETON_WIDTH              640
#define SKELETON_HIGHT              480
#define CHANNEL                     3
#define WIDTH                       80
#define HEIGHT                      40//---Image stream 分辨率-------------------------------
NUI_IMAGE_RESOLUTION colorResolution = NUI_IMAGE_RESOLUTION_640x480;    //设置图像分辨率
NUI_IMAGE_RESOLUTION depthResolution = NUI_IMAGE_RESOLUTION_640x480;//---显示图像------------------------------------------;
Mat depthRGB(DEPTH_HIGHT,DEPTH_WIDTH,CV_8UC3,Scalar(0,0,0));//---颜色------------------------------
const Scalar SKELETON_COLORS[NUI_SKELETON_COUNT] =
{Scalar(0, 0, 255),      // BlueScalar(0, 255, 0),      // GreenScalar(255, 255, 64),   // YellowScalar(64, 255, 255),   // Light blueScalar(255, 64, 255),   // PurpleScalar(255, 128, 128)   // Pink
};//深度信息转换为彩色信息,显示人体所在区域;
HRESULT DepthToRGB(USHORT depth, uchar& red, uchar& green, uchar& blue)
{SHORT realDepth        = NuiDepthPixelToDepth(depth);USHORT playerIndex    = NuiDepthPixelToPlayerIndex(depth);//获得深度图该点像素位置对应的 UserID;// Convert depth info into an intensity for displayBYTE b = 255 - static_cast<BYTE>(256 * realDepth / 0x0fff);switch(playerIndex){//场景中没有对象时,只绘制灰度图,灰度值[0,128];case 0:red      = b/2;green = b/2;blue  = b/2;break;case 1:red      = b;green   = 0;blue    = 0;break;case 2:red        = 0;green   = b;blue    = 0;break;case 3:red        = b/4;green = b;blue    = b;break;case 4:red        = b;green   = b;blue    = b/4;break;case 5:red      = b;green   = b/4;blue  = b;break;case 6:red        = b/2;green = b/2;blue  = b;break;case 7:red        = 255 - b/2;green   = 255 - b/2;blue    = 255 - b/2;break;default:red       = 0;green   = 0;blue    = 0;break;}return S_OK;
}//获取彩色图像数据,并进行显示
int DrawColor(HANDLE h)
{  const NUI_IMAGE_FRAME * pImageFrame = NULL;  HRESULT hr = NuiImageStreamGetNextFrame( h, 0, &pImageFrame );  if(FAILED( hr ) )  {  cout<<"GetColor Image Frame Failed"<<endl;  return-1;  }  INuiFrameTexture* pTexture = pImageFrame->pFrameTexture;  NUI_LOCKED_RECT LockedRect;  pTexture->LockRect(0, &LockedRect, NULL, 0 );  if(LockedRect.Pitch != 0 )  {  BYTE* pBuffer = (BYTE*) LockedRect.pBits;  //OpenCV显示彩色视频if(true){Mat temp(COLOR_HIGHT,COLOR_WIDTH,CV_8UC4,pBuffer);imshow("Color",temp);  }int c = waitKey(1);//按下ESC结束  //如果在视频界面按下ESC,q,Q都会导致整个程序退出  if(c == 27 || c == 'q' || c == 'Q' )  {  SetEvent(m_hEvNuiProcessStop);  }  }  NuiImageStreamReleaseFrame(h, pImageFrame );  return 0;
} //获取深度图像数据,并进行显示
int DrawDepth(HANDLE h)
{  const NUI_IMAGE_FRAME * pImageFrame = NULL;  HRESULT hr = NuiImageStreamGetNextFrame( h, 0, &pImageFrame );  if(FAILED( hr ) )  {  cout<<"GetDepth Image Frame Failed"<<endl;  return-1;  }  INuiFrameTexture* pTexture = pImageFrame->pFrameTexture;  NUI_LOCKED_RECT LockedRect;  pTexture->LockRect(0, &LockedRect, NULL, 0 );  if(LockedRect.Pitch != 0 )  {  BYTE* pBuff = (BYTE*) LockedRect.pBits;//OpenCV显示深度视频if(true){Mat depthTmp(DEPTH_HIGHT,DEPTH_WIDTH,CV_16U,pBuff);//imshow("Depth",depthTmp);depthRGB.setTo(0);//显示骨骼人体区域信息;for (int y=0; y<DEPTH_HIGHT; y++){const USHORT* p_depthTmp = depthTmp.ptr<USHORT>(y); uchar* p_depthRGB = depthRGB.ptr<uchar>(y); for (int x=0; x<DEPTH_WIDTH; x++){USHORT depthValue = p_depthTmp[x];if (depthValue != 63355){uchar redValue,greenValue,blueValue;DepthToRGB(depthValue,redValue,greenValue,blueValue);p_depthRGB[3*x] = blueValue;p_depthRGB[3*x+1] = greenValue;p_depthRGB[3*x+2] = redValue;}else{p_depthRGB[3*x]=0;p_depthRGB[3*x+1]=0;p_depthRGB[3*x+2]=0;}}}//imshow("DepthRGB",depthRGB);}int c = waitKey(1);//按下ESC结束  if(c == 27 || c == 'q' || c == 'Q' )  {  SetEvent(m_hEvNuiProcessStop);  }  }  NuiImageStreamReleaseFrame(h, pImageFrame );  return 0;
}  //获取骨骼数据,并在深度图上进行显示
int DrawSkeleton()
{  NUI_SKELETON_FRAME SkeletonFrame;  cv::Point pt[20];    HRESULT hr = NuiSkeletonGetNextFrame( 0, &SkeletonFrame );  if(FAILED( hr ) )  {  cout<<"GetSkeleton Image Frame Failed"<<endl;  return -1;  }bool bFoundSkeleton = false;  for(int i = 0 ; i < NUI_SKELETON_COUNT ; i++ )  {  if(SkeletonFrame.SkeletonData[i].eTrackingState == NUI_SKELETON_TRACKED )  {  bFoundSkeleton= true;  }  }  //Has skeletons!  if(bFoundSkeleton )  {  NuiTransformSmooth(&SkeletonFrame,NULL);  //cout<<"skeleton num:"<<NUI_SKELETON_COUNT<<endl;for(int i = 0 ; i < NUI_SKELETON_COUNT ; i++ )  {  NUI_SKELETON_TRACKING_STATE trackingState = SkeletonFrame.SkeletonData[i].eTrackingState;if(trackingState == NUI_SKELETON_TRACKED )  //骨骼位置跟踪成功,则直接定位;{  //NUI_SKELETON_DATA *pSkel = &(SkeletonFrame.SkeletonData[i]);NUI_SKELETON_DATA  SkelData = SkeletonFrame.SkeletonData[i];Point jointPositions[NUI_SKELETON_POSITION_COUNT];for (int j = 0; j < NUI_SKELETON_POSITION_COUNT; ++j){LONG x, y;USHORT depth;cout<<j<<" :("<<SkelData.SkeletonPositions[j].x<<","<<SkelData.SkeletonPositions[j].y<<") ";NuiTransformSkeletonToDepthImage(SkelData.SkeletonPositions[j], &x, &y, &depth, depthResolution);//circle(depthRGB, Point(x,y), 5, Scalar(255,255,255), -1, CV_AA);jointPositions[j] = Point(x, y);}for (int j = 0; j < NUI_SKELETON_POSITION_COUNT; ++j){if (SkelData.eSkeletonPositionTrackingState[j] == NUI_SKELETON_POSITION_TRACKED){circle(depthRGB,jointPositions[j],5,SKELETON_COLORS[i],-1,CV_AA);circle(depthRGB,jointPositions[j],6,Scalar(0,0,0),1,CV_AA);}else if (SkelData.eSkeletonPositionTrackingState[j] == NUI_SKELETON_POSITION_INFERRED){circle(depthRGB, jointPositions[j], 5, Scalar(255,255,255), -1, CV_AA);}}cout<<endl;}else if(trackingState == NUI_SKELETON_POSITION_INFERRED) //如果骨骼位置跟踪未成功,通过推测定位骨骼位置;{LONG x, y;USHORT depth=0;NuiTransformSkeletonToDepthImage(SkeletonFrame.SkeletonData[i].Position,&x, &y,&depth,depthResolution);cout<<SkeletonFrame.SkeletonData[i].Position.x<<";"<<SkeletonFrame.SkeletonData[i].Position.y<<endl;circle(depthRGB, Point(x, y), 7, CV_RGB(0,0,0), CV_FILLED);}}}  imshow("SkeletonDepth",depthRGB);  int c = waitKey(1);//按下ESC结束  if(c == 27 || c == 'q' || c == 'Q' )  {  SetEvent(m_hEvNuiProcessStop);  }  return 0;
} //kinect读取数据流线程;
DWORD WINAPI KinectDataThread(LPVOID pParam)
{  HANDLE hEvents[4] = {m_hEvNuiProcessStop,m_hNextVideoFrameEvent,  m_hNextDepthFrameEvent,m_hNextSkeletonEvent};  while(1)  {  int nEventIdx;  nEventIdx=WaitForMultipleObjects(sizeof(hEvents)/sizeof(hEvents[0]),  hEvents,FALSE,100);  if(WAIT_OBJECT_0 == WaitForSingleObject(m_hEvNuiProcessStop, 0))  {  break;  }  //Process signal events  if(WAIT_OBJECT_0 == WaitForSingleObject(m_hNextVideoFrameEvent, 0))  {  DrawColor(m_pVideoStreamHandle);  }  if(WAIT_OBJECT_0 == WaitForSingleObject(m_hNextDepthFrameEvent, 0))  {  DrawDepth(m_pDepthStreamHandle);  }  if(WAIT_OBJECT_0 == WaitForSingleObject(m_hNextSkeletonEvent, 0))  {  DrawSkeleton();  }  }  CloseHandle(m_hEvNuiProcessStop);  m_hEvNuiProcessStop= NULL;  CloseHandle(m_hNextSkeletonEvent );  CloseHandle(m_hNextDepthFrameEvent );  CloseHandle(m_hNextVideoFrameEvent );  return 0;
}int main(int argc,char * argv[])
{//初始化NUI  HRESULT hr =NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX|NUI_INITIALIZE_FLAG_USES_COLOR|NUI_INITIALIZE_FLAG_USES_SKELETON);  if(hr != S_OK )  {  cout<<"Nui Initialize Failed"<<endl;  return hr;  }//打开KINECT设备的彩色图信息通道  m_hNextVideoFrameEvent  = CreateEvent( NULL, TRUE, FALSE, NULL );  m_pVideoStreamHandle   = NULL;  hr= NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,colorResolution, 0, 2,m_hNextVideoFrameEvent, &m_pVideoStreamHandle);  if(FAILED( hr ) )  {  cout<<"Could not open image stream video"<<endl;  return hr;  }  m_hNextDepthFrameEvent  = CreateEvent( NULL, TRUE, FALSE, NULL );  m_pDepthStreamHandle    = NULL;  hr= NuiImageStreamOpen(NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX,depthResolution, 0,2, m_hNextDepthFrameEvent,&m_pDepthStreamHandle);  if(FAILED( hr ) )  {  cout<<"Could not open depth stream video"<<endl;  return hr;  }  m_hNextSkeletonEvent   = CreateEvent( NULL, TRUE, FALSE, NULL );//骨骼跟踪事件flag设置: //坐姿 flag|NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT;只有头肩手臂10个节点;//站姿 flag&~(NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT);有20个节点;hr= NuiSkeletonTrackingEnable( m_hNextSkeletonEvent,  NUI_SKELETON_TRACKING_FLAG_ENABLE_IN_NEAR_RANGE/*&(~NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT)*/); if(FAILED( hr ) )  {  cout<<"Could not open skeleton stream video"<<endl;  return hr;  }  m_hEvNuiProcessStop     = CreateEvent(NULL,TRUE,FALSE,NULL);//用于结束的事件对象;//开启一个线程---用于读取彩色、深度、骨骼数据;  HANDLE m_hProcesss        = CreateThread(NULL, 0, KinectDataThread, 0, 0, 0);  ///  while(m_hEvNuiProcessStop!=NULL){WaitForSingleObject(m_hProcesss,INFINITE);  CloseHandle(m_hProcesss);  m_hProcesss= NULL; }//Clean up.  NuiShutdown();  return 0; }



显示图像(坐姿,因此只有上肢10个骨骼点):







更多

Kinect SDK(1):读取彩色、深度、骨骼信息并用OpenCV显示相关推荐

  1. Kinect开发学习笔记之(四)提取颜色数据并用OpenCV显示

    Kinect开发学习笔记之(四)提取颜色数据并用OpenCV显示 zouxy09@qq.com http://blog.csdn.net/zouxy09 我的Kinect开发平台是: Win7 x86 ...

  2. Led方阵和串口通信COM2(读取字膜的数据并用LED显示)

    文章目录 Led方阵和串口通信COM2(读取字膜的数据并用LED显示) Author:Luis Time:2022-04-06 Version:v2.0 功能 环境 代码 Keil Proteus 主 ...

  3. FFmpeg读取远程摄像头视频并用opencv显示

    首先要配置好FFmpeg和opencv环境,ffpmeg可以直接读取tcp视频流数据,只需将设置好ip地址和端口即可直接打开这个数据流.然后设置好解码器, //配置静态库,也可直接添加到附加依赖项中 ...

  4. [译]Kinect for Windows SDK开发入门(九):骨骼追踪进阶 下

    1. 基于景深数据的用户交互   到目前为止我们只用了骨骼数据中关节点的X,Y值.然而Kinect产生的关节点数据除了X,Y值外还有一个深度值.基于Kinect的应用程序应该利用好这个深度值.下面的部 ...

  5. ]Kinect for Windows SDK开发入门(六):骨骼追踪基础 上

    原文来自:http://www.cnblogs.com/yangecnu/archive/2012/04/06/KinectSDK_Skeleton_Tracking_Part1.html Kinec ...

  6. [译]Kinect for Windows SDK开发入门(八):骨骼追踪进阶 上

    前7篇文件我们介绍了Kinect SDK中各种传感器的各种基本知识,我们用实验的方式演示了这些基本对象和方法的如何使用,这些都是Kinect开发最基本的知识.了解了这些基本知识后,就可以开发出一个基于 ...

  7. Kinect SDK 正式版(二)骨骼追踪,实现PC版体感水果忍者

    这篇来介绍一下几个正式版SDK提供的新特性,还有比较被主流应用的骨骼数据操作. 首先先介绍一个新特性,Kinect角度调整,继续上篇的项目工程,在界面上添加两个控件,一个下拉列表,一个按钮. < ...

  8. Kinect SDK v1.7 新特性、交互框架与新概念

    Kinect SDK v1.7 新特性.交互框架与新概念 zouxy09@qq.com http://blog.csdn.net/zouxy09 2013年3月18日,微软Kinect for Win ...

  9. Azure Kinect SDK Ubuntu18.04安装使用

    Azure Kinect SDK Ubuntu18.04安装使用 文章目录 Azure Kinect SDK Ubuntu18.04安装使用 1.下载 1.4.0 版本 SDK 2.安装依赖 3.下载 ...

最新文章

  1. Linux CPU cache
  2. tomcat servlet容器请求时序
  3. TCP 的有限状态机
  4. 如何给视频中插入视频,字幕,以及去掉前后广告
  5. 《开源框架那点事儿33》极限挑战:用一条循环语句正确输出99表!【前两名奖图书一本】...
  6. Glory Ex坚持发展核心竞争力,致力于让更多人走进数字资产
  7. matlab 运行 AlexNet
  8. [转载] python在内网服务器安装第三方库
  9. Python实现表格转shapefile方法
  10. java编译提示错误信息_java常见编译错误信息
  11. 漫威超级英雄大全(二)
  12. 小米笔记本备份、SSD分区、U盘Ghost详解及对产品的建议
  13. 论文解析:Neural Graph Collaborative Filtering
  14. LabVIEW formula node
  15. 今日头条视频采集方法
  16. 15个C语言“谜题”,有你知道的吗?
  17. 杨辉三角寄数列求和c语言,杨辉三角与高阶等差数列的求和
  18. #error和#warning使用分析
  19. 技术分享 | OpenVINO及EdgeX摄像头管理和推理平台
  20. 无聊领养一个QQ宠物

热门文章

  1. 项目二:使用交换机构建局域网——交换机的基本命令
  2. 零售银行的“制胜秘籍”:大数据驱动营销及管理
  3. 单链表实例(1)实现水浒传排行增删改查和练习
  4. IP地址的格式与分类
  5. 超市零售数据可视化分析(Plotly 指南)
  6. 【沐风老师】3DMAX一款神级一键四边面重拓扑插件Quad Remesher使用教程
  7. HTML实现炫酷的流星雨
  8. java 小项目:简单扑克牌游戏
  9. 蒜头君的旅游计划(DFS)
  10. 关于小米2来电显示和短信不显示姓名问题