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 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 n. progressivo TIPOLOGIA COORDINATE x, y, z del centro (z è la slice) “RAGGIO” DEL NODULO (mm)
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
Ricerca dei candidati noduli in una CT (0) 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)
Maschera di segmentazione
Ricerca dei candidati noduli in una CT (1) cerca_VOIs.m 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)', []) … questa parte è identica a mostra_nodulo!
Ricerca dei candidati noduli in una CT (2) % 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])); imshow(M(:, :, 134)', []) % Operazione di masking (AND logico) D = D & M; imshow(D(:, :, 134)', []) … cerca_VOIs.m Soglia individuata tramite pixval; andrebbe trovato un valore buono per TUTTI i noduli, impresa difficile…
Ricerca dei candidati noduli in una CT (3) % 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); … 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
Ricerca dei candidati noduli in una CT (4) cerca_VOIs.m 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 >> stats stats = 219x1 struct array with fields: Area Centroid BoundingBox Image PixelList Frontier Surfarea RadiusC Sphericity
Visualizzazione noduli n1 cerca_VOIs.m % 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 nn = nn(1); figure, mostra3D(stats(nn).Image) N = 95 102, 300, 232 N = 41
Visualizzazione noduli n2 cerca_VOIs.m N = 158 291,382,228 nodulo 2 (tipo n2) Il nodulo 3 (tipo n2) non si distingue 70,300,255 102, 300, 232
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) 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) 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 <omissis> 1 0 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)
Distribuzioni e scatter-plot delle features