| 
 | 
 
 本帖最后由 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) 
 |   
- 
 
 
 
 
 
 
 
 |