티스토리 뷰

OpenCV

[Kinect with OpenCV] C로 구현하는 SkeletonStream

생각많은 소심남 2012. 10. 14. 22:05

드디어 월요일의 전날인 일요일 저녁입니다. 누구나 이때쯤 개콘의 노래를 들으면서 '아 내일이 월요일이구나..' 라는 걸 느끼실겁니다. 뭐 그래도 다들 즐거운 주말을 보내셨으리라 생각합니다. 

 저는 뭐했느냐.. 그냥 집에서 코딩했습니다. 그냥 어떡하면 키넥트로 관절을 쉽게 인식하게 할까에 대한 고민을 했고, 그냥 그 답을 조금 찾은듯 합니다. 하지만.. 뭔가 아쉽네요. 그냥 주말을 이렇게 보냈다는게..

 

아무튼 잡소리였고, 이번 포스트에서 다뤄볼 내용은 Kinect로 할 수 있는 기본적인 내용중 하나인 Skeleton Stream을 처리하는 방법을 해보려고 합니다. 물론 그냥 처리하기에는 힘드니까 OpenCV를 사용해서 간단하게 표현해보고자 합니다. 참고로 MS에서 제공하는 소스는 Kinect Developer toolkit에 포함되어 있습니다.

 

 

아주 간편하게 예제의 구조를 실행해볼 수 있는 샘플이긴 한데 D2D라는 용어가 붙어있다시피 DirectX2D를 사용한 샘플입니다. 즉, 이말은 DirectX에 대한 문법에 대해서 알고 있는 사람한테 유용하다는 거지요. 저야 한번도 다뤄본적이 없고 이 대신에 OpenCV로 해보려고 합니다. 하지만 내부구조나 이제 소개해드릴 내용도 샘플과 거의 비슷합니다.

 

자 기본적인 출발은 ColorStream에서 출발합니다.

우선 제 소스를 보면 InitializeKinect라는 부분이 있었습니다. 그안에는 NuiInitialize라는 함수가 있었는데 몇번 언급했던 내용이지만 이안에서는 자신이 사용할 기능에 대한 플래그를 선언해주는 부분입니다. 며칠전에 댓글로 문의를 주신분이 있었습니다. 만약 여러 기능을 한꺼번에 사용하고 싶은데 어떻게 처리를 해야되는거냐라고 말입니다. 그 부분은 그 답에도 나와있다시피 or 처리를 해주면 됩니다. 즉, 다음과 같이 해주면 되겠지요.

 

 

위와 같이 표현하면 개발자는 Color와 Skeleton 기능을 사용하겠다는 겁니다. 추가적으로 Depth 정보가 필요하신 분이라면 옆에 또 or 연산자를 붙여서 setting을 해주시면 됩니다. 그 다음에 다른 부분과 마찬가지로 Skeleton 기능을 돌릴 이벤트 핸들러가 필요합니다. 그래서 main함수의 선언에서 다음과 같이 추가시켜 줍니다. 거기에 Skeleton을 그릴 이미지 버퍼와 창도 같이 선언해줍니다.


// 20130206 수정사항 : Skeleton 에 관해서 따로 HANDLE을 만들 경우 초기화 문제가 발생해서 아예 빼줬습니다. 


 

기존에 ColorImageStream을 Open시키는 함수가 있었습니다. 그 다음부분에 Skeleton 기능을 활성화 시키는 과정을 다음과 같이 표현해줍니다.

 

 

참고로 Skeleton Tracking에서는 아시는 분도 계시겠지만 Seated Mode라고 해서 앉은 자세에서도 골격을 인식하는 모드가 존재하고 바로 이 Enable함수에서 특별히 처리해줍니다. 이 부분에 대해서는 추후에 다시 언급드리도록 하겠습니다. 아무튼 위와 같이 초기에 생성한 이벤트를 넣어주고 SkeletonTracking기능을 활성화 시켜줍니다.

 이제 기능까지 활성화시켰으니까 루프내에서 지속적으로 SkeletonData를 받아야 합니다. 그 부분은 Color나 Depth를 뽑아낼때와 똑같이 구성됩니다. 참고로 저는 createSkeleton이라는 함수를 만들어서 이안에서 처리하도록 했습니다.


