Switch to side-by-side view

--- a
+++ b/MATLAB/analysis/analyzeBones.m
@@ -0,0 +1,141 @@
+%ANALYZEBONES checks & analyzes the surface models of the VSD bones.
+%
+% AUTHOR: Maximilian C. M. Fischer
+% COPYRIGHT (C) 2023 Maximilian C. M. Fischer
+% LICENSE: EUPL v1.2
+%
+
+clearvars; close all
+
+addpath(genpath('..\src'))
+VSD_addPathes('..\..\..\..\..\')
+
+% Load subjects & meta data
+subjectXLSX = '..\res\VSD_Subjects.xlsx';
+Subjects = readtable(subjectXLSX);
+NoS = size(Subjects, 1);
+
+load(['..\..\Bones\' Subjects.ID{1} '.mat'], 'B')
+NoB = length(B);
+boneNames = {B.name};
+clear B
+
+%% Mesh sanity checks
+NoCC = nan(NoS, NoB);
+for s=1:NoS
+    % Import the bones
+    load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B')
+    for b=1:length(B)
+        if ~isempty(B(b).mesh)
+            stats = statistics(B(b).mesh.vertices, B(b).mesh.faces, 'MinArea', 1e-8);
+            sanityStats = rmfield(stats, {'num_faces', 'num_vertices', 'num_edges', ...
+                'num_connected_components', 'num_handles', 'euler_characteristic'});
+            if ~all(full(cell2mat(struct2cell(sanityStats)))==0)
+                warning(['Failed mesh check for subject ' ...
+                    Subjects.ID{s} ' (' num2str(s) '): ' B(b).name ' (' num2str(b) ')'])
+            end
+            NoCC(s,b) = stats.num_connected_components;
+        end
+    end
+end
+
+save('NumberOfConnComp.mat','NoCC')
+
+% Check for intersections of adjacent bones
+for s=1:NoS
+    % Import the bones
+    load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B')
+    mesh = concatenateMeshes([B(1:end).mesh]);
+    stats = statistics(mesh.vertices, mesh.faces);
+    if stats.num_selfintersecting_pairs ~= 0
+        error([Subjects.ID{s} ' has intersections between adjacent bones!'])
+    end
+end
+
+%% Mesh volume
+% Remove incomplete or inconsistent subjects
+Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'cut off')), Subjects.Comment),:); %#ok<STREMP>
+Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'total knee')), Subjects.Comment),:); %#ok<STREMP>
+Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'gender')), Subjects.Comment),:); %#ok<STREMP>
+Subjects = Subjects(cellfun(@(x) isempty(strfind(lower(x),'conflicting')), Subjects.Comment),:); %#ok<STREMP>
+Subjects = Subjects(~isnan(Subjects.Weight),:);
+% Calculate volume of the bone models
+NoS = size(Subjects, 1);
+volume = nan(NoS, NoB);
+for s=1:NoS
+    % Import the bones
+    load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B')
+    for b=1:length(B)
+        volume(s,b) = VolumeIntegrate(B(b).mesh.vertices,B(b).mesh.faces);
+        % volume(s,b) = sum(arrayfun(@(x) meshVolume(x), splitMesh(B(b).mesh)));
+    end
+end
+
+cmm3toccm3 = 0.001;
+volumeTable = cell2table(cell(NoB,13), 'VariableNames', {...
+    'Bone name', 'Min.', 'Mean', 'Median', 'Max.',...
+    'M_Min.', 'M_Mean', 'M_Median', 'M_Max.',...
+    'F_Min.', 'F_Mean', 'F_Median', 'F_Max.'});
+[minIdx, maxIdx] = deal(nan(NoS, 1));
+mIdx = strcmp(Subjects.Sex, 'M');
+assert(sum(mIdx) == 10)
+fIdx = strcmp(Subjects.Sex, 'F');
+assert(sum(fIdx) == 10)
+assert(sum(fIdx & mIdx) == 0)
+for b=1:NoB
+    % All
+    volumeTable(b,1) = boneNames(b);
+    [minValue, minIdx(b)] = min(volume(:,b)*cmm3toccm3);
+    volumeTable{b,2} = {minValue};
+    volumeTable(b,3) = meanStats(volume(:,b)*cmm3toccm3,'% 1.0f','format','short');
+    volumeTable(b,4) = medianStats(volume(:,b)*cmm3toccm3,'% 1.0f','format','short');
+    [maxValue, maxIdx(b)] = max(volume(:,b)*cmm3toccm3);
+    volumeTable{b,5} = {maxValue};
+    % Male
+    [minValue, minIdx(b)] = min(volume(mIdx,b)*cmm3toccm3);
+    volumeTable{b,6} = {minValue};
+    volumeTable(b,7) = meanStats(volume(mIdx,b)*cmm3toccm3,'% 1.0f','format','short');
+    volumeTable(b,8) = medianStats(volume(mIdx,b)*cmm3toccm3,'% 1.0f','format','short');
+    [maxValue, maxIdx(b)] = max(volume(mIdx,b)*cmm3toccm3);
+    volumeTable{b,9} = {maxValue};
+    % Female
+    [minValue, minIdx(b)] = min(volume(fIdx,b)*cmm3toccm3);
+    volumeTable{b,10} = {minValue};
+    volumeTable(b,11) = meanStats(volume(fIdx,b)*cmm3toccm3,'% 1.0f','format','short');
+    volumeTable(b,12) = medianStats(volume(fIdx,b)*cmm3toccm3,'% 1.0f','format','short');
+    [maxValue, maxIdx(b)] = max(volume(fIdx,b)*cmm3toccm3);
+    volumeTable{b,13} = {maxValue};
+end
+
+writetable(volumeTable, 'volumeResults.xlsx', 'Sheet','Volume', 'Range','B5',...
+    'WriteVariableNames',0, 'WriteRowNames',0)
+
+figure('color','w','numbertitle','off','name','Bone Volume')
+bph = boxplot(volume*cmm3toccm3,boneNames,'LabelOrientation','inline');
+set(findobj(get(bph(1), 'parent'), 'type', 'text'), 'interpreter','tex');
+ylabel('[cm³]')
+
+%% Age, weight, height
+SubjectStats(1,1) = meanStats(Subjects.Age,'% 1.0f','format','short');
+SubjectStats(1,2) = meanStats(Subjects.Weight,'% 1.0f','format','short');
+SubjectStats(1,3) = meanStats(Subjects.Height,'% 1.1f','format','short');
+SubjectStats(2,1) = medianStats(Subjects.Age,'% 1.0f','format','short');
+SubjectStats(2,2) = medianStats(Subjects.Weight,'% 1.0f','format','short');
+SubjectStats(2,3) = medianStats(Subjects.Height,'% 1.1f','format','short');
+
+%% Number of vertices
+NoV = nan(NoS, NoB);
+for s=1:NoS
+    % Import the bones
+    load(['..\..\Bones\' Subjects.ID{s} '.mat'], 'B')
+    for b=1:length(B)
+        NoV(s,b) = size(B(b).mesh.vertices,1);
+    end
+end
+
+figure('color','w','numbertitle','off','name','Number of mesh vertices')
+bph = boxplot(NoV,boneNames,'LabelOrientation','inline');
+set(findobj(get(bph(1), 'parent'), 'type', 'text'), 'interpreter','tex');
+
+% [List.f, List.p] = matlab.codetools.requiredFilesAndProducts([mfilename '.m']);
+% List.f = List.f'; List.p = List.p';
\ No newline at end of file