Saltar a contenido

Tutorial y ejercicios

La morfología matemática estudia las imágenes a partir de la forma de los objetos. En imágenes binarias, es natural interpretar los píxeles blancos como el conjunto del objeto y los píxeles negros como el fondo. Las operaciones morfológicas modifican este conjunto utilizando una forma pequeña, llamada elemento estructurante, que actúa como una herramienta de exploración geométrica.

El elemento estructurante es una parte esencial del método. Su tamaño determina la escala de los detalles que se eliminan o se conservan, y su forma determina qué estructuras encajan mejor con la operación. Por tanto, escoger un disco, una línea, un cuadrado u otra forma no es solo una decisión técnica, sino una manera de expresar qué tipo de geometría se desea analizar.

Cargamos las imagenes binarias que usaremos durante la sesión práctica

figuras = imread("Imagenes\figures.png");
head = imread("Imagenes\head.png");
agujeros = imread("Imagenes\agujeros.png");
simbolos = imread("Imagenes\Simbols.jpg");
scissors = imread("Imagenes\scissors.tif");

Transformamos en imagen binaria todas aquellas imagenes que no lo son

En esta práctica se utiliza la convención habitual de que los objetos son blancos y el fondo es negro. Esta convención es importante porque las operaciones morfológicas actúan sobre los píxeles del primer plano. Si la imagen está invertida, una operación que esperábamos que hiciera crecer un objeto puede acabar afectando al fondo, y la interpretación del resultado cambia.

figuras = rgb2gray(figuras);
figuras = figuras<128;

agujeros = agujeros>128;

simbolos = rgb2gray(simbolos);
simbolos = imresize(simbolos,[512,512]);
simbolos = simbolos<128;

montage({figuras,head,agujeros,simbolos,scissors})

figure_0.png

Operadores morfológicos primarios

Los operadores primarios son la dilatación y la erosión. La dilatación hace crecer las regiones blancas y puede unir partes cercanas de un objeto. La erosión produce el efecto contrario: reduce las regiones blancas y elimina partes pequeñas donde el elemento estructurante no puede encajar completamente. Estos dos operadores son la base de muchas operaciones morfológicas más complejas.

Definición de elementos estructurales. Se usa el comando strel, que tiene como input el tipo de elemento estructural, que puede ser de tipo disco, diamante, recta, rectangulo, cuadrado, y formas definidas por matrices.

El elemento estructurante define qué vecinos se consideran en cada operación morfológica. Un disco trata las direcciones de forma más uniforme, una línea potencia una orientación concreta y un cuadrado considera un vecindario compacto. Por eso, antes de aplicar la operación hay que pensar qué forma tiene el detalle que se quiere conservar o eliminar.

SE = strel("diamond",1);

Tipos de elementos estructurales y sus parametros:

  • diamond -- distancia del origen al extremo del diamante (1 para cruz)
  • disk -- radio
  • line -- longitud y ángulo
  • rectangle -- longitud y altura (introducir como vector, es decir, [l,a])
  • square -- lado
  • nhood -- forma definida por una matriz de 0s y 1s

Dilatacion. Se usa el comando imdilate, que tiene como input la imagen binaria a dilatar y el elemento estructural por el que se dilata. Probar con diamante 1, cuadrado 3, disco 10.

Cuando se cambia el elemento estructurante, también cambia la forma en que crece el objeto. Un elemento pequeño solo modifica ligeramente el contorno, mientras que un disco grande puede unir regiones cercanas y eliminar separaciones estrechas. Esta prueba permite ver que la morfología depende mucho de la escala escogida.

SE = strel("square",4);

%SE = strel("disk",10);

scissor_dilate = imdilate(scissors,SE);

montage({scissors,scissor_dilate})

figure_1.png

simbolos_dilate = imdilate(simbolos,SE);

El efecto será el esperado, es decir, dilatar los objetos de la imagen.

montage({simbolos,simbolos_dilate})

figure_2.png

