Conociendo la clase cv::Mat de OpenCV

La clase cv::Mat es parte fundamental de la biblioteca OpenCV, esta clase nos permite almacenar y manipular los pixeles de una imagen, las funciones incluidas en la librería por lo general requieren un objeto cv::Mat de entrada, el mismo es procesado y se genera otro objeto cv::Mat de salida, dedicaremos este tutorial es estudiar esta clase, aprenderemos a utilizarla y comprenderemos como funciona.

Imágenes y matrices

Para representar una imagen en formato digital se utilizan las matrices, donde cada celda representa un pixel, el color de este pixel estará representado por un valor numérico que pude variar según el formato que estemos utilizando para representar la imagen.

Representación de una imagen en escala de grises:

imagen a escala de grises

La matriz para representar esta imagen, seria algo parecido a la imagen inferior, donde cada celda tiene asignada un valor entero que esta entre 0 - 255, este valor representa la intensidad del color gris, siendo cero el color negro y 255 el blanco.

matriz de imagen en escala de grises

Cuando se trate de una imagen a color tendremos una matriz multidimensional para representar cada unos de los canales de la imagen, el formato usado por OpenCV por defecto para representar las imágenes a color es el BGR que corresponde a los canales: azul, verde y rojo.

Representación de una imagen a color en formato BGR:

imagen BGR

En una imagen a color cada canal almacena su correspondiente nivel de intensidad, combinando todos los canales se obtiene el color final.

canales de imagen BGR

Un objeto cv::Mat puede tener mas canales, por ejemplo, un cuarto canal para almacenar el nivel de opacidad en imágenes que utilicen transparencias, además la organización y los valores de cada uno de los canales puede variar según el formato que utilicemos.

Crear un objeto cv::Mat

La manera más común de crear un objeto cv::Mat es usando la función cv::imread() para cargar un archivo de imagen, puedes ver el tutorial: cargar imagen en OpenCV, también es posible utilizar el constructor de la clase, veamos como:

cv::Mat::Mat(int rows, int cols, int type)

Usando esta sobre-carga del constructor debemos indicar el número de filas (rows) y columnas (cols), al final indicamos el tipo de datos y los canales que usaremos para almacenar la matriz, ejemplo:

cv::Mat M(64, 64, CV_8UC3)

Este código crea una matriz  de 64x64, el tipo CV_8UC3 indica que el rango de valores estará entre 0 - 255 pues utilizaremos 8 bits sin signo (8U) y que manejaremos 3 canales (C3), para definir el tipo de datos de un cv::Mat se utiliza la siguiente convención:

CV_<bit depth>{U|S|F}C(<number of channels>)

En esta forma <bit depth> indica el numero de bits que se utilizarán para el almacenamiento, puede ser: 8, 16, 32, etc., seguido se indica una de las letras: U, S, F que establece el tipo de dato, U sin signo, ejemplo: (0 - 255), S con signo, ejemplo: (-127 - +127), o un dato flotante F, ejemplo: (0.001 - 1.000), respectivamente, al final se indica la cantidad de canales que usaremos en <number of channels>, ejemplo: C3, C1, también se puede usar la forma: C(3), C(1).

Otra posible forma es utilizar el método cv::Mat::create() para inicializar la matriz, este método utiliza los mismos parámetros definidos anteriormente, ejemplo: M.create(4,4, CV_8SC(2)), crea una matriz de 4x4 que almacenará un rango de valores entre (-127 - +127) en dos canales.

Podemos inicializar los objetos cv::Mat usando el estilo MATLAB, con las funciones, cv::Mat::zeros, cv::Mat::ones, cv::Mat::eye, al igual que los métodos anteriores necesitamos indicar las filas, columnas y el tipo de datos.

Mat E = Mat::eye(5, 5, CV_32F);

opencv matriz (eye, ones, zeros)

Otra forma de crear una matriz OpenCV es usar el método copyTo(Mat) de la clase cv::Mat para copiar la matriz en el destino indicado como parámetro.

Acceder a un pixel en OpenCV

Para tener acceso a un elemento de la matriz o al pixel que este representa, usaremos el método at<> de la clase cv::Mat, para usarle debemos indicarle el índice de fila y columna que deseamos obtener, empiezan de cero, también debemos indicar el tipo de dato de retorno, este debe ser igual o compatible con el tipo usado por la matriz.

