티스토리 뷰

사실 지난번에 어떤 분께서 c로 칼라이미지를 구현하는 방법은 나와있는데 뎁스 이미지를 구하는 법도 소개해주면 좋겠다고 하셔서 늦었지만 간단하게 소개해드리고자 합니다. 전체적인 코드 구성은 사실 지난번에 소개해드렸던 컬러 이미지 호출과 거의 유사합니다. 다만 뎁스 이미지는 컬러이미지와 다르게 그냥 컬러 데이터를 뿌려주는 것이 아니라 중간에 depth값을 컬러로 변환시켜주는 과정이 필요합니다. 이 부분은 뒤에서 소개해드리도록 하고 일단 기본적으로 Kinect SDK Developer Toolkit의 예제를 통해서도 depth를 어떻게 빼는지를 확인할 수 있습니다.



그런데 밑에 있는 Depth-D3D와는 무슨 차이인지를 궁금해 하실 분도 계실겁니다. 이 밑에 있는 Depth-D3d와 DepthWithColor-D3D는 기본적으로 빼올 수 있는 depth 정보를 Direct X의 월드상에 3D로 매핑을 한겁니다. 일종의 3D point cloud라고 할까요? 그래서 실제로 카메라뷰가 이동함에 따라서 개체의 입체적인 영상을 볼 수 있습니다. Color가 포함되고 있다는 건 일종의 Depth와 Color간의 매핑을 통해서 실제로 Color로도 간단하게 입체적으로 보여줄 수 있는 예제입니다. 첨언입니다만 SimpleOpenNI 라이브러리를 사용해서 프로세싱에서 프로그래밍했을 때는 이 과정이 간단했습니다. 물론 C에서도 지난 포스트에서 소개해드린 Coordinate 함수를 사용하면 위와 같은 효과를 누릴 수 있긴 합니다.

 일단 결론적으로 말하자면 키넥트가 제공하는 본연의 기능을 사용하기 위해서는 이 Depth Map을 뽑는 것이 중요하며, 여기서 간단하게 배운 것을 토대로 다양하게 응용할 수 있습니다. 자 그럼 하나하나씩 해봅시다.


우선은 프로젝트를 생성하고 나서 헤더 파일과 라이브러리를 연결하는 과정이 필요합니다. 이부분은 이 OpenCV 섹션 거의 맨처음 글인 환경설정에서도 언급했던 내용이기도 합니다만 간단하게 소개를 하면



와 같이 헤더파일을 추가해줍니다. 저는 OpenCV 헤더파일이 있는 곳을 위와 같이 환경변수로 선언했기 때문에 저런 변수로 선언했고 경로적는게 익숙하신 분은 그냥 경로를 찾아서 설정해주시면 됩니다. 만약 환경변수가 뭔지를 알고 싶다 하면 밑에 있는 Macros를 누르게 되면 환경변수가 뭘로 설정되어 있는지를 확인할 수 있습니다.



다음은 Libraries 폴더 지정인데 여기서는 사전에 debug모드로 했는지 Release모드로 설정했는지가 중요합니다. 이걸 결정하지 않고 무작정 경로 지정부터 해버린다면 나중에 모드 변경시 경로가 초기화되는 경우가 발생합니다. 아무튼 Opencv 라이브러리도 Release 모드와 debug 모드로 나눠져 있으므로 그에 따라서 다음과 같이 경로를 지정해줍니다. 참고로 저는 이번에는 debug 모드로 해줬습니다.

 


이 다음에는 linker 부분의 Additional Dependency를 지정해주는 건데 여기는 기존 libraries 폴더에 들어있는 라이브러리를 나열해주면 됩니다. 물론 키넥트 라이브러리랑 OpenCV라이브러리가 있을텐데 이건 모드에 따라서 파일명이 달라집니다. 아마 직접 확인해보시면 디버그용 OpenCV 라이브러리에는 파일명에 d가 붙어있을겁니다. 그걸 그대로 넣어주시면 됩니다. 역시 디버그용에서는 다음과 같이 구성됩니다.



이제 새로운 코드를 삽입하고 키넥트 헤더파일과 openCV 헤더파일을 추가해봅시다. 만약 오류가 나지 않는다면 정상적으로 헤더파일을 불러올 수 있다는 겁니다. 




환경설정이 끝났으니 이제 프로그래밍을 해봅시다. 이번에도 역시 윈도우용 어플리케이션을 만들 것이기 때문에 몇가지 헤더파일이 추가되어야 합니다. 참고로 제가 사용한 헤더파일은 다음과 같습니다.




이게 되었으니까 미리 지정해줄 전역변수를 정의해줄 필요가 있겠습니다. 

