Pull to refresh

Фильтруем людей или как заблюрить одного человека на видео

Reading time5 min
Views2.7K
Добрый день. Хочу предложить вам небольшую статью о своей работе с кинектом.

Сейчас я делаю небольшую часть рекламного проекта, где используется кинект. Одной из задач является «наложение фильтра» на одного человека в толпе. Об этом и поговорим.

В работе использую OpenNI, OpenCV и Visual Studio 2010.

Начало


Раньше я никогда плотно не работал с изображениями, поэтому не представлял с какой стороны браться за дело. После непродолжительных размышлений сложилась такая схема:
1 — получаю картинку с обычной камеры;
2 — получаю userPixels (пиксели, которые принадлежат пользователю);
3 — делаю копию картинки и применяю фильтр;
4 — те пиксели, которые помечены как «пользовательские» перерисовываю из картинки с фильтром в исходное изображение.

Примерный план понятен, поехали!

Объявим нужные переменные.
xn::Context		context; 
xn::ImageGenerator	imageGenerator; // Этот генератор даст нам картинку с обычной камеры
xn::ImageMetaData	imageMD; 
xn::DepthGenerator	depthGenerator; // глубина
xn::DepthMetaData	depthMD;
xn::UserGenerator   userGenerator; // если есть определенные пользователи, мы об этом узнаем
xn::SceneMetaData 	userPixels;
XnUserID		users[15]; 
XnUInt16		nUsers = 15; 
const XnLabel		*pLabels; // метки, если !=0 значит это пользователь
const XnRGB24Pixel	*pImageRow; // наша картинка
XnStatus			rc;
unsigned int		pTexMapX;// = 0;
unsigned int		pTexMapY;// = 0;
XnMapOutputMode     outputMode; // формат вывода для камер
XnPixelFormat		pixelFormat;
		
bool			mirrored; // Если хочется зеркального отражения, то его нужно включить
bool 			blurOn;
int 			currentUserId;

struct MyPixel
{
	int	posX;
	int	posY;
	unsigned char *vBlue;
	unsigned char *vGreen;
	unsigned char *vRed;
	int		uLabel; 
	bool		border; 

};
MyPixel 		pixels[640][480];
// Почти все объявлено, остались только IplImages
IplImage 		*frame;
IplImage		*frameBlured;

Собственно теперь все готово, чтобы начать. Создаем генераторы:

int main(){
	outputMode.nFPS = 10;
	outputMode.nXRes = 640;
	outputMode.nYRes = 480;
	XnStatus rc;
	pTexMap = NULL;
	pTexMapX = 0;
	pTexMapY = 0;
	rc = context.Init();
	checkStatus(rc, "  create context"); // 	метод проверяет rc == XN_STATUS_OK	и если нет, завершает программу с ошибкой
		
	rc = depthGenerator.Create(context);
	checkStatus(rc, " depth create");
	rc = imageGenerator.Create(context);
	checkStatus(rc, " image create");
	rc = userGenerator.Create(context);
	checkStatus(rc," user create");
	return 0;
}
	


Дальше нужно сделать важную вещь. Камеры расположены рядом друг с другом, но не на одном месте, а значит картинка будет разной. Для приведения к одному виду есть специальный метод SetViewPoint. Использоваеть его можно будет после того, как дадим команду StartGeneratingAll(). И важно чтобы OutputMode для обеих камер был одинаковый, иначе будет ошибка.

int main (){
	.....
	imageGenerator.SetMapOutputMode(outputMode);
	depthGenerator.SetMapOutputMode(outputMode);
	imageGenerator.SetPixelFormat(XN_PIXEL_FORMAT_RGB24); // говорим что нужна RGB 									картинка
	context.StartGeneratingAll(); 
	rc = depthGenerator.GetAlternativeViewPointCap().SetViewPoint(imageGenerator);
	checkStatus(rc, " user and image view");
	
	// зарегистрируем коллбеки для отслеживания пользователей
	XnCallbackHandle h1;
	userGenerator.RegisterUserCallbacks(gotUser,lostUser,NULL, h1);
	
	// установим текущего пользователя и флаги. 
	currentUserId = -1; // этот id нам нужен чтобы наложить фильтр только на 						одного пользователя
	mirrored = false;
	blurOn = false;
	frame = cvCreateImage(cvSize(640,480),8,3);
	frameBlured = cvCreateImage(cvSize(640,480),8,3);
	
	//создадим окно
	cvNamedWindow ("Filter demo", CV_WINDOW_AUTOSIZE);
	// и запускаем основной цикл программы.
	showVideo();  
	return 0;
}

Обработка кадра


Прежде чем приступить к написанию showVideo() с основным циклом, нам нужно преобразовать изображение с камеры, для этого напишем функцию рисующую IplImage из XnRGB24Pixel и в ней же определим какие пиксели принадлежат пользователю.

