Saltar a contenido

Tutorial y ejercicios

La segmentación consiste en dividir una imagen en regiones que tengan algún significado para la tarea. Estas regiones pueden corresponder a objetos, fondo, bordes o partes de una escena. No existe un único método que funcione bien en todas las imágenes, porque cada técnica se basa en una hipótesis distinta: intensidades semejantes, cambios bruscos, conectividad o propiedades morfológicas.

Cargamos las imágenes que usaremos durante esta parte de la sesión práctica.

objetos = imread("Imagenes\objects.tif");
monedas = imread("Imagenes\coins.png");
lake = imread("Imagenes\lake.tif");
cameraman = imread("Imagenes\cameraman.tif");

Como lake tiene dos canales, cogemos uno!

lake = lake(:,:,1);

Mostramos las imagenes

montage({objetos,monedas,lake,cameraman})

figure_0.png

Segmentación por umbralización

La segmentación por umbralización es una de las técnicas más sencillas. Consiste en escoger un valor de intensidad y clasificar los píxeles según si están por encima o por debajo de ese valor. Funciona especialmente bien cuando el objeto y el fondo tienen intensidades claramente diferenciadas. En cambio, puede fallar si hay sombras, reflejos o una iluminación poco uniforme.

Selección de umbrales mediante el estudio directo del histograma

Cuando se escoge un umbral manualmente, el histograma sirve como guía. Si hay dos grupos de intensidades bien separados, el umbral puede situarse en el valle entre ambos grupos. Si los grupos se solapan, el resultado será más sensible a errores.

h_objetos = imhist(objetos);

bar(h_objetos)

figure_1.png

Umbral (calculado a ojo a partir del histograma)

T=75;

objetos_bin = objetos>T;

Tambien se puede usar el comando imbinarize si hacemos T/255.

montage({objetos,objetos_bin})

figure_2.png

Otro ejemplo

h_lake = imhist(lake);

bar(h_lake)

figure_3.png

Umbrales (calculados a ojo a partir del histograma)

T1 = 130;
T2 = 205;

lake_bin = imquantize(lake,[0,T1,T2,255]);

imshow(lake_bin,[])

figure_4.png

Metodo de Otsu

El método de Otsu calcula automáticamente un umbral a partir del histograma. La idea es separar los píxeles en dos clases de manera que cada clase sea lo más compacta posible y que las dos clases queden bien diferenciadas. Como es un método global, utiliza un único umbral para toda la imagen; por eso no siempre funciona bien cuando la iluminación cambia según la zona.

T_otsu = graythresh(objetos);

objetos_bin = imbinarize(objetos,T_otsu);

montage({objetos,objetos_bin})

figure_5.png

Etiquetar y colorear componentes conexas

Después de binarizar, es habitual identificar componentes conexas. Una componente conexa es un conjunto de píxeles del primer plano que están unidos según una regla de vecindad. Esta regla puede considerar solo conexiones horizontales y verticales, o también conexiones diagonales. La elección de la conectividad puede cambiar el número de objetos detectados.

Primero hacemos not(objetos), puesto que queremos que las componentes conexas sean blancas sobre fondo negro.

objetos_neg = not(objetos_bin);

Usamos el comando bwlabel, que etiqueta las componentes conexas en una imagen binaria.

comps_conexas = bwlabel(objetos_neg);

Para visualizar el resultado (lo que es totalmente opcional), hacemos label2rgb, que colorea cada componente conexa atendiendo a una paleta de colores.

objetos_col = label2rgb(comps_conexas,"jet","k","shuffle");

montage({objetos,objetos_neg,objetos_col})

figure_6.png

Metodos de segmentación basados en la detección de bordes

Los métodos basados en bordes buscan puntos donde la intensidad cambia de forma brusca. Estos puntos suelen indicar el límite entre objetos o entre regiones distintas de la escena. Aun así, un borde detectado no siempre forma un contorno cerrado y, por tanto, no siempre define directamente una región segmentada. Por este motivo, la detección de bordes se combina a menudo con otros pasos de postprocesado.

Detección de rectas usando el Laplaciano

Un kernel Laplaciano diseñado con una orientación concreta puede responder fuertemente a estructuras lineales. En este ejemplo se utiliza para destacar rectas verticales, es decir, zonas donde la intensidad cambia de una forma compatible con esta estructura.

lineas = imread("Imagenes\lineas.png");
lineas = rgb2gray(lineas);

Kernel Laplaciano para detectar rectas verticales

w_verticales = [-1,2,-1;-1,2,-1;-1,2,-1];

lineas_filt = abs(imfilter(lineas,w_verticales));

Umbral para discriminar las rectas calculadas y eliminar las más débiles (que probablemente o no serán rectas o no serán rectas verticales).

