|
a |
|
b/MATLAB/analysis/analyzePhantom.m |
|
|
1 |
%ANALYZEPHANTOM analyzes the surface model of the VSD phantom. |
|
|
2 |
% |
|
|
3 |
% AUTHOR: Maximilian C. M. Fischer |
|
|
4 |
% COPYRIGHT (C) 2023 Maximilian C. M. Fischer |
|
|
5 |
% LICENSE: EUPL v1.2 |
|
|
6 |
% |
|
|
7 |
|
|
|
8 |
clearvars; close all |
|
|
9 |
|
|
|
10 |
addpath(genpath('..\src')) |
|
|
11 |
VSD_addPathes('..\..\..\..\..\') |
|
|
12 |
|
|
|
13 |
%% Settings |
|
|
14 |
dicomDBpath = 'D:\sciebo\SMIR\VSDFullBodyBoneReconstruction'; |
|
|
15 |
phantomXLSX = '..\res\VSD_Phantom.xlsx'; |
|
|
16 |
|
|
|
17 |
visualizeSubjects = 1; |
|
|
18 |
|
|
|
19 |
% Load phantom |
|
|
20 |
phantom = readtable(phantomXLSX); |
|
|
21 |
|
|
|
22 |
%% Import |
|
|
23 |
patchProps.FaceAlpha = 0.3; |
|
|
24 |
load(['..\..\Bones\' phantom.ID{1} '.mat'], 'B') |
|
|
25 |
phantomMesh = B(1).mesh; |
|
|
26 |
visualizeMeshes(phantomMesh,patchProps) |
|
|
27 |
anatomicalViewButtons('ASR') |
|
|
28 |
set(gcf,'Name',['VSD phantom: ' phantom.ID{1}],'NumberTitle','off') |
|
|
29 |
|
|
|
30 |
phantomAreaFile = '..\res\VSD_PhantomAnnotations.mat'; |
|
|
31 |
|
|
|
32 |
if exist(phantomAreaFile, 'file') |
|
|
33 |
load(phantomAreaFile,'areas','points') |
|
|
34 |
else |
|
|
35 |
areas = {... |
|
|
36 |
'LV_Body'; 'LV_SuperiorEndPlate'; 'LV_InferiorEndPlate'; 'LV_RightSpinProcess'; 'LV_LeftSpinProcess';... |
|
|
37 |
'MV_Body'; 'MV_SuperiorEndPlate'; 'MV_InferiorEndPlate'; 'MV_RightSpinProcess'; 'MV_LeftSpinProcess';... |
|
|
38 |
'HV_Body'; 'HV_SuperiorEndPlate'; 'HV_InferiorEndPlate'; 'HV_RightSpinProcess'; 'HV_LeftSpinProcess';... |
|
|
39 |
'PostArch'; 'LV_AntArch'; 'MV_AntArch'; 'HV_AntArch';... |
|
|
40 |
'LV_SpinProcessBase'; 'MV_SpinProcessBase'; 'HV_SpinProcessBase'}; |
|
|
41 |
points = {'LV_SpinProcess'; 'MV_SpinProcess'; 'HV_SpinProcess'}; |
|
|
42 |
end |
|
|
43 |
|
|
|
44 |
%% Define areas |
|
|
45 |
% areas = selectFaces(phantomMesh, areas); |
|
|
46 |
% save(phantomAreaFile,'areas','points') |
|
|
47 |
% |
|
|
48 |
% % Define points |
|
|
49 |
% points = selectLandmarks(phantomMesh, points); |
|
|
50 |
% save(phantomAreaFile,'areas','points') |
|
|
51 |
|
|
|
52 |
%% Analyze areas |
|
|
53 |
pointProps.Marker = 'o'; |
|
|
54 |
pointProps.MarkerSize = 8; |
|
|
55 |
pointProps.MarkerEdgeColor = 'k'; |
|
|
56 |
pointProps.MarkerFaceColor = 'k'; |
|
|
57 |
|
|
|
58 |
resTable{1,1} = 'Body diameter'; |
|
|
59 |
resTable{2,1} = 'Arch diameter'; |
|
|
60 |
resTable{3,1} = 'Body height'; |
|
|
61 |
resTable{4,1} = 'Arch thickness'; |
|
|
62 |
resTable{5,1} = 'Spinous process thickness'; |
|
|
63 |
resTable{6,1} = 'Spinous process length'; |
|
|
64 |
|
|
|
65 |
resTableRef = resTable; |
|
|
66 |
resTableRef{1,2} = 36; resTableRef{1,3} = 36; resTableRef{1,4} = 36; |
|
|
67 |
resTableRef{2,2} = 28; resTableRef{2,3} = 28; resTableRef{2,4} = 28; |
|
|
68 |
resTableRef{3,2} = 25; resTableRef{3,3} = 25; resTableRef{3,4} = 25; |
|
|
69 |
resTableRef{4,2} = 5.2; resTableRef{4,3} = 6.0; resTableRef{4,4} = 7.0; |
|
|
70 |
resTableRef{5,2} = 6.0; resTableRef{5,3} = 8.0; resTableRef{5,4} = 10.0; |
|
|
71 |
resTableRef{6,2} = 11.7; resTableRef{6,3} = 14.6; resTableRef{6,4} = 21.0; |
|
|
72 |
|
|
|
73 |
[PostArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'PostArch'),2}); |
|
|
74 |
PostArchCyl=cylindricalFit(PostArchVerts'); |
|
|
75 |
ArchDia = PostArchCyl.radius*2; |
|
|
76 |
drawCylinder(cylindricalFitToCylinder(PostArchCyl), 128, 'FaceColor','g', 'FaceAlpha',0.5) |
|
|
77 |
|
|
|
78 |
resTable{2,2} = ArchDia; resTable{2,3} = ArchDia; resTable{2,4} = ArchDia; |
|
|
79 |
|
|
|
80 |
disp('--- Low Vertebra ---') |
|
|
81 |
% Body diameter |
|
|
82 |
[LV_BodyVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_Body'),2}); |
|
|
83 |
LV_BodyCyl = cylindricalFit(LV_BodyVerts'); |
|
|
84 |
drawCylinder(cylindricalFitToCylinder(LV_BodyCyl), 128, 'FaceColor','r', 'FaceAlpha',0.5) |
|
|
85 |
resTable{1,2} = LV_BodyCyl.radius*2; |
|
|
86 |
|
|
|
87 |
% Body height |
|
|
88 |
[LV_SuperiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_SuperiorEndPlate'),2}); |
|
|
89 |
LV_SuperiorEndPlane = fitPlane(LV_SuperiorEndPlateVertices); |
|
|
90 |
[LV_InferiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_InferiorEndPlate'),2}); |
|
|
91 |
LV_InferiorEndPlane = fitPlane(LV_InferiorEndPlateVertices); |
|
|
92 |
drawPlatform(LV_SuperiorEndPlane, 36); |
|
|
93 |
drawPlatform(LV_InferiorEndPlane, 36); |
|
|
94 |
resTable{3,2} = abs(LV_InferiorEndPlane(2)-LV_SuperiorEndPlane(2)); |
|
|
95 |
|
|
|
96 |
% Arch thickness |
|
|
97 |
[LV_AntArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_AntArch'),2}); |
|
|
98 |
LV_AntArchCyl = cylindricalFit(LV_AntArchVerts'); |
|
|
99 |
drawCylinder(cylindricalFitToCylinder(LV_AntArchCyl), 128, 'FaceColor','b', 'FaceAlpha',1) |
|
|
100 |
resTable{4,2} = PostArchCyl.radius-LV_AntArchCyl.radius; |
|
|
101 |
|
|
|
102 |
% Spinous process thickness |
|
|
103 |
[LV_RightSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_RightSpinProcess'),2}); |
|
|
104 |
LV_RightSpinPlane = fitPlane(LV_RightSpinProcess); |
|
|
105 |
[LV_LeftSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_LeftSpinProcess'),2}); |
|
|
106 |
LV_LeftSpinPlane = fitPlane(LV_LeftSpinProcess); |
|
|
107 |
resTable{5,2} = abs(LV_LeftSpinPlane(3)-LV_RightSpinPlane(3)); |
|
|
108 |
|
|
|
109 |
% Spinous process length |
|
|
110 |
[LV_SpinProcessBaseVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_SpinProcessBase'),2}); |
|
|
111 |
LV_SpinProcessBase = fitPlane(LV_SpinProcessBaseVerts); |
|
|
112 |
LV_SpinProcess = points{strcmp(points(:,1),'LV_SpinProcess'),2}; |
|
|
113 |
LV_SpinProcessProj = projPointOnPlane(LV_SpinProcess, LV_SpinProcessBase); |
|
|
114 |
drawEdge([LV_SpinProcess, LV_SpinProcessProj], 'Color', 'k', pointProps, 'LineWidth',2) |
|
|
115 |
drawPlatform(LV_SpinProcessBase, 10); |
|
|
116 |
resTable{6,2} = distancePoints3d(LV_SpinProcess, LV_SpinProcessProj); |
|
|
117 |
|
|
|
118 |
disp('--- Medium Vertebra ---') |
|
|
119 |
% Body diameter |
|
|
120 |
[MV_BodyVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_Body'),2}); |
|
|
121 |
MV_BodyCyl = cylindricalFit(MV_BodyVerts'); |
|
|
122 |
drawCylinder(cylindricalFitToCylinder(MV_BodyCyl), 128, 'FaceColor','r', 'FaceAlpha',0.5) |
|
|
123 |
resTable{1,3} = MV_BodyCyl.radius*2; |
|
|
124 |
|
|
|
125 |
% Body height |
|
|
126 |
[MV_SuperiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_SuperiorEndPlate'),2}); |
|
|
127 |
MV_SuperiorEndPlane = fitPlane(MV_SuperiorEndPlateVertices); |
|
|
128 |
[MV_InferiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_InferiorEndPlate'),2}); |
|
|
129 |
MV_InferiorEndPlane = fitPlane(MV_InferiorEndPlateVertices); |
|
|
130 |
drawPlatform(MV_SuperiorEndPlane, 36); |
|
|
131 |
drawPlatform(MV_InferiorEndPlane, 36); |
|
|
132 |
resTable{3,3} = abs(MV_InferiorEndPlane(2)-MV_SuperiorEndPlane(2)); |
|
|
133 |
|
|
|
134 |
% Arch thickness |
|
|
135 |
[MV_AntArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_AntArch'),2}); |
|
|
136 |
MV_AntArchCyl = cylindricalFit(MV_AntArchVerts'); |
|
|
137 |
drawCylinder(cylindricalFitToCylinder(MV_AntArchCyl), 128, 'FaceColor','b', 'FaceAlpha',1) |
|
|
138 |
resTable{4,3} = PostArchCyl.radius-MV_AntArchCyl.radius; |
|
|
139 |
|
|
|
140 |
% Spinous process thickness |
|
|
141 |
[MV_RightSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_RightSpinProcess'),2}); |
|
|
142 |
MV_RightSpinPlane = fitPlane(MV_RightSpinProcess); |
|
|
143 |
[MV_LeftSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_LeftSpinProcess'),2}); |
|
|
144 |
MV_LeftSpinPlane = fitPlane(MV_LeftSpinProcess); |
|
|
145 |
resTable{5,3} = abs(MV_LeftSpinPlane(3)-MV_RightSpinPlane(3)); |
|
|
146 |
|
|
|
147 |
% Spinous process length |
|
|
148 |
MV_SpinProcess = points{strcmp(points(:,1),'MV_SpinProcess'),2}; |
|
|
149 |
[MV_SpinProcessBaseVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_SpinProcessBase'),2}); |
|
|
150 |
MV_SpinProcessBase = fitPlane(MV_SpinProcessBaseVerts); |
|
|
151 |
MV_SpinProcessProj = projPointOnPlane(MV_SpinProcess, MV_SpinProcessBase); |
|
|
152 |
drawEdge([MV_SpinProcess, MV_SpinProcessProj], 'Color', 'k', pointProps, 'LineWidth',2) |
|
|
153 |
drawPlatform(MV_SpinProcessBase, 10); |
|
|
154 |
resTable{6,3} = distancePoints3d(MV_SpinProcess, MV_SpinProcessProj); |
|
|
155 |
|
|
|
156 |
disp('--- High Vertebra ---') |
|
|
157 |
% Body diameter |
|
|
158 |
[HV_BodyVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_Body'),2}); |
|
|
159 |
HV_BodyCyl = cylindricalFit(HV_BodyVerts'); |
|
|
160 |
drawCylinder(cylindricalFitToCylinder(HV_BodyCyl), 128, 'FaceColor','r', 'FaceAlpha',0.5) |
|
|
161 |
resTable{1,4} = HV_BodyCyl.radius*2; |
|
|
162 |
|
|
|
163 |
% Body height |
|
|
164 |
[HV_SuperiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_SuperiorEndPlate'),2}); |
|
|
165 |
HV_SuperiorEndPlane = fitPlane(HV_SuperiorEndPlateVertices); |
|
|
166 |
[HV_InferiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_InferiorEndPlate'),2}); |
|
|
167 |
HV_InferiorEndPlane = fitPlane(HV_InferiorEndPlateVertices); |
|
|
168 |
drawPlatform(HV_SuperiorEndPlane, 36); |
|
|
169 |
drawPlatform(HV_InferiorEndPlane, 36); |
|
|
170 |
resTable{3,4} = abs(HV_InferiorEndPlane(2)-HV_SuperiorEndPlane(2)); |
|
|
171 |
|
|
|
172 |
% Arch thickness |
|
|
173 |
[HV_AntArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_AntArch'),2}); |
|
|
174 |
HV_AntArchCyl=cylindricalFit(HV_AntArchVerts'); |
|
|
175 |
drawCylinder(cylindricalFitToCylinder(HV_AntArchCyl), 128, 'FaceColor','b', 'FaceAlpha',1) |
|
|
176 |
resTable{4,4} = PostArchCyl.radius-HV_AntArchCyl.radius; |
|
|
177 |
|
|
|
178 |
% Spinous process thickness |
|
|
179 |
[HV_RightSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_RightSpinProcess'),2}); |
|
|
180 |
HV_RightSpinPlane = fitPlane(HV_RightSpinProcess); |
|
|
181 |
[HV_LeftSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_LeftSpinProcess'),2}); |
|
|
182 |
HV_LeftSpinPlane = fitPlane(HV_LeftSpinProcess); |
|
|
183 |
resTable{5,4} = abs(HV_LeftSpinPlane(3)-HV_RightSpinPlane(3)); |
|
|
184 |
|
|
|
185 |
% Spinous process length |
|
|
186 |
HV_SpinProcess = points{strcmp(points(:,1),'HV_SpinProcess'),2}; |
|
|
187 |
[HV_SpinProcessBaseVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_SpinProcessBase'),2}); |
|
|
188 |
HV_SpinProcessBase = fitPlane(HV_SpinProcessBaseVerts); |
|
|
189 |
HV_SpinProcessProj = projPointOnPlane(HV_SpinProcess, HV_SpinProcessBase); |
|
|
190 |
drawEdge([HV_SpinProcess, HV_SpinProcessProj], 'Color', 'k', pointProps, 'LineWidth',2) |
|
|
191 |
drawPlatform(HV_SpinProcessBase, 10); |
|
|
192 |
resTable{6,4} = distancePoints3d(HV_SpinProcess, HV_SpinProcessProj); |
|
|
193 |
|
|
|
194 |
grid off; axis off |
|
|
195 |
|
|
|
196 |
% Error |
|
|
197 |
errorsCell = cellfun(@(x,y) x-y, resTable(:,2:4), resTableRef(:,2:4), 'UniformOutput', false); |
|
|
198 |
errors = cell2mat(errorsCell); |
|
|
199 |
errors = unique(errors(:)); |
|
|
200 |
disp(['Mean error: ' num2str(mean(errors), 1) ' ' char(177) ' ' num2str(std(errors), 1)]) |
|
|
201 |
disp(['Mean absolute error: ' num2str(mean(abs(errors)), 1) ' ' char(177) ' ' num2str(std(abs(errors)), 1)]) |
|
|
202 |
|
|
|
203 |
writecell([resTable, resTableRef(:,2:4), errorsCell], 'phantomResults.xlsx', 'Sheet','Phantom', 'Range','B5') |
|
|
204 |
|
|
|
205 |
% [List.f, List.p] = matlab.codetools.requiredFilesAndProducts([mfilename '.m']); |
|
|
206 |
% List.f = List.f'; List.p = List.p'; |
|
|
207 |
|
|
|
208 |
%% Helper functions |
|
|
209 |
function [cylinder, cylLine] = cylindricalFitToCylinder(cylFit) |
|
|
210 |
|
|
|
211 |
cylTfm = eulerAnglesToRotation3d(cylFit.yaw, cylFit.pitch, cylFit.roll); |
|
|
212 |
cylVector = transformVector3d([1 0 0], cylTfm); |
|
|
213 |
cylLine = [cylFit.center cylVector]; |
|
|
214 |
cylPoints = transformPoint3d(cylFit.XYZ', cylTfm'); |
|
|
215 |
[~, minIdx] = min(cylPoints(:,1)); |
|
|
216 |
[~, maxIdx] = max(cylPoints(:,1)); |
|
|
217 |
minPtProj = projPointOnLine3d(cylFit.XYZ(:,minIdx)', cylLine); |
|
|
218 |
maxPtProj = projPointOnLine3d(cylFit.XYZ(:,maxIdx)', cylLine); |
|
|
219 |
cylinder = [minPtProj, maxPtProj, cylFit.radius]; |
|
|
220 |
|
|
|
221 |
end |