Tutorial i exercisis
La segmentació consisteix a dividir una imatge en regions que tinguin algun significat per a la tasca. Aquestes regions poden correspondre a objectes, fons, vores o parts d’una escena. No existeix un únic mètode que funcioni bé en totes les imatges, perquè cada tècnica es basa en una hipòtesi diferent: intensitats semblants, canvis bruscos, connectivitat o propietats morfològiques.
Carreguem les imatges que farem servir durant aquesta part de la sessió pràctica.
objectes = imread("Imagenes\objects.tif");
monedes = imread("Imagenes\coins.png");
lake = imread("Imagenes\lake.tif");
cameraman = imread("Imagenes\cameraman.tif");
Com que lake té dos canals, n’agafem un!
lake = lake(:,:,1);
Mostrem les imatges
montage({objectes,monedes,lake,cameraman})

Segmentació per llindarització
La segmentació per llindarització és una de les tècniques més senzilles. Consisteix a triar un valor d’intensitat i classificar els píxels segons si estan per sobre o per sota d’aquest valor. Funciona especialment bé quan l’objecte i el fons tenen intensitats clarament diferenciades. En canvi, pot fallar si hi ha ombres, reflexos o una il·luminació poc uniforme.
Selecció de llindars mitjançant l’estudi directe de l’histograma
Quan es tria un llindar manualment, l’histograma serveix com a guia. Si hi ha dos grups d’intensitats ben separats, el llindar es pot situar a la vall entre els dos grups. Si els grups se solapen, el resultat serà més sensible a errors.
h_objectes = imhist(objectes);
bar(h_objectes)

Llindar, calculat a ull a partir de l’histograma
T=75;
objectes_bin = objectes>T;
També es pot utilitzar la comanda imbinarize si fem T/255.
montage({objectes,objectes_bin})

Un altre exemple
h_lake = imhist(lake);
bar(h_lake)

Llindars, calculats a ull a partir de l’histograma
T1 = 130;
T2 = 205;
lake_bin = imquantize(lake,[0,T1,T2,255]);
imshow(lake_bin,[])

Mètode d’Otsu
El mètode d’Otsu calcula automàticament un llindar a partir de l’histograma. La idea és separar els píxels en dues classes de manera que cada classe sigui el més compacta possible i que les dues classes quedin ben diferenciades. Com que és un mètode global, utilitza un únic llindar per a tota la imatge; per això no sempre funciona bé quan la il·luminació canvia segons la zona.
T_otsu = graythresh(objectes);
objectes_bin = imbinarize(objectes,T_otsu);
montage({objectes,objectes_bin})

Etiquetar i acolorir components connexes
Després de binaritzar, és habitual identificar components connexes. Una component connexa és un conjunt de píxels del primer pla que estan units segons una regla de veïnatge. Aquesta regla pot considerar només connexions horitzontals i verticals, o també connexions diagonals. L’elecció de la connectivitat pot canviar el nombre d’objectes detectats.
Primer fem not(objectes), ja que volem que les components connexes siguin blanques sobre fons negre.
objectes_neg = not(objectes_bin);
Utilitzem la comanda bwlabel, que etiqueta les components connexes en una imatge binària.
comps_connexes = bwlabel(objectes_neg);
Per visualitzar el resultat, cosa que és totalment opcional, fem label2rgb, que acoloreix cada component connexa segons una paleta de colors.
objectes_col = label2rgb(comps_connexes,"jet","k","shuffle");
montage({objectes,objectes_neg,objectes_col})

Mètodes de segmentació basats en la detecció de vores
Els mètodes basats en vores busquen punts on la intensitat canvia de manera brusca. Aquests punts sovint indiquen el límit entre objectes o entre regions diferents de l’escena. Tot i això, una vora detectada no sempre forma un contorn tancat, i per tant no sempre defineix directament una regió segmentada. Per aquest motiu, la detecció de vores sovint es combina amb altres passos de postprocessament.
Detecció de rectes utilitzant el Laplacià
Un kernel Laplacià dissenyat amb una orientació concreta pot respondre fortament a estructures lineals. En aquest exemple s’utilitza per destacar rectes verticals, és a dir, zones on la intensitat canvia d’una manera compatible amb aquesta estructura.
linies = imread("Imagenes\lineas.png");
linies = rgb2gray(linies);
Kernel Laplacià per detectar rectes verticals
w_verticals = [-1,2,-1;-1,2,-1;-1,2,-1];
linies_filt = abs(imfilter(linies,w_verticals));
Llindar per discriminar les rectes calculades i eliminar les més febles, que probablement o no seran rectes o no seran rectes verticals.
maxi = max(linies_filt(:));
linies_filt_bin = linies_filt>=maxi;
montage({linies,linies_filt,linies_filt_bin})