maxi = max(lineas_filt(:));

lineas_filt_bin = lineas_filt>=maxi;

montage({lineas,lineas_filt,lineas_filt_bin})

figure_7.png

Se usa el comando edge para la detección de bordes con el gradiente digital y como alternativa a los comandos específicos de Roberts/Sobel y Prewitt que se obtenían con el comando fspecial.

La función edge agrupa varios detectores de bordes bajo una misma interfaz. Esto permite comparar métodos como Roberts, Sobel o Prewitt sin construir manualmente todos los kernels. Lo más importante es interpretar que cada método aproxima el cambio de intensidad con una sensibilidad diferente al ruido y a la orientación.

objetos_contornos_roberts = edge(objetos,"roberts");

objetos_contornos_sobel = edge(objetos,"sobel");

objetos_contornos_prewitt = edge(objetos,"prewitt");

montage({objetos,objetos_contornos_roberts,objetos_contornos_sobel,objetos_contornos_prewitt})

figure_8.png

Metodo de Marr-Hildreth o de paso por cero para detectar bordes

El método de Marr-Hildreth combina suavizado y detección de cambios de intensidad. Primero se reduce el ruido con un filtro gaussiano y después se aplica el Laplaciano. Los bordes se localizan buscando pasos por cero, es decir, puntos donde la respuesta cambia de signo. Esta idea permite detectar transiciones importantes, pero el resultado depende de la escala del suavizado y del umbral utilizado.

house = imread("Imagenes\house.tif");
house = house(:,:,1);
w_log = fspecial("log",25,1);

T = 0.018 (empirico, es decir, calculado vía prueba y error).

El umbral controla qué respuestas se consideran suficientemente fuertes para formar parte de un borde. Si es demasiado bajo, aparecen muchos bordes falsos; si es demasiado alto, se pueden perder contornos reales. Por eso a menudo se ajusta empíricamente observando el resultado.

T = 0.018;

Se usa edge para MH con la opción zerocross añadiendo el umbral T y el kernel de un filtro, en este caso un filtro Laplacian of a Gaussian (LoG).

house_contornos = edge(house,"zerocross",T,w_log);

montage({house,house_contornos})

figure_9.png

Metodo de Canny

El detector de Canny es un método más completo de detección de bordes. Incluye un suavizado inicial, el cálculo del gradiente, una selección de máximos locales y una decisión final con dos umbrales. El umbral alto identifica bordes fuertes, mientras que el umbral bajo permite conservar bordes débiles si están conectados con bordes fuertes. Esto ayuda a obtener contornos más limpios y continuos.

Definimos los umbrales inferior y superior (tambien calculados empíricamente).

T_canny = [0.04,0.2];

Se usa el comando edge para aplicar el método de Canny (con la opción "canny"). Como inputs extra, hay que añadir un umbral (en este caso un vector de dos umbrales (inferior y superior)) y la varianza del primer paso, que sirve para suavizar la imagen con un filtro Gaussiano.

sigma = 4;

house_canny = edge(house,"canny",T_canny,sigma);

montage({house,house_canny})

figure_10.png

Jugamos con los umbrales inferior y superior (Importante! T1 siempre tiene que ser menor que T2 y T2<1)

T1 = 0.01:0.03:0.07;
T2 = 0.1:0.2:0.9;
sigma = 4;

% for i = 1:length(T1)
% 
%     for j = 1:length(T2)
% 
%         if T1(i)<T2(j)
% 
%             house_canny_loop = edge(house,"canny",[T1(i),T2(j)],sigma);
% 
%             figure;
%             imshow(house_canny_loop)
% 
%         end
% 
%     end
%
% end

Metodos de segmentación basados en la similitud

Los métodos basados en similitud parten de una idea distinta: los píxeles de una misma región deberían compartir alguna propiedad. Esta propiedad puede ser la intensidad, el color, la textura o la proximidad espacial. En lugar de buscar solo los límites entre regiones, estos métodos intentan construir regiones coherentes desde el interior.

Cargamos la imagen que usaremos.

monedas = imread("Imagenes\coins.png");

imshow(monedas)

figure_11.png

Watershed --> Método de segmentación basado en un proceso de inundación de cuencas, es decir, de los distintos mínimos de la imagen, y de construcción de diques, es decir, bordes, separando las distintas regiones.

Una manera intuitiva de entender watershed es imaginar la imagen como un relieve topográfico. Las zonas de mínimo se llenan como si fueran cuencas de agua, y cuando dos cuencas se encuentran se construye una frontera. Estas fronteras son las líneas de separación entre regiones.

monedas_watershed = watershed(monedas);