앞에서 잠깐 언급드린 것처럼 뎁스데이터를 우리가 시각적으로 보기 위해서는 변환과정이 필요합니다. 당연히 뎁스데이터는 그냥 키넥트가 쏘는 IR이 측정한 거리, 즉 숫자라는 겁니다. 그런게 각 픽셀마다 있는 것이고, 그걸 하나하나읽어서 표현하기 위해서는 창의 크기가 있어야 된다는 것이지요. 그 읽어오는 과정에서 창의 가로크기와 세로크기가 쓰입니다. 그 쓰임법은 뒤에서 소개해드리고 가급적 다음과 같이 사전에 정의해두는 것이 필요합니다.

 

자 일단 메인 함수부터 하겠습니다. win32용 어플리케이션을 구현하는데 있어서 중요한 변수는 바로 HANDLE이라는 겁니다. 이게 일종의 쓰레드와 맞물려서 해당 이벤트동안 계속 상태를 유지해주는 역할을 합니다. 우선은 지금 상태의 handle과 그 다음 상태에 대한 handle을 미리 준비하는 게 좋습니다. 



자 그다음에 OpenCV에서 이미지를 보여주기 위한 변수를 선언해줘야겠지요. 크기야 앞에서 정의했고, 8bit형 unsigned int 를 사용합니다. 그리고 알파값이 포함되기 때문에 채널은 RGBA 4채널로 한번 해보겠습니다. 물론 그냥 A값만 사용하기 때문에 1채널로 해줘도 결과는 똑같습니다. 그 다음에 이 이미지를 띄울 창까지 하나 선언해줍니다.



이쯤에서 뭔가를 초기화 시켜주는 함수를 하나 만드는게 필요하겠지요. 저는 InitializeKinect 라는 함수를 두고 따로 지정했습니다. 뭐 이 과정이 왜 필요하냐고 하시는 분은 그냥 스킵해도 됩니다. 저는 뭔가 중간에 오류가 나는 것을 대비하려고 선언했던 것이니까요. 그럼 일단 앞에 InitializeKinect() 함수를 만들어줍니다.



연결 상태에 대한 bool형 변수를 선언하고 그게 true일때만 이 과정을 거치게 하면 되겠지요. 여기서 지난 컬러 이미지 출력시 잠깐 나왔던 NuiInitialize 함수가 나옵니다. 여기서 플래그를 선언해줘야 하는데 위와 같이 Depth를 사용하기 위해서는  NUI_INITIALIZE_FLAG_USES_DEPTH라고 집어넣어주면 됩니다. 그런데 제 포스트를 유심히 보신분이라면 뎁스 데이터 안에 그 뎁스안에 사람이 몇명 포함되어 있는지에 대한 playerIndex가 있다는 것을 아실 수 있을겁니다. 이정보를 활용하기 위해서는 이에 대한 플래그를 다음과 같이 선언해줍니다.


뭐 안쓰면 상관없습니다. 만약 이 initialize 과정이 안된다는 소리는 연결이 안되었다는 소리겠지요. 그래서 그에 대한 조건문을 만들어주면 됩니다. 저는 다음과 같이 구성했습니다.


이제 Initialize도 되었으니까 해당 imageStream을 쓰기위한 절차를 삽입해줘야겠지요. 이과정에서는 depth를 어떤 창에 어떻게 뿌릴건지에 대한 설정이 들어가는데 참고로 앞에서 선언한 핸들러가 여기서 사용됩니다. 다음과 같이 작성합니다.


여기서 추가적으로 가할 수 있는게 있는데 마지막에서 소개하기로 하고 여기서도 받아온 hr에 대한 예외구문을 선언해주면 좋겠지요.



자 이제 이미지 스트림도 열었으니까 그걸 계속 출력시켜줘야 합니다. 그런데 아시다시피 지금 다루고 있는건 끊기지 않는 하나의 영상입니다. 그걸 출력하기 위해서는 앞에서 선언한 이벤트가 무한하게 반복되어야 하겠지요. 그리고 사용자가 원할때 그 창이 닫히게 해야 합니다. 그게 기본적인 OpenCV에서의 동영상 출력이 됩니다. 그래서 다음과 같이 구성해줍니다.



여기서 밑에 있는 createDepthImage는 만들어야 되는 함수입니다. 일단 여기까지 하면 동작에 필요한 코드 구성은 끝납니다. 마지막으로 이렇게 계속 스트림을 열어두면 메모리에 누적이 되기 때문에 이걸 풀어주는 함수가 필요합니다. 그리고 끝으로 키넥트 기능도 끄는 함수가 필요하겠지요. 창도 닫아야 하겠구요.



이게 전체적인 코드입니다. 자 이제 동작함수인 createDepthImage()를 만들어봅시다. 지금 넘겨주는 값에 대해서 인자로 넣어주는 과정이 우선이 됩니다.



키넥트에서는 키넥트에 대한 image frame을 하나의 변수형으로 제공해줍니다. 이른바 NUI_IMAGE_FRAME이지요. 처음에는 변수 선언이니까 비워줍니다. 그러고 나서 또 등장하는 NuiImageStreamGetNextFrame이라는 함수를 통해서 여기에 depth data가 담기게 됩니다. 저는 여기에 예외과정까지 하나 더 붙여서 작성합니다.