// 20130206 수정사항 : Skeleton 에 관해서 따로 HANDLE을 만들 경우 초기화 문제가 발생해서 아예 빼줬습니다.


 자 이제 만들었던 createSkeleton에 대한 정의를 앞에다가 해줄 차례입니다. 그런데 일련의 프로세스가 매우 간단합니다. 간단히 정의하면 다음과 같습니다.

- SkeletonFrame 생성후 그 안에 매 프레임별로 정보를 넣어줌

- Frame내에서 관절정보를 매끄럽게 만듬

- 18개내에서 관절별로 탐지된 부분에 대해서 정보를 사용자에게 보여줌 - > 이과정에서는 받은 정보를 토대로 그리면 되겠지요.

 

이렇게 나눠집니다. 그래서 그 과정이 다음과 같이 구현됩니다.


 // 20130206 수정사항 : Skeleton 에 관해서 따로 HANDLE을 만들 경우 초기화 문제가 발생해서 아예 빼줬습니다.



 

부연설명해야 될 부분이 바로 NUI_SKELETON_TRACKING_STATE 이 부분인데 이 부분에 대한 설명은 msdn에서 정의되어 있습니다.

http://msdn.microsoft.com/en-us/library/nuisensor.nui_skeleton_tracking_state.aspx

간단히 언급하자면 받아온 SkeletonData 별로 각각의 State가 세가지로 구분됩니다. 완벽히 추적이 되는 상태인 TRACKED / 추적이 되는 건 아니고 그냥 관절이 있는 전체적인 위치만을 표현해준 POSITION_ONLY / 그리고 추적이 안되는 NOT_TRACKED 이렇게 나눠집니다. 우선은 TRACKED가 된 상태에서만 해당 Skeleton이 그려지게끔 구현을 했습니다. 그러면 궁금할 수있는게 그럼 두 플래그의 차이가 뭐냐 라는건데,,, 정지된 상태에서 관절이 어느정도의 위치에 있구나 라는것만 판단할 수 있는 요소가 POSITION_ONLY이고 그 각각의 관절 정보를 알 기 위해서는 위 예시에 나와있는 것처럼 TRACKED 상태의 관절만 따라가야 되는 것이지요. 이와중에 drawSkeleton이라는 함수를 만들었습니다. 그러니 앞에서 또 이 함수에 대한 정의를 해줘야 하겠지요.

참고로 주석처리가 되어 있는 부분에 대한 건 뒤에서 설명하겠습니다. 조금 중요한 요소입니다.

 

그럼 다음으로 구현해야 되는 부분이 drawSkeleton()인데 우선 이전 createSkeleton함수에서 받는 정보는 사실 그 SkeletonData가 어느 관절에서 나온정보를 내포하고 있기 때문에 그냥 그걸 화면상에 뿌려주기만 하면 됩니다. 하지만 잘 보면 아시겠지만 이 SkeletonData라는 정보가 가진 정보는 Vector4라는 자료형입니다. 즉, 화면상의 x,y픽셀로 바로 정의할 수 없다는 것이지요. 중간에 이걸 변환해주는 함수가 필요하고, 또 이정보를 담을 배열이 필요하겠지요. OpenCV에서는 이 x,y픽셀에 대한 위치정보를 담을 수 있는 CvPoint라는 자료형이 존재합니다. 그래서 CvPoint로 구성된 배열을 하나 전역적으로 선언해줍니다.

 

 

여기서 Position_count라는게 앞에서 잠깐 나온 18개의 관절에 대한 값이 define된겁니다. 그래서 위와 같이 해주면 전역적으로 CvPoint로 구성된 배열 18개가 생성되는 것이지요.