Erosion. Se usa el comando imerode, que tiene como input la imagen binaria a erosionar y el elemento estructural por el que se erosiona.

La erosión elimina píxeles del contorno cuando el elemento estructurante no cabe completamente. Por eso puede separar objetos conectados por puentes estrechos o eliminar componentes pequeñas. También puede hacer desaparecer detalles útiles si el elemento estructurante es demasiado grande.

simbolos_erode = imerode(simbolos,SE);

El efecto es el esperado, los objetos en la imagen se reducen.

montage({simbolos,simbolos_erode})

figure_3.png

Extracción de bordes en imagenes binarias.

En imágenes binarias, un borde se puede obtener comparando el objeto original con una versión erosionada o dilatada. Esta idea es diferente de la detección de bordes basada en gradientes, porque aquí no se miden cambios de intensidad, sino cambios en la forma del conjunto de píxeles blancos. Por eso los bordes morfológicos son especialmente adecuados cuando la imagen ya está segmentada.

Forma 1: imagen-erosion(imagen)

head_erode = imerode(head,SE);

head_bordes = head-head_erode;

montage({head,head_bordes})

figure_4.png

Esto se considera un borde interior (que viene de un residuo interior, que siempre se genera a partir de la erosion por un elemento estructural).

Borde exterior (residuo exterior, que siempre se genera a partir de la dilatación por un elemento estructural).

head_dilate = imdilate(head,SE);

Como la dilatación de una imagen es siempre más grande que la imagen (recordar que, de hecho, contiene la imagen), en vez de hacer imagen -dilatacion(imagen), tenemos que hacer dilatacion(imagen)-imagen.

head_bordes_2 = head_dilate-head;

Comparamos los dos tipos de bordes. Fijaos en los pequeños detalles (como las pestañas y los labios) para entender porque uno se llama borde interior y el otro exterior.

montage({head_bordes,head_bordes_2})

figure_5.png

Residuo interno-externo (es decir, ambos juntos). Se produce al restar a la dilatacion de la imagen, la erosion de la imagen.

head_bordes_3 = head_dilate-head_erode;

Fijaos que este borde es siempre más grueso que el borde interior o exterior. Esto se debe a que que este borde contiene a los bordes interior y exterior, puesto que se ha calculado con los dos residuos.

montage({head_bordes,head_bordes_2,head_bordes_3})

figure_6.png

Rellanar agujeros e interiores. Se utiliza el comando imfill, donde el input es la imagen con agujeros. Hay que añadir la opción "holes".

Un agujero es una región de fondo completamente rodeada por píxeles del objeto. Rellenar agujeros es útil cuando la segmentación ha dejado huecos internos que no forman parte real del fondo. Esta operación conserva el contorno exterior pero modifica la topología interna del objeto.

agujeros_fill = imfill(agujeros,"holes");
montage({agujeros,agujeros_fill})

figure_7.png

Reconstrucción de componentes conexas por dilatación.

La reconstrucción morfológica se puede entender como un crecimiento controlado. Se parte de un marcador, que indica qué píxeles sabemos que pertenecen al objeto que queremos recuperar, y de una máscara, que limita el crecimiento a las zonas permitidas. De este modo, se pueden reconstruir componentes conexas concretas sin modificar el resto de la imagen.

Descomentar esta parte cuando se llegue aquí. Este comando, imprime la imagen en una pantalla independiente para que podais clicar en uno de los simbolos, que será el que reconstruyamos.

% imshow(simbolos)

Este comando se queda con las coordenadas (en double) del punto marcado con el raton en la imagen simbolos. Tenemos que seleccionar un punto para recontruir la componente conexa asociada a dicho punto.

%[x,y] = getpts;

Imagen binaria de zeros. El comando false crea una imagen binaria del tamaño del input. A esta imagen, en este caso del mismo tamaño que la imagen original, le cambiaremos solo el valor de un pixel, que será el valor del pixel marcado con el raton usando el comando getspts.

%pointer = false(size(simbolos));

