Segmentación de instancias con OpenCV DNN

La segmentación de instancias es el proceso mediante el cual buscamos detectar un objeto en una escena y generar una máscara que nos permita extraer con mayor presición el objeto detectado, puede verse como el conjunto de dos procesos primero detectar el área rectángular que contiene el objeto y luego obtener la máscara que segmenta dicho objeto.

segmentación de instancias

MASK R-CNN

Esta arquitectura de aprendizaje profundo desarrollada por invetigadores de (FAIR) Facebook AI Research es una extensión a la arquitectura de detección de objetos Faster R-CNN, Mask R-CNN agrega una capa extra que permite generar una máscara para el objeto detectado.

mask r-cnn

Para nuestra ejmplo usaremos un modelo pre-entrenado usando Tensorflow con el conjunto de datos COCO, al igual que lo hicimos en el tutorial detección de objetos con DNN cargamos los archivos .pb y .pbtxt que corresponden al modelo y la descripción de la arquitectura del mismo.

void main() {

	// cargar el modelo pre-entrenado, debemos indicar la ruta de los archivos .pb y .pbtxt 
	dnn::Net net = dnn::readNet("data/files/mask_rcnn_inception_v2_coco_2018_01_28.pb",
								"data/files/mask_rcnn_inception_v2_coco_2018_01_28.pbtxt");
								
	// cargar la imagen de prueba
	Mat image = imread("data/files/image.jpg");
	Mat blob;

	// crear el blob 4D a partir de la imagen de entrada
	dnn::blobFromImage(image, blob, 1.0, Size(image.cols, image.rows), Scalar(), true, false);

	vector<String> outputNames = { "detection_out_final" , "detection_masks" };
	vector<Mat> outs;

	// establecer como entrada el blob creado anteriormente 
	net.setInput(blob);
	// correr la red neuronal para obtener las salidas para los nombres de la capas indicadas por outputNames
	net.forward(outs, outputNames);

	// leer el archivo que contiene los nombres de las classes
	load_classes_names();
	// procesar los resultados devueltos por la red y mostrarlos sobre la imagen de entrada
	post_process(image, outs);

	// visualizar los resultados
	imshow("Mask-RCNN", image);
	waitKey();
}

Detalles importantes de este código, creamos la red neuronal con readNet(...) luego usamos blobFromImage(...) para convertir la imagen cargada con OpenCV en una entrada válida para la red, usando net.forward(...) corremos la red y obtenemos los resultados, estos serán procesados por la función post_process(...) que describiremos más adelante.

Los resultados obtenidos los procesamos con la función que pasamos a describir, esta es la primera parte del código, es la encargada de obtener el rectángulo que contiene al objeto detectado, puede ser mas de uno y para cada uno de ellos guardamos su ID de clase, puntaje y objeto cv::Rect.

void post_process(cv::Mat& frame, const std::vector<Mat>& outs)
{
	Mat boxes = outs[0];  // guarda las regiones rectangulares encontradas
	Mat masks = outs[1];  // guarda las mascaras correspondientes a las regiones 

	const int numDetections = boxes.size[2]; // cantidad de regiones 
	const int numClasses = masks.size[1];    // cantidad de mascaras

	const int frameW = frame.cols;
	const int frameH = frame.rows;

	std::vector<int>   classIds;    // ID de clase a la que pertenece el objeto
	std::vector<float> confidences; // puntaje 
	std::vector<Rect>  predBoxes;   // rectangulo

	for (int i = 0; i < numDetections; ++i) {

		// obtener datos de cada una de las regiones encontradas por la red, se organizan de la siguiente manera:
		// [batchId, classId, confidence, left, top, right, bottom] - 1x1xNx7
		float* box = (float*)boxes.ptr<float>(0, 0, i);
		float score = box[2];

		// nos quedamos con aquellas que superen el umbral establecido
		if (score > confThreshold) {

			int classId   = static_cast<int>(box[1]);
			int boxLeft   = static_cast<int>(frameW * box[3]);
			int boxTop    = static_cast<int>(frameH * box[4]);
			int boxRight  = static_cast<int>(frameW * box[5]);
			int boxBottom = static_cast<int>(frameH * box[6]);

			// convertimos los datos a uno de tipo cv::Rect 
			cv::Rect rect{ cv::Point{ boxLeft, boxTop }, cv::Point{ boxRight, boxBottom } };
			rect &= cv::Rect({ 0,0 }, frame.size());

			// guardamos los datos que nos interesen para su uso posterior
			classIds.emplace_back(classId);
			predBoxes.emplace_back(rect);
			confidences.emplace_back(score);
		}
	}
//...
}

La siguiente parte del código corresponde a la extracción de la máscara para cada uno de los objetos, es necesario redimencionarla ya que la red nos devuelve una máscara de 15x15, luego la dibujamos sobre la imagen para poder visualizarla, dibujamos también el nombre de clase y rectángulo.

mask cat dog

Estas son las dos máscaras obtendas para nuestra imagen de prueba.

void post_process(cv::Mat& frame, const std::vector<Mat>& outs)
{
//...
	for (size_t i = 0; i < indices.size(); ++i) {

		const int idx     = indices[i];
		const Rect box    = predBoxes[idx];
		const int classId = classIds[idx];
		const float conf  = confidences[idx];
  
		const Scalar color(255, 120, 147);
		const Scalar rect_color(243, 150, 33);

		// obtener la mascara definida para cada uno de los objetos detectados en la imagen de entrada
		Mat mask(masks.size[2], masks.size[3], CV_32F, masks.ptr<float>(static_cast<int>(i), classId));
		resize(mask, mask, box.size()); // redimensionar la mascara para que coincida con el tamano de la region
		mask = mask > maskThreshold;    // elimina aquellas partes de la mascara que no superen el umbral

		Mat coloredRoi;

		// resaltar en color diferente la mascara correspondiente al objeto detectado
		addWeighted(frame(box), 0.3, color, 0.7, 0, coloredRoi);
		coloredRoi.copyTo(frame(box), mask);

		// Dibujar rectangulo correspondiente a la region detectada
		rectangle(frame, box, rect_color);
		std::string label = format("%.2f", conf);

		// obtener el nombre de clase usando si ID devuelto por la red MASK-RCNN
		if (!classes.empty()) {
			label = classes[classId] + ": " + label + " %";
		}

		int baseLine = 0;

		// obtener las posiciones para dibujar el texto
		cv::Size label_size = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.6, 1, &baseLine);
		cv::Rect label_rect{ box.tl() - cv::Point2i(0, baseLine + label_size.height), cv::Size(box.width, label_size.height + baseLine) };
		
		// dibujar rectangulo de fondo y texto informativo (nombre de clase + puntaje)
		rectangle(frame, label_rect, rect_color, cv::FILLED);
		putText(frame, label, box.tl() - cv::Point2i(-4, baseLine - 1), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar::all(225), 1, LINE_AA);
	}
}

También tenemos la función load_classes_names() esta es simple solo lee un archivo de texto y guarda una lista con los nombres de las classes, estos está ordenados por ID y están en inglés.

Código en GitHub: opencv mask r-cnn

Si deseas un ejemplo en Python pudes ver el que se encuentra de manera oficial en OpenCV: mask_r-cnn.py

Comentarios

Publicar un comentario

Temas relacionados

Entradas populares de este blog

tkinter Grid

Conectar SQL Server con Java

Controles y Contenedores JavaFX 8 - I

Histogramas OpenCV Python