S’utilitza la comanda edge per a la detecció de vores amb el gradient digital i com a alternativa a les comandes específiques de Roberts/Sobel i Prewitt que s’obtenien amb la comanda fspecial.
La funció edge agrupa diversos detectors de vores sota una mateixa interfície. Això permet comparar mètodes com Roberts, Sobel o Prewitt sense construir manualment tots els kernels. El més important és interpretar que cada mètode aproxima el canvi d’intensitat amb una sensibilitat diferent al soroll i a l’orientació.
objectes_contorns_roberts = edge(objectes,"roberts");
objectes_contorns_sobel = edge(objectes,"sobel");
objectes_contorns_prewitt = edge(objectes,"prewitt");
montage({objectes,objectes_contorns_roberts,objectes_contorns_sobel,objectes_contorns_prewitt})

Mètode de Marr-Hildreth o de pas per zero per detectar vores
El mètode de Marr-Hildreth combina suavitzat i detecció de canvis d’intensitat. Primer es redueix el soroll amb un filtre gaussià i després s’aplica el Laplacià. Les vores es localitzen buscant passos per zero, és a dir, punts on la resposta canvia de signe. Aquesta idea permet detectar transicions importants, però el resultat depèn de l’escala del suavitzat i del llindar utilitzat.
house = imread("Imagenes\house.tif");
house = house(:,:,1);
w_log = fspecial("log",25,1);
T = 0.018, empíric, és a dir, calculat mitjançant prova i error.
El llindar controla quines respostes es consideren prou fortes per formar part d’una vora. Si és massa baix, apareixen moltes vores falses; si és massa alt, es poden perdre contorns reals. Per això sovint s’ajusta empíricament observant el resultat.
T = 0.018;
S’utilitza edge per a MH amb l’opció zerocross afegint el llindar T i el kernel d’un filtre, en aquest cas un filtre Laplacian of a Gaussian (LoG).
house_contorns = edge(house,"zerocross",T,w_log);
montage({house,house_contorns})

Mètode de Canny
El detector de Canny és un mètode més complet de detecció de vores. Inclou un suavitzat inicial, el càlcul del gradient, una selecció de màxims locals i una decisió final amb dos llindars. El llindar alt identifica vores fortes, mentre que el llindar baix permet conservar vores febles si estan connectades amb vores fortes. Això ajuda a obtenir contorns més nets i continus.
Definim els llindars inferior i superior, també calculats empíricament.
T_canny = [0.04,0.2];
S’utilitza la comanda edge per aplicar el mètode de Canny, amb l’opció "canny". Com a inputs extra, cal afegir un llindar, en aquest cas un vector de dos llindars, inferior i superior, i la variància del primer pas, que serveix per suavitzar la imatge amb un filtre Gaussià.
sigma = 4;
house_canny = edge(house,"canny",T_canny,sigma);
montage({house,house_canny})

Juguem amb els llindars inferior i superior. Important! T1 sempre ha de ser menor que T2 i 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
Mètodes de segmentació basats en la similitud
Els mètodes basats en similitud parteixen d’una idea diferent: els píxels d’una mateixa regió haurien de compartir alguna propietat. Aquesta propietat pot ser la intensitat, el color, la textura o la proximitat espacial. En lloc de buscar només els límits entre regions, aquests mètodes intenten construir regions coherents des de l’interior.
Carreguem la imatge que farem servir.
monedes = imread("Imagenes\coins.png");
imshow(monedes)

Watershed --> Mètode de segmentació basat en un procés d’inundació de conques, és a dir, dels diferents mínims de la imatge, i de construcció de dics, és a dir, vores, separant les diferents regions.
Una manera intuïtiva d’entendre watershed és imaginar la imatge com un relleu topogràfic. Les zones de mínim s’omplen com si fossin conques d’aigua, i quan dues conques es troben es construeix una frontera. Aquestes fronteres són les línies de separació entre regions.
monedes_watershed = watershed(monedes);
Ens quedem amb les línies de separació, que són negres, en l’output de la comanda watershed. És a dir, "vores" és una imatge binària que té tots els píxels negres de l’output, "monedes_watershed", que són precisament aquestes línies de separació, com a píxels amb valor d’intensitat 1, és a dir, blancs.
vores = monedes_watershed == 0;
Ho ajuntem amb la imatge original i convertim les línies de separació en línies blanques perquè destaquin quan siguin superposades a la imatge original. Primer fem una còpia de la imatge original per no alterar-la.
monedes_copia = monedes;
I després canviem els píxels que en la imatge binària "vores" no pertanyen al fons de la imatge per píxels blancs, és a dir, amb nivell d’intensitat 255.
monedes_copia(vores) = 255;
Finalment, mostrem el resultat.
montage({monedes,monedes_copia})