Asignamos el punto seleccionado y lo convertimos en blanco. Recordar que para hacer la reconstrucción de componentes conexas, el algoritmo empezaba con un punto que sabemos que pertenece a la componente conexa. En este caso, será el punto marcado con el raton.

%pointer(uint16(y),uint16(x)) = 1;

El comando uint16 solo convierte x e y en enteros, para que sepamos que pixel es el del marcador. No usamos uint8 porque el marcador esta en una escala mayor a 255 (porque el comando getspts se puede utilizar con imagenes de cualquier tipo, en particular, de gran intensidad). Reconstruimos el objeto conexo al que pertenece el punto seleccionado. Para ello se utiliza el comando imreconstruct que tiene como parametros la imagen con el punto o puntos de las componentes conexas a reconstruir y la imagen original que servira de patron en la reconstrucción.

%simbolos_recons = imreconstruct(pointer,simbolos);

Si con el raton clicais en mas de un simbolo, imreconstruct te los reconstruira todos.

%montage({simbolos,simbolos_recons});

Operadores morfológicos secundarios

La apertura y la clausura son operaciones secundarias porque combinan erosión y dilatación. La apertura primero erosiona y después dilata, por lo que tiende a eliminar objetos pequeños o salientes finos. La clausura primero dilata y después erosiona, por lo que tiende a rellenar agujeros pequeños, cerrar separaciones estrechas y suavizar entrantes del contorno.

Apertura. El operador apertura se aplica usando el comando imopen. Este comando tiene como parametros la imagen que se quiere abrir y el elemento estructural que se utilizará.

SE = strel("disk",5);

figures_open = imopen(figuras,SE);

Fijaos que las esquinas exteriores del triangulo y el pentagono son ahora romas en vez de puntiagudas. Este es uno de los efectos tipicos de aplicar el operador apertura.

montage({figuras,figures_open})

figure_8.png

Clausura. Al igual que el operador apertura, este operador se aplica usando el comando imclose, que tiene los mismos inputs que el operador apertura.

figures_close = imclose(figuras,SE);

Fijaos que en este caso no se observan cambios en la imagen. Esto se debe a que el operador clausura redondea esquinar interiores, que en este caso no estan presentes. Busquemos otro ejemplo.

montage({figuras,figures_close})

figure_9.png

scissors_close = imclose(scissors,SE);

Fijaos que en este caso, las esquinas interiores del objeto aparecen romas en vez de puntiagudas, lo que es un efecto tipico del operador clausura.

montage({scissors,scissors_close})

figure_10.png

Filtrado morfologico. Es un filtro de "suavizado" pero basado en operadores morfologicos. En particular, es muy bueno con ruido de tipo sal y/o pimienta.

El filtrado morfológico no calcula una media de los valores de los píxeles, sino que utiliza operaciones de forma. Esto es útil cuando el ruido aparece como pequeños puntos blancos o negros, porque estos puntos pueden desaparecer si son más pequeños que el elemento estructurante. Al mismo tiempo, las estructuras grandes y coherentes pueden conservarse mejor que con un suavizado lineal.

Recordad --> Filtrado = Clausura de la apertura de la imagen

Aplicar primero una apertura ayuda a eliminar pequeños puntos blancos o salientes finos. Aplicar después una clausura ayuda a recuperar continuidad y rellenar pequeños huecos. La combinación es útil como filtro morfológico porque elimina ruido pequeño sin depender de un promedio de intensidades.

huellas = imread("Imagenes\Huella.jpg");

Binarizamos la imagen Huella

huellas = rgb2gray(huellas);
huellas = huellas>128;

SE = strel('square',5);

huellas_open = imopen(huellas,SE);

huellas_filt = imclose(huellas_open,SE);

montage({huellas,huellas_filt})

figure_11.png

Transformada hit-or-miss y reconstruccion por dilatación. La transformada hit-or-miss toma un elemento estructural que sea igual a la componente conexa que se quiere encontrar en la imagen original, y te devuelve un % punto de la componente, para luego reconstruir como hemos visto antes. Es una forma de detectar automataticamente puntos de las componentes conexas que queremos reconstruir, sin tener que clicar con el raton.

