본문 바로가기
1_ 맛있는프로그래밍/C# and Visual C++

C# - MS Kinect SDK를 사용해 보자. 키넥트 개발 첫걸음

by 준환이형님 2011. 9. 28.

키넥트 관련 자료를 포스팅합니다.

예전에 화상캠에 빨간색 값 하나 잘 받아오려고 필터 씌우고 알고리즘 쓰고 별 걸 다 했었는데..  어느날 닌텐도 위(wii)가 나오더니만

그것 마저 다 쌈싸먹은 마소의 키넥트(Kinect )!!

일년이 지난 지금..  영상관련 해서 정말 쉽게 개발 할 수 있는 시대가 왔군요. 주재.. 보고있나? 





출처 : 지평선너머(snscho66) - http://snscho66.blog.me/100131461219

MS Kinect SDK Beta가 공개된지도 좀 지났다.

윈도우즈7과 연동되어 사용이 가능해짐으로써, 컴퓨터를 이용하는 방식이

많이 바뀔 것 같다. 이러한 변화는 게임에서 시작이 될 것이고,

이미 시작되고 있다.

이 포스트는 C#을 사용하여 간단한 키넥트 응용 프로그램을 작성하는

방법을 다룬다. 독자는 C#과 MS Visual Studio2010에 이미 익숙해져 있다고

가정한다.

 

1단계 - 개발환경 구축

 

Kinect .NET SDK를 사용하려면 다음의 요구사항을 만족해야 한다.

운영체제

   - Windows 7 (x86 또는 x64)

하드웨어

   - 듀얼코어, 2.66GHz 이상의 컴퓨터

   - MS DirectX 9.0c 를 지원하는 그래픽 카드

   - 2GB 이상의 램

   - 키넥트 센서

소프트웨어

   - MS Visual Studio 2010 (Express 에디션도 가능)

   - MS .NET framework 4.0

   - Kinect for Windows SDK (Beta)

 

2단계 - WPF project를 생성

 

Microsoft.Research.Kinect.Nui 에 대한 레퍼런스를 추가한다.

(C:\Program Files (x86)\Microsoft Research KinectSDK)에 위치해 있다.

샘플 타겟을 반드시 x86 platform으로 한다. 이 SDK는 오직 x86 만을 지원하기 때문이다.


 

응용 프로그램은 반드시 키넥트 센서를 초기화 해주어야 한다. 초기화는 Runtime.Initialize

를 호출함으로써 이루어 진다. 이 함수를 호출하면 프레임 캡쳐 엔진을 초기화 하고, 센서 데이터를

수신하는 스레드를 만들며, 프레임이 준비되었음을 알리는 시그날을 응용프로그램에 보내는 일을

한다. 또한 센서 데이터를 처리하고 수집하는 서브시스템을 초기화 한다. 이 초기화 함수가

키넥트 센서를 찾는데 실패하면 InvalidOperationException 을 발생시킨다.

 

윈도우즈 로드 이벤트를 만들고 InitializeNui를 호출한다.

private void InitializeNui()
{
    try
    {
        // _kinectNui 를 Runtime 오브젝트로 선언, 이 것은 키넥트 센서 인스턴스이다.

        _kinectNui = new Runtime();

        //비디오와 뎁스 스트림을 오픈하고, 이벤트 핸들러를 초기화한다.

        //다른 키넥트 함수를 부르기 전에 반드시 초기화를 해준다.

          _kinectNui.Initialize(RuntimeOptions.UseDepthAndPlayerIndex |
                        RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseColor);

        //스트림 칼라 이미지:

        // • UseColor옵션을 반드시 포함한다.

        // • 해상도는 1280x1024 와 640x480 두 가지 이다.

        // • 이미지 타입은 Color, ColorYUV, 와ColorYUVRaw이다.

       _kinectNui.VideoStream.Open(ImageStreamType.Video, 2,
                                ImageResolution.Resolution640x480, ImageType.ColorYuv);

        //스트림 뎁스와 플레이어 색인 데이터:

        // • UseDepthAndPlayerIndex 옵션은 반드시 포함한다.

        // • 인텍스의 유효한 해상도는 320x240 와 80x60이다.

        // • 유효한 이미지 타입은 DepthAndPlayerIndex뿐이다.

        _kinectNui.DepthStream.Open(ImageStreamType.Depth, 2,

                ageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex);

         lastTime = DateTime.Now;


        _kinectNui.VideoFrameReady += new EventHandler<ImageFrameReadyEventArgs>

              NuiVideoFrameReady);
        _kinectNui.DepthFrameReady += new EventHandler<ImageFrameReadyEventArgs>

             (nui_DepthFrameReady);
    }
    catch (InvalidOperationException ex)
    {
        MessageBox.Show(ex.Message);
    }
}

 

3단계 : 비디오 출력

 

비디오와 뎁스는 평평한 이미지를 리턴하므로 비트맵을 만들어서 UI에 뿌려주면 된다.