Clarament hi ha sobresegmentació; provem maneres diferents de resoldre aquesta sobresegmentació.
La sobresegmentació es produeix quan un mètode divideix una regió que visualment correspon a un sol objecte en moltes parts petites. En el cas del watershed, això passa sovint perquè la imatge conté molts mínims locals, alguns dels quals són causats per soroll o petites variacions d’intensitat. Per millorar el resultat, cal reduir aquests mínims no rellevants o imposar marcadors més fiables.
1a forma --> opció connectivitat. La comanda "watershed" accepta un segon input, que és la connectivitat de la imatge original. Aquesta connectivitat pot millorar el resultat, mitjançant l’eliminació dels dics associats a components connexes no afins amb la connectivitat imposada.
La connectivitat defineix quins píxels es consideren veïns. En una imatge 2D, una connectivitat més restrictiva pot separar regions que només es toquen en diagonal, mentre que una connectivitat més permissiva les pot considerar unides. Aquesta decisió afecta directament les regions finals del watershed.
monedes_watershed_2 = watershed(monedes,8);
vores_2 = monedes_watershed_2 == 0;
monedes_copia_2 = monedes;
monedes_copia_2(vores_2) = 255;
montage({monedes_copia_2})

2a forma --> ús de la transformada de la distància. La transformada de la distància permet trobar el centre geomètric dels objectes, no de les components connexes, sinó dels objectes; dos objectes que es toquen poden conformar una única component connexa. De fet, aquesta és una de les grans avantatges del watershed, que permet segmentar imatges amb objectes que es toquen o estan parcialment solapats.
En objectes binaris, els màxims de la transformada de la distància solen estar prop del centre de cada objecte. Si s’utilitza aquesta informació com a base del watershed, es poden separar objectes que es toquen però que tenen centres diferents. Aquesta estratègia és molt habitual en imatges amb monedes, cèl·lules o partícules.
Per aplicar la transformada de la distància, necessitem una imatge binària. Per a això, binaritzem utilitzant el mètode d’Otsu.
T_otsu_monedes = graythresh(monedes);
monedes_bin = imbinarize(monedes,T_otsu_monedes);
Calculem la transformada de la distància.
D = bwdist(monedes_bin);
Apliquem al resultat l’algorisme watershed.
D_watershed = watershed(D);
vores_D = D_watershed == 0;
monedes_copia_3 = monedes;
monedes_copia_3(vores_D) = 255;
montage(monedes_copia_3)

3a forma --> ús del gradient. La magnitud del gradient de la imatge permet detectar TOTS els mínims de la imatge, els reals i els que produeixen la sobresegmentació.
Quan watershed s’aplica sobre el gradient, les vores fortes actuen com a barreres naturals entre regions. El problema és que el gradient també pot contenir mínims petits causats per soroll o textura. Per això després es filtren o s’imposen mínims, per guiar millor la segmentació.
w_sobel = fspecial("sobel");
gradient_mag = abs(imfilter(double(monedes),w_sobel))+abs(imfilter(double(monedes),w_sobel'));
Filtrem els mínims de manera que només sobrevisquin els més profunds. La comanda "imextendedmin" pren una imatge de mínims regionals, com la que retorna el gradient, i elimina tots els píxels amb un valor d’intensitat més gran que el segon input. L’output de la comanda és una imatge binària amb els mínims més profunds.
minims = imextendedmin(gradient_mag, 200);
Forcem que els mínims del gradient siguin només els que sobreviuen a la comanda anterior. Per a això s’utilitza la comanda "imimposemin", que imposa al primer input un conjunt de mínims, que és el segon input.
minims_forcats = imimposemin(gradient_mag, minims);
monedes_watershed_3 = watershed(minims_forcats);
vores_3 = monedes_watershed_3 == 0;
monedes_copia_3 = monedes;
monedes_copia_3(vores_3) = 255;
montage({vores_3,monedes_copia_3})