La transformada hit-or-miss permite buscar una configuración concreta dentro de una imagen binaria. Una parte del patrón debe coincidir con píxeles del objeto y otra parte debe coincidir con píxeles del fondo. Por eso es útil para detectar formas muy específicas, aunque puede fallar si el objeto está deformado, girado o presenta ruido.

Tomamos un elemento estructura que tenga la forma de uno de los simbolos en la imagen.

SE1 = simbolos(25:110,285:335);

montage({simbolos,SE1})

figure_12.png

Recordad que el segundo elemento estructural ha de ser el complementario del primero.

SE2 = not(SE1);

montage({SE1,SE2})

figure_13.png

La transformada hit-or-miss se aplica con el comando bwhitmis que tiene como inputs la imagen original y los dos elementos estructurales.

pointer = bwhitmiss(simbolos,SE1,SE2);

Una vez obtenido el punto de la componente conexa a reconstruir, reconstruimos como hemos visto antes.

simbolos_re=imreconstruct(pointer,simbolos);

montage({simbolos,pointer,simbolos_re})

figure_14.png

Esqueletización. Una de las aplicaciones de la transformada hit-or-miss, es calcular el esqueleto de los elementos en una imagen binaria. Hay principalmente dos formas.

La esqueletización intenta reducir un objeto a una estructura delgada, idealmente de un solo píxel de anchura, manteniendo su conectividad principal. Esta representación es útil para estudiar formas alargadas, ramas o centros geométricos. Sin embargo, si el contorno es irregular o tiene ruido, pueden aparecer pequeñas ramas no deseadas, llamadas elementos espurios.

1 forma, usando el comando bwskel, cuyo input es la imagen original.

figures_sk = bwskel(figuras);

2 forma, usando el comando bwmorph, cuyos inputs son la imagen original "skel" para indicar que la transformación morfologica que queremos hacer es obtener el esqueleto e Inf, que poda elementos espurios (es decir, ramas del esqueleto que no deberían estar).

figures_ske = bwmorph(figuras,'skel',Inf);

Comparamos ambas formas. Aunque parezca que bwmorph produce un mejor resultado, en general, la mejor forma de calcular el esqueleto es con bwskel. Veamos otros ejemplos.

montage({figuras,figures_sk,figures_ske})

figure_15.png

scissors_sk = bwskel(scissors);

scissors_ske = bwmorph(scissors,'skel',Inf);

Aqui se observa que bwmorph produce un resultado peor (y con elementos espurios a pesar del comando Inf).

montage({scissors,scissors_sk,scissors_ske})

figure_16.png

head_sk = bwskel(head);

head_ske = bwmorph(head,'skel',Inf);

Mismo comentario que antes.

montage({head,head_sk,head_ske})

figure_17.png

En casos como los anteriores en los que bwmorph muestra un esqueleto con elementos indeseables (espurios), hay un comando para podar aun más el esqueleto. Se trata de nuevo de bwmorph. En este caso, lo único que cambia es que en vez de pedir "skel" para que nos haga el esqueleto, pedimos "spur" para que nos pode elementos espurios.

scissors_poda = bwmorph(scissors_ske,"spur",Inf);
head_poda = bwmorph(head_ske,"spur",Inf);

En ambos casos el resultado sigue siendo malo, lo que confirma que la opcion bwskel es la mejor.

montage({scissors_ske,scissors_poda})

figure_18.png

montage({head_ske,head_poda})

figure_19.png

El calculo de esqueletos también se puede usar para segmentar la imagen (si es binaria). Para ello, el primer paso es hace not() de la imagen, puesto que el esqueleto siempre se calcula en la zona con pixeles de valor uno (blancos aqui).

figuras = not(figuras);

agujero = not(agujeros_fill);

montage({figuras,agujero})

figure_20.png

