Zhang X, Yan J, Feng S, et al. Water Filling: Unsupervised People Counting via Vertical Kinect Sensor[C]// IEEE Ninth International Conference on Advanced Video and Signal-Based Surveillance. IEEE, 2012:215-220.
将 Kinect 放在天花板上,朝向地面,由 Kinect 传感器生成RGB和深度图,深度信息是用来达到外观不变的目的。由于人头部到 Kinect 的距离总是比身体其他部位离得更近,人数计算的问题也就等价于寻找本地数据中 局部深度值最小的区域。根据深度图的特点,water-filling算法能鲁棒地标度不变地找到这些局部区域。
整体思路:
int createImgPath(char *path, int imgType, int seqNo);
//read a frame
int getAnRGBImgFrame(IplImage *img, int seqNo);
//读取文本型深度图信息
int txtDepthDataInput(int **f_depth_map, char path[]);
//黑区补偿处理
int txtDepthDataInputWithMask(int **f_depth_map, char path[]);
//将water filling 后的检测的结果转为图像
int saveBinaryDepthImage(int **depth_map, char path[]);
//计算联通区域的中心
int calcLabelCenter(int** labeled_depth_map, vector &manSet);
//绘制人头标记框图
int drawBoundingBoxes( IplImage *depth_image,char path[], const vector &manSet);
//绘制人头标记框图,并将图片存入本地path 同时在图片上,绘制进、出人数信息
int drawBoxesWithIO( IplImage *depth_image,char path[], const vector &manSet, double *people );
//输出txt图像文件
int saveTxtImage(int **txtImage, char path[]);
//两遍扫方法连通区域求解
int twoPassTxt(int **water_filled) ;
//基于上一帧,计算当前帧的人头数
int calcDirByNearestNeighbor(vector &curSet, int curNum, vector &lastSet, int lastNum);
//模拟降雨填充效果,得到相应的g_function
int waterFilling(int **f_depth_map,int **g_depth_map,int **result_depth_map);
… … … … … …
具体实现:
Kinect本身的红外发射器功率较小,场景中的光照会产生影响,得到的深度数据不稳定的。要想获得更准确的结果,需要对深度图进行去噪处理。
去噪的方法有很多,中值滤波、高斯滤波、双边滤波等,
试了一下均值滤波(八邻域)和双边滤波(delta_r=0.5,delta_s=0,5)
知道滤波方法后,代码很很容易实现。八邻域滤波的最终实验效果会好很多,也更能满足实时性。
预处理时,要从Mat图中获取txt深度信息,转换的同时将非法距离数值过滤,然后用卷积模板进行补偿。
预处理完后就可以模拟降雨过程了
int waterFilling(int **f_depth_map, int **g_depth_map, int **result_depth_map)
主要代码:
/*************************XuMengqian*********************************/
for (int j=0;j<8;j++)
{
tmp_y = y+direction[j].y;
tmp_x = x+direction[j].x;
d[j]=f_depth_map[tmp_y][tmp_x]+g_depth_map[tmp_y][tmp_x]
-f_depth_map[y][x]-g_depth_map[y][x];//(tmp_x,tmp_y)是(x,y)的邻域
if (d[j]if ( (min_d+Rain_droppedonetime)<=0 )
{
x=min_d_x;
y=min_d_y;
}
else
{
g_depth_map[y][x]+=Rain_droppedonetime;
sum_rain+=Rain_droppedonetime;
w=w-Rain_droppedonetime;
}
降雨之后要找连通区域,对区域进行计数才是总人数。
连通区域标记大多数都用的是两遍扫描法,具体原理和流程网上能搜到很多解释。
其他:
openNI是Kinect的官方API,用来采集视频数据。实验中用到的一些基本操作:
/*************************XuMengqian*********************************/
//初始化OpenNI2运行环境,看是否有错误
result = OpenNI::initialize();
if(checkOpenNIError( result, "initialize content" )!=0) return -1; //OpenNI运行环境有误
Device device;
result = device.open( openni::ANY_DEVICE ); //打开设备
if(checkOpenNIError( result, "initialize content" )!=0) return -2; //不存在可用的Kinect设备
//深度视频流,创建与设置
VideoStream oniDepthStream; //创建深度图像流
result = oniDepthStream.create( device, openni::SENSOR_DEPTH );
VideoMode modeDepth; //设置深度视频模式
modeDepth.setResolution( WIDTH, HEIGHT );
modeDepth.setFps(fps );
modeDepth.setPixelFormat( PIXEL_FORMAT_DEPTH_1_MM );
oniDepthStream.setVideoMode(modeDepth);
result = oniDepthStream.start(); //开启深度数据流
//深度视频流,创建与设置
VideoStream oniColorStream; //创建彩色视频流
result = oniColorStream.create( device, openni::SENSOR_COLOR );
VideoMode modeColor; //设置彩色视频流模式
modeColor.setResolution( WIDTH*2, HEIGHT*2 );
modeColor.setFps( fps);
modeColor.setPixelFormat( PIXEL_FORMAT_RGB888 );
oniColorStream.setVideoMode( modeColor);
result = oniColorStream.start(); //开启彩色视频流
//读取深度视频帧
isDepthOk = false;
if( oniDepthStream.readFrame( &oniDepthImg ) == STATUS_OK )
{
isDepthOk = true;
//创建16位1通道,深度值图像
cv::Mat cvRawImg16U( oniDepthImg.getHeight(), oniDepthImg.getWidth(), CV_16UC1, (void*)oniDepthImg.getData() );
//提取深度值,并存入二维数组中
cvtMatToTxtDepth(cvRawImg16U, f_depth_map);
//cvtMatToTxtDepthWithMask(cvRawImg16U, f_depth_map);
//将深度距离转化为灰度图
cvRawImg16U.convertTo( cvDepthImg, CV_8U, 255.0/(oniDepthStream.getMaxPixelValue()));
}
//读取彩色视频帧
isRGBOk = false;
if( oniColorStream.readFrame( &oniColorImg ) == STATUS_OK )
{
isRGBOk = true;
//将数据转换至OpenCV类型,8位3通道
cv::Mat cvRGBImg( oniColorImg.getHeight(), oniColorImg.getWidth(), CV_8UC3, (void*)oniColorImg.getData() );
cv::cvtColor( cvRGBImg, cvBGRImg, CV_RGB2BGR );
}
跟踪直接帧差,选距离小于阈值的认为是同一人。