티스토리 뷰

 최근에 키넥트의 골격 인식을 XNA에 연결 시켜서 하나의 게임을 만들어봤습니다. 사실 키넥트에서 가장 기능을 효과적으로 보여 줄 수 있는 것중에 대표적인 것이 바로 Skeleton Tracking입니다. 물론 Depth를 보여주는 것도 일반 웹캠과 차별성을 둘 수 있는 기능이지만 그래도 온몸으로 그 기능을 체험하는데는 골격만큼 좋은 도구도 없지요. 그래서 요 며칠동안은 간단하게 그 기능에 대해서 예제를 통해 분석하는 시간을 가져보고자 합니다. 


자 우선 XNA프로젝트를 하나 생성하고 프로젝트 내에서 키넥트를 쓰기 위해서는 관련 라이브러리와 네임스페이스를 추가해줘야 합니다.




우선은 프로젝트 내에서 현재 연결된 키넥트 센서에 관한 선언을 해줘야 합니다. 그리고 XNA의 특성상 무언가를 뿌려줄 Texture2D도 필요하고 그안에다가 키넥트의 Data를 RGB로 바꿔줄 byte형 배열도 필요합니다.



이제 게임의 창에 대한 속성을 지정할 수 있는 부분이 바로 Game1()이라는 부분인데 보통 이안에서 창의 크기를 지정해줍니다. 저는 640x480 창에서 출력하는 것을 목적으로 하고 다음과 같이 작성해줬습니다.



물론 전체화면으로 게임을 하고 싶다 하시는 분은 다음과 같은 코드만 한줄 추가시켜주면 됩니다.



그런데 당연한 이야기 이겠지만 전체화면에서 다시 원래의 화면으로 돌아오기 위해서는 어떤 키에 대한 피드백이 존재해야 합니다. 그부분은 밑의 Update에서 이뤄지는데 여기서는 그에 대한 고려를 하지 않고 그냥 640x480에서 동작하는 모습을 보여드리고자 합니다. 

 다음으로는 LoadContent 부분에서 Kinect에 대한 기능 초기화 밑 설정을 해줘야 합니다. 일단은 사용하고자 하는 기능이 Color와 Skeleton이므로 그에 대한 기능을 부여하고 동작하는 것까지 작성해봅니다. 참고로 두 기능을 같이 사용하기 위한 이벤트로 AllFrameReady를 씁니다. 물론 두 개 각각에 대한 이벤트를 따로 써도 상관없습니다. 



그리고 Skeleton Data 같은 경우는 실시간으로 계속 위치가 바뀝니다. 그래서 따로 배열로 선언하고 계속 읽어오게 하면 키넥트에서 제공하는 20개 관절에 대한 위치를 파악할 수 있습니다. SDK에서는 따로 Skeleton이라는 자료형을 두고 저장할 수 있게 해놨습니다. 그래서 전역적으로 하나 선언해주고 LoadContent부분에서 초기화시켜주는 과정을 거칩니다.



자 이제 AllFrameReady를 채울 차례인데 우선은 ColorStream부터 먼저 뽑아봅니다. ColorStream을 뽑기 위해서는 해당 이벤트에서 ColorStream을 여는 과정을 거치게 되고 내부에서 앞에서 미리 선언한 ColorPixel에 대한 크기를 지정하면서 그안에 복사하는 일련의 과정을 거치게 됩니다. 그래서 다음과 같이 작성됩니다.



이렇게 하면 colorFrame안에 있던 ColorStream이 ColorPixel에 담기게 되겠지요. 이제 이걸 우리가 보기 위해서는 RGB로 된 무언가가 필요하겠지요.

WPF 상에서는 BitmapImage라는 컨트롤이 있어서 그안에 colorPixel을 넣으면 자동으로 이미지 전환이 되었습니다만, XNA에서는 매초에 60번씩 pixel을 그리는 형태를 가집니다. 그에 맞게 색깔이 실시간으로 바뀌게 하는 과정을 거쳐야 하는 거지요. 제공하는 자료형 중에 Color라는 자료형이 있습니다. 이걸로 한번 표현해봅니다.



Color에는 기본적으로 RGB에 대한 속성들이 들어가 있고 각각이 배열 형태로 배치되어 있습니다. 그런데 재미있는 건 키넥트가 colorPixel로 데이터를 복사할 때 저장 순서가 BGRXBGRX.... 이런식으로 저장이 된다는 겁니다. 그래서 그냥 착각하고 colorPixel의 저장 순서와 color에 대입하는 순서를 동일하게 해버리면 결과가 조금 괴기한 색상으로 나옵니다.