비디오 프레임 레디 이벤트 핸들러

void NuiVideoFrameReady(object sender, ImageFrameReadyEventArgs e)
{
    PlanarImage Image = e.ImageFrame.Image;

    image.Source = BitmapSource.Create(
        Image.Width, Image.Height, 9696, PixelFormats.Bgr32, null,
        Image.Bits, Image.Width * Image.BytesPerPixel);

    imageCmyk32.Source = BitmapSource.Create(
        Image.Width, Image.Height, 9696, PixelFormats.Cmyk32, null,
        Image.Bits, Image.Width * Image.BytesPerPixel);
}

 

뎁스 프레임 레디 이벤트 핸들러

void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
    var Image = e.ImageFrame.Image;
    var convertedDepthFrame = convertDepthFrame(Image.Bits);

    depth.Source = BitmapSource.Create(
        Image.Width, Image.Height, 9696, PixelFormats.Bgr32, null, convertedDepthFrame, Image.Width * 4);

    CalculateFps();
}

// 플레이어 인덱스를 포함한 16비트 그레이스케일 뎁스 프레임을

// 32비트 프레임으로 바꾼다.

// 다른 사용자는 다른 컬러로 표현한다.

byte[] convertDepthFrame(byte[] depthFrame16)
{
    for (int i16 = 0, i32 = 0; i16 < depthFrame16.Length && i32 < depthFrame32.Length; i16 += 2, i32 += 4)
    {
        int player = depthFrame16[i16] & 0x07;
        int realDepth = (depthFrame16[i16 + 1] << 5) | (depthFrame16[i16] >> 3);
        // 13비트 뎁스 정보를 8비트 로 바꾼다.

        // 이것은 화면 표시를 위한 것이다.

        byte intensity = (byte)(255 - (255 * realDepth / 0x0fff));

        depthFrame32[i32 + RED_IDX] = intensity;
        depthFrame32[i32 + BLUE_IDX] = intensity;
        depthFrame32[i32 + GREEN_IDX] = intensity;
    }
    return depthFrame32;
}        

void CalculateFps()
{
    ++totalFrames;

    var cur = DateTime.Now;
    if (cur.Subtract(lastTime) > TimeSpan.FromSeconds(1))
    {
        int frameDiff = totalFrames - lastFrames;
        lastFrames = totalFrames;
        lastTime = cur;
        frameRate.Text = frameDiff.ToString() + " fps";
    }


 

위에서 정상 화면과 32비트로 변환한 것, 뎁스만 추출한 것의 이미지를 보여준다.

 

4단계 : 카메라 각도 조절

 

키넥트에서 카메라 콘트롤은 매우 쉽다.

카메라 앵글의 최대값과 최소값이 있지만 단순히 키넥트 센서를 손으로 옮김으로서

자동으로 카메라 앵글을 조정한다.


 


 


 

키낵트 초기화를 한 다음

카메라 오브젝트를 만든다.

private Camera _cam; 

_cam = _kinectNui.NuiCamera;
txtCameraName.Text = _cam.UniqueDeviceName;

다음은 카메라 클래스의 정의이다.

namespace Microsoft.Research.Kinect.Nui
{
    public class Camera
    {
        public static readonly int ElevationMaximum;
        public static readonly int ElevationMinimum;

        public int ElevationAngle { getset; }
        public string UniqueDeviceName { get; }

        public void GetColorPixelCoordinatesFromDepthPixel(ImageResolution colorResolution, ImageViewArea viewArea,
                                                           int depthX, int depthY, short depthValue, out int colorX,
                                                           out int colorY);
    }
}

 

5단계 : 업 다운

 private void BtnCameraUpClick(object sender, RoutedEventArgs e)
{
    try
    {
        _cam.ElevationAngle = _cam.ElevationAngle + 5;
    }
    catch (InvalidOperationException ex)
    {
        MessageBox.Show(ex.Message);
    }
    catch (ArgumentOutOfRangeException outOfRangeException)
    {
        //Elevation angle must be between Elevation Minimum/Maximum"

        MessageBox.Show(outOfRangeException.Message);
    }
}

다음은 다운이다.

private void BtnCameraDownClick(object sender, RoutedEventArgs e)
{
    try
    {
        _cam.ElevationAngle = _cam.ElevationAngle - 5;
    }
    catch (InvalidOperationException ex)
    {
        MessageBox.Show(ex.Message);
    }
    catch (ArgumentOutOfRangeException outOfRangeException)
    {
        //Elevation angle must be between Elevation Minimum/Maximum"

        MessageBox.Show(outOfRangeException.Message);
    }
}

 

배경 지식 : 뼈대 트랙킹

 

키넥트의 강력한 기능 중 하나는 센서 앞에 서 있는 사람의 뼈대를 발견해 내는 능력이다.

이 기능은 매우 빠르고 연습이 필요 없이 가능하다.

NUI Skeleton API는 센서앞에 서 있는 두 사람을 분별한다.

분별한 데이터는 좌표로 제공되는데 뼈대 포지션이라 한다.

이 데이터는 사람의 위치와 현재의 자세를 표현해 준다.

키넥트는 20개의 포인트로 사람의 자세를 판별하는데

아래의 그림으로 이를 나타낸다.


 


 

6 단계 : SkeletonFrameReady 에 등록

 

UseSkeletalTracking 옵션으로 초기화 한다. 이 옵션을 안 주면 뼈대 추적이 안된다.

_kinectNui.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseColor);
_kinectNui.SkeletonFrameReady += new EventHandler<SkeletonFrameReadyEventArgs>(SkeletonFrameReady);

 

