Tema 1- Introducción a OpenCV
En este tema veremos las funcionalidades básicas de OpenCV: cargar una imagen o un vídeo, mostrarlo por pantalla y guardar ficheros.
Carga y visualización de imágenes
Vamos a comprobar que la instalación se ha hecho de forma correcta compilando el siguiente programa de ejemplo.
#include <opencv2/opencv.hpp> // Incluimos OpenCV
#include <iostream>
using namespace cv;
using namespace std;
int main( int argc, char* argv[] ) {
if( argc != 2) {
cout <<" Uso: display_image <imagen>" << endl;
return -1;
}
Mat image = imread(argv[1]); // Leer fichero de imagen
if (!image.data ) { // Comprobar lectura
cout << "Could not open or find the image" << endl;
return -1;
}
namedWindow( "Ventana", WINDOW_AUTOSIZE ); // Crear una ventana
imshow( "Ventana", image ); // Mostrar la imagen en la ventana
waitKey(0); // Esperar a pulsar una tecla en la ventana
return 0;
}
Llamamos a este fichero lectura.cpp
y desde un terminal ejecutamos:
g++ lectura.cpp -o lectura `pkg-config opencv --cflags --libs`
Si todo va bien debería crearse un fichero ejecutable llamado lectura
. Descargamos esta imagen de ejemplo y ejecutamos el programa de la siguiente forma:
./lectura lena.jpg
Como puede verse, se crea una ventana con la imagen que se cerrará cuando pulsamos una tecla.
Vamos a analizar este código en detalle. En la primera línea, el programa incluye OpenCV, y después se añade el espacio de nombres cv
. Esto tenemos que hacerlo siempre que queramos usar la librería. En ocasiones es posible que necesitemos incluir algún fichero de cabecera adicional de OpenCV, pero en la mayoría de casos no es necesario.
Preparamos la función main para recibir parámetros, y comprobamos que el usuario ha introducido uno (si no es así, damos un mensaje de error y terminamos).
A continuación creamos una matriz (Mat
) para guardar la imagen, que leemos con imread
usando el nombre de fichero que se le pasa por parámetro. Cada vez que se intenta cargar una imagen es importante comprobar que se ha leido correctamente.
En este punto ya tenemos la imagen cargada en una matriz. Podríamos procesarla, pero de momento sólo vamos a mostrarla en una ventana. Para ello creamos primero una ventana con el nombre "Ventana" y tamaño variable usando la función namedWindow
. En la siguiente línea llamamos a la función imshow
para mostrar la imagen en la ventana.
En este ejemplo la llamada a
namedWindow
puede omitirse sin consecuencias. Siimshow
recibe como primer parámetro el nombre de una ventana que todavía no está creada, ésta se crea automáticamente con los parámetros por defecto.
Siempre que mostremos una imagen en pantalla debemos llamar a la función waitKey(0)
para que la ventana se cierre cuando se pulse una tecla. Si no añadimos esta línea, la ventana no llegará a aparecer (mejor dicho, aparecerá y se cerrará de inmediato).
Carga de imágenes
La siguiente instrucción carga en una matriz la imagen cuyo nombre recibe por parámetro.
Mat image = imread(argv[1]);
Como sabes, las imágenes digitales se representan con matrices.
Los formatos principales de imágenes soportadas por OpenCV son:
- Windows bitmaps (
bmp
,dib
) - Portable image formats (
pbm
,pgm
,ppm
) - Sun rasters (
sr
,ras
)
También soporta otros formatos a través de librerías auxiliares:
- JPEG (
jpeg
,jpg
,jpe
) - JPEG 2000 (
jp2
) - Portable Network Graphics (
png
) - TIFF (
tiff
,tif
) - WebP (
webp
).
La función imread
tiene un parámetro opcional. Cuando carguemos una imagen en escala de grises debemos usar IMREAD_GRAYSCALE
:
// Cargamos la imagen (tanto si es en color como si no) en escala de grises
Mat image = imread("lena.jpg", IMREAD_GRAYSCALE);
Esto es porque la opción por defecto es IMREAD_COLOR
, y por tanto se cargará la imagen con 3 canales independientemente de que esté o no en escala de grises. Podemos ver todos los modos de apertura con imread
en este enlace.
La clase Mat
La clase Mat
contiene los valores numéricos de la matriz que almacena, y además una cabecera (con el tipo de datos que contiene, las dimensiones de la matriz, etc). Esta clase de C++ tiene su constructor y destructor, por lo que no hace falta gestionar manualmente su memoria. La clase Mat
sustituye al registro de C llamado IplImage
que se usaba en las primeras versiones de OpenCV, y que se desaconseja usar actualmente ya que necesita liberar a mano la memoria.
En OpenCV podemos crear una matriz arbitraria, contenga o no los datos de una imagen:
// Para crear una matriz: Mat M(filas, columnas, tipo, valores iniciales)
Mat m(10, 10, CV_8UC3, Scalar(0,0,255));
cout << "m = " << endl << " " << m << endl << endl;
Como puede verse, los dos primeros parámetros son las filas y columnas. El tipo de dato en este ejemplo es CV_8UC3
. Significa que tenemos datos de 8 bits (entre 0 y 255) de tipo unsigned char
, y con 3 canales. Por tanto, se trata de una matriz preparada para almacenar una imagen en color (por ejemplo, RGB).
Es importante resaltar que cuando OpenCV carga una imagen RGB, internamente la almacena como BGR. Es decir, el canal 0 es el azul, el canal 1 el verde, y el canal 2 el rojo. Por tanto, en el ejemplo anterior hemos creado una imagen de 10x10 píxeles y color rojo.
Los formatos más comunes son CV_8UC3
para imágenes en color, y CV_8UC1
para escala de grises, pero hay más.
El rango de valores que representa la matriz puede ser:
CV_8U
: Unsigned char (0~255)CV_8S
: Signed char (-128~127)CV_16U
: Unsigned short (0~65535)CV_16S
: Signed short (-32768~32767)CV_32S
: Signed int (-2147483648~2147483647)CV_32F
: Signed float (-1.1810-38~3.4010-38)CV_64F
: Signed double (mucho!)
El número de canales puede ser C1
, C2
, C3
y C4
con cualquier tipo de dato.
En la inicialización de esta matriz de ejemplo hemos usado el constructor Scalar
. Esta instrucción declara un vector de tres elementos con los valores indicados. Scalar
permite representar vectores de 4 elementos como máximo. En el contexto de la declaración de la matriz, inicializamos a cero todos los valores de los canales 0 y 1, y a 255 los del canal 2 (rojo). Si queremos asignar valores a una matriz que esté ya creada, podemos usar por ejemplo:
m.setTo(50); // si la imagen es de 1 canal
m.setTo(Scalar(10, 4, 50)); // si la imagen es de 3 canales (por ejemplo, BGR)
Si por ejemplo quisiéramos inicializar todos los valores de la matriz a cero, a uno, o a valores arbitrarios también podríamos escribir lo siguiente:
Mat m1 = Mat::zeros(3,3, CV_8UC1); // Inicialización con ceros
Mat m2 = Mat::ones(3,3, CV_8UC1); // Inicialización con unos
Mat m3 = (Mat_<double>(3,3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0); // Inicialización con valores dados
Para copiar los datos de una matriz en otra hay que usar el constructor de copia o el método clone
:
Mat m1(m2); // Constructor de copia (sólo en declaración)
m1 = m2.clone(); // Alternativa usando clone (cuando la primera matriz está ya declarada)
Ojo, el operador de asignación (
=
) no hace una copia de la matriz, sino que crea un puntero que apunta a la segunda matriz e incrementa el contador de referencias. Como alternativa aclone
existe también el métodocopyTo
, pero sólo puede usarse si la cabecera de la matriz destino (tipo, resolución, etc) es idéntica a la matriz origen.
Para modificar una matriz ya creada:
Mat m(7, 7, CV_32FC2, Scalar(1,3));
// la cambiamos por una matriz uchar de 3 canales y tamaño 100x60
m.create(100, 60, CV_8UC3);
También podemos crear una matriz que contenga una región de interés (una zona rectangular dada) de otra imagen:
Mat roi(m, Rect(50, 50, 150, 150) );
Para acceder a valores individuales de una matriz, podemos hacer uso de los siguientes valores:
channels()
: Devuelve el número de canales de la matrizcols
: Devuelve el número de columnas de una matrizrows
: Devuelve el número de filas de una matriz
Si queremos tener información sobre la estructura de la matriz, podemos usar las siguientes instrucciones:
unsigned channels = image.channels();
unsigned rows = image.rows;
unsigned columns = image.cols;
La clase Mat
también tiene métodos para invertir matrices, transponerlas, etc.
Otros tipos básicos
Además de la clase Mat
, OpenCV define otros tipos básicos. Todos ellos tienen sobrecargado el operador salida, por lo que podemos mostrar su valor usando <<
como con cualquier otra variable.
- VecAB. Se pueden declarar vectores en el formato
VecAB
, donde A (número de dimensiones) es un valor entre 2 y 5, y B es el tipo de dato: b (uchar), s (short), i (int), f (float) o d (double). Por ejemplo:
Vec3d v(1.1, 2.2, 3.3);
Vec3b bgr(1, 2, 3);
Para vectores de más dimensiones debemos usar la clase vector
de C++.
- Scalar. Nos permite declarar un vector de una dimensión que contiene como máximo 4 valores escalares (reales). Por ejemplo:
Scalar s(1, 3);
cout << s[1] << endl; // Imprime 3
Hay funciones en OpenCV que necesitan parámetros escalares, como el constructor de Mat
que hemos visto anteriormente.
- PointAB. Se suele usar para representar, por ejemplo, puntos de contorno (la silueta de un objeto). La sintaxis es
PointAB
, donde A es la dimensión (2 o 3), y B es el tipo de dato: i (int
), f (float
), o d (double
). Por ejemplo:
Point3d p;
p.x = 0;
p.y = 0;
p.z = 0;
- Size. Especifica un tamaño (anchura por altura). Por ejemplo:
Size s;
s.width = 30;
s.height = 40;
- Rect. La clase rectángulo es parecida a
Size
pero indicando unas coordenadas de origen. Ejemplo:
Rect r;
r.x = r.y = 0;
r.width = r.height = 100;
Acceso a los píxeles
Para recorrer los valores de una matriz que tiene un canal y es de tipo unsigned char
, podemos usar el método at
:
if (m.channels() == 1) {
for( int i = 0; i < m.rows; i++)
for( int j = 0; j < m.cols; j++ )
uchar value = m.at<uchar>(i,j);
}
En el caso de que tengamos una matriz de más canales (por ejemplo, una imagen de color):
if (m.channels() == 3) {
uchar r, g, b;
for (int i = 0; i < m.rows; i++) {
for (int j = 0; j < m.cols; j++) {
Vec3b pixel = m.at<Vec3b>(i,j);
b = pixel[0];
g = pixel[1];
r = pixel[2];
}
}
}
En OpenCV hay muchas formas de acceder a los píxeles individuales y algunas de ellas son más eficientes que at
, aunque también hacen el código algo más lioso. El problema de at
es que necesita calcular la posición exacta de memoria de la fila y columna de un píxel. Una alternativa muy eficiente y frecuentemente usada es esta:
if (m.channels == 3) {
uchar r, g, b;
for (int i = 0; i < m.rows; i++) {
Vec3b* pixelRow = m.ptr<Vec3b>(i);
for (int j = 0; j < m.cols; j++) {
b = pixelRow[j][0];
g = pixelRow[j][1];
r = pixelRow[j][2];
}
}
}
También podemos usar iteradores (MatIterator
) para movernos por todos los píxeles, pero en esta asignatura se desaconseja (es limpio, pero más ineficiente que at
).
Guardar imágenes
Para guardar una imagen en disco se usa la función imwrite
. Ejemplo:
imwrite("output.jpg", m);
Esta función determina el formato del fichero de salida a partir de la extensión proporcionada en el nombre del fichero (en este caso, JPG). Existe un tercer parámetro opcional en el que podemos indicar un vector con opciones de escritura. Por ejemplo:
vector<int> compression_params;
compression_params.push_back(CV_IMWRITE_PNG_COMPRESSION);
compression_params.push_back(9); // Compresion de nivel 9
imwrite("alpha.png", m, compression_params);
Podemos escribir directamente con imwrite
, pero hay casos en los que esta operación puede fallar (por ejemplo, cuando intentamos acceder a un directorio sin permisos). Si esto ocurre, la función devolverá false
. Si queremos saber si se ha escrito la imagen, tenemos que comprobarlo. Siempre que escribamos una imagen, es recomendable comprobar que se ha podido hacer la operación:
bool ok = imwrite("alpha.png", m, compression_params);
if (ok) {
// Se ha podido escribir
}
Ejercicio
Haz un programa llamado grayscale.cpp
que lea una imagen que está en color y la escriba en escala de grises. El programa recibirá como argumento el nombre del fichero de la imagen en color y el del fichero en el que vamos a almacenar la misma imagen pero en escala de grises. Sintaxis:
grayscale <imagen_entrada_color> <imagen_salida_gris>
Si la imagen no puede guardarse, el programa debe imprimir por pantalla el mensaje Error de escritura
.
Persistencia
Además de las funciones específicas para leer y escribir imágenes y vídeo, en OpenCV hay otra forma genérica de guardar o cargar datos. Esto se conoce como persistencia de datos. Los valores de los objetos y variables en el programa pueden guardarse (serializados) en disco, lo cual es útil para almacenar resultados y cargar datos de configuración.
Estos datos suelen guardarse en un fichero xml
mediante un diccionario (en algunos lenguajes de programación como C++, a los diccionarios se les llama también mapas) usando pares clave/valor. Por ejemplo, si quisiéramos guardar una variable que contiene el número de objetos detectados en una imagen:
FileStorage fs("config.xml", FileStorage::WRITE); // Abrimos el fichero para escritura
fs << "numero_objetos" << num_objetos; // Guardamos el numero de objetos
fs.release(); // Cerramos el fichero
Asumiendo que nuestra variable contiene el valor 10, se almacenará en disco el siguiente fichero config.xml
:
<?xml version="1.0"?>
<opencv_storage>
<numero_objetos>10</numero_objetos>
</opencv_storage>
Si posteriormente queremos cargar esta información del fichero, podemos usar el siguiente código:
FileStorage fs("config.xml", FileStorage::READ);
fs["numero_objetos"] >> num_objetos;
fs.release();
Elementos visuales
Como hemos visto al principio, podemos crear una ventana para mostrar una imagen mediante namedWindow
. El segundo parámetro que recibe la función puede ser:
WINDOW_NORMAL
: El usuario puede cambiar el tamaño de la ventana una vez se muestra por pantalla.WINDOW_AUTOSIZE
: El tamaño de la ventana se ajusta al tamaño de la imagen y el usuario no puede redimensionarla. Es la opción por defecto.WINDOW_OPENGL
: Se crea la ventana con soporte para OpenGL (no es necesario en esta asignatura).
Dentro de la ventana de OpenCV en la que mostramos la imagen podemos añadir trackbars, botones, capturar la posición del ratón, etc. En este enlace podemos ver las funciones y constantes relacionadas con la gestión del entorno visual estándar.
Para capturar la posición del ratón podemos usar el método setMouseCallback
, que recibe tres parámetros:
- El nombre de la ventana en la que se captura el ratón
- El nombre de la función que se invocará cuando se produzca cualquier evento del ratón (pasar por encima, clickar con el botón, etc.)
- Un puntero (opcional) a cualquier objeto que queramos pasarle a nuestra función.
La función callback
que hemos creado recibe cuatro parámetros: El código del evento, los valores x e y, unas opciones (flags) y el puntero al elemento pasado a la función.
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void handleMouseEvent(int event, int x, int y, int flags, void* param)
{
Mat* m = (Mat*)param;
if (event == CV_EVENT_LBUTTONDOWN) {
Vec3b d = m->at<Vec3b>(y,x);
cout << x << ", " << y << ": " << d << endl;
}
}
int main(int argc, char* argv[])
{
Mat m = imread("lena.jpg");
if ( m.empty() ) {
cout << "Error loading the image" << endl;
return -1;
}
namedWindow("Ventana", 1);
// asignar la función callback para cualquier evento de raton
setMouseCallback("Ventana", handleMouseEvent, &m);
imshow("Ventana", m);
waitKey(0);
return 0;
}
Podemos crear un trackbar (también llamado slider) para ajustar algún valor en la ventana de forma interactiva usando la función createTrackbar
. Al igual que ocurre con la función que gestiona el ratón, puede recibir como último parámetro un puntero a un objeto (en el siguiente ejemplo, this
). Este puntero lo recibirá la función que se le pasa a createTrackbar
como penúltimo parámetro (en el siguiente ejemplo, onChange
):
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
class LinearBlend {
private:
static const int MAXALPHA = 100;
Mat *m1, *m2; // Punteros por eficiencia
public:
LinearBlend(Mat &img1, Mat &img2, int initialSliderValue) {
// Inicializar valores
m1 = &img1;
m2 = &img2;
// Crear slider. Importante: el ultimo parametro es this. Se recibe desde onChange.
createTrackbar("Blend slider", "Window", &initialSliderValue, MAXALPHA, onChange, this);
// Llamar a la funcion para mostrar algo antes de llegar a modificar el slider
process(initialSliderValue);
}
static void onChange(int v, void *ptr) {
// Conversion de tipo y llamada a la funcion que procesa
LinearBlend *lb = (LinearBlend*)ptr;
lb->process(v);
}
void process(int v) { // Funcion que procesa el resultado
double alpha = (double)v/MAXALPHA;
double beta = 1 - alpha;
// Guardamos en dst la imagen procesada
Mat dst = *m1 * alpha + *m2 * beta;
// Mostramos la imagen
imshow("Window",dst);
}
};
int main() {
// Importante: Las imagenes deben ser del mismo tipo y dimensiones
Mat img1 = imread("LinuxLogo.jpg");
Mat img2 = imread("WindowsLogo.jpg");
if (!img1.data || ! img2.data || img1.channels()!=img2.channels() || img1.cols!=img2.cols || img1.rows!=img2.rows) {
cout << "Error cargando imagenes" << endl;
return -1;
}
namedWindow("Window", CV_WINDOW_AUTOSIZE);
LinearBlend(img1, img2, 20);
waitKey(0);
return 0;
}
Necesitarás estas dos imágenes para probar el código:
Importante: la función que gestiona los cambios (en este caso,
onChange
) sólo puede recibir un puntero a una variable. Por tanto si, tal como ocurre en este ejemplo, es necesario pasar varias variables (variosMat
, etc.), lo limpio es hacer una clase y pasar un puntero a un objeto que sea una instancia de la misma como hemos hecho en este caso. Si buscáis por internet ejemplos de elementos visuales en OpenCV veréis que para evitar esto mucha gente usa variables globales. Se trata de una opción más rápida a la hora de implementar, pero no es limpia en absoluto.
Además de los eventos de ratón y los sliders, podemos añadir botones con la función createButton
(sólo si hemos compilado OpenCV con la librería QT), y también existen opciones para dibujar sobre la ventana.
Como alternativa a usar los elementos visuales nativos de la interfaz de OpenCV, puedes usar otras librerías más potentes como imgui o QT, aunque no nos hará falta para esta asignatura.
Vídeo
OpenCV permite cargar ficheros de vídeo o usar una webcam para realizar procesamiento en tiempo real. Veamos un ejemplo de detección de bordes usando una webcam (dará un error al ejecutarlo porque el laboratorio no está equipado con cámaras, pero si tienes un portátil puedes probarlo):
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main() {
Mat edges, frame;
// Abrimos la camara por defecto y comprobamos que se ha podido
VideoCapture cap(0);
if(!cap.isOpened()) {
return -1;
}
// Creamos la ventana
namedWindow("edges");
// Bucle infinito (hasta pulsar una tecla)
while (true) {
// Cogemos un nuevo frame de la camara
cap >> frame;
// Detectamos los bordes con Canny (en otro tema veremos en detalle este algoritmo)
cvtColor(frame, edges, COLOR_BGR2GRAY);
GaussianBlur(edges, edges, Size(7,7), 1.5, 1.5);
Canny(edges, edges, 0, 30, 3);
// Mostramos la salida en la ventana
imshow("edges", edges);
// La función waitKey(n) se usa para introducir un retardo de n milisegundos al renderizar imagenes en un bucle.
// Cuando se usa como waitKey(0) devuelve la tecla pulsada por el usuario en la ventana activa.
if(waitKey(30) >= 0) break;
}
return 0;
}
Como puede verse, el código es bastante sencillo. Simplemente tenemos que inicializar una variable de captura de vídeo, y con el operador de entrada >>
podemos obtener los frames para procesarlos.
En caso de que queramos cargar un fichero de vídeo (por ejemplo, este ), sólo hay que cambiar una línea:
VideoCapture capture("Megamind.avi");
Para guardar un fichero de vídeo hay que llamar a la función VideoWriter
especificando el formato, fps (frames por segundo) y las dimensiones. Por ejemplo:
VideoWriter video("out.avi", CV_FOURCC('M','J','P','G'), 20, Size(frame_width,frame_height)); // AVI, 20fps widthxheight
Estos son algunos de los formatos aceptados, aunque existen muchos más:
CV_FOURCC('M','J','P','G') // AVI, recomendado en la asignatura
CV_FOURCC('D','I', 'V', '3') // DivX MPEG-4 codec
CV_FOURCC('M','P','E','G') // MPEG-1 codec
CV_FOURCC('M','P', 'G', '4') // MPEG-4 codec
CV_FOURCC('D','I', 'V', 'X') // DivX codec
Para escribir un frame de vídeo podemos usar el operador salida:
video << frame;
Puedes encontrar información detallada sobre procesamiento de vídeo en OpenCV en este enlace.