Vec3b pixel = image.at<Vec3b>(25, 17);

uchar B = pixel[0];
uchar G = pixel[1];
uchar R = pixel[2];

Usamos la clase Vec3b para almacenar el pixel correspondiente, esta clase representa un arreglo de 3 valores de tipo uchar, cada uno de estos valores representa un canal de color del pixel y podemos acceder a ellos indicando el índice.

En esta clase VecAB, A establece el número de canales y B el tipo de dato, que puede ser: b, s, i, f, d, para los tipos: uchar, short, int, float, double, respectivamente.

Si la matriz tiene una sola componente, por ejemplo, en una imagen a escala de grises indicamos directamente el tipo en el método at<>, ejemplo: uchar pixel = m.at<uchar>(0,0);.

Este método también lo podemos usar para cambiar el valor de un pixel, vemos un ejemplo aplicando lo que hemos aprendido hasta ahora, lo que haremos será cargar una imagen a colores, luego recorremos cada uno de los pixeles y los convertimos a escala de grises, el resultado lo guardamos en otra matriz.

Mat image = imread("../../../opencv-3.2.0/samples/data/lena.jpg");
Mat gray(image.rows, image.cols, CV_8UC1);

for (size_t i = 0; i < image.rows; i++)
{
    for (size_t j = 0; j < image.cols; j++)
    {
        Vec3b pixel = image.at<Vec3b>(i, j);

        uchar B = pixel[0];
        uchar G = pixel[1];
        uchar R = pixel[2];

        gray.at<uchar>(i, j) = (B + G + R) / 3;
    }
}

Para obtener la cantidad de filas y columnas de un cv::Mat usamos rows y cols respectivamente, una formula simple para convertir a escala de grises es sumar las componentes BGR y dividir entre 3, debes saber que OpenCV ya tiene funciones más eficientes para esta tarea.

Una forma más eficiente de acceso a los pixeles de una imagen es usar el método ptr<> el cual retorna un puntero a una fila de la matriz, veamos el mismo ejemplo, ahora usando punteros.

for (int i = 0; i < image.rows; i++)
{
    Vec3b* imgrow = image.ptr<Vec3b>(i);
    uchar* grayrow = gray.ptr<uchar>(i);

    for (int j = 0; j < image.cols; j++)
    {
        uchar B = imgrow[j][0];
        uchar G = imgrow[j][1];
        uchar R = imgrow[j][2];

        grayrow[j] = (B + G + R) / 3;
    }
}

Podemos medir la eficiencia del segundo método con respecto al primero usando las funciones getTickCount() y getTickFrequency() que nos permiten medir el tiempo transcurrido al ejecutar un código, por ejemplo:

double start = (double)getTickCount();
// aqui va la operación a medir...
double elapsed = ((double)getTickCount() - start) / getTickFrequency();

La variable elapsed contiene el tiempo transcurrido en segundos.

Siempre que lo deseemos podemos ver el contenido de un objeto cv::Mat en la pantalla de salida, veamos un código de ejemplo:

Mat m(5, 5, CV_8UC1);
randu(m, Scalar::all(0), Scalar::all(255));

cout << "Matriz: \n" << m << endl;
cout << "Matriz (csv): \n" << format(m, Formatter::FMT_CSV) << endl;

El método randu() es usado para rellenar la matriz con valores aleatorios, estos se encuentren ente 0 y 255, la clase Scalar no permite definir un arreglo de cuatro componentes que representen los 4 canales de una imagen, en este caso solo usamos 1, la salida es:

imprimir matriz opencv

El formato de salida se puede cambiar con la función format(...) utilizamos la enumeración Formatter::FMT_CSV para indicar el formato, por ejemplo, para una salida CSV.

imprimir matriz opencv con formato

Existe muchas otras cosas que debemos conocer de la clase cv::Mat, para ello continuaremos viendo mas temas en el siguiente tutorial, entre ellos: copiar una matriz, seleccionar una región de interés, realizar operaciones lógicas y matemáticas, etc., no vemos en la próxima.

Este tutorial continua en: Explorando la clase Mat.

Comentarios

Entradas populares de este blog

Conectar SQL Server con Java

Entrenar OpenCV en Detección de Objetos

Procesamiento de imágenes en OpenCV

Acceso a la webcam con OpenCV