Nos quedamos con las lineas de separación (que son negras) en el output del comando watershed. Es decir, "bordes" es una imagen binaria que tiene todos los pixeles negros del output, "monedas_watershed", que son precisamente dichas lineas de separación, como pixeles con valor de intensidad 1, es decir, blancos.

bordes = monedas_watershed == 0;

Lo juntamos con la imagen original y convertimos la lineas de separación en lineas blancas para que destaquen cuando sean superpuestas a la imagen original. Primero hacemos una copia de la imagen original para no alterarla.

monedas_copia = monedas;

Y luego cambiamos los pixeles que en la imagen binaria "bordes" no pertenecen al fondo de la imagen por pixeles blancos, es decir, con nivel de intensidad 255.

monedas_copia(bordes) = 255;

Finalmente, mostramos el resultado.

montage({monedas,monedas_copia})

figure_12.png

Claramente hay sobresegmentación, probamos formas diferentes de resolver dicha sobresegmentación.

La sobresegmentación se produce cuando un método divide una región que visualmente corresponde a un solo objeto en muchas partes pequeñas. En el caso del watershed, esto ocurre a menudo porque la imagen contiene muchos mínimos locales, algunos de los cuales están causados por ruido o pequeñas variaciones de intensidad. Para mejorar el resultado, hay que reducir estos mínimos no relevantes o imponer marcadores más fiables.

1º forma --> opción conectivitidad. El comando "watershed" acepta un segundo input, que es la conectividad de la imagen original. Esta conectividad puede mejorar el resultado, por medio de eliminar las los diques asociados a componentes conexas no afines con la connectividad impuesta.

La conectividad define qué píxeles se consideran vecinos. En una imagen 2D, una conectividad más restrictiva puede separar regiones que solo se tocan en diagonal, mientras que una conectividad más permisiva puede considerarlas unidas. Esta decisión afecta directamente a las regiones finales del watershed.

monedas_watershed_2 = watershed(monedas,8);

bordes_2 = monedas_watershed_2 == 0;

monedas_copia_2 = monedas;

monedas_copia_2(bordes_2) = 255;

montage({monedas_copia_2})

figure_13.png

2º forma --> uso de transformada de la distancia. La transformada de la distancia permite encontrar el centro geométrico de los objetos (no de las componentes conexas, sino de los objetos (dos objetos que se tocan pueden conformar una única componente conexa)). De hecho, esta es una de las grandes ventajas del watershed, que permite segmentar imagenes con objetos que se tocan o estan parcialmente solapados.

En objetos binarios, los máximos de la transformada de la distancia suelen estar cerca del centro de cada objeto. Si se utiliza esta información como base del watershed, se pueden separar objetos que se tocan pero que tienen centros distintos. Esta estrategia es muy habitual en imágenes con monedas, células o partículas.

Para aplicar la transformada de la distancia, necesitamos una imagen binaria. Para ello, binarizamos usando el método de Otsu.

T_otsu_monedas = graythresh(monedas);

monedas_bin = imbinarize(monedas,T_otsu_monedas);

Calculamos la transformada de la distancia.

D = bwdist(monedas_bin);

Aplicamos al resultado el algoritmo watershed.

D_watershed = watershed(D);

bordes_D = D_watershed == 0;

monedas_copia_3 = monedas;

monedas_copia_3(bordes_D) = 255;

montage(monedas_copia_3)

figure_14.png

3º forma --> uso del gradiente. La magnitud del gradiente de la imagen permite detectar TODOS los minimos de la imagen (los reales y los que producen la sobresegmentación).

Cuando watershed se aplica sobre el gradiente, los bordes fuertes actúan como barreras naturales entre regiones. El problema es que el gradiente también puede contener mínimos pequeños causados por ruido o textura. Por eso después se filtran o se imponen mínimos, para guiar mejor la segmentación.

w_sobel = fspecial("sobel");

gradient_mag = abs(imfilter(double(monedas),w_sobel))+abs(imfilter(double(monedas),w_sobel'));

Filtramos los minimos de forma que solo los mas profundos sobreviven. El comando "imextendedmin" toma una imagen de mínimos regionales (como la que devuelve el gradiente) y elimina todos los pixeles con un valor de intensidad mayor que el segundo input. El output del comando es una imagen binaria con los mínimos más profundos.

minimos = imextendedmin(gradient_mag, 200);

Forzamos que los mínimos del gradiente sean solo los que sobreviven al comando anterior. Para ello se usa el comando "imimposemin" que impone al primer input un conjunto de mínimos, que es el segundo input.

minimos_forzados = imimposemin(gradient_mag, minimos);

monedas_watershed_3 = watershed(minimos_forzados);

bordes_3 = monedas_watershed_3 == 0;

monedas_copia_3 = monedas;

monedas_copia_3(bordes_3) = 255;

montage({bordes_3,monedas_copia_3})

figure_15.png