뼈대 추적은 3인 이상은 안된다.

키넥트 개발에서 디버깅은 쉬운일이 아니다. - 매번 당신은 똑같은 동작을 반복해야 한다.

뼈대 추적의 위치값은 다음의 enum 값이다.

namespace Microsoft.Research.Kinect.Nui
{
    public enum JointID
    {
        HipCenter,
        Spine,
        ShoulderCenter,
        Head,
        ShoulderLeft,
        ElbowLeft,
        WristLeft,
        HandLeft,
        ShoulderRight,
        ElbowRight,
        WristRight,
        HandRight,
        HipLeft,
        KneeLeft,
        AnkleLeft,
        FootLeft,
        HipRight,
        KneeRight,
        AnkleRight,
        FootRight,
        Count,
    }
}

 

 7 댠계 : 뼈대 위치(Joint Position) 얻기

 

 조인트 포지션은 카메라 공간에 있는 것이므로 우리는 사이즈와 위치를 변환할 필요가 있다.

뎁스 이미지 공간

뎁스 맵은 640x480, 320×240, or 80x60  픽셀 값을 가지는데 각 픽셀은

미리미터 단위로 거리를 나타낸다. 픽셀 값 0 은 센서가 물체를 찾지 못한 것을

의미한다.

 

뼈대 이미지 공간

사람의 뼈대 위치는 x,y,z 축으로 나타낸다. 뎁스 이미지 공간과 달리,

이 세가지 좌표는 메터 단위로 표현된다. 이것은 오른손 좌표계 시스템이다.

Z 축은 센서로 부터 멀어지는 값, Y축은 위로, X축은 왼쪽이다.

이를 아래의 그림에 나타낸다.


 

private Point getDisplayPosition(Joint joint)
{
    float depthX, depthY;
    _kinectNui.SkeletonEngine.SkeletonToDepthImage(joint.Position, out depthX, out depthY);
    depthX = Math.Max(0, Math.Min(depthX * 320320));  //convert to 320, 240 space

    depthY = Math.Max(0, Math.Min(depthY * 240240));  //convert to 320, 240 space

    int colorX, colorY;
    ImageViewArea iv = new ImageViewArea();
    // only ImageResolution.Resolution640x480 is supported at this point     _kinectNui.NuiCamera.GetColorPixelCoordinatesFromDepthPixel(ImageResolution.Resolution640x480, iv, (int)depthX, (int)depthY, (short)0out colorX, out colorY);

    // map back to skeleton.Width & skeleton.Height     return new Point((int)(imageContainer.Width * colorX / 640.0) - 30, (int)(imageContainer.Height * colorY / 480) - 30);
}

 

8단계 : 뼈대 타입에 따라 이미지 삽입

 

Vector4(x,y,z,w)를 사용하여 뼈대의 중심점을 표현한다. 여기서 x,y,z는 카메라 공간 좌표이고,

w는 품질 레벨을 말한다.(0-1 사이의 값이다.)

void SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
    foreach (SkeletonData data in e.SkeletonFrame.Skeletons)
    {
        //트랙킹이 되는 뼈대인지 아닌지 판별한다 트랙킹이 안되는 것이면 위치값만

얻을 수 있다..

          if (SkeletonTrackingState.Tracked != data.TrackingState) continue;

        //각 관절의 위치값은 Vector4: (x, y, z, w)로 표현된다.

          foreach (Joint joint in data.Joints)
        {
            if (joint.Position.W < 0.6f) return;// Quality check

              switch (joint.ID)
            {
                case JointID.Head:
                    var heanp = getDisplayPosition(joint);

                    Canvas.SetLeft(imgHead, heanp.X);
                    Canvas.SetTop(imgHead, heanp.Y);

                    break;
                case JointID.HandRight:
                    var rhp = getDisplayPosition(joint);

                    Canvas.SetLeft(imgRightHand, rhp.X);
                    Canvas.SetTop(imgRightHand, rhp.Y);
                    break;
                case JointID.HandLeft:
                    var lhp = getDisplayPosition(joint);

                    Canvas.SetLeft(imgLefttHand, lhp.X);
                    Canvas.SetTop(imgLefttHand, lhp.Y);
                    break;
            }
        }
    }
}


 이상으로 뼈대를 추적하여 주먹과 얼굴을 덧 씌우는 예제 프로그램을 보였다.