Como la imagen agujero tiene dos franjas negras alrededor, usamos un pequeño truco. Este consiste en definir un marco blanco alrededor de la imagen y aplicar el operador clausura. Este automaticamente, convertira en blanco todo el fondo que aun no lo era.

Comando para hacer que el borde exterior de la imagen sea blanco.

agujero(1,:)=1; agujero(end,:)=1; agujero(:,1)=1; agujero(:,end)=1;

agujero = imclose(agujero,SE);

Resultado.

montage(agujero)

figure_21.png

Ahora si, esqueletizamos con bwskel y vemos que nos ha delimitado las areas en las que se encuentran cada una de las figuras o cada uno de los objetos, lo que de hecho es segmentar la imagen en areas distintas, atendiento a los objetos.

figuras_sk = bwskel(figuras);

agujero_sk = bwskel(agujero);

montage({figuras,figuras_sk})

figure_22.png

montage({agujero,agujero_sk})

figure_23.png

bwmorph también se puede usar para hacer todas las otras operaciones que hemos visto antes (dilatación, erosion, apertura, etc) pero solo con elementos estructurales cuadrados 3x3, por lo que en la práctica no se utiliza.

Si ahora quisieramos segmentar un imagen binaria con elementos en el borde, como la mostrada a continuación, descubririamos que la segmentación no funciones. Un truco para arreglar este problema consiste en eliminar los elementos del borde.

test_morph = imread("Morfologia_I.jpg");

test_morph = rgb2gray(test_morph);
test_morph = test_morph > 128;

montage(test_morph)

figure_24.png

Para ello, vamos a crear una imagen binaria que solo tendrá valor blanco en el borde y la reconstruiremos basandonos en la imagen original. Esto nos dará los elementos del borde, que sinmplemente tenemos que substraer de la imagen original.

pointer = false(size(test_morph));

Definimos bordes blancos

pointer(1,:)=1; pointer(:,1)=1; pointer(end,:)=1; pointer(:,end)=1;

Reconstruimos

test_morph_recon = imreconstruct(pointer,test_morph);

Restamos para quedarnos con los elementos del interior.

test_morph_no_bordes = test_morph - test_morph_recon;

montage({test_morph,test_morph_recon,test_morph_no_bordes})

figure_25.png

Transformada de la distancia

La transformada de la distancia asigna a cada píxel del objeto un valor relacionado con la distancia hasta el fondo más cercano. El resultado ya no es una imagen binaria, sino una imagen en escala de grises que describe el interior de las formas. Los valores más altos suelen situarse en las zonas centrales de los objetos, lo que resulta útil para calcular esqueletos o separar objetos que se tocan.

Transformada de la distancia. Esta calcula la distancia al borde más cercano de cada elemento blanco de la imagen original. De esta forma, convertimos la imagen binaria, en una imagen en escala de grises. El elemento más alejado de los bordes será un 255, y así en proporción, conforme nos acercamos a los bordes.

Como queremos aplicarlo a la silueta, tenemos que convertir la silueta en blanco y el fondo en negro.

head_neg = not(head);

imshow(head_neg)

figure_26.png

La transformada de la distancia se aplica con el comando bwdist donde el unico input es la imagen original que se quiere transformar.

head_d = bwdist(head_neg);

Para ver las imagenes, necesitamos añadir []

imshow(uint8(head_d))

figure_27.png

Otras distancias (cityblock -- Manhattan o d4, chessboard -- Tablero de ajedres o d8).

La distancia euclídea mide la distancia geométrica habitual, mientras que cityblock solo permite desplazamientos horizontales y verticales, y chessboard permite también diagonales con el mismo coste. Cambiar la métrica modifica la forma de los niveles de distancia y, por tanto, puede afectar a esqueletos o centros estimados.

head_d4 = bwdist(head_neg,"cityblock");
head_d8 = bwdist(head_neg,"chessboard");
imshow(uint8(head_d4))

figure_28.png

imshow(uint8(head_d8))

figure_29.png