Kinect V2开发(4)抠图
实际上,除了深度图像、彩色图像、红外线图像这三种算是原始图像数据之外,Kinect还提供一种经过分析的图像数据——BodyIndex,直译过来是人物索引的意思。
简单来说,就是在图像中对人物相关的数据进行了标记,把人体和背景区分开来了,我们可以用这个数据还做简单的位置检测,或者抠图。
1.人体轮廓表示
BodyIndex数据的获取方法也和其他数据一样,IBodyIndexFrameSource
→ IBodyIndexFrameReader
→ IBodyIndexFrame
这样三层,但是它读取到的画面中每一个像素是一位(BYTE),代表的是这个点的人的编号。因为Kinect V2最多只能追踪6个人,所以实际上这个编号只有0-5号是有用的,超过这个范围的话,代表这个点不是人体。
效果图如下:
代码如下:
using namespace std;
using namespace cv;
int main(void)
{
// 1a.获取感应器
IKinectSensor* pSensor = nullptr;
GetDefaultKinectSensor(&pSensor);
// 1b. 打开感应器
pSensor->Open();
// 2a. 取得BodyIndex数据
IBodyIndexFrameSource* pFrameSource = nullptr;
pSensor->get_BodyIndexFrameSource(&pFrameSource);
// 2b. 取得BodyIndex数据的描述信息(宽、高)
int iWidth = 0;
int iHeight = 0;
IFrameDescription* pFrameDescription = nullptr;
pFrameSource->get_FrameDescription(&pFrameDescription);
pFrameDescription->get_Width(&iWidth);
pFrameDescription->get_Height(&iHeight);
pFrameDescription->Release();
pFrameDescription = nullptr;
// 3a. 打开BodyIndex数据阅读器
IBodyIndexFrameReader* pFrameReader = nullptr;
pFrameSource->OpenReader(&pFrameReader);
IBodyIndexFrame* pFrame = nullptr;
// 建立图像矩阵,mBodyIndexImg用来存储图像数据
Mat mBodyIndexImg(iHeight, iWidth, CV_8UC3);
namedWindow("BodyIndexImage");
// color array
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)
{
// 4a. 取得最新数据
if (pFrameReader->AcquireLatestFrame(&pFrame) == S_OK)
{
// 4c. 把数据存入图像矩阵中
UINT uSize = 0;
BYTE* pBuffer = nullptr;
pFrame->AccessUnderlyingBuffer(&uSize, &pBuffer);
for (int y = 0; y < iHeight; ++y)
{
for (int x = 0; x < iWidth; ++x)
{
int uBodyIdx = pBuffer[x + y * iWidth];//0-5代表人体,其他值代表背景
if (uBodyIdx < 6)
mBodyIndexImg.at<Vec3b>(y, x) = color[uBodyIdx];
else
mBodyIndexImg.at<Vec3b>(y, x) = color[6];
}
}
imshow("Body Index Image", mBodyIndexImg);
// 4e. 释放变量pFrame
pFrame->Release();
}
if (waitKey(30) == VK_ESCAPE) {
break;
}
}
// 3b. 释放变量pFrameReader
pFrameReader->Release();
pFrameReader = nullptr;
// 2c.释放变量pFramesSource
pFrameSource->Release();
pFrameSource = nullptr;
// 1c.关闭感应器
pSensor->Close();
// 1d.释放感应器
pSensor->Release();
pSensor = nullptr;
return 0;
}
color array建立的是颜色表,储存的是7种颜色,包括对应0-5号人体编号的颜色以及背景颜色。
另外,在4c那一步,和以前的步骤不太一样,是先通过 AccessUnderlyingBuffer()
读取原始的BodyIndex数据,然后再通过两层循环扫过画面里面每一个像素点,把代表人体的点筛选出来,然后从color array里面取出对应的颜色,填到OpenCV显示的图像里面。
2.抠图
读取BodyIndex数据可以将环境与人体区分开来,利用这个特点,我们可以将前景中的人合成到静态图片当中,和PS中的抠图类似,但是抠出来的人像是动态的。
BodyIndex的数据用的是深度数据,只能用来判断是否是人体,而Color中的图像只能显示不能做其他处理,用的是彩色凸显数据,所以在这个地方需要让彩色图像数据和深度数据进行配合。
彩色图像帧的分辨率是1920*1080,深度帧的分辨率是512*424,我们需要利用ICoordinateMapper
这个类,将彩色图像中的点转换到深度图的坐标系中。
事实上,Kinect V2里面有三种不同的坐标系统,包括:
1. 彩色空间坐标系统(Color Space) ——ColorSpacePoint(x,y)
2. 深度空间坐标系统(Depth Space) ——DepthSpacePoint(x,y)
3. 摄像头空间坐标系统(Camera Space)——CameraSpacePoint(x,y,z)
其中,彩色图像就是彩色空间坐标系统,红外线图像、深度图像以及BodyIndex图像都使用深度空间坐标系统,这两个坐标系统都是2维的,就是以左上角为原点,往右是+X,往下是+Y,单位是像素。摄像头坐标系则是以感应器为原点的3维空间坐标系统,单位是米(m),做人体骨架追踪需要用到这个坐标系统。
针对这三个不同的坐标系统,微软提供了多个不同的函数,可以用来做单点、多点或是整个画面的转换。
在这里我们用的是将彩色空间坐标系转换到深度空间坐标系的函数
第一个参数depthDataPointCount
代表的是深度图像数据的个数,基本上就是512*424,第二个参数depthFrameData
代表的是深度图像数据,格式是 UINT16*,是使用 CopyFrameDataToArray()
复制出来的阵列。
而depthPointCount
则是彩色图像的像素点数,是 1920 * 1080,depthSpacePoints
就是用来存贮转换结果的阵列,类型是DepthSpacePoint
这样转换完成之后, depthSpacePoints
就会是 1920 * 1080 个DepthSpacePoint
,而每个DepthSpacePoint
记录的就是该点对应到深度空间坐标系的位置。
转换过程实际上就是:
1. 获取彩色图像、深度图像以及人物轮廓
2. 把彩色图像转换到深度图像坐标系中
3. 判断某一点是不是人体
4. 如果是,就取出来替换掉背景
效果图:
#include <iostream>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <Kinect.h>
using namespace std;
using namespace cv;
int main(void)
{
// 1a.获取感应器
IKinectSensor* pSensor = nullptr;
GetDefaultKinectSensor(&pSensor);
// 1b. 打开感应器
pSensor->Open();
/****************2.打开深度图像阅读器************************/
// 取得深度数据
IDepthFrameSource* pDepthSource = nullptr;
pSensor->get_DepthFrameSource(&pDepthSource);
// 取得深度数据的描述信息(宽、高)
int iDepthWidth = 0;
int iDepthHeight = 0;
IFrameDescription* pDepthDescription = nullptr;
pDepthSource->get_FrameDescription(&pDepthDescription);
pDepthDescription->get_Width(&iDepthWidth);
pDepthDescription->get_Height(&iDepthHeight);
// 打开深度数据阅读器
IDepthFrameReader* pDepthReader = nullptr;
pDepthSource->OpenReader(&pDepthReader);
pDepthDescription->Release();
pDepthDescription = nullptr;
// 释放变量pDepthSource
pDepthSource->Release();
pDepthSource = nullptr;
/******************3.打开彩色图像阅读器**********************/
// 取得彩色数据
IColorFrameSource* pColorSource = nullptr;
pSensor->get_ColorFrameSource(&pColorSource);
// 取得彩色数据的描述信息(宽、高)
int iColorWidth = 0;
int iColorHeight = 0;
IFrameDescription* pColorDescription = nullptr;
pColorSource->get_FrameDescription(&pColorDescription);
pColorDescription->get_Width(&iColorWidth);
pColorDescription->get_Height(&iColorHeight);
// 打开彩色数据阅读器
IColorFrameReader* pColorReader = nullptr;
pColorSource->OpenReader(&pColorReader);
pColorDescription->Release();
pColorDescription = nullptr;
// 释放变量pColorSource
pColorSource->Release();
pColorSource = nullptr;
/*******************4.打开人体索引的阅读器*******************/
// 取得BodyIndex数据
IBodyIndexFrameSource* pBodyIndexSource = nullptr;
pSensor->get_BodyIndexFrameSource(&pBodyIndexSource);
// 取得深度数据的描述信息(宽、高)
int iBodyIndexWidth = 0;
int iBodyIndexHeight = 0;
IFrameDescription* pBodyIndexDescription = nullptr;
pBodyIndexSource->get_FrameDescription(&pBodyIndexDescription);
pBodyIndexDescription->get_Width(&iBodyIndexWidth);
pBodyIndexDescription->get_Height(&iBodyIndexHeight);
// 打开深度数据阅读器
IBodyIndexFrameReader* pBodyIndexReader = nullptr;
pBodyIndexSource->OpenReader(&pBodyIndexReader);
pBodyIndexDescription->Release();
pBodyIndexDescription = nullptr;
// 释放变量pBodyIndexSource
pBodyIndexSource->Release();
pBodyIndexSource = nullptr;
/*************5.为各种图像建立buffer,准备坐标转换*************/
UINT iColorDataSize = iColorHeight * iColorWidth;
UINT iDepthDataSize = iDepthHeight * iDepthWidth;
UINT iBodyIndexDataSize = iBodyIndexHeight * iBodyIndexWidth;
//获取背景图并调整至彩色图像的大小
Mat temp = imread("test.jpg"), background;
resize(temp, background, Size(iColorWidth, iColorHeight));
//开启mapper
ICoordinateMapper * myMaper = nullptr;
pSensor->get_CoordinateMapper(&myMaper);
//准备buffer
Mat ColorData(iColorHeight, iColorWidth, CV_8UC4);
UINT16 * DepthData = new UINT16[iDepthDataSize];
BYTE * BodyData = new BYTE[iBodyIndexDataSize];
DepthSpacePoint * output = new DepthSpacePoint[iColorDataSize];
/***********6.把各种图像读进buffer里,然后进行坐标转换以及替换**************/
while (1)
{
//读取color图
IColorFrame * pColorFrame = nullptr;
while (pColorReader->AcquireLatestFrame(&pColorFrame) != S_OK);
pColorFrame->CopyConvertedFrameDataToArray(iColorDataSize * 4, ColorData.data, ColorImageFormat_Bgra);
pColorFrame->Release();
//读取depth图
IDepthFrame * pDepthframe = nullptr;
while (pDepthReader->AcquireLatestFrame(&pDepthframe) != S_OK);
pDepthframe->CopyFrameDataToArray(iDepthDataSize, DepthData);
pDepthframe->Release();
//读取BodyIndex图
IBodyIndexFrame * pBodyIndexFrame = nullptr;
while (pBodyIndexReader->AcquireLatestFrame(&pBodyIndexFrame) != S_OK);
pBodyIndexFrame->CopyFrameDataToArray(iBodyIndexDataSize, BodyData);
pBodyIndexFrame->Release();
//复制一份背景图来做处理
Mat copy = background.clone();
if (myMaper->MapColorFrameToDepthSpace(iDepthDataSize, DepthData, iColorDataSize, output) == S_OK)
{
for (int i = 0; i < iColorHeight; ++i)
for (int j = 0; j < iColorWidth; ++j)
{
//取得彩色图像上包含对应到深度图上的坐标的一点
DepthSpacePoint tPoint = output[i * iColorWidth + j];
//判断这个点是否合法
if (tPoint.X >= 0 && tPoint.X < iDepthWidth && tPoint.Y >= 0 && tPoint.Y < iDepthHeight)
{
//取得彩色图上那点对应在BodyIndex里的值
int index = (int)tPoint.Y * iDepthWidth + (int)tPoint.X;
//判断出彩色图上某点是人体,就用替换背景图上对应点
if (BodyData[index] <= 5)
{
Vec4b color = ColorData.at<Vec4b>(i, j);
copy.at<Vec3b>(i, j) = Vec3b(color[0], color[1], color[2]);
}
}
}
imshow("Background Remove", copy);
}
if (waitKey(30) == VK_ESCAPE) {
break;
}
}
delete[] DepthData;
delete[] BodyData;
delete[] output;
// 1c.关闭感应器
pSensor->Close();
// 1d.释放感应器
pSensor->Release();
pSensor = nullptr;
return 0;
}
参考文档
官方文档 Kinect for Windows C++ Reference
Hersey博客 K4W v2 C++ Part 5:簡單的去背程式