【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 6862|回复: 1

[体感与外设] Kinect v2.0 for Unity---深度帧

[复制链接]
发表于 2016-2-1 15:16:44 | 显示全部楼层 |阅读模式
本帖最后由 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)
q.jpg
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2024-4-24 08:24

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表