|
本帖最后由 ycyipman 于 2016-2-16 15:08 编辑
一、使用方式:
红外线帧获取需要四个脚本,ColorSourceManager,DepthSourceManager,MultiSourceManager和DepthSourceView,使用时新建一个空物体,并为其添加ColorSourceManager,DepthSourceManager,MultiSourceManager,再新建一个plane或者cube,添加DepthSourceView作为输出展示载体。InfraredSourceView需要三个公共变量,将对应的空物体拖入即可。
二、脚本执行过程:
深度帧具备两种数据读取模式,一种是从ColorSourceManager和DepthSourceManager分别读取彩色数据和深度数据,再将读取到的数据传输给DepthSourceView脚本展示;另一种模式为直接读取MultiSourceManager的数据,数据中包含彩色数据和深度数据,最后传输给DepthSourceView脚本展示。(注:MultiSourceManager脚本执行过程其实就是ColorSourceManager和DepthSourceManager脚本执行过程的简单叠加,至于微软为何如此设计,不懂)
深度帧的显示需要三个管理类脚本,ColorSourceManager的代码实现过程参考彩色帧篇,此篇主要做MultiSourceManager和DepthSourceManager的解析。
DepthSourceManager脚本:
由于深度数据将更多的计算和绘制放在DepthSourceView脚本中,因此DepthSourceManager只负责了简单的数据读取,过程与ColorSourceManager和InfraredSourceManager相同,此处只放代码,不再过多赘述。
[mw_shl_code=csharp,true] public ushort[] GetData()
{
return _Data;
}
void Start ()
{
_Sensor = KinectSensor.GetDefault();
if (_Sensor != null)
{
_Reader = _Sensor.DepthFrameSource.OpenReader();
_Data = new ushort[_Sensor.DepthFrameSource.FrameDescription.LengthInPixels];
}
}[/mw_shl_code]
[mw_shl_code=csharp,true] void Update ()
{
if (_Reader != null)
{
var frame = _Reader.AcquireLatestFrame();
if (frame != null)
{
frame.CopyFrameDataToArray(_Data);
frame.Dispose();
frame = null;
}
}
}
void OnApplicationQuit()
{
if (_Reader != null)
{
_Reader.Dispose();
_Reader = null;
}
if (_Sensor != null)
{
if (_Sensor.IsOpen)
{
_Sensor.Close();
}
_Sensor = null;
}
}[/mw_shl_code]
MultiSourceManager脚本:
MultiSourceManager为ColorSourceManager和DepthSourceManager脚本执行过程的简单叠加,详细参阅各自的脚本说明,此处只解释叠加过程
[mw_shl_code=csharp,true] /// <summary>
/// 返回彩色纹理,同ColorSourceManager的GetColorTexture函数
/// </summary>
/// <returns>彩色纹理</returns>
public Texture2D GetColorTexture()
{
return _ColorTexture;
}
/// <summary>
/// 返回深度数据,同DepthSourceManager的GetData函数
/// </summary>
/// <returns>深度数据</returns>
public ushort[] GetDepthData()
{
return _DepthData;
}
void Start ()
{
//获取,默认的传感器对象
_Sensor = KinectSensor.GetDefault();
if (_Sensor != null)
{
//打开多重帧阅读器对象,该对象可获取彩色帧数据以及深度帧数据
_Reader = _Sensor.OpenMultiSourceFrameReader(FrameSourceTypes.Color | FrameSourceTypes.Depth);
//对于彩色帧数据的初始化操作,同ColorSourceManager脚本的Start函数
var colorFrameDesc = _Sensor.ColorFrameSource.CreateFrameDescription(ColorImageFormat.Rgba);
ColorWidth = colorFrameDesc.Width;
ColorHeight = colorFrameDesc.Height;
_ColorTexture = new Texture2D(colorFrameDesc.Width, colorFrameDesc.Height, TextureFormat.RGBA32, false);
_ColorData = new byte[colorFrameDesc.BytesPerPixel * colorFrameDesc.LengthInPixels];
//对于深度帧数据的初始化操作,同DepthSourceManager脚本的Start函数
var depthFrameDesc = _Sensor.DepthFrameSource.FrameDescription;
_DepthData = new ushort[depthFrameDesc.LengthInPixels];
if (!_Sensor.IsOpen)
{
_Sensor.Open();
}
}
}[/mw_shl_code]
[mw_shl_code=csharp,true] void Update ()
{
if (_Reader != null)
{
//获取最近一帧有效帧
var frame = _Reader.AcquireLatestFrame();
if (frame != null)
{
//获取frame中属于彩色帧的部分
var colorFrame = frame.ColorFrameReference.AcquireFrame();
if (colorFrame != null)
{
//获取frame中属于深度帧的部分
var depthFrame = frame.DepthFrameReference.AcquireFrame();
if (depthFrame != null)
{
//与ColorSourceManager脚本的Update函数相同
colorFrame.CopyConvertedFrameDataToArray(_ColorData, ColorImageFormat.Rgba);
_ColorTexture.LoadRawTextureData(_ColorData);
_ColorTexture.Apply();
//与DepthSourceManager脚本的Update函数相同
depthFrame.CopyFrameDataToArray(_DepthData);
depthFrame.Dispose();
depthFrame = null;
}
colorFrame.Dispose();
colorFrame = null;
}
frame = null;
}
}
}[/mw_shl_code]
[mw_shl_code=csharp,true] void OnApplicationQuit()
{
if (_Reader != null)
{
_Reader.Dispose();
_Reader = null;
}
if (_Sensor != null)
{
if (_Sensor.IsOpen)
{
_Sensor.Close();
}
_Sensor = null;
}
}[/mw_shl_code]
DepthSourceView脚本:
首先是脚本主要参数的释义:
[mw_shl_code=csharp,true] private KinectSensor _Sensor; //传感器对象
private CoordinateMapper _Mapper; //坐标映射对象
private Mesh _Mesh; //代码动态创建的Mesh对象
private Vector3[] _Vertices; //顶点集合
private Vector2[] _UV; //顶点的UV坐标集合
private int[] _Triangles; //顶点点阵组成的三角形的顶点集合,数组元素为_Vertices数组中的序号,每三个元素代表唯一的一个三角形
// Only works at 4 right now
private const int _DownsampleSize = 4; //降频采样大小
private const double _DepthScale = 0.1f; //实际距离与unity内模型距离的缩放系数,若实际距离为4m,则unity内显示的距离为0.4m
private const int _Speed = 50; //深度模型旋转速度[/mw_shl_code]
在Start函数中,_Mapper对象用于深度坐标的映射,在下文会有提及,frameDesc.Width和frameDesc.Height代表获取的帧的分辨率,以像素点为基本单位,同时还有一个降低分辨率的操作,降低的系数目前只能为4
[mw_shl_code=csharp,true]void Start()
{
_Sensor = KinectSensor.GetDefault();
if (_Sensor != null)
{
//创建_Mapper对象
_Mapper = _Sensor.CoordinateMapper;
//创建frameDesc对象
var frameDesc = _Sensor.DepthFrameSource.FrameDescription;
// Downsample to lower resolution
//降频采样至更低的分辨率
//frameDesc中以像素点为基本单位
CreateMesh(frameDesc.Width / _DownsampleSize, frameDesc.Height / _DownsampleSize);
if (!_Sensor.IsOpen)
{
_Sensor.Open();
}
}
}[/mw_shl_code]
在CreateMesh函数中,根据传入的降频之后的分辨率进行动态的Mesh绘制,绘制需要顶点集合,UV集合以及三角形顶点集合。顶点集合直接使用像素点的坐标,即每一个像素点即为一个顶点(经测试,深度模型的顶点数达到了26.7K左右)因此顶点集合长度为width*height;由于没有进行UV分割,因此UV集合的长度与顶点集合相同,转换计算公式为UVx=Verticalx/width,UVy=Verticaly/height;三角形顶点数为6*(width-1)*(height-1)(具体换算过程见文末),其中width和height即为降频之后的分辨率的宽度值与高度值;具体代码实现过程如下:
[mw_shl_code=csharp,true] /// <summary>
/// 动态创建Mesh
/// </summary>
/// <param name="width">每一帧数据的宽度值,以像素为基本单位</param>
/// <param name="height">每一帧数据的高度值,以像素为基本单位</param>
void CreateMesh(int width, int height)
{
_Mesh = new Mesh();
GetComponent<MeshFilter>().mesh = _Mesh;
//_Vertices与_UV数量相等,没有进行UV分割
//_Vertices的长度为宽度*长度,说明顶点数与像素点数相同,因此有多少个像素点,就在Unity中绘制多少个顶点
_Vertices = new Vector3[width * height];
_UV = new Vector2[width * height];
_Triangles = new int[6 * ((width - 1) * (height - 1))];
int triangleIndex = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int index = (y * width) + x;
//取出了每一个像素点的坐标作为_Vertices数组中每一个顶点的坐标值
_Vertices[index] = new Vector3(x, -y, 0);
//转换得到UV坐标
_UV[index] = new Vector2(((float)x / (float)width), ((float)y / (float)height));
// Skip the last row/col
if (x != (width - 1) && y != (height - 1))
{
int topLeft = index;
int topRight = topLeft + 1;
int bottomLeft = topLeft + width;
int bottomRight = bottomLeft + 1;
//将_Vertices数组中的序号依次放入_Triangles数组
_Triangles[triangleIndex++] = topLeft;
_Triangles[triangleIndex++] = topRight;
_Triangles[triangleIndex++] = bottomLeft;
_Triangles[triangleIndex++] = bottomLeft;
_Triangles[triangleIndex++] = topRight;
_Triangles[triangleIndex++] = bottomRight;
}
}
}
_Mesh.vertices = _Vertices;
_Mesh.uv = _UV;
_Mesh.triangles = _Triangles;
_Mesh.RecalculateNormals();
}[/mw_shl_code]
上文提到过,深度值有两种读取方式SeparateSourceReaders,分别从两个脚本获取数据,以及MultiSourceReader,只从MultiSourceManager一个脚本获取数据,同时,为了更加直观的观察,深度模型可以通过上下左右四个按键调节角度,此功能在Update函数中实现:
[mw_shl_code=csharp,true] void Update()
{
if (_Sensor == null)
{
return;
}
//单机鼠标左键,切换读取模式
if (Input.GetButtonDown("Fire1"))
{
if(ViewMode == DepthViewMode.MultiSourceReader)
{
ViewMode = DepthViewMode.SeparateSourceReaders;
}
else
{
ViewMode = DepthViewMode.MultiSourceReader;
}
}
//上下左右四个按键旋转深度模型
float yVal = Input.GetAxis("Horizontal");
float xVal = -Input.GetAxis("Vertical");
transform.Rotate(
(xVal * Time.deltaTime * _Speed),
(yVal * Time.deltaTime * _Speed),
0,
Space.Self);
//处于SeparateSourceReaders模式时,从两个脚本分别读取数据
if (ViewMode == DepthViewMode.SeparateSourceReaders)
{
if (ColorSourceManager == null)
{
return;
}
_ColorManager = ColorSourceManager.GetComponent<ColorSourceManager>();
if (_ColorManager == null)
{
return;
}
if (DepthSourceManager == null)
{
return;
}
_DepthManager = DepthSourceManager.GetComponent<DepthSourceManager>();
if (_DepthManager == null)
{
return;
}
gameObject.GetComponent<Renderer>().material.mainTexture = _ColorManager.GetColorTexture();
RefreshData(_DepthManager.GetData(),
_ColorManager.ColorWidth,
_ColorManager.ColorHeight);
}
//处于MultiSourceReader模式时,从MultiSourceManager脚本读取数据
else
{
if (MultiSourceManager == null)
{
return;
}
_MultiManager = MultiSourceManager.GetComponent<MultiSourceManager>();
if (_MultiManager == null)
{
return;
}
gameObject.GetComponent<Renderer>().material.mainTexture = _MultiManager.GetColorTexture();
RefreshData(_MultiManager.GetDepthData(),
_MultiManager.ColorWidth,
_MultiManager.ColorHeight);
}
}[/mw_shl_code]
同时在屏幕上显示一个文本框,用于显示当前处于哪种模式:
[mw_shl_code=csharp,true] void OnGUI()
{
GUI.BeginGroup(new Rect(0, 0, Screen.width, Screen.height));
GUI.TextField(new Rect(Screen.width - 250 , 10, 250, 20), "DepthMode: " + ViewMode.ToString());
GUI.EndGroup();
}[/mw_shl_code]
RefreshData函数用于实时更新深度模型,深度模型的更新需要顶点坐标集合_Vertices,顶点UV坐标集合_UV,以及每个顶点所处的三角形的位置集合_Triangles(注:由于_Triangles中存放的是每个顶点的引用,因此只要指向不变,顶点的坐标变化时,_Triangles中的值不会发生变化)在调用该函数时,由于传入的分辨率还未经过降频处理,因此需要在for循环中进行降频操作
[mw_shl_code=csharp,true] /// <summary>
/// 更新深度模型
/// </summary>
/// <param name="depthData">深度数据</param>
/// <param name="colorWidth">彩色帧宽度</param>
/// <param name="colorHeight">彩色帧高度</param>
private void RefreshData(ushort[] depthData, int colorWidth, int colorHeight)
{
var frameDesc = _Sensor.DepthFrameSource.FrameDescription;
ColorSpacePoint[] colorSpace = new ColorSpacePoint[depthData.Length];
//深度帧坐标数据映射到色彩空间
_Mapper.MapDepthFrameToColorSpace(depthData, colorSpace);
for (int y = 0; y < frameDesc.Height; y += _DownsampleSize)
{
for (int x = 0; x < frameDesc.Width; x += _DownsampleSize)
{
int indexX = x / _DownsampleSize;
int indexY = y / _DownsampleSize;
//indexX和indexY是原来二维数组中的坐标号,下一行表示将二维数组的坐标号转化成一维数组的序号,若下列等式成立,则重排的顺序为(0,0),(1,0),(2,0),...,(0,1),(1,1),(2,1),...,(0,2),(1,2),(2,2),...,......
int smallIndex = (indexY * (frameDesc.Width / _DownsampleSize)) + indexX;
double avg = GetAvg(depthData, x, y, frameDesc.Width, frameDesc.Height);
avg = avg * _DepthScale;
_Vertices[smallIndex].z = (float)avg;
// Update UV mapping with CDRP
var colorSpacePoint = colorSpace[(y * frameDesc.Width) + x];
_UV[smallIndex] = new Vector2(colorSpacePoint.X / colorWidth, colorSpacePoint.Y / colorHeight);
}
}
_Mesh.vertices = _Vertices;
_Mesh.uv = _UV;
_Mesh.triangles = _Triangles;
_Mesh.RecalculateNormals();
}[/mw_shl_code]
GetAvg函数通过某一点的xy坐标,以及分辨率大小,求得该点在depthData中的序号,从而得到该点的深度值,求取序号的公式为Index=y*width+x,是一个二维数组元素重新排列成一维数组的过程,该公式上文提到的函数中也有提及,具体排列方式参考上文:
[mw_shl_code=csharp,true] /// <summary>
/// 根据读取到的深度数据,顶点的xy坐标,当前帧数据的宽度和高度,计算出该点在depthData中的序号,从而得到该点的深度值
/// </summary>
/// <param name="depthData">读取到的深度数据</param>
/// <param name="x">顶点的x坐标</param>
/// <param name="y">顶点的y坐标</param>
/// <param name="width">当前帧数据的宽度</param>
/// <param name="height">当前帧数据的高度</param>
/// <returns>顶点的z坐标</returns>
private double GetAvg(ushort[] depthData, int x, int y, int width, int height)
{
double sum = 0.0;
for (int y1 = y; y1 < y + 4; y1++)
{
for (int x1 = x; x1 < x + 4; x1++)
{
int fullIndex = (y1 * width) + x1;
if (depthData[fullIndex] == 0)//表示超出范围
sum += 4500;
else
sum += depthData[fullIndex];
}
}
return sum / 16;
}[/mw_shl_code]
附:三角形顶点数为6*(width-1)*(height-1)的推导过程:
一个3行4列的点阵组成的矩形,能够分割成6个小矩形,每一行每一列除去左右2个端点后剩下的点的个数即为可以切开的次数,切成的小矩形的个数即为切开次数+1,因此,对于一个x行y列的点阵,可以看做由[(x-2)+1]*[(y-2)+1]个最小单元矩形组成,即一共为(x-1)*(y-1)个单元矩形,对于每一个单元矩形而言,可以分成两个三角形,对于每一个三角形而言,共有3个顶点,因此,三角形的顶点总数为(x-1)*(y-1)*2*3,即6*(width-1)*(height-1)
|
-
|