--- a +++ b/MATLAB/analysis/analyzePhantom.m @@ -0,0 +1,221 @@ +%ANALYZEPHANTOM analyzes the surface model of the VSD phantom. +% +% AUTHOR: Maximilian C. M. Fischer +% COPYRIGHT (C) 2023 Maximilian C. M. Fischer +% LICENSE: EUPL v1.2 +% + +clearvars; close all + +addpath(genpath('..\src')) +VSD_addPathes('..\..\..\..\..\') + +%% Settings +dicomDBpath = 'D:\sciebo\SMIR\VSDFullBodyBoneReconstruction'; +phantomXLSX = '..\res\VSD_Phantom.xlsx'; + +visualizeSubjects = 1; + +% Load phantom +phantom = readtable(phantomXLSX); + +%% Import +patchProps.FaceAlpha = 0.3; +load(['..\..\Bones\' phantom.ID{1} '.mat'], 'B') +phantomMesh = B(1).mesh; +visualizeMeshes(phantomMesh,patchProps) +anatomicalViewButtons('ASR') +set(gcf,'Name',['VSD phantom: ' phantom.ID{1}],'NumberTitle','off') + +phantomAreaFile = '..\res\VSD_PhantomAnnotations.mat'; + +if exist(phantomAreaFile, 'file') + load(phantomAreaFile,'areas','points') +else + areas = {... + 'LV_Body'; 'LV_SuperiorEndPlate'; 'LV_InferiorEndPlate'; 'LV_RightSpinProcess'; 'LV_LeftSpinProcess';... + 'MV_Body'; 'MV_SuperiorEndPlate'; 'MV_InferiorEndPlate'; 'MV_RightSpinProcess'; 'MV_LeftSpinProcess';... + 'HV_Body'; 'HV_SuperiorEndPlate'; 'HV_InferiorEndPlate'; 'HV_RightSpinProcess'; 'HV_LeftSpinProcess';... + 'PostArch'; 'LV_AntArch'; 'MV_AntArch'; 'HV_AntArch';... + 'LV_SpinProcessBase'; 'MV_SpinProcessBase'; 'HV_SpinProcessBase'}; + points = {'LV_SpinProcess'; 'MV_SpinProcess'; 'HV_SpinProcess'}; +end + +%% Define areas +% areas = selectFaces(phantomMesh, areas); +% save(phantomAreaFile,'areas','points') +% +% % Define points +% points = selectLandmarks(phantomMesh, points); +% save(phantomAreaFile,'areas','points') + +%% Analyze areas +pointProps.Marker = 'o'; +pointProps.MarkerSize = 8; +pointProps.MarkerEdgeColor = 'k'; +pointProps.MarkerFaceColor = 'k'; + +resTable{1,1} = 'Body diameter'; +resTable{2,1} = 'Arch diameter'; +resTable{3,1} = 'Body height'; +resTable{4,1} = 'Arch thickness'; +resTable{5,1} = 'Spinous process thickness'; +resTable{6,1} = 'Spinous process length'; + +resTableRef = resTable; +resTableRef{1,2} = 36; resTableRef{1,3} = 36; resTableRef{1,4} = 36; +resTableRef{2,2} = 28; resTableRef{2,3} = 28; resTableRef{2,4} = 28; +resTableRef{3,2} = 25; resTableRef{3,3} = 25; resTableRef{3,4} = 25; +resTableRef{4,2} = 5.2; resTableRef{4,3} = 6.0; resTableRef{4,4} = 7.0; +resTableRef{5,2} = 6.0; resTableRef{5,3} = 8.0; resTableRef{5,4} = 10.0; +resTableRef{6,2} = 11.7; resTableRef{6,3} = 14.6; resTableRef{6,4} = 21.0; + +[PostArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'PostArch'),2}); +PostArchCyl=cylindricalFit(PostArchVerts'); +ArchDia = PostArchCyl.radius*2; +drawCylinder(cylindricalFitToCylinder(PostArchCyl), 128, 'FaceColor','g', 'FaceAlpha',0.5) + +resTable{2,2} = ArchDia; resTable{2,3} = ArchDia; resTable{2,4} = ArchDia; + +disp('--- Low Vertebra ---') +% Body diameter +[LV_BodyVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_Body'),2}); +LV_BodyCyl = cylindricalFit(LV_BodyVerts'); +drawCylinder(cylindricalFitToCylinder(LV_BodyCyl), 128, 'FaceColor','r', 'FaceAlpha',0.5) +resTable{1,2} = LV_BodyCyl.radius*2; + +% Body height +[LV_SuperiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_SuperiorEndPlate'),2}); +LV_SuperiorEndPlane = fitPlane(LV_SuperiorEndPlateVertices); +[LV_InferiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_InferiorEndPlate'),2}); +LV_InferiorEndPlane = fitPlane(LV_InferiorEndPlateVertices); +drawPlatform(LV_SuperiorEndPlane, 36); +drawPlatform(LV_InferiorEndPlane, 36); +resTable{3,2} = abs(LV_InferiorEndPlane(2)-LV_SuperiorEndPlane(2)); + +% Arch thickness +[LV_AntArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_AntArch'),2}); +LV_AntArchCyl = cylindricalFit(LV_AntArchVerts'); +drawCylinder(cylindricalFitToCylinder(LV_AntArchCyl), 128, 'FaceColor','b', 'FaceAlpha',1) +resTable{4,2} = PostArchCyl.radius-LV_AntArchCyl.radius; + +% Spinous process thickness +[LV_RightSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_RightSpinProcess'),2}); +LV_RightSpinPlane = fitPlane(LV_RightSpinProcess); +[LV_LeftSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_LeftSpinProcess'),2}); +LV_LeftSpinPlane = fitPlane(LV_LeftSpinProcess); +resTable{5,2} = abs(LV_LeftSpinPlane(3)-LV_RightSpinPlane(3)); + +% Spinous process length +[LV_SpinProcessBaseVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'LV_SpinProcessBase'),2}); +LV_SpinProcessBase = fitPlane(LV_SpinProcessBaseVerts); +LV_SpinProcess = points{strcmp(points(:,1),'LV_SpinProcess'),2}; +LV_SpinProcessProj = projPointOnPlane(LV_SpinProcess, LV_SpinProcessBase); +drawEdge([LV_SpinProcess, LV_SpinProcessProj], 'Color', 'k', pointProps, 'LineWidth',2) +drawPlatform(LV_SpinProcessBase, 10); +resTable{6,2} = distancePoints3d(LV_SpinProcess, LV_SpinProcessProj); + +disp('--- Medium Vertebra ---') +% Body diameter +[MV_BodyVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_Body'),2}); +MV_BodyCyl = cylindricalFit(MV_BodyVerts'); +drawCylinder(cylindricalFitToCylinder(MV_BodyCyl), 128, 'FaceColor','r', 'FaceAlpha',0.5) +resTable{1,3} = MV_BodyCyl.radius*2; + +% Body height +[MV_SuperiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_SuperiorEndPlate'),2}); +MV_SuperiorEndPlane = fitPlane(MV_SuperiorEndPlateVertices); +[MV_InferiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_InferiorEndPlate'),2}); +MV_InferiorEndPlane = fitPlane(MV_InferiorEndPlateVertices); +drawPlatform(MV_SuperiorEndPlane, 36); +drawPlatform(MV_InferiorEndPlane, 36); +resTable{3,3} = abs(MV_InferiorEndPlane(2)-MV_SuperiorEndPlane(2)); + +% Arch thickness +[MV_AntArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_AntArch'),2}); +MV_AntArchCyl = cylindricalFit(MV_AntArchVerts'); +drawCylinder(cylindricalFitToCylinder(MV_AntArchCyl), 128, 'FaceColor','b', 'FaceAlpha',1) +resTable{4,3} = PostArchCyl.radius-MV_AntArchCyl.radius; + +% Spinous process thickness +[MV_RightSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_RightSpinProcess'),2}); +MV_RightSpinPlane = fitPlane(MV_RightSpinProcess); +[MV_LeftSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_LeftSpinProcess'),2}); +MV_LeftSpinPlane = fitPlane(MV_LeftSpinProcess); +resTable{5,3} = abs(MV_LeftSpinPlane(3)-MV_RightSpinPlane(3)); + +% Spinous process length +MV_SpinProcess = points{strcmp(points(:,1),'MV_SpinProcess'),2}; +[MV_SpinProcessBaseVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'MV_SpinProcessBase'),2}); +MV_SpinProcessBase = fitPlane(MV_SpinProcessBaseVerts); +MV_SpinProcessProj = projPointOnPlane(MV_SpinProcess, MV_SpinProcessBase); +drawEdge([MV_SpinProcess, MV_SpinProcessProj], 'Color', 'k', pointProps, 'LineWidth',2) +drawPlatform(MV_SpinProcessBase, 10); +resTable{6,3} = distancePoints3d(MV_SpinProcess, MV_SpinProcessProj); + +disp('--- High Vertebra ---') +% Body diameter +[HV_BodyVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_Body'),2}); +HV_BodyCyl = cylindricalFit(HV_BodyVerts'); +drawCylinder(cylindricalFitToCylinder(HV_BodyCyl), 128, 'FaceColor','r', 'FaceAlpha',0.5) +resTable{1,4} = HV_BodyCyl.radius*2; + +% Body height +[HV_SuperiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_SuperiorEndPlate'),2}); +HV_SuperiorEndPlane = fitPlane(HV_SuperiorEndPlateVertices); +[HV_InferiorEndPlateVertices, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_InferiorEndPlate'),2}); +HV_InferiorEndPlane = fitPlane(HV_InferiorEndPlateVertices); +drawPlatform(HV_SuperiorEndPlane, 36); +drawPlatform(HV_InferiorEndPlane, 36); +resTable{3,4} = abs(HV_InferiorEndPlane(2)-HV_SuperiorEndPlane(2)); + +% Arch thickness +[HV_AntArchVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_AntArch'),2}); +HV_AntArchCyl=cylindricalFit(HV_AntArchVerts'); +drawCylinder(cylindricalFitToCylinder(HV_AntArchCyl), 128, 'FaceColor','b', 'FaceAlpha',1) +resTable{4,4} = PostArchCyl.radius-HV_AntArchCyl.radius; + +% Spinous process thickness +[HV_RightSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_RightSpinProcess'),2}); +HV_RightSpinPlane = fitPlane(HV_RightSpinProcess); +[HV_LeftSpinProcess, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_LeftSpinProcess'),2}); +HV_LeftSpinPlane = fitPlane(HV_LeftSpinProcess); +resTable{5,4} = abs(HV_LeftSpinPlane(3)-HV_RightSpinPlane(3)); + +% Spinous process length +HV_SpinProcess = points{strcmp(points(:,1),'HV_SpinProcess'),2}; +[HV_SpinProcessBaseVerts, ~] = removeMeshFaces(phantomMesh, ~areas{strcmp(areas(:,1),'HV_SpinProcessBase'),2}); +HV_SpinProcessBase = fitPlane(HV_SpinProcessBaseVerts); +HV_SpinProcessProj = projPointOnPlane(HV_SpinProcess, HV_SpinProcessBase); +drawEdge([HV_SpinProcess, HV_SpinProcessProj], 'Color', 'k', pointProps, 'LineWidth',2) +drawPlatform(HV_SpinProcessBase, 10); +resTable{6,4} = distancePoints3d(HV_SpinProcess, HV_SpinProcessProj); + +grid off; axis off + +% Error +errorsCell = cellfun(@(x,y) x-y, resTable(:,2:4), resTableRef(:,2:4), 'UniformOutput', false); +errors = cell2mat(errorsCell); +errors = unique(errors(:)); +disp(['Mean error: ' num2str(mean(errors), 1) ' ' char(177) ' ' num2str(std(errors), 1)]) +disp(['Mean absolute error: ' num2str(mean(abs(errors)), 1) ' ' char(177) ' ' num2str(std(abs(errors)), 1)]) + +writecell([resTable, resTableRef(:,2:4), errorsCell], 'phantomResults.xlsx', 'Sheet','Phantom', 'Range','B5') + +% [List.f, List.p] = matlab.codetools.requiredFilesAndProducts([mfilename '.m']); +% List.f = List.f'; List.p = List.p'; + +%% Helper functions +function [cylinder, cylLine] = cylindricalFitToCylinder(cylFit) + +cylTfm = eulerAnglesToRotation3d(cylFit.yaw, cylFit.pitch, cylFit.roll); +cylVector = transformVector3d([1 0 0], cylTfm); +cylLine = [cylFit.center cylVector]; +cylPoints = transformPoint3d(cylFit.XYZ', cylTfm'); +[~, minIdx] = min(cylPoints(:,1)); +[~, maxIdx] = max(cylPoints(:,1)); +minPtProj = projPointOnLine3d(cylFit.XYZ(:,minIdx)', cylLine); +maxPtProj = projPointOnLine3d(cylFit.XYZ(:,maxIdx)', cylLine); +cylinder = [minPtProj, maxPtProj, cylFit.radius]; + +end \ No newline at end of file