자 그럼 다시 drawSkeleton으로 와서 함수를 작성합니다. 여기서도 추가적으로 생성되는 함수가 drawBone이라는 것과 앞에서 언급한 Vector4정보를 Pixel로 바꿔주는 함수입니다.

 

 

맨처음 과정을 통해서는 Position에 포함되어 있는 Vector4 정보를 cvPoint로 바꿔주는 과정이고 밑의 drawBone은 그냥 뼈를 그려주는 과정이 됩니다. 지금은 일단 예시로 오른손과 왼손만 구현했는데 아마 다른 부분의 관절을 구현하실 분이라면 다음의 표를 참고하시면 될거 같습니다.

 

 

우선은 맨처음에 나온 SkeletonToScreen 함수에 대한 정의가 필요한데 여기에는 이전 포스트에서 언급했던 함수가 나옵니다. 저도 무척 기능을 찾는데 헤맸던 함수인 NuiTransformSkeletonToDepthImage()입니다.

 

 

당연한 이야기겠지만 지금 변환하고자 하는게 PixelPoint므로 반환값도 cvPoint로 해주는게 좋겠지요.보시면 아시겠지만 키넥트가 가진 정보는 Vector4입니다. 여기에는 Kinect를 기준으로 얼마나 떨어져 있는지 x,y,z,w로 표현되는 값입니다. 그 값에 대한 기준은 다음 그림을 참고하시면 됩니다.

 

 

w는 그냥 scale 값입니다. 아무튼 위 함수를 사용하게 되면 결과값으로 cvPoint(x,y)값이 나오게 됩니다. 전역적으로 선언해줬기 때문에 points라는 배열안에는 cvPoint값을 가지면서 어느 함수에서나 사용할 수 있게 됩니다. 그걸 drawBone에서 써먹으면 됩니다. 그런데 여기가 이 함수의 끝이 아닙니다. 사실 우리가 지금 프로그램을 만들고 있는 창은 VGA(640x480) 포멧입니다. 그런데 SkeletonTracking은 Default환경이 320x240 입니다. 그래서 위 함수 그대로 쓰면 출력화면 중심에 사람이 나오는게 아니라 그냥 640x480 안에서 320x240 내에서만 동작하게 되는 것이지요. 그래서 이걸 조절해주는 과정이 필요하고 그에 대한 인수를 앞에서 소개했던 Transform함수안에 또하나로 추가시켜줍니다. 그래서 최종적으로는 다음과 같습니다.

 

 

이제 마지막으로 drawBone이라는 함수를 정의해주면 됩니다. 그런데 왜 position에 대한 정보를 받느냐에 대해서 의아해하실 수 있는데 별거 없습니다. 그냥 관절이 tracking이 되는지에 대한 정보를 얻고 싶어서 그렇게 정의한 것일 뿐입니다. 실제로 뼈를 그리는데 있어서 필요한 값은 앞에서 넣어준 points속 픽셀위치값입니다. 이 부분도 단순하게 cvLine을 쓰면 처리가 됩니다.

 

 

자 전체적인 구조및 코드 작성은 끝났습니다. 한번 실행시켜보세요.

 

 

이게 뭐야 하실겁니다. 계속해서 오른손과 왼손이 움직인 궤적이 누적되서 나타납니다. 여기서 아까 주석처리한 부분이 필요해집니다.

지금 OpenCV상에서는 계속적으로 Frame을 뿌리면서 기존의 데이터에 대해서는 초기화 시켜주는 과정이 없습니다. 그래서 빈 이미지 버퍼를 하나 만들고 복사하는 과정을 추가시켜준 것이 주석 처리된 부분입니다. 그 부분을 풀어주고 다시 해보면 다음과 같이 나옵니다.

 

 

이게 SkeletonStream의 전부입니다. 뭐 응용한다면 다음과 같이 ColorImage 상에서도 뿌릴 수 있겠지요.

 

이밖에도 말씀드릴 내용이 많은데 조금 길어지는 것 같아서 후에 다른 포스트로 다시 언급드리도록 하겠습니다.

댓글