天天看點

第四章 利用Kinect摳圖和自動拍照程式第四章  利用Kinect摳圖和自動拍照程式詳細說明:

第四章  利用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摳圖和自動拍照程式詳細說明:

二、結合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摳圖和自動拍照程式第四章  利用Kinect摳圖和自動拍照程式詳細說明:

從圖上比較容易看到人體周圍有許多毛刺,并不是很光滑,其實也正常,因為這是通過Kinect的BodyIndex資料源擷取的人體部分,也就是深度圖像,精确度肯定沒有達到那麼高,是以邊緣有很多毛刺很正常。應該可以利用一些圖成像處理技術進行去除或者改善,關于這個方面大家可以大膽去嘗試,看下能不能把這個效果做的更好一點,邊緣更平滑一點。

參考文章:http://www.cnblogs.com/xz816111/p/5185766.html

http://www.cnblogs.com/xz816111/p/5185010.html

到這裡為止,都還隻是顯示圖像,并沒有照相功能,也就是把圖檔儲存下來。這部分需要用到動作識别,通過指定動作來控制Kinect拍照,這些動作就比較随意了,可以是各種動作和pose,隻要能被Kinect識别出來就可以了。由于這部分程式太長了,這裡不便貼出來,文末提供完整程式下載下傳。效果圖如下

第四章 利用Kinect摳圖和自動拍照程式第四章  利用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 體感器,可以考慮此方案。

好了,本篇本章到此就結束了,歡迎底下留言和讨論。下一篇見~~~

超跑開起來欣賞養眼

第四章 利用Kinect摳圖和自動拍照程式第四章  利用Kinect摳圖和自動拍照程式詳細說明:

繼續閱讀