그래서 위와 같은 과정을 거침으로서 하나의 픽셀에 BGRA값이 다 저장되게끔 합니다.


우선 Color만 뽑아보려면 Draw에서 spriteBatch에다가 이 image가 담긴 KinectTexture를 그려야 하겠지요. 그래서 다음과 같이 작성해줍니다.

참고로 저 KinectTexture가 있을때라는 전제 조건이 들어가야 합니다. 키넥트가 texture로 정보를 넘겨주기 전에 Draw를 통해서 그려버리면 오류가 발생하게 됩니다.

그래서 일단 이미지 출력을 하면 다음과 같이 나오게 됩니다.



자 여기다가 앞에서 선언한 Skeleton Data를 적용해보도록 하겠습니다. 저는 일단 머리에는 아이언 맨 마스크를 씌워보도록 하겠습니다. 다른 부분은 각자가 생각하는 이미지를 넣으면 되겠지요. 


 우선은 아이언맨 이미지를 Content 프로젝트에 삽입하고 Texture2D로 선언해둡니다.




그 다음에 LoadContent를 통해서 해당이미지를 Texture2D로 매칭하는 과정을 거칩니다,



이제는 본격적으로 AllFrameReady에서 Skeleton Data를 처리해야 될 차례입니다. 역시 구조는 ColorStream이나 DepthStream을 뽑는 과정과 거의 비슷합니다.



요 밑이 이제 조금 복잡한데요. 키넥트는 항상 Skeleton을 잡으려고 노력하지만 보이는 대상에 사람이 없으면 못 뽑아내겠지요. 당연히 그때는 Skeleton은 없을거고 그런 순간에도 계속 골격을 분석하는 과정을 거치면 조금 비효율적이게 됩니다. 그래서 해당 골격이 잡혔을 때만 위치를 잡는 것을 택하기 위해서 foreach 구문을 사용합니다.



여기서 이제 골격 20개에 대한 인식을 시킬 수 있는데 저는 테스트로 머리만 해보도록 하겠습니다. 물론 다른 부분은 JoinType 부분만 수정해주면 얼마든지 다른 부분도 인식시킬 수 있습니다.



그런데 이게 끝이 아닙니다. 앞에서도 언급드렸다시피 XNA상에서 모든 그리는 것은 Draw 함수에서 정의됩니다. 그런데 해보신 분은 아시겠지만 그 Draw중에서 SpriteBatch.Draw를 해줘야 Texture를 정확히 화면상에 표현할 수 있습니다. 그러기 위해서 필요한게 Texture, Rectangle, Color 입니다. 그런데 Rectangle을 구성하는 요소가 바로 Position인데 우리가 원하는 건 head의 Point에 따라 그 Rectangle이 움직여야 하는 것이겠지요. 그래서 하나의 Rectangle을 만들어둡니다. 



그리고 위의 SkeletonData를 처리하는 과정에서 이 값을 정의해주면 되겠지요. 그런데 유의할 점이 있습니다. Joint에 보면 Position이라는 속성이 있는데 이 안의 값은 실 좌표계의 x,y 입니다. 그리고 우리가 화면상으로 나타내야 할 점은 픽셀의 x,y 이지요. 한마디로 기준이 다르다는 겁니다. Kinect SDK에서는 이를 변환해주는 함수로 ColorImagePoint라는 자료형과 CoordinateMapper를 제공해줍니다. 그래서 다음과 같이 적용해줘야 합니다.



이렇게 하면 실좌표계의 x,y,값이 픽셀의 x,y,좌표로 바뀌게 됩니다. 

이제 Draw에서 아이언 맨 마스크만 그려주면 될겁니다. 이제 Draw에 필요한 Texture와 Rectangle, Color가 다 나왔으니까 그냥 그려주면 됩니다.



참고로 hPoint의 위치가 조금 엇나가니까 그 값을 적절히 수정해주면 머리에 딱 맞게 맞출 수 있습니다.



사실 예전에는 복잡하게 실좌표계와 픽셀좌표계를 변환하기 위한 코드가 있었지만 SDK v1.6으로 넘어오면서 CoordinateMapper라는 속성함수가 생겨서 손쉽게 매핑할 수 있게 되었습니다. 그걸 사용해서 한번 다뤄봤고 물론 손이나 발같은 것도 여러분들이 하실 수 있겠지요. 그냥 head 한 것처럼 추가시키면 됩니다.



댓글