第四章 利用Kinect摳圖和自動拍照程式
在本篇部落格中,我将詳細介紹Kinect的一種特殊資料源,BodyIndex(人物索引二值圖),Kinect就是利用這個資料源來區分目标是人體還是其他物體,有沒有覺得功能很強大。說到這裡,很多朋友就應該會想到如何利用Kinect去摳圖了,主要是依靠這個資料源,把 Kinect擷取的圖像中的人體和其他物體(主要是背景)區分開來。另外一點是關于自動拍照程式的,大概想要實作的是,自己從網上找一些比較好的圖檔,結合摳圖技術,把自己“PS”到指定的圖檔。這個程式感覺很有可玩性,而且這隻是一種最簡單和基本的玩法,大家可以根據自己的創意做出更好玩有趣有意義的東西。
Kinect中帶了一種資料源,叫做BodyIndex,簡單來說就是它利用深度攝像頭識别出最多6個人體,并且用資料将屬于人體的部分标記,将人體和背景差別開來。利用這一特性,就可以在環境中顯示出人體的輪廓而略去背景的細節。我采用了下面兩種方式來實作。
一、利用Kinect摳圖
還是一樣的風格,先上菜,再分析
摳圖程式:
#include <iostream>
#include <Kinect.h>
#include <opencv2\highgui.hpp>
using namespace std;
using namespace cv;
int main(void)
{
IKinectSensor * mySensor = nullptr; //Sensor
GetDefaultKinectSensor(&mySensor);
mySensor->Open();
IBodyIndexFrameSource * mySource = nullptr; //Source
mySensor->get_BodyIndexFrameSource(&mySource);
int height = 0, width = 0;
IFrameDescription * myDescription = nullptr;
mySource->get_FrameDescription(&myDescription);
myDescription->get_Height(&height);
myDescription->get_Width(&width);
IBodyIndexFrameReader * myReader = nullptr; //Reader
mySource->OpenReader(&myReader);
IBodyIndexFrame * myFrame = nullptr; //Frame
Mat img(height,width,CV_8UC3);
Vec3b color[7] = { Vec3b(0,0,255),Vec3b(0,255,255),Vec3b(255,255,255),Vec3b(0,255,0),Vec3b(255,0,0),Vec3b(255,0,255),Vec3b(0,0,0) };
while (1)
{
if (myReader->AcquireLatestFrame(&myFrame) == S_OK)
{
UINT size = 0;
BYTE * buffer = nullptr;
myFrame->AccessUnderlyingBuffer(&size,&buffer);
for (int i = 0; i < height; i++)
for (int j = 0; j < width; j++)
{
int index = buffer[i * width + j]; //0-5代表人體,其它值代表背景,用此将人體和背景渲染成不同顔色
if (index <= 5) //index小于5是人體部分,
img.at<Vec3b>(i, j) = color[index];
Else //否則,不是人體部分,則給他顯示黑色
img.at<Vec3b>(i, j) = color[6];
}
imshow("TEST",img);
myFrame->Release();
}
if (waitKey(30) == VK_ESCAPE)
break;
}
myReader->Release();
myDescription->Release();
mySource->Release();
mySensor->Close();
mySensor->Release();
return 0;
}
看過前面幾篇部落格,就會發現,Kinect程式的複用性很大,也就是說不同程式之間有很多部分是共用的,相似度非常高,就算不同的部分你也會發現他們是有規律可循的,就像本系列第二篇第二部分介紹的常用API一樣,基本步驟都差不多的。步驟和前面相似,不再贅述,關鍵在于對資料的處理。IBodyIndexFrame裡的資料分兩種,值在0-5之間的點代表的是人體(是以最多識别出6個人),大于5的值代表的是背景。是以要顯示人體時,隻要簡單的把代表人體的點渲染成一種顔色,背景渲染成另外一種顔色就可以了。值得注意的是在寫顔色表color時,要用Vec3b把資料強轉一下,不然會有問題。
最終的效果就是這樣:
二、結合Kinect自動插圖的拍照程式
前面一部分講到了Kinect可以從環境中區分出人體來。是以可以利用這個功能,來把攝像頭前的人合成進照片裡,和利用Photoshop不同的是,這樣合成進去的人是動态且實時的。
簡單的思路
BodyIndex用的是深度資料,隻能用來判斷畫面中的點屬不屬于人體而不能用來直接顯示畫面,Color圖裡的資料隻能用來顯示而沒有其他功能。是以如果深度資料能和彩色資料配合的話,就能利用深度資料來識别出彩色資料中的哪些點屬于人體。但是深度幀的分辨率是512 x 424,而彩色幀的分辨率是1920 x 1080,無法将他們對應起來。然而微軟提供了一個叫ICoordinateMapper的類。
簡單來說:将彩色圖上的點轉換到深度圖的坐标系中->判斷某點是否是人體->是的話從彩色圖中取出此點,與背景替換。
照片合成程式:
#include <iostream>
#include <opencv2/imgproc.hpp>
#include <opencv2\highgui.hpp>
#include <Kinect.h>
using namespace std;
using namespace cv;
int main(void)
{
IKinectSensor * mySensor = nullptr;
GetDefaultKinectSensor(&mySensor);
mySensor->Open();
//************************準備好彩色圖像的Reader并擷取尺寸*******************************
int colorHeight = 0, colorWidth = 0;
IColorFrameSource * myColorSource = nullptr;
IColorFrameReader * myColorReader = nullptr;
IFrameDescription * myDescription = nullptr;
{
mySensor->get_ColorFrameSource(&myColorSource);
myColorSource->OpenReader(&myColorReader);
myColorSource->get_FrameDescription(&myDescription);
myDescription->get_Height(&colorHeight);
myDescription->get_Width(&colorWidth);
myDescription->Release();
myColorSource->Release();
}
//************************準備好深度圖像的Reader并擷取尺寸*******************************
int depthHeight = 0, depthWidth = 0;
IDepthFrameSource * myDepthSource = nullptr;
IDepthFrameReader * myDepthReader = nullptr;
{
mySensor->get_DepthFrameSource(&myDepthSource);
myDepthSource->OpenReader(&myDepthReader);
myDepthSource->get_FrameDescription(&myDescription);
myDescription->get_Height(&depthHeight);
myDescription->get_Width(&depthWidth);
myDescription->Release();
myDepthSource->Release();
}
//************************準備好人體索引圖像的Reader并擷取尺寸****************************
int bodyHeight = 0, bodyWidth = 0;
IBodyIndexFrameSource * myBodyIndexSource = nullptr;
IBodyIndexFrameReader * myBodyIndexReader = nullptr;
{
mySensor->get_BodyIndexFrameSource(&myBodyIndexSource);
myBodyIndexSource->OpenReader(&myBodyIndexReader);
myDepthSource->get_FrameDescription(&myDescription);
myDescription->get_Height(&bodyHeight);
myDescription->get_Width(&bodyWidth);
myDescription->Release();
myBodyIndexSource->Release();
}
//************************為各種圖像準備buffer,并且開啟Mapper*****************************
UINT colorDataSize = colorHeight * colorWidth;
UINT depthDataSize = depthHeight * depthWidth;
UINT bodyDataSize = bodyHeight * bodyWidth;
Mat temp = imread("test.jpg"),background; //擷取背景圖
resize(temp,background,Size(colorWidth,colorHeight)); //調整至彩色圖像的大小
ICoordinateMapper * myMaper = nullptr; //開啟mapper
mySensor->get_CoordinateMapper(&myMaper);
Mat colorData(colorHeight, colorWidth, CV_8UC4); //準備buffer
UINT16 * depthData = new UINT16[depthDataSize];
BYTE * bodyData = new BYTE[bodyDataSize];
DepthSpacePoint * output = new DepthSpacePoint[colorDataSize];
//************************把各種圖像讀進buffer裡,然後進行處理*****************************
while (1)
{
IColorFrame * myColorFrame = nullptr;
while (myColorReader->AcquireLatestFrame(&myColorFrame) != S_OK); //讀取color圖
myColorFrame->CopyConvertedFrameDataToArray(colorDataSize * 4, colorData.data, ColorImageFormat_Bgra);
myColorFrame->Release();
IDepthFrame * myDepthframe = nullptr;
while (myDepthReader->AcquireLatestFrame(&myDepthframe) != S_OK); //讀取depth圖
myDepthframe->CopyFrameDataToArray(depthDataSize, depthData);
myDepthframe->Release();
IBodyIndexFrame * myBodyIndexFrame = nullptr; //讀取BodyIndex圖
while (myBodyIndexReader->AcquireLatestFrame(&myBodyIndexFrame) != S_OK);
myBodyIndexFrame->CopyFrameDataToArray(bodyDataSize, bodyData);
myBodyIndexFrame->Release();
Mat copy = background.clone(); //複制一份背景圖來做處理
if (myMaper->MapColorFrameToDepthSpace(depthDataSize, depthData, colorDataSize, output) == S_OK)
{
for (int i = 0; i < colorHeight; ++ i)
for (int j = 0; j < colorWidth;++ j)
{
DepthSpacePoint tPoint = output[i * colorWidth + j]; //取得彩色圖像上的一點,此點包含了它對應到深度圖上的坐标
if (tPoint.X >= 0 && tPoint.X < depthWidth && tPoint.Y >= 0 && tPoint.Y < depthHeight) //判斷是否合法
{
int index = (int)tPoint.Y * depthWidth + (int)tPoint.X; //取得彩色圖上那點對應在BodyIndex裡的值(注意要強轉)
if (bodyData[index] <= 5) //如果判斷出彩色圖上某點是人體,就用它來替換背景圖上對應的點
{
Vec4b color = colorData.at<Vec4b>(i, j);
copy.at<Vec3b>(i, j) = Vec3b(color[0], color[1], color[2]);
}
}
}
imshow("TEST",copy);
}
if (waitKey(30) == VK_ESCAPE)
break;
}
delete[] depthData; //記得各種釋放
delete[] bodyData;
delete[] output;
myMaper->Release();
myColorReader->Release();
myDepthReader->Release();
myBodyIndexReader->Release();
mySensor->Close();
mySensor->Release();
return 0;
}
詳細說明:
SDK中提供了一個叫ICoordinateMapper的類,功能就是坐标系之間的互相轉換,用來解決資料源的分辨率不同導緻點對應不起來的問題。我們需要的是将彩色圖像中的點與深度圖像中的點一一對應起來,是以使用其中的MapColorFrameToDepthSpace()這個函數。
首選,需要準備好三種資料源:Color、BodyIndex、Depth,其中前兩個是完成功能本來就需要的,第三個是轉換坐标系時需要,無法直接把Color的坐标系映射到BodyIndex中,隻能映射到Depth中。
然後是讀取背景圖,讀取之後也要轉換成Color圖的尺寸,這樣把Color中的點貼過去時坐标就不用再轉換,直接替換就行。接下來也要讀取三種Frame,為了易讀性,不如把準備工作在前面都做完,在這一步直接用Reader就行。
然後,利用MapColorFrameToDepthSpace(),将彩色幀映射到深度坐标系,它需要4個參數,第1個是深度幀的大小,第2個是深度資料,第3個是彩色幀的大小,第4個是一個DepthSpacePoint的數組,它用來儲存彩色空間中的每個點對應到深度空間的坐标。
要注意,這個函數隻是完成坐标系的轉換,也就是說它對于彩色坐标系中的每個點,都給出了一個此點對應到深度坐标系中的坐标,并不涉及到具體的ColorFrame。
最後,周遊彩色圖像,對于每一點,都取出它對應的深度坐标系的坐标,然後把這個坐标放入BodyIndex的資料中,判斷此點是否屬于人體,如果屬于,就把這點從彩色圖中取出,跟背景圖中同一坐标的點替換。
要注意的是,DepthSpacePoint中的X和Y的值都是float的,用它們來計算在BodyIndex裡的坐标時,需要強轉成int,不然畫面就會不幹淨,一些不屬于人體的地方也被标記成了人體被替換掉。
實驗效果圖:
從圖上比較容易看到人體周圍有許多毛刺,并不是很光滑,其實也正常,因為這是通過Kinect的BodyIndex資料源擷取的人體部分,也就是深度圖像,精确度肯定沒有達到那麼高,是以邊緣有很多毛刺很正常。應該可以利用一些圖成像處理技術進行去除或者改善,關于這個方面大家可以大膽去嘗試,看下能不能把這個效果做的更好一點,邊緣更平滑一點。
參考文章:http://www.cnblogs.com/xz816111/p/5185766.html
http://www.cnblogs.com/xz816111/p/5185010.html
到這裡為止,都還隻是顯示圖像,并沒有照相功能,也就是把圖檔儲存下來。這部分需要用到動作識别,通過指定動作來控制Kinect拍照,這些動作就比較随意了,可以是各種動作和pose,隻要能被Kinect識别出來就可以了。由于這部分程式太長了,這裡不便貼出來,文末提供完整程式下載下傳。效果圖如下
這裡提供一些關鍵的代碼,主要是動作識别部分的。代碼片段如下:
//儲存深度圖像
void CBodyBasics::SaveDepthImg()
{
//string str = (num2str)depthnumber;
stringstream stream;
string str;
stream << depthnumber; //從long型資料輸入
stream >> str; //轉換為 string
imwrite(str + "depthnumber.bmp", depthImg);
cout << str + "depthnumber.bmp" << endl;
}
//照相
void CBodyBasics::TakePhoto()
{
//定義人體一些骨骼點,方面表示
Joint righthand = joints[JointType_HandRight];
Joint lefthand = joints[JointType_HandLeft];
Joint spinemid = joints[JointType_SpineMid];
Joint head = joints[JointType_Head];
stringstream stream;
string str;
if (spinemid.Position.Z < 0.5) //判斷人體重心離Kinect 的距離,小于0.5則直接傳回,這使得資料已經不準确了,避免誤操作
return;
//判斷原則:右手的中心離身體重心在Z軸上的距離大于給定門檻值(Z_THRESHOUD)且現在沒在拍照,
//避免一直觸發拍照,也可以設定等待時間,這樣可以實作連拍
if (spinemid.Position.Z - righthand.Position.Z >= Z_THRESHOUD&&bTakePhoto)
{
bTakePhoto = false;
photocount++;
stream << photocount; //從long型資料輸入
stream >> str; //轉換為 string
string filepath = "D:/pic/"; //儲存到指定檔案夾裡面
imwrite(filepath+str + ".jpg", copy);
cout << "成功照第" << photocount << "張相" << endl;
}
if (spinemid.Position.Z - righthand.Position.Z < Z_THRESHOUD) //沒有檢測到指定動作,則表示沒有在拍照
{
bTakePhoto = TRUE;
return;
}
}
//切換背景
bool CBodyBasics::ChangeBackground()
{
//定義人體一些骨骼點,方面表示
Joint righthand = joints[JointType_HandRight];
Joint head = joints[JointType_Head];
Joint spinebase = joints[JointType_SpineBase];
if (spinebase.Position.Z<0.5) //判斷人體重心離Kinect 的距離,小于0.5則直接傳回,這使得資料已經不準确了,避免誤操作
return false;
//判斷原則:右手的中心離身體重心在X軸上的距離大于給定門檻值(X_THRESHOUD)且現在沒在切換背景時,
//避免一直觸發切換,也可以另一種方式,設定一個等待時間,這樣可以實作快速切換多張背景。
if (righthand.Position.X - head.Position.X >= X_THRESHOUD&&bChange)
{
bChange = FALSE;
if (fscanf(fp, "%s ", imagepath) > 0) //讀取背景圖檔的本地路徑
backjpg = imread(imagepath); //讀取背景圖檔
else
{
rewind(fp); //檔案指針複位,即重新指向最開始位置
fscanf(fp, "%s ", imagepath); //讀取背景圖檔的本地路徑
backjpg = imread(imagepath); //讀取背景圖檔
}
return true;
}
if (righthand.Position.X - head.Position.X < X_THRESHOUD) //沒有檢測到指定作,則表明沒有在切換背景,
{
bChange = TRUE;
return false;
}
}
由于本段代碼已經做了非常詳細的注釋,這裡不再對代碼内容詳細分析,有什麼問題歡迎留言讨論交流。
本篇文章詳細講解了利用kinect對人體進行摳圖,人體和背景照片的合成,以及結合動作識别進行拍照的技術。完整代碼下載下傳位址:
https://download.csdn.net/download/baolinq/10406054
補充一些其他學習資料,在本系列第二章有講到。
Kinect v2 for OpenNI 2:
https://github.com/mvm9289/openni2_kinect2_driver
https://github.com/occipital/OpenNI2/tree/kinect2
libfreenect 2
https://github.com/OpenKinect/libfreenect2
理論上,這個版本的驅動程式除了支援Windows以外,還可以支援Mac 和Ubtuntu等系統。如果想在非 Windows 環境下使用 Kinect v2 體感器,可以考慮此方案。
好了,本篇本章到此就結束了,歡迎底下留言和讨論。下一篇見~~~
超跑開起來欣賞養眼