Ricerca di noduli polmonari in immagini di tomografia
computerizzata (CT)
Com’è fatto un nodulo?
• Un nodulo polmonare è una formazione
generalmente compatta, tendenzialmente sferica, di densità media (numero di Hounsfield) paragonabile a quella dell’acqua
• I noduli possono essere classificati in base alla loro posizione nel polmone, in n1, n2, n3
(ATTENZIONE! Classificazione NON standard!):
– n1: nodulo interno
– n2: nodulo cresciuto nelle vicinanze della pleura e ad essa connesso
– n3: nodulo originatosi nella pleura, alla quale è connesso in genere tramite un peduncolo
Tipologie di noduli polmonari
n 1 n 2 n 3
3
Esempio…
• Consideriamo una CT: cdPi152_S3
• Il radiologo, operando tramite un software dotato di
interfaccia grafica per la visualizzazione delle fette e la refertazione, costruisce il seguente record di informazioni:
1,45,301,134,3.15,0,n1,NC,SD,PD,T1,ND,ND,504,RadGS,2008-05-15 2,291,382,228,2.52,0,n2,NC,SD,PD,T1,ND,ND,504,RadGS,2008-05-15 3,70,300,255,4.57,0,n2,NC,SD,PD,T1,ND,ND,504,RadGS,2008-05-15 4,102,300,232,5.39,0,n1,ND,ND,ND,ND,ND,ND,504,RadGS,2008-05-15
COORDINATE x, y, z
del centro (z è la slice) “RAGGIO” DEL NODULO (mm)
TIPOLOGIA
n. progressivo
Immagini 2D dei noduli
1
2
3
4
Proviamo a visualizzare i noduli!
• Abbiamo bisogno del file contenente il volume della CT (in formato analyze, di cui sfrutteremo i dati raw):
cdPi152_S3_D0.hdr e cdPi152_S3_D0.img
(hdr: header; img: voxel, dimensione file = w x h x d)
• Un file analyze può essere aperto da software free e.g. MRIcro
• Le coordinate indicate in MRIcro (in basso a sx) sono in mm;
per ottenerle, moltiplicare le coordinate qui riportate per il voxel size (mm): [45 301 134] .* [0.63 0.63 1] = [28.35 189.63 134];
• La CT cdPi152_S3_D0 ha dimensioni 512x512x334
• Carichiamo il file nel workspace di MATLAB
Codice MATLAB per caricare l’immagine e mostrare il nodulo in sezione
% apriamo e leggiamo il file contenente i dati dei voxel, 2 byte/voxel, int16 fid = fopen('cdPi152_S3_D0.img', 'r');
D = fread(fid, 'int16=>int16'); % senza spazi nella stringa!
fclose(fid);
nslices = size(D,1) / (512*512);
% il file e’ una collezione lineare di int16, senza struttura di matrice 3D:
% per ridare la struttura originale, usiamo
% l’istruzione reshape (controllare
% il workspace prima e dopo l’istruzione) D = reshape(D, [512, 512, nslices]);
% visualizziamo una fetta in cui compaia il nodulo imshow(D(:, :, 134)', [])
mostra_nodulo.m
Visualizzazione del nodulo in 3D (1)
• Esistono diverse modalità di rappresentazione
tridimensionale; la più semplice è la visualizzazione
prospettica di una isosuperficie opportuna della densità (valore di grigio) [il nodulo non è un oggetto immerso nel vuoto!]
• Siccome un nodulo è un oggetto compatto e denso rispetto all’ambiente in cui si sviluppa (il parenchima polmonare), per visualizzare il nodulo in 3D occorre applicare
all’immagine cdPi152_S3_D0 un’operazione di sogliatura (con valore di soglia deciso usando per esempio lo
strumento impixelinfo), ottenendo cdPi152_S3_TH;
• Si evidenziano così nella CT gli oggetti costituiti da voxel densi; la frontiera degli oggetti così definiti (isosuperficie) sarà rappresentata in prospettiva
• La ricerca dei candidati noduli nella CT si effettua tramite l’operazione bwlabel applicata all’immagine dopo
sogliatura cdPi152_S3_TH
• Per limitare il numero di falsi positivi, è opportuno
preliminarmente segmentare la CT, ossia limitare al solo parenchima polmonare il volume in cui effettuare la
ricerca
• L’operazione di segmentazione (che non sarà qui dettagliata) fornisce in output una maschera
(cdPi152_S3_M4) che delimita il volume polmonare
• Combinando M4 con la CT originale sogliata, per ottenere il solo tessuto polmonare (cdPi152_S3_D2)
Ricerca dei candidati noduli in una CT (0)
Maschera di segmentazione
close all, clear
% apriamo e leggiamo il file contenente i dati dei voxel, 2 byte/voxel, int16 fid = fopen('cdPi152_S3_D0.img', 'r');
D = fread(fid, 'int16=>int16');
fclose(fid);
nslices = size(D, 1) / (512*512);
% il file e’ una collezione lineare di int16, senza struttura di matrice 3D:
% per ridare la struttura originale, usiamo l’istruzione reshape (controllare
% il workspace prima e dopo l’istruzione) D = reshape(D, [512, 512, nslices]);
% visualizziamo una fetta in cui compaia il nodulo figure
imshow(D(:, :, 134)', [])
…
Ricerca dei candidati noduli in una CT (1)
cerca_VOIs.m
% Ora applichiamo la soglia opportuna (grigio > -300) D = D > -300;
figure
imshow(int8(D(:, :, 134)'), [])
% Prima di cercare gli oggetti connessi, usiamo la matrice
% di segmentazione per limitare lo spazio di ricerca
% (e quindi i falsi positivi e il tempo di calcolo) fid = fopen('cdPi152_S3_M4.img', 'r');
M = fread(fid, 'int8=>int8');
fclose(fid);
M = logical(reshape(M, [512, 512, nslices]));
figure
imshow(M(:, :, 134)', [])
% Operazione di masking (AND logico) D = D & M;
figure
imshow(D(:, :, 134)', [])
…
Ricerca dei candidati noduli in una CT (2)
cerca_VOIs.m
Soglia individuata tramite pixval; andrebbe trovato un valore buono per TUTTI i noduli, impresa difficile…
% Cerchiamo oggetti connessi L = bwlabeln(D); % n dimensioni!
figure
imshow(L(:, :, 134)', [])
stats = regionprops(L, 'Area', 'BoundingBox', 'Centroid', 'Image', 'PixelList');
% Filtro sul volume: consideriamo solo oggetti di dimensioni maggiori di 30
% pixel circa (cioe' noduli circa maggiori di 3x3x3 mm) Areas = [stats.Area];
ind = find(Areas > 30);
stats = stats(ind);
% l’oggetto di statistiche stats(1) ha etichetta ind(1) in L
% ordiniamo per volume decrescente
Areas = [stats.Area]; % lo rifaccio perche’ stats e’ cambiato [newAreas, s] = sort(Areas, 'descend');
stats = stats(s);
ind = ind(s);
…
Ricerca dei candidati noduli in una CT (3)
cerca_VOIs.m
Come usare il vettore di statistiche
>> stats stats =
219x1 struct array with fields:
Area Centroid BoundingBox Image
PixelList
>> stats(1) ans =
Area: 607327
Centroid: [271.9238 260.5258 175.6243]
BoundingBox: [101.5000 29.5000 19.5000 317 454 291]
Image: [454x317x291 logical]
PixelList: [607327x3 double]
>> stats(130).PixelList ans =
261 230 105 262 230 105 261 226 106 261 227 106 261 228 106 261 229 106 ...
262 213 112 262 214 112 263 210 112 263 211 112
mostra3D(stats(30).Image)
per il codice di mostra3D, vedere le prox slide!
Quali features scegliere?
• volume = numero di voxel accesi nella roi
• raggio = raggio della sfera più piccola contenente la ROI (sfera circoscritta)
• sfericità = rapporto tra l’area della superficie di una sfera avente volume pari a quello della ROI e l’area della superficie della ROI
DA CALCOLARE DA CALCOLARE
Sfericità
• Wikipedia (http://en.wikipedia.org/wiki/Sphericity)
• Sphericity is a measure of how spherical (round) an object is. As such, it is a specific example of a compactness measure of a shape. Defined by
Wadell in 1935, the sphericity, Ψ, of a particle is the ratio of the surface area of a sphere (with the same volume as the given particle) to the surface area of the particle:
where Vp is volume of the particle and Ap is the surface area of the particle
for n = 1:length(stats)
stats(n).RadiusC = 0.5 * sqrt(stats(n).BoundingBox(4) ^ 2 + \ stats(n).BoundingBox(5) ^ 2 + \ stats(n).BoundingBox(6) ^ 2);
stats(n).Sphericity = stats(n).Area / (4.0/3 * pi * stats(n).RadiusC ^ 3);
end
Ricerca dei candidati noduli in una CT (4)
cerca_VOIs.m
>> stats stats =
219x1 struct array with fields:
Area Centroid BoundingBox Image
PixelList Frontier Surfarea RadiusC Sphericity
Visualizzazione noduli n1
% individuiamo un nodulo
% sappiamo che il nodulo (1) si trova a 45,301,134,
% ma dobbiamo INVERTIRE x con y!!!
nn = [];
for n = 1:length(stats) c = stats(n).Centroid;
if pdist([301, 45, 134 ; c]) < 10 disp ('Trovato!')
disp (n)
nn = [nn, n];
end end
nn = nn(1);
figure, mostra3D(stats(nn).Image)
cerca_VOIs.m
102, 300, 232 N = 95
N = 41
cerca_VOIs.m
102, 300, 232
N = 158
291,382,228 nodulo 2 (tipo n2)
Il nodulo 3 (tipo n2) non si distingue 70,300,255
Visualizzazione noduli n2
mostra3D
function mostra3D(D)
% %%% 3D
%
% figure;
%
% Inseriamo l'oggetto in una matrice un po' piu' grande: ho verificato che
% se non lo si fa, compaiono dei buchi nella isosuperficie laddove essa
% incontra la box s = size(D);
D1 = zeros(s(1) + 4, s(2) + 4, s(3) + 4);
D1(3:3+s(1)-1, 3:3+s(2)-1, 3:3+s(3)-1) = D;
D = D1;
clear D1
D = smooth3(double(D));
hiso = patch(isosurface(D),...
'FaceColor',[1,.75,.65],...
'EdgeColor','none');
view(45,30) axis tight
daspect([1,1,.67]) % il .67 viene da 1/1.5 lightangle(45,30);
set(gcf,'Renderer','zbuffer'); lighting phong isonormals(D,hiso)
%set(hcap,'AmbientStrength',.6)
set(hiso,'SpecularColorReflectance',0,'SpecularExponent',50) box
VOIs
save stats stats clear
load stats
mostra3D(stats(2).Image)
Statistiche sui noduli
Nodulo 1 (indice 95)
>> stats(95) ans =
Area: 80
Centroid: [301.9750 46.2250 135.4250]
BoundingBox: [298.5000 43.5000 133.5000 7 6 4]
Image: [6x7x4 logical]
PixelList: [80x3 double]
RadiusC: 5.0249 Sphericity: 0.1505 Nodulo 4 (indice 41)
>> stats(41) ans =
Area: 222
Centroid: [300.3108 102.4324 233.8739]
BoundingBox: [293.5000 95.5000 228.5000 12 12 8]
Image: [12x12x8 logical]
PixelList: [222x3 double]
RadiusC: 9.3808 Sphericity: 0.0642 Nodulo 2 (indice 158)
>> stats(158) ans =
Area: 43
Centroid: [383.5349 291.4651 229.5116]
BoundingBox: [380.5000 287.5000 227.5000 7 8 3]
Image: [8x7x3 logical]
PixelList: [43x3 double]
RadiusC: 5.5227 Sphericity: 0.0609
noduli.m
noduli.m: carica “noduli_per_master.mat” e calcola delle statistiche sui dati, allenando poi un classificatore lineare o una rete neurale artificiale
>> noduli
>> confronto = [labtest y]
confronto = 0 0 0 0
<omissis>
1 0 1 1 1 1 1 1 1 1 TP = 12 FN = 2 FP = 31 TN = 1087
sensitivity = 0.8571 specificity = 0.9723
Il file di dati contiene le feature calcolate su un gran numero di noduli e non-noduli (rispettivamente 27 e 2236) provenienti da alcune TC polmonari.
Le feature sono:
volume - raggio mm - sfericita' e intensita' media (colonne da 1 a 4)