|
a |
|
b/MATLAB/analysis/analyzeBones.m |
|
|
1 |
%ANALYZEBONES checks & analyzes the surface models of the VSD bones. |
|
|
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 |
% Load subjects & meta data |
|
|
14 |
subjectXLSX = '..\res\VSD_Subjects.xlsx'; |
|
|
15 |
Subjects = readtable(subjectXLSX); |
|
|
16 |
NoS = size(Subjects, 1); |
|
|
17 |
|
|
|
18 |
load(['..\..\Bones\' Subjects.ID{1} '.mat'], 'B') |
|
|
19 |
NoB = length(B); |
|
|
20 |
boneNames = {B.name}; |
|
|
21 |
clear B |
|
|
22 |
|
|
|
23 |
%% Mesh sanity checks |
|
|
24 |
NoCC = nan(NoS, NoB); |
|
|
25 |
for s=1:NoS |
|
|
26 |
% Import the bones |
|
|
27 |
load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B') |
|
|
28 |
for b=1:length(B) |
|
|
29 |
if ~isempty(B(b).mesh) |
|
|
30 |
stats = statistics(B(b).mesh.vertices, B(b).mesh.faces, 'MinArea', 1e-8); |
|
|
31 |
sanityStats = rmfield(stats, {'num_faces', 'num_vertices', 'num_edges', ... |
|
|
32 |
'num_connected_components', 'num_handles', 'euler_characteristic'}); |
|
|
33 |
if ~all(full(cell2mat(struct2cell(sanityStats)))==0) |
|
|
34 |
warning(['Failed mesh check for subject ' ... |
|
|
35 |
Subjects.ID{s} ' (' num2str(s) '): ' B(b).name ' (' num2str(b) ')']) |
|
|
36 |
end |
|
|
37 |
NoCC(s,b) = stats.num_connected_components; |
|
|
38 |
end |
|
|
39 |
end |
|
|
40 |
end |
|
|
41 |
|
|
|
42 |
save('NumberOfConnComp.mat','NoCC') |
|
|
43 |
|
|
|
44 |
% Check for intersections of adjacent bones |
|
|
45 |
for s=1:NoS |
|
|
46 |
% Import the bones |
|
|
47 |
load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B') |
|
|
48 |
mesh = concatenateMeshes([B(1:end).mesh]); |
|
|
49 |
stats = statistics(mesh.vertices, mesh.faces); |
|
|
50 |
if stats.num_selfintersecting_pairs ~= 0 |
|
|
51 |
error([Subjects.ID{s} ' has intersections between adjacent bones!']) |
|
|
52 |
end |
|
|
53 |
end |
|
|
54 |
|
|
|
55 |
%% Mesh volume |
|
|
56 |
% Remove incomplete or inconsistent subjects |
|
|
57 |
Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'cut off')), Subjects.Comment),:); %#ok<STREMP> |
|
|
58 |
Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'total knee')), Subjects.Comment),:); %#ok<STREMP> |
|
|
59 |
Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'gender')), Subjects.Comment),:); %#ok<STREMP> |
|
|
60 |
Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'conflicting')), Subjects.Comment),:); %#ok<STREMP> |
|
|
61 |
Subjects = Subjects(~isnan(Subjects.Weight),:); |
|
|
62 |
% Calculate volume of the bone models |
|
|
63 |
NoS = size(Subjects, 1); |
|
|
64 |
volume = nan(NoS, NoB); |
|
|
65 |
for s=1:NoS |
|
|
66 |
% Import the bones |
|
|
67 |
load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B') |
|
|
68 |
for b=1:length(B) |
|
|
69 |
volume(s,b) = VolumeIntegrate(B(b).mesh.vertices,B(b).mesh.faces); |
|
|
70 |
% volume(s,b) = sum(arrayfun(@(x) meshVolume(x), splitMesh(B(b).mesh))); |
|
|
71 |
end |
|
|
72 |
end |
|
|
73 |
|
|
|
74 |
cmm3toccm3 = 0.001; |
|
|
75 |
volumeTable = cell2table(cell(NoB,13), 'VariableNames', {... |
|
|
76 |
'Bone name', 'Min.', 'Mean', 'Median', 'Max.',... |
|
|
77 |
'M_Min.', 'M_Mean', 'M_Median', 'M_Max.',... |
|
|
78 |
'F_Min.', 'F_Mean', 'F_Median', 'F_Max.'}); |
|
|
79 |
[minIdx, maxIdx] = deal(nan(NoS, 1)); |
|
|
80 |
mIdx = strcmp(Subjects.Sex, 'M'); |
|
|
81 |
assert(sum(mIdx) == 10) |
|
|
82 |
fIdx = strcmp(Subjects.Sex, 'F'); |
|
|
83 |
assert(sum(fIdx) == 10) |
|
|
84 |
assert(sum(fIdx & mIdx) == 0) |
|
|
85 |
for b=1:NoB |
|
|
86 |
% All |
|
|
87 |
volumeTable(b,1) = boneNames(b); |
|
|
88 |
[minValue, minIdx(b)] = min(volume(:,b)*cmm3toccm3); |
|
|
89 |
volumeTable{b,2} = {minValue}; |
|
|
90 |
volumeTable(b,3) = meanStats(volume(:,b)*cmm3toccm3,'% 1.0f','format','short'); |
|
|
91 |
volumeTable(b,4) = medianStats(volume(:,b)*cmm3toccm3,'% 1.0f','format','short'); |
|
|
92 |
[maxValue, maxIdx(b)] = max(volume(:,b)*cmm3toccm3); |
|
|
93 |
volumeTable{b,5} = {maxValue}; |
|
|
94 |
% Male |
|
|
95 |
[minValue, minIdx(b)] = min(volume(mIdx,b)*cmm3toccm3); |
|
|
96 |
volumeTable{b,6} = {minValue}; |
|
|
97 |
volumeTable(b,7) = meanStats(volume(mIdx,b)*cmm3toccm3,'% 1.0f','format','short'); |
|
|
98 |
volumeTable(b,8) = medianStats(volume(mIdx,b)*cmm3toccm3,'% 1.0f','format','short'); |
|
|
99 |
[maxValue, maxIdx(b)] = max(volume(mIdx,b)*cmm3toccm3); |
|
|
100 |
volumeTable{b,9} = {maxValue}; |
|
|
101 |
% Female |
|
|
102 |
[minValue, minIdx(b)] = min(volume(fIdx,b)*cmm3toccm3); |
|
|
103 |
volumeTable{b,10} = {minValue}; |
|
|
104 |
volumeTable(b,11) = meanStats(volume(fIdx,b)*cmm3toccm3,'% 1.0f','format','short'); |
|
|
105 |
volumeTable(b,12) = medianStats(volume(fIdx,b)*cmm3toccm3,'% 1.0f','format','short'); |
|
|
106 |
[maxValue, maxIdx(b)] = max(volume(fIdx,b)*cmm3toccm3); |
|
|
107 |
volumeTable{b,13} = {maxValue}; |
|
|
108 |
end |
|
|
109 |
|
|
|
110 |
writetable(volumeTable, 'volumeResults.xlsx', 'Sheet','Volume', 'Range','B5',... |
|
|
111 |
'WriteVariableNames',0, 'WriteRowNames',0) |
|
|
112 |
|
|
|
113 |
figure('color','w','numbertitle','off','name','Bone Volume') |
|
|
114 |
bph = boxplot(volume*cmm3toccm3,boneNames,'LabelOrientation','inline'); |
|
|
115 |
set(findobj(get(bph(1), 'parent'), 'type', 'text'), 'interpreter','tex'); |
|
|
116 |
ylabel('[cm³]') |
|
|
117 |
|
|
|
118 |
%% Age, weight, height |
|
|
119 |
SubjectStats(1,1) = meanStats(Subjects.Age,'% 1.0f','format','short'); |
|
|
120 |
SubjectStats(1,2) = meanStats(Subjects.Weight,'% 1.0f','format','short'); |
|
|
121 |
SubjectStats(1,3) = meanStats(Subjects.Height,'% 1.1f','format','short'); |
|
|
122 |
SubjectStats(2,1) = medianStats(Subjects.Age,'% 1.0f','format','short'); |
|
|
123 |
SubjectStats(2,2) = medianStats(Subjects.Weight,'% 1.0f','format','short'); |
|
|
124 |
SubjectStats(2,3) = medianStats(Subjects.Height,'% 1.1f','format','short'); |
|
|
125 |
|
|
|
126 |
%% Number of vertices |
|
|
127 |
NoV = nan(NoS, NoB); |
|
|
128 |
for s=1:NoS |
|
|
129 |
% Import the bones |
|
|
130 |
load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B') |
|
|
131 |
for b=1:length(B) |
|
|
132 |
NoV(s,b) = size(B(b).mesh.vertices,1); |
|
|
133 |
end |
|
|
134 |
end |
|
|
135 |
|
|
|
136 |
figure('color','w','numbertitle','off','name','Number of mesh vertices') |
|
|
137 |
bph = boxplot(NoV,boneNames,'LabelOrientation','inline'); |
|
|
138 |
set(findobj(get(bph(1), 'parent'), 'type', 'text'), 'interpreter','tex'); |
|
|
139 |
|
|
|
140 |
% [List.f, List.p] = matlab.codetools.requiredFilesAndProducts([mfilename '.m']); |
|
|
141 |
% List.f = List.f'; List.p = List.p'; |