void fromXnRGBToIplImage(const XnRGB24Pixel* pImageMap, IplImage** iplRGBImage) 
{ 
	userGenerator.GetUsers(aUsers,nUsers);
	userGenerator.GetUserPixels(aUsers[0],userPixels);
	pLabels = userPixels.Data(); 
	
for(int l_y=0;l_y<XN_VGA_Y_RES;++l_y) //XN_VGA_Y_RES = 480
{ 
	for(int l_x=0;l_x<XN_VGA_X_RES;++l_x, ++pLabels) //XN_VGA_X_RES= 640
	{
		pixels[l_x][l_y].uLabel = 0;
		if(pixels[l_x][l_y].border != true)
			pixels[l_x][l_y].border = false;
			if(*pLabels !=0) // а вот и пользователь
			{
				currentUserId = (currentUserId == -1)?(*pLabels):currentUserId; // если нет активного юзера, то назначаем его
				pixels[l_x][l_y].uLabel = *pLabels;
// проверяем пограничный ли это пиксель (пригодится если вы захотите улучшить выделенную фигуру)
			if((l_x >0) && pixels[l_x-1][l_y].uLabel == 0 
				|| (l_x < XN_VGA_X_RES-1) && pixels[l_x+1][l_y].uLabel == 0
				|| (l_y >0 ) && pixels[l_x][l_y-1].uLabel == 0
				|| (l_y < XN_VGA_Y_RES-1) && pixels[l_x][l_y+1].uLabel == 0 )
				        {
						pixels[l_x][l_y].border = true;
					}				
				}
				
			// Этот вариант перевода в IplImage был честно подсмотрен где-то 					в OpenNI Group			
			((unsigned char*)(*iplRGBImage)->imageData)[(l_y*XN_VGA_X_RES +l_x)*3+0] = pImageMap[l_y*XN_VGA_X_RES+l_x].nBlue; 
			((unsigned char*)(*iplRGBImage)->imageData)[(l_y*XN_VGA_X_RES +l_x)*3+1] = pImageMap[l_y*XN_VGA_X_RES+l_x].nGreen; 
			((unsigned char*)(*iplRGBImage)->imageData)[(l_y*XN_VGA_X_RES +l_x)*3+2] = pImageMap[l_y*XN_VGA_X_RES+l_x].nRed; 
			
		}
}	

	// Если включен режим блюра, необходимо перерисовать помеченные пиксели
	if(blurOn){
		cvSmooth(*iplRGBImage,frameBlured,CV_BLUR,14,14,0,0);
	
		
		for(int j = 0 ; j < 480; ++j)
		{
			for(int i = 0 ; i < 640; ++i)
			{
				if( pixels[i][j].border == true && pixels[i][j].uLabel == currentUserId  || pixels[i][j].uLabel == currentUserId ){


					((unsigned char*)(*iplRGBImage)->imageData)[(j*XN_VGA_X_RES +i)*3+0] = frameBlured->imageData[(j*XN_VGA_X_RES +i)*3+0];
					((unsigned char*)(*iplRGBImage)->imageData)[(j*XN_VGA_X_RES +i)*3+1] = frameBlured->imageData[(j*XN_VGA_X_RES +i)*3+1]; 
					((unsigned char*)(*iplRGBImage)->imageData)[(j*XN_VGA_X_RES +i)*3+2] = frameBlured->imageData[(j*XN_VGA_X_RES +i)*3+2]; 
				}
			
				pixels[i][j].border = false;
				pixels[i][j].uLabel = 0;
			}
		}
	}
		
}

Осталось написать коллбэки и метод showVideo:
void XN_CALLBACK_TYPE gotUser(xn::UserGenerator& generator, XnUserID nId, void* pCookie)
{	
    // делаем что-то полезное
}
void XN_CALLBACK_TYPE lostUser(xn::UserGenerator& generator, XnUserID nId, void* pCookie)
{
 	if((int)nId == currentUserId)
	{	
		currentUserId = -1; // 
	} 
}

void showVideo()
{
	while(1)
	{		
		rc = context.WaitOneUpdateAll(imageGenerator);
		imageGenerator.GetMetaData(imageMD);
		pImageRow = imageGenerator.GetRGB24ImageMap();
		char c = cvWaitKey(33);
		if(c == 27) // esc завершает цикл 
			break;
		if(c == 109)
		{
			mirrored = (mirrored == true)?false:true;
		}
		if(c == 98) // b 
		{
			blurOn = (blurOn == true)?false:true;
		}
		fromXnRGBToIplImage(pImageRow,&frame);
		// Вообще, в Context есть метод SetGlobalMirror(bool), но у меня при его использовании появляются артефакты и я заменил на cvFlip
		if(mirrored) 
			 cvFlip(frame, NULL, 1);
		
		cvShowImage("Filter demo", frame);
			
	}
cvReleaseImage( &frame );
cvDestroyWindow("Filter demo" );
	
}

Итог


В результате у вас должно получиться что-то вроде этого:

image

На скриншоте ниже показан результат немножко подогнанный под мои цели, с увеличенными границами:

image

Спасибо за внимание, успехов!
Tags:
Hubs:
Total votes 39: ↑36 and ↓3+33
Comments13

Articles