이과정을 거치면 pImageFrame에는 1000ms마다 한번식 DepthImageStream이 넘어오게 됩니다. 그런데 단순히 이 정보만으로는 우리가 볼 수 없습니다. 하나의 텍스처로 만들어준 후에 각 픽셀에 입혀줘야 우리가 비로소 depth Map을 볼 수 있게 되지요. 게임에서는 이런 과정을 하나의 texture로 만들어서 표현합니다. 이와 과정이 비슷합니다. 여기서도 하나의 texture를 만들고 그 안의 픽셀을 조작하기 위해서 하나의 Locked_Rect을 만들어주는데, 키넥트에서는 이와 관련된 변수를 제공해줍니다. 그래서 frame으로부터 locked_rect까지 데이터가 이동하는 과정은 다음과 같이 표현됩니다.



여기까지 거치면 이제 LockedRect에 조작이 가능한 textureFrame이 담기게 됩니다.여기서 필요한 정보는 바로 pbit이라는 속성에 저장된 데이터입니다. 요 안에 뎁스 데이터가 들어있지요. 이걸 이제 해당 픽셀에 입히면 되는데 필요한 변수들이 있습니다.



여기서 새로운 자료형인 RGBQUAD 구조체가 나타납니다. 그냥 영상 처리때 많이 사용하는 자료형으로 여기에는 사전에 R,G,B에 대한 값들이 선언되어 있어서 이 구조체로 선언하면 자동으로 RGB에 대한 속성을 가지게 됩니다. 여기다가 앞에서 언급한대로 거리를 픽셀로 변환한 값을 넣어주게 됩니다. 자 여기서 앞에서 말한 창의 크기와 폭이 사용됩니다.




보통 위와 같은 방식이 픽셀중에서 조건에 맞는 픽셀을 검색할 때 사용하는 알고리즘입니다. x와 y값이 변하면서 픽셀에 데이터를 삽입하면 되는 것이지요. 그걸 이 조건문 내에서는 Nui_ShortToQuad_Depth라는 함수에서 처리해줄겁니다. 그래서 이 구문에는 다음과 같이 처리해줄 겁니다.



그냥 계속 픽셀 단위로 검색하면서 변환된 데이터를 rgbrun이라는 변수에 넣어주는 것이죠. 여기까지 해주면 rgb라는 배열에는 우리가 그토록 원하던 depthImage에 대한 정보를 가지게 됩니다. 이걸 앞에서 선언한 Depth 이미지에 넣어주고 창에 출력해주는게 끝이 될겁니다.



역시 이것도 메모리에 누적되는 것이니까 동작이 종료되었을때는 해당 Frame을 풀어주는 과정이 필요하겠지요. 이로써 프레임에다가 픽셀 데이터를 삽입하는 과정도 끝납니다. 역시 이에 대해서는 키넥트가 관련함수를 제공해줍니다.



자 그럼 마지막으로 오류가 나는 변수에 대한 정의가 앞에서 이뤄져야 되겠지요.



여기서도 나오는 거지만 뎁스데이터가 담겨나오는 short형 변수에서 상위 13bit만 데이터를 가지고 있고 하위 3bit은 playerindex를 가지고 있습니다.

그래서 그 정보만 활용하려면 3bit을 shift해주는 게 필요합니다. 그러고 난 후에 해당 구조체의 속성인 rgbRed와 rgbBlue, rgbGreen에다가 그냥 넣어주는게 끝이 됩니다. 


자 끝났습니다. 한번 실행시켜보겠습니다.


 

여기서 넘어오는 I값을 반전시키면 어떻게 될까요?

(참고로 ReleaseImage에서 오류가 나시는 분은 main구문의 맨마지막 cvReleaseImage부분을 주석처리해주세요)



이런식으로 색 반전이 됩니다.


자 마지막으로 아까 얼핏 트릭을 알려드린다고 했는데 메인 구문에서 imageStream을 여는 부분이 있었지요. 그 뒤에 하나의 플래그를 달아줍시다. 바로 Kinect for windows의 기능인 NearMode 활성화 시키는 플래그입니다.



이것도 결과를 보면 아마 앞에서 인식안되었던 부분이 인식되는 것을 확인할 수 있을겁니다.



한번 비교해보시면 근접센서가 켜졌을때와 안켜졌을 때의 차이가 약간 보이실 겁니다. (예시상에서는 반사때문에 정확히 잘 안나타났네요.)


간단하게 C로 구현해보는 DepthImage였습니다. 조금 어려울 수 있으나 지금까지 했던 ColorImage나 DepthImage만을 잘 사용한다면 이제 OpenCV와 곁들여서 멋진 어플리케이션이 나올거라고 생각합니다. 저도 지금 이런 방식에서 조금씩 발전시켜서 만들고 있고, 언젠가는 여러분들 앞에 공개할 날이 오겠지요. 여기까지입니다.

댓글