diff --git a/DIGIGUI.fig b/DIGIGUI.fig index 711f513..09e2cd8 100644 Binary files a/DIGIGUI.fig and b/DIGIGUI.fig differ diff --git a/DIGIGUI.m b/DIGIGUI.m index 790d8d4..daccdbc 100644 --- a/DIGIGUI.m +++ b/DIGIGUI.m @@ -3,24 +3,24 @@ % Copyright (C) Reuben W. Nixon-Hill (formerly Reuben W. Hill) % % ------------------------------------------------------------------------- -% -- DIGIGUI v1.2.1 for Matlab R2016b -- +% -- DIGIGUI v1.3.0 for Matlab R2016b -- % ------------------------------------------------------------------------- % % For the Polhemus PATRIOT digitiser, attached to stylus pen with button. % -% A list of points to digitise is imported from a text file, where each +% A list of points to digitise is imported from a text file, where each % point is on a new line. % -% The baud rate is set via the variable "BaudRate" in +% The baud rate is set via the variable "BaudRate" in % DIGIGUI_OutputFcn and has default value 115200. % % Points are digitised by pressing the stylus button. % -% After getting 5 reference cardinal points 'Nasion','Inion','Ar','Al' and -% 'Cz', an Atlas reference baby head, with these points marked, is mapped -% onto the graph display of points. +% The default atlas requires 5 reference cardinal points: 'Nasion','Inion', +% 'Ar','Al' and 'Cz'. Once measured, an Atlas reference baby head, with +% these points marked, is mapped onto the graph display of points. % -% Before the allignment of the head model, a coordinte transform is done: +% Before the allignment of the head model, a coordinate transform is done: % 1: place the 'inion' at the origin % 2: rotate the 'Al' into the y axis % 3: rotate the 'Ar' into the xy plane about the new 'Inion'-'Al' y axis @@ -28,8 +28,27 @@ % plane, thus alligning the inion and nasion. % This coordinate transform is then applied to all measured points. % -% A tab delimited list of points and their XYZ coords is outputted to -% a file of the users choosing. +% At present this is the only atlas that has been tested - see the +% 'default_atlas' directory for what is needed for a different atlas. I +% expect that some work will need to be done to make different atlasses +% importable when the program has been compiled: in particular changes to +% the working directory to try and include new .m files. +% +% Once a list of points has been found, the atlas can be realigned at any +% time. This is useful if one wishes to remeasure an atlas point. +% +% A list of points in absolute coordinates of the Polhemus can be specified +% to measure as 'expected coordinates'. These are specified in a CSV file +% as a list of locations and x-y-z coordinates in centimetres - see +% 'expected_coordinates_example.txt' for the format required. The tolerance +% for accepting a measurement is specified by providing a location called +% "Tolerance" with the X coordinate used as the tolerance. The list of +% points must correspond to at least some of the imported locations, +% otherwise the expected coordinates will not be imported and an error will +% be displayed. +% +% A CSV file, mat file, or excel spreadsheet of points and their XYZ coords is +% outputted to a location of the user's choosing. % % % MATLAB GUIDE Generated comments: @@ -56,7 +75,7 @@ % Edit the above text to modify the response to help DIGIGUI -% Last Modified by GUIDE v2.5 24-Jun-2016 14:56:20 +% Last Modified by GUIDE v2.5 03-Jul-2023 15:48:11 % Begin initialization code - DO NOT EDIT gui_Singleton = 1; @@ -89,6 +108,9 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) % Choose default command line output for DIGIGUI handles.output = hObject; +disp(['Working dir: ', pwd]); +disp(['ctfroot: ', ctfroot]); + %-------------------Get the executable/.m directory-------------------- if isdeployed @@ -118,7 +140,7 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) % --- Outputs from this function are returned to the command line. -function varargout = DIGIGUI_OutputFcn(hObject, eventdata, handles) +function varargout = DIGIGUI_OutputFcn(hObject, eventdata, handles) % varargout cell array for returning output args (see VARARGOUT); % hObject handle to figure % eventdata reserved - to be defined in a future version of MATLAB @@ -155,28 +177,28 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) %------------------------CREATE SERIAL OBJECT--------------------------- -% Create serial object and set baud rate +% Create serial object and set baud rate BaudRate = 115200; % Find interface objects that are set to 'on' i.e. enabled... InterfaceObj=findobj(handles.figure1,'Enable','on'); % ... and turn them off. set(InterfaceObj,'Enable','off'); - + % find serial com port [handles.COMport, handles.sensors] = FindPatriotSerial(BaudRate); - + % Re-enable the interface objects. set(InterfaceObj,'Enable','on'); if(handles.COMport ~= 0) %patriot found handles.serial = serial(handles.COMport,'BaudRate', BaudRate); else - - %-------------------QUIT & ERROR IF DEVICE NOT FOUND-------------------- + + %-------------------QUIT & ERROR IF DEVICE NOT FOUND-------------------- str1 = 'Polhemus Patriot Device not found or communicated with successfully.'; str2 = ['Check the device is on and its baud rate is set to '... - sprintf('%i',BaudRate) ... + sprintf('%i',BaudRate) ... ' on both the hardware switches of the device and the settings of'... ' any USB link cable used.']; str3 = ['If running in MATLAB, try restarting MATLAB to scan for new'... @@ -190,7 +212,7 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) guidata(hObject, handles); CloseFcn(hObject,eventdata,handles); return; - + end @@ -198,6 +220,7 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) % Set the initial point count to 0. This is incremented before each % measured head point until the last head point is measured. handles.point_count = 0; +handles.point_count_history = []; % this is true when opening save dialogues for example handles.disable_measurements = false; @@ -207,26 +230,145 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) % this is true if the atlas point names has been edited. handles.editedAtlasPoints = false; +handles.error_distance = 0.1; +handles.double_tap_error_enabled = false; +if ~isdeployed + atlas_dir = fullfile(pwd, 'default_atlas'); +else + % Note that for some reason the default atlas directory is + % located directly in ctfroot + atlas_dir = fullfile(ctfroot, 'default_atlas'); +end + +%Get settings +try + if ~isdeployed + settings_loc = fullfile(pwd, 'settings.mat'); + else + settings_loc = fullfile(ctfroot, 'DIGIGUI', 'settings.mat'); + end + disp(['looking for settings.mat at ', settings_loc]); + load(settings_loc,'error_distance','double_tap_error_enabled','atlas_dir','save_expected_coords', 'only_measure_with_expected_coords', 'insert_by_location_name', 'previousStartNumber', 'previousEndNumber', 'previousStartLetter', 'previousEndLetter'); + disp(['error_distance: ', num2str(error_distance)]); + disp(['double_tap_error_enabled: ', num2str(double_tap_error_enabled)]); + disp(['atlas_dir: ', atlas_dir]); + disp(['save_expected_coords: ', num2str(save_expected_coords)]); + disp(['only_measure_with_expected_coords: ', num2str(only_measure_with_expected_coords)]); + disp(['insert_by_location_name: ', insert_by_location_name]); + disp(['previousStartNumber: ', previousStartNumber]); + disp(['previousEndNumber: ', previousEndNumber]); + disp(['previousStartLetter: ', previousStartLetter]); + disp(['previousEndLetter: ', previousEndLetter]); +catch + uiwait(errordlg('Settings missing or not found, falling back on default settings.','Settings Error')); + try + load('default_settings.mat','error_distance','double_tap_error_enabled','atlas_dir','save_expected_coords','only_measure_with_expected_coords', 'insert_by_location_name', 'previousStartNumber', 'previousEndNumber', 'previousStartLetter', 'previousEndLetter'); + disp(['error_distance: ', num2str(error_distance)]); + disp(['double_tap_error_enabled: ', num2str(double_tap_error_enabled)]); + if ~isdeployed + atlas_dir = fullfile(pwd, atlas_dir); + else + atlas_dir = fullfile(ctfroot, atlas_dir); + end + disp(['atlas_dir: ', atlas_dir]); + disp(['save_expected_coords: ', num2str(save_expected_coords)]); + disp(['only_measure_with_expected_coords: ', num2str(only_measure_with_expected_coords)]); + disp(['insert_by_location_name: ', insert_by_location_name]); + disp(['previousStartNumber: ', previousStartNumber]); + disp(['previousEndNumber: ', previousEndNumber]); + disp(['previousStartLetter: ', previousStartLetter]); + disp(['previousEndLetter: ', previousEndLetter]); + catch + uiwait(errordlg('Default settings missing or not found! Quitting.','Settings Error')); + %Quit the gui + guidata(hObject, handles); + CloseFcn(hObject,eventdata,handles); + return + end +end + +handles.error_distance = error_distance; +handles.double_tap_error_enabled = double_tap_error_enabled; +handles.save_expected_coords = save_expected_coords; +handles.only_measure_with_expected_coords = only_measure_with_expected_coords; +handles.menu_options_insert_by_location_name.Checked = insert_by_location_name; +if strcmp(insert_by_location_name, 'on') + handles.menu_options_insert_by_number.Checked = 'off'; +else + handles.menu_options_insert_by_number.Checked = 'on'; +end +handles.previousStartNumber = previousStartNumber; +handles.previousEndNumber = previousEndNumber; +handles.previousStartLetter = previousStartLetter; +handles.previousEndLetter = previousEndLetter; + +%Import atlas +noatlas = true; +isdefault = false; +while noatlas + try + %load other data needed for headpoint plotting - these are the required + %names after getting atlas_dir from settings.mat + [landmarks, landmark_names, mesh] = load_atlas_data(atlas_dir); + if good_atlas_data(landmarks, landmark_names, mesh) + handles.atlas_dir = atlas_dir; + handles.AtlasLandmarks = landmarks; + handles.AtlasLandmarkNames = landmark_names; + handles.mesh = mesh; + noatlas = false; + end + catch + try + disp('Bad atlas directory, using default'); + load(which('default_settings.mat'), 'atlas_dir'); + if ~isdeployed + atlas_dir = fullfile(pwd, atlas_dir); + else + atlas_dir = fullfile(ctfroot, atlas_dir); + end + disp(['atlas_dir: ', atlas_dir]); + disp(['is default atlas dir? ' num2str(isdefault)]); + assert(~isdefault); + isdefault = true; + catch + uiwait(errordlg('Default atlas directory missing or not found! Quitting.','Settings Error')); + %Quit the gui + guidata(hObject, handles); + CloseFcn(hObject,eventdata,handles); + return + end + end +end + +% Include the atlas directory to get necessary functions +addpath(handles.atlas_dir); + %--------------------HEADPOINTS TO DIGITISE INPUT----------------------- +if ~isdeployed + saved_location_names_loc = fullfile(pwd, 'savedLocationNames.mat'); +else + saved_location_names_loc = fullfile(ctfroot, 'DIGIGUI', 'savedLocationNames.mat'); +end try - load(which('savedLocationNames.mat'),'locations'); + disp(['looking for savedLocationNames.mat at ', saved_location_names_loc]) + load(saved_location_names_loc, 'locations'); catch uiwait(warndlg('Could not find previously used location list.',... 'Location Warning','modal')); - + % Ask user to load a location list file. Note that the default deployed % location is handles.currentDir instead of handles.userDir since the - % example locations list file can be found in the executable directory + % example locations list file can be found in the executable directory % (handles.currentDir). if ~isdeployed - [filename,pathname] = ... + [filename,pathname] = ... uigetfile({'*.txt;*.dat;*.csv', ... 'Text Files (*.txt) (*.dat) (*.csv)'} ... ,['Select Location List File - Each Measurement Point' ... ' should be on a New Line']); else - [filename,pathname] = ... + [filename,pathname] = ... uigetfile({'*.txt;*.dat;*.csv', ... 'Text Files (*.txt) (*.dat) (*.csv)'} ... ,['Select Location List File - Each Measurement Point' ... @@ -251,31 +393,20 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) locations = textscan(FileID,'%s','delimiter','\n'); % append to list of reference points and convert to string array - locations = ['Nasion';'Inion';'Ar';'Al';'Cz'; ... - locations{1,1}]; + locations = [handles.AtlasLandmarkNames; locations{1,1}]; % Close file fclose(FileID); % Save locations variable to be loaded next time - if ~isdeployed - save('savedLocationNames.mat','locations'); - else - save(fullfile(ctfroot,'savedLocationNames.mat'),'locations'); - end - + disp(['Saving locations to: ', saved_location_names_loc]); + save(saved_location_names_loc,'locations'); end -%load other data needed for headpoint plotting -handles.AtlasLandmarks = load('refpts_landmarks.mat'); -handles.AtlasLandmarks = handles.AtlasLandmarks.pts; -handles.mesh = load('scalpSurfaceMesh.mat'); -handles.mesh = handles.mesh.mesh; - %error test the first serial port functions... -try +try %------------------------SERIAL CALLBACK SETUP--------------------- - %setup callback function to run when the polhemus system sends the + %setup callback function to run when the polhemus system sends the %number of bytes assosciated with one or two sensors. NB: the stated %number of bytes is generally position data. if(handles.sensors == 1) @@ -324,9 +455,84 @@ function DIGIGUI_OpeningFcn(hObject, eventdata, handles, varargin) ylabel(handles.coord_plot,'Y'); zlabel(handles.coord_plot,'Z'); + +%--------------------LOAD EXPECTED COORDS--------------------------------- +if ~isdeployed + saved_expected_coords_loc = fullfile(pwd, 'saved_expected_coords.mat'); +else + saved_expected_coords_loc = fullfile(ctfroot, 'DIGIGUI', 'saved_expected_coords.mat'); +end +if handles.save_expected_coords + try + disp(['Looking for expected_coords at: ', saved_expected_coords_loc]); + disp(['Looking for expected_coords_tolerance at: ', saved_expected_coords_loc]); + load(saved_expected_coords_loc); + handles = load_expected_coords(handles, expected_coords, expected_coords_tolerance); + handles.expected_coords = expected_coords; + handles.expected_coords_tolerance = expected_coords_tolerance; + catch + uiwait(warndlg('Could not load the expected coordinates and tolerance from saved_expected_coords.mat.',... + 'Expected Coordinates Warning','modal')); + if isfield(handles, 'expected_coords') + handles = rmfield(handles, 'expected_coords'); + end + if isfield(handles, 'expected_coords_tolerance') + handles = rmfield(handles, 'expected_coords_tolerance'); + end + handles = remove_expected_coords(handles); + end +else + % remove the saved file to avoid confusion + if exist(saved_expected_coords_loc, 'file')==2 + delete(saved_expected_coords_loc); + end +end + +% Get the checkmark icon for use when checking if coordinates match +handles.checkmark_icon = imread('checkmark.tif'); + % Update handles structure guidata(hObject, handles); - + + + + +% --- Import data from the given atlas directory. The directory must contain a +% file called mesh.mat which contains a struct called `mesh' within which there +% is mesh information (see the default example) and a file called +% atlas_landmarks.mat within which there is an array of landmarks called `pts' +% and a cell list of strings called `names'. +function [landmarks, landmark_names, mesh] = load_atlas_data(atlas_dir) + +disp(['Loading atlas_landmarks.mat from: ', fullfile(atlas_dir, 'atlas_landmarks.mat')]) +landmarks = load(fullfile(atlas_dir, 'atlas_landmarks.mat')); +landmark_names = landmarks.names; +landmarks = landmarks.pts; +disp(['Loading mesh.mat from: ', fullfile(atlas_dir, 'mesh.mat')]); +mesh = load(fullfile(atlas_dir, 'mesh.mat')); +mesh = mesh.mesh; + + + + +% --- Check atlas data matches requried specification +function [good] = good_atlas_data(landmarks, landmark_names, mesh) + +good = false; +if size(landmarks, 1) < 4 + errordlg('Too few landmark points in atlas data!', 'atlas_landmarks.mat'); +elseif size(landmark_names, 1) ~= size(landmarks, 1) + errordlg('Too few/many landmark names!', 'atlas_landmarks.mat'); +elseif ~all([mesh.nnode, 3] == size(mesh.node)) + errordlg('Node matrix should be nnode by 3 matrix!', 'mesh.mat'); +elseif 3 ~= size(mesh.face, 2) + errordlg('Face matrix should be an m by 3 matrix!', 'mesh.mat'); +elseif any(max(mesh.face)) > mesh.nnode + errordlg('Face matrix contains indices which exceed the dimensions of the node matrix!', 'mesh.mat'); +else + good = true; +end + @@ -336,65 +542,83 @@ function HeadAlign_Callback(hObject, eventdata, handles) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) -if(handles.point_count >= 5) - +% handles.landmark_measurements is dynamically grown so we can always +% do an alignment when it is fully populated +if(size(handles.landmark_measurements, 1) == size(handles.AtlasLandmarks, 1)) + % extract the locations locations = get(handles.coords_table,'Data'); - - % extract the landmark locations (the first five data points)... - landmarks = locations(1:5,2:4); - % ... and convert to ordinary array from cell array - landmarks = cell2mat(landmarks); - + measurements = cell2mat(locations(:, 2:4)); + %get transformation matrix to new coord system - [TransformMatrix,TransformVector] = GetCoordTransform(landmarks); - %save tranformation - handles.TransformMatrix = TransformMatrix; - handles.TransformVector = TransformVector; - - % reset list of points to just show locations to find so transformed + [TransformMatrix,TransformVector] = CoordinateTransform(handles.landmark_measurements); + + % reset list of points to just show locations to find so transformed % points can be plotted - locations = get(handles.coords_table,'Data'); - + + hold on - - for k = 1:size(landmarks,1) + + for k = 1:size(measurements, 1) + if isfield(handles, 'TransformMatrix') + % undo old transform + measurements(k,:) = measurements(k,:) * handles.TransformMatrix; + measurements(k,:) = measurements(k,:) - handles.TransformVector; + end + %transform cardinal points - landmarks(k,:) = landmarks(k,:) + TransformVector; - landmarks(k,:) = landmarks(k,:)*TransformMatrix'; + measurements(k,:) = measurements(k,:) + TransformVector; + measurements(k,:) = measurements(k,:)*TransformMatrix'; %remove old point from graph delete(handles.pointhandle(k)); - + %replot point - handles.pointhandle(k) = plot3(landmarks(k,1), ... - landmarks(k,2), ... - landmarks(k,3), ... - 'm.', 'MarkerSize', 30, ... - 'Parent' , handles.coord_plot); - + if k <= size(handles.AtlasLandmarks, 1) + % plot as landmark + handles.pointhandle(k) = plot3(measurements(k,1), ... + measurements(k,2), ... + measurements(k,3), ... + 'm.', 'MarkerSize', 30, ... + 'Parent' , handles.coord_plot); + else + % plot as measurement + handles.pointhandle(k) = plot3(measurements(k,1), ... + measurements(k,2), ... + measurements(k,3), ... + 'b.', 'MarkerSize', 30, ... + 'Parent' , handles.coord_plot); + end + %replot axes... axis(handles.coord_plot,'equal'); - - %update newly transformed cardinal point coords (converting back + + %update newly transformed point coords (converting back %to a cell array first) - locations(k,2:4) = num2cell(landmarks(k,1:3)); - + locations(k,2:4) = num2cell(measurements(k,1:3)); + end - + hold off - - + + % Show newly transformed cardinal point coords on table set(handles.coords_table,'Data',locations); - + + transformed_landmarks = measurements(1:size(handles.AtlasLandmarks, 1), :); + %find matrix (A) and vector (B) needed to map head to cardinal points %with affine transformation - [A,B] = affinemap(handles.AtlasLandmarks,landmarks); + [A,B] = affinemap(handles.AtlasLandmarks, transformed_landmarks); mesh_trans = handles.mesh; mesh_trans.node = affine_trans_RJC(handles.mesh.node,A,B); + % Remove any existing head plot + if isfield(handles, 'headplot') + delete(handles.headplot) + end + %Then plot the transformed mesh as visual reference for further points... %note: this plots the head model hold on @@ -404,96 +628,108 @@ function HeadAlign_Callback(hObject, eventdata, handles) 'FaceColor',[239/255 208/255 207/255], ... (skin tone rgb vals) 'EdgeColor','none', ... 'Parent',handles.coord_plot); - + % set lighting of head - light; - lighting gouraud; + if ~isfield(handles, 'plotlight') + handles.plotlight = light; + end + set(handles.headplot, 'FaceLighting', 'gouraud'); + axis equal; hold off; - - - % disable headalign button - set(hObject,'Enable','off'); - + + %save tranformation + handles.TransformMatrix = TransformMatrix; + handles.TransformVector = TransformVector; + % Update handles structure guidata(hObject, handles); end +function save_settings(handles) -%This function outputs a vector to transform inion to origin -%also outputs rotation transformation matrix to allign the head model to -%intuitive coordinates. -%To apply to row vector, the vector should be multiplied by the transpose -%with the vector on the left -function [Matrix,vector] = GetCoordTransform(landmarks) -%calculate lengths between vectors - -%these are the untransformed reference points, defined here in case I want -%to use them -Nasion = landmarks(1,:); -Inion = landmarks(2,:); -Ar = landmarks(3,:); -Al = landmarks(4,:); -Cz = landmarks(5,:); - -%------TRANSLATION------- - -%translate inion to origion -vector = -Inion; - -%translate Al -Al = Al + vector; - -%------ROTATE AL TO Y AXIS------- - -%calculate rotation to y axis -AlToYAxisRot = vrrotvec(Al,[0,1,0]); - -%convert to rotation matrix -AlToYAxisMatrix = vrrotvec2mat(AlToYAxisRot); - -%repmat -% Apply translation and rotation to points -for k = 1:5 - landmarks(k,:) = landmarks(k,:)+vector; - landmarks(k,:) = landmarks(k,:)*AlToYAxisMatrix'; +if ~isdeployed + settings_loc = fullfile(pwd, 'settings.mat'); +else + settings_loc = fullfile(ctfroot, 'DIGIGUI', 'settings.mat'); end -%------ROTATE AR TO XY PLANE ABOUT INION-AL AXIS------- - -% find angle to rotate nasion into XY plane about the new y axis -[ArToXYRotAngle,~] = cart2pol(landmarks(3,1),landmarks(3,3)); - -%Find second rotation matrix -ArToXYMatrix = vrrotvec2mat([0,1,0,ArToXYRotAngle]); - -% Apply second rotation to points -for k = 1:5 - landmarks(k,:) = landmarks(k,:)*ArToXYMatrix'; +% save settings +if isfield(handles, 'atlas_dir') + disp(['Saving atlas_dir to ', settings_loc]); + atlas_dir = handles.atlas_dir; + save(settings_loc, 'atlas_dir'); +end +if isfield(handles, 'error_distance') + disp(['Saving error_distance to ', settings_loc]); + error_distance = handles.error_distance; + save(settings_loc, 'error_distance', '-append'); +end +if isfield(handles, 'double_tap_error_enabled') + disp(['Saving double_tap_error_enabled to ', settings_loc]); + double_tap_error_enabled = handles.double_tap_error_enabled; + save(settings_loc, 'double_tap_error_enabled', '-append'); +end +if isfield(handles, 'save_expected_coords') + disp(['Saving save_expected_coords to ', settings_loc]); + save_expected_coords = handles.save_expected_coords; + save(settings_loc, 'save_expected_coords', '-append'); +end +if isfield(handles, 'only_measure_with_expected_coords') + disp(['Saving only_measure_with_expected_coords to ', settings_loc]); + only_measure_with_expected_coords = handles.only_measure_with_expected_coords; + save(settings_loc, 'only_measure_with_expected_coords', '-append'); +end +disp(['Saving insert_by_location_name to ', settings_loc]); +insert_by_location_name = handles.menu_options_insert_by_location_name.Checked +save(settings_loc, 'insert_by_location_name', '-append'); +if isfield(handles, 'previousStartNumber') + disp(['Saving previousStartNumber to ', settings_loc]); + previousStartNumber = handles.previousStartNumber; + save(settings_loc, 'previousStartNumber', '-append'); +end +if isfield(handles, 'previousEndNumber') + disp(['Saving previousEndNumber to ', settings_loc]); + previousEndNumber = handles.previousEndNumber; + save(settings_loc, 'previousEndNumber', '-append'); +end +if isfield(handles, 'previousStartLetter') + disp(['Saving previousStartLetter to ', settings_loc]); + previousStartLetter = handles.previousStartLetter; + save(settings_loc, 'previousStartLetter', '-append'); +end +if isfield(handles, 'previousEndLetter') + disp(['Saving previousEndLetter to ', settings_loc]); + previousEndLetter = handles.previousEndLetter; + save(settings_loc, 'previousEndLetter', '-append'); end - -%------FINAL ROTATION ABOUT AL-AR AXIS TO ALLIGN INION AND NASION------- - -%find angle of nasion to xy plane -[~,NasionToXYRotAngle,~] = cart2sph(landmarks(1,1),landmarks(1,2),landmarks(1,3)); -%define vector to rotate around (the line joining AL and AR) -NasionRotVector = landmarks(4,:) - landmarks(3,:); -%find rotation matrix -NasionToXYRotMatrix = vrrotvec2mat([NasionRotVector, NasionToXYRotAngle]); - -%------OUTPUT FINAL MATRIX------- -Matrix = NasionToXYRotMatrix*ArToXYMatrix*AlToYAxisMatrix; - +function save_locations(handles) +if handles.editedLocationsList + choice = questdlg('The locations list has been edited. Do you want to save it for loading next time or forget it? If you choose to forget it, the last saved locations list will be used when DIGIGUI is next loaded.', ... + 'Locations List Edited', ... + 'Save Locations List','Forget Locations List','Forget Locations List'); +else + % just in case, we've missed an edit somewhere we'll save it anyway! + choice = 'Forget Locations List'; +end +if strcmp(choice, 'Forget Locations List') + % Save locations variable to be loaded next time + if ~isdeployed + saved_location_names_loc = fullfile(pwd,'savedLocationNames.mat'); + else + saved_location_names_loc = fullfile(ctfroot, 'DIGIGUI', 'savedLocationNames.mat'); + end + locations = handles.coords_table.Data(:, 1); + disp(['Saving locations to: ', saved_location_names_loc]); + save(saved_location_names_loc,'locations'); +end -function CloseFcn(source,event,handles) -%my user-defined close request function -%closes the serial port -handles = guidata(handles.figure1); +function handles = close_serial_port(handles) %close port only if not closed if(isfield(handles,'COMport')) @@ -505,6 +741,22 @@ function CloseFcn(source,event,handles) end end +return + + +function CloseFcn(source,event,handles) +%my user-defined close request function + +try + handles = guidata(handles.figure1); +catch + handles = struct([]); +end + +save_settings(handles); +save_locations(handles); +close_serial_port(handles); + delete(gcf); @@ -520,95 +772,227 @@ function ReadCoordsCallback(s,BytesAvailable,handles) if(handles.sensors == 2) data_str(2,:) = fgetl(s); end - -%don't run most of the callback if waiting to do alignment... -if(handles.point_count == 5 && ... - strcmp(get(handles.HeadAlign,'Enable'),'on') && ... - handles.disable_measurements == false) - % Warn user that points aren't collected until alignment done - warndlg('Atlas points must be aligned before continuing.',... - 'Measurement Issue...','modal'); -elseif(handles.disable_measurements == false) - %increment the point count before measurement - handles.point_count = handles.point_count + 1; - - data_num=str2num(data_str); - - % Format of data obtained for the current settings - % 1 2 3 4 5 6 7 - % 1 Detector Number (should be 1 for 1 stylus) - % 2 X position in cms - % 3 Y position in cms - % 4 Z position in cms - % 5 Azimuth of stylus in degrees - % 6 Elevation of stylus in degrees - % 7 Roll of stylus degrees - - % extract coords - Coords = data_num(:,2:4); - - % if there are 2 sensors do vector subtraction to get position of - % stylus sensor relative to second sensor - if(handles.sensors == 2) - Coords = Coords(1,:) - Coords(2,:); + +if isfield(handles, 'disable_measurements') && handles.disable_measurements + return +end +% Don't measure if we have a double tap error open +if isfield(handles, 'doubleTapErrorFigure') && isvalid(handles.doubleTapErrorFigure) + return +end +if (... + isfield(handles, 'only_measure_with_expected_coords') && ... + handles.only_measure_with_expected_coords && ... + ~isfield(handles, 'expected_coords') ... +) + msg = 'Measurements cannot be made until a list of expected coordinates has been imported! This error can be disabled in the settings.'; + errordlg(msg, 'Missing Expected Coordinates', 'modal'); + return +end + + +data_num=str2num(data_str); + +% Format of data obtained for the current settings +% 1 2 3 4 5 6 7 +% 1 Detector Number (should be 1 for 1 stylus) +% 2 X position in cms +% 3 Y position in cms +% 4 Z position in cms +% 5 Azimuth of stylus in degrees +% 6 Elevation of stylus in degrees +% 7 Roll of stylus degrees + +% extract coords +Coords = data_num(:,2:4); + +% if there are 2 sensors do vector subtraction to get position of +% stylus sensor relative to second sensor +if(handles.sensors == 2) + Coords = Coords(1,:) - Coords(2,:); +end + +% Extract previous data from table +data = get(handles.coords_table,'Data'); + +%increment the point count to the next unmeasured point before measurement +previous_point_count = handles.point_count; +at_unmeasured_point = false; +while ~at_unmeasured_point + try + handles.point_count = handles.point_count + 1; + next_x_coord = data(handles.point_count, 2); + if isempty(next_x_coord{1}) + at_unmeasured_point = true; + end + catch + % will get 'Index exceeds matrix dimensions' error for very first + % measurement since we have no cells in column 2 yet + at_unmeasured_point = true; end +end +point_count_increment = handles.point_count - previous_point_count; - % disable head alignment butten for first five points (they are the - % landmark positions) - if(handles.point_count < 5) - set(handles.HeadAlign,'Enable','off'); - % enable head allign after 5 points... - elseif(handles.point_count == 5) - set(handles.HeadAlign,'Enable','on'); - % Do coord transform on points measured after landmark points - else - Coords = Coords + handles.TransformVector; - Coords = Coords*handles.TransformMatrix'; +% Save original coordinates of landmark positions if measuring those +if(handles.point_count <= size(handles.AtlasLandmarks, 1)) + handles.landmark_measurements(handles.point_count, :) = Coords; +end +% disable head alignment butten until we have all landmark positions. +% Note that landmark_measurements is a dynamically resized array so +% the below check works! +if ~isfield(handles, 'landmark_measurements') || size(handles.landmark_measurements, 1) < size(handles.AtlasLandmarks, 1) + set(handles.HeadAlign,'Enable','off'); +else + set(handles.HeadAlign,'Enable','on'); +end +% Do coord transform on points measured after alignment - i.e. if +% there is a head plotted. +if isfield(handles, 'headplot') + Coords = Coords + handles.TransformVector; + Coords = Coords*handles.TransformMatrix'; +end + +location_list_expanded = false; +% Check if table is currently full - if it is then adding a new point +% will expand the table... +if(handles.point_count > size(data,1)) + % ... so update the bool that tracks if location names have been + % edited. When the user saves their data they will therefore be + % prompted to save the locations list too. + handles.editedLocationsList = true; + location_list_expanded = true; +end + +% Update table with newly measured x y and z values +data(handles.point_count,2:4) = num2cell(Coords); +set(handles.coords_table,'Data',data); + +% Double tap warning... +if handles.double_tap_error_enabled && handles.point_count > 1 + % extract the previous measured point for comparison + last_point = cell2mat(data(handles.point_count-point_count_increment, 2:4)); + % If our last point is empty then we can't do a comparison. This + % will happen when you select a different row to measure, in which + % case a double tap isn't going to happen + if length(last_point) == 3 + distance = norm(Coords - last_point); + if distance < handles.error_distance + last_point = data(handles.point_count-point_count_increment, 1); + this_point = data(handles.point_count, 1); + msg = sprintf('%s measurement was only %0.2g cm from %s measurement!\nCurrent error distance is %0.2g cm. This can be changed or disabled in the options.', this_point{1}, distance, last_point{1}, handles.error_distance); + handles.doubleTapErrorFigure = errordlg(msg, 'Double tap error', 'modal'); + if location_list_expanded + % delete row + data(handles.point_count, :) = []; + else + % remove data, restore measurement status and distance + data(handles.point_count,2:4) = {[], [], []}; + if size(data, 2) > 4 && isfield(handles, 'expected_coords') + measurement_status = data(handles.point_count, 8); + measurement_status = measurement_status{1}; + if ~isempty(measurement_status) + data(handles.point_count, 8) = {'Unmeasured'}; + end + data(handles.point_count, 10) = {[]}; + end + end + set(handles.coords_table,'Data',data); + % reset point count, update guidata and exit + handles.point_count = previous_point_count; + guidata(handles.figure1,handles); + return + end end +end - % Extract previous data from table - data = get(handles.coords_table,'Data'); - - % Check if table is currently full - if it is then adding a new point - % will expand the table... - if(handles.point_count > size(data,1)) - % ... so update the bool that tracks if location names have been - % edited. When the user saves their data they will therefore be - % prompted to save the locations list too. - handles.editedLocationsList = true; +% Remove any open expected or unexpected measurement figures +if isfield(handles, 'expectedMeasurementFigure') && isvalid(handles.expectedMeasurementFigure) + close(handles.expectedMeasurementFigure) +end +if isfield(handles, 'unexpectedMeasurementWarnFigure') && isvalid(handles.unexpectedMeasurementWarnFigure) + close(handles.unexpectedMeasurementWarnFigure) +end +% Check against expected coordinates if any are specified +if isfield(handles, 'expected_coords') + rowmatch = find(strcmp(data{handles.point_count, 1}, handles.expected_coords.Properties.RowNames)); + if ~isempty(rowmatch) + distance = norm(Coords - handles.expected_coords(rowmatch, :).Variables); + this_point = data(handles.point_count, 1); + if distance > handles.expected_coords_tolerance + msg = sprintf('%s measurement is %0.2g cm from the expected location of (%0.2g, %0.2g, %0.2g) cm!\nCurrent tolerance is %0.2g cm. Tolerance is set in the expected coordinates file.', this_point{1}, distance, handles.expected_coords(rowmatch, 1).Variables, handles.expected_coords(rowmatch, 2).Variables, handles.expected_coords(rowmatch, 3).Variables, handles.expected_coords_tolerance); + handles.unexpectedMeasurementWarnFigure = warndlg(msg, 'Unexpected Measurement!'); + % Set measurement status... + data(handles.point_count, 8) = {'No'}; + else + msg = sprintf('%s measurement is within tolerance of (%0.2g, %0.2g, %0.2g) cm.\nCurrent tolerance is %0.2g cm. Tolerance is set in the expected coordinates file.', this_point{1}, handles.expected_coords(rowmatch, 1).Variables, handles.expected_coords(rowmatch, 2).Variables, handles.expected_coords(rowmatch, 3).Variables, handles.expected_coords_tolerance); + handles.expectedMeasurementFigure = msgbox(msg, 'Expected Measurement Success!', 'custom', handles.checkmark_icon); + % Set measurement status... + data(handles.point_count, 8) = {'Yes'}; + end + % Display distance + data(handles.point_count, 10) = {distance}; + set(handles.coords_table,'Data',data); end - - % Update table with newly measured x y and z values - data(handles.point_count,2:4) = num2cell(Coords); - set(handles.coords_table,'Data',data); +end - % update point to look for (unless at end of list as given by the - % length of data - ie the number of headpoints) - if( handles.point_count < size(data,1) ) - set(handles.infobox,'string', data(handles.point_count+1,1)); - % (Set to the next position on the table) +% update point to look for (unless at end of list as given by the +% length of data - ie the number of headpoints) +if( handles.point_count < size(data,1) ) + % search for next unmeasured point + point_count_increment = 0; + at_unmeasured_point = false; + while ~at_unmeasured_point + try + point_count_increment = point_count_increment + 1; + next_x_coord = data(handles.point_count+point_count_increment, 2); + if isempty(next_x_coord{1}) + at_unmeasured_point = true; + end + catch + % will get 'Index exceeds matrix dimensions' error for very first + % measurement since we have no cells in column 2 yet + at_unmeasured_point = true; + end + end + if handles.point_count+point_count_increment <= size(data,1) + set(handles.infobox,'string', data(handles.point_count+point_count_increment,1)); else set(handles.infobox,'string','End of locations list reached'); - end - - %add the measured point to the 3d graph - hold(handles.coord_plot,'on'); - %save the handle of the point so it can be removed later... - if(handles.point_count <= 5) - handles.pointhandle(handles.point_count) = plot3(Coords(1), ... - Coords(2),Coords(3), ... - 'm.', 'MarkerSize', 30, ... - 'Parent' , handles.coord_plot); - else %Note: above marker points are plotted differently - handles.pointhandle(handles.point_count) = plot3(Coords(1), ... - Coords(2),Coords(3), ... - 'b.', 'MarkerSize', 30, ... - 'Parent',handles.coord_plot); end - hold(handles.coord_plot,'off'); - %replot axes... - axis(handles.coord_plot,'equal'); +else + set(handles.infobox,'string','End of locations list reached'); +end + + +% Remove point from graph if present... +if isfield(handles, 'pointhandle') + if handles.point_count <= size(handles.pointhandle, 2) + delete(handles.pointhandle(handles.point_count)); + % and replot axes. + axis(handles.coord_plot,'equal'); + end +end + +%add the measured point to the 3d graph +hold(handles.coord_plot,'on'); +%save the handle of the point so it can be removed later... +if(handles.point_count <= size(handles.AtlasLandmarks, 1)) + handles.pointhandle(handles.point_count) = plot3(Coords(1), ... + Coords(2),Coords(3), ... + 'm.', 'MarkerSize', 30, ... + 'Parent' , handles.coord_plot); +else %Note: above marker points are plotted differently + handles.pointhandle(handles.point_count) = plot3(Coords(1), ... + Coords(2),Coords(3), ... + 'b.', 'MarkerSize', 30, ... + 'Parent',handles.coord_plot); end +hold(handles.coord_plot,'off'); +%replot axes... +axis(handles.coord_plot,'equal'); + +% save the point_count so it can be undone +handles.point_count_history(end+1) = handles.point_count; % Update handles structure guidata(handles.figure1,handles); @@ -621,41 +1005,47 @@ function remove_last_pt_Callback(hObject, eventdata, handles) % handles structure with handles and user data (see GUIDATA) %don't delete points if alignment already done or at first point -if (handles.point_count ~= 0) - if(handles.point_count ~= 5 || strcmp(get(handles.HeadAlign,'Enable'),'on') ) +if ~isempty(handles.point_count_history) - data = get(handles.coords_table,'Data'); - - % Set the last measured values of x, y and z to be empty cells - data{handles.point_count,2} = []; % x - data{handles.point_count,3} = []; % y - data{handles.point_count,4} = []; % z - - set(handles.coords_table,'Data',data); + data = get(handles.coords_table,'Data'); - % Remove point from graph... - delete(handles.pointhandle(handles.point_count)); - % and replot axes. - axis(handles.coord_plot,'equal'); - - % Decrement point_count so next measurement is of the point which - % has just been deleted - handles.point_count = handles.point_count - 1; - - data = get(handles.coords_table,'Data'); - - % update next point to look for string - set(handles.infobox,'string', data(handles.point_count+1,1)); - - % Disable align if now not enough points - if(handles.point_count <= 5) - set(handles.HeadAlign,'Enable','off'); + last_point_count = handles.point_count_history(end); + handles.point_count_history(end) = []; + + % remove data, restore measurement status and distance + data(last_point_count,2:4) = {[], [], []}; + if size(data, 2) > 4 && isfield(handles, 'expected_coords') + measurement_status = data(last_point_count, 8); + measurement_status = measurement_status{1}; + if ~isempty(measurement_status) + data(last_point_count, 8) = {'Unmeasured'}; end + data(last_point_count, 10) = {[]}; + end + set(handles.coords_table,'Data',data); + + % Remove point from graph... + delete(handles.pointhandle(last_point_count)); + % and replot axes. + axis(handles.coord_plot,'equal'); + + % Reset point_count so next measurement is of the point which + % has just been deleted + handles.point_count = last_point_count-1; + + data = get(handles.coords_table,'Data'); - % Update handles structure - guidata(handles.figure1,handles); + % update next point to look for string + set(handles.infobox,'string', data(handles.point_count+1,1)); + % Disable align if now not enough points + if ~all(ismember(1:size(handles.AtlasLandmarks, 1), handles.point_count_history)) + set(handles.HeadAlign,'Enable','off'); end + + % Update handles structure + guidata(handles.figure1,handles); + end @@ -678,26 +1068,26 @@ function save_Callback(hObject, eventdata, handles) % Open a "Save As..." Dialogue with different saving options as shown. % The filterIndex gives the index (1, 2 or 3) of the chosen save type. if ~isdeployed - [fileName,pathName,filterIndex] = ... - uiputfile({'*.csv;*.dat;*.txt', ... + [fileName,pathName,filterIndex] = ... + uiputfile({'*.csv;*.dat;*.txt', ... 'Comma-delimited text files (*.csv) (*.dat) (*.txt)'; ... ... '*.mat', ... 'MAT-file (*.mat)'; ... ... '*.xls;*.xlsb;*.xlsm;*.xlsx', ... - 'Excel® spreadsheet files (*.xls) (*.xlsb) (*.xlsm) (*.xlsx)'; ... + 'Excel� spreadsheet files (*.xls) (*.xlsb) (*.xlsm) (*.xlsx)'; ... },'Save As...'); else - [fileName,pathName,filterIndex] = ... - uiputfile({'*.csv;*.dat;*.txt', ... + [fileName,pathName,filterIndex] = ... + uiputfile({'*.csv;*.dat;*.txt', ... 'Comma-delimited text files (*.csv) (*.dat) (*.txt)'; ... ... '*.mat', ... 'MAT-file (*.mat)'; ... ... '*.xls;*.xlsb;*.xlsm;*.xlsx', ... - 'Excel® spreadsheet files (*.xls) (*.xlsb) (*.xlsm) (*.xlsx)'; ... + 'Excel� spreadsheet files (*.xls) (*.xlsb) (*.xlsm) (*.xlsx)'; ... },'Save As...',handles.userDir); end @@ -709,7 +1099,7 @@ function save_Callback(hObject, eventdata, handles) guidata(hObject,handles); if(filterIndex ~= 0) % if == 0 then user selected "cancel" in "Save As" - + data = get(handles.coords_table,'Data'); % check data cell array has same number of columns as there are column @@ -717,22 +1107,23 @@ function save_Callback(hObject, eventdata, handles) if(size(data,2) < length(get(handles.coords_table,'ColumnName'))) % dont save table if not enough data is available - errordlg('Cannot save without recorded location data.', ... + errordlg('Cannot save without recorded location data.', ... 'Save Error','modal'); % exit function here guidata(hObject,handles); return; end - + % If the chosen save type is .mat then use a standard matlab save command if(filterIndex == 2) disp(['Data saving to ' pathName fileName]); dataOutput = get(handles.coords_table,'Data'); - save([pathName fileName],'dataOutput'); - disp('Data is stored in cell array "dataOutput"'); - - % Otherwise create a table from the cell array and output that to file. + dataOutputHeadings = handles.coords_table.ColumnName; + save([pathName fileName],'dataOutput', 'dataOutputHeadings'); + disp('Data is stored in cell array "dataOutput" with headings in "dataOutputHeadings"'); + + % Otherwise create a table from the cell array and output that to file. else % find any empty cells in Locations data emptyLocationNames = cellfun('isempty',data(:,1)); @@ -743,7 +1134,7 @@ function save_Callback(hObject, eventdata, handles) buttonPressed = questdlg({'Some location names are unspecified.'; 'Missing location names will be replaced by the symbol "-".';... 'Would you like to continue?'},... - 'Warning','Yes','No','Yes'); + 'Warning','Yes','No','Yes'); end %Only save data if user presses Yes or Yes has been set previously. @@ -752,16 +1143,18 @@ function save_Callback(hObject, eventdata, handles) disp(['Data saving to ' pathName fileName]); %Mark empty location names as '-' - data(emptyLocationNames,1) = {'-'}; + data(emptyLocationNames,1) = {'-'}; - tableToOutput = cell2table(data,'VariableNames', ... - get(handles.coords_table,'ColumnName')); + % replace spaces with underscores in column names + columnnames = regexprep(handles.coords_table.ColumnName, ' +', '_'); + + tableToOutput = cell2table(data,'VariableNames', columnnames); % Note that writetable changes its output depending on the fileName % type. writetable(tableToOutput,[pathName fileName]); end end - + if(handles.editedLocationsList) % true if edited % check if user wants to save locations list too button = 'No'; @@ -776,7 +1169,7 @@ function save_Callback(hObject, eventdata, handles) ExportHeadpoints_Callback(hObject, eventdata, handles); end end - + end guidata(hObject,handles); @@ -789,11 +1182,11 @@ function coords_table_CellSelectionCallback(hObject, eventdata, handles) % handles structure with handles and user data (see GUIDATA) if(isempty(eventdata.Indices)) - % delete 'selectedRow' field of 'handles' if the callback is triggered + % delete 'selectedRow' field of 'handles' if the callback is triggered % by deselection (eg by the removal or addition of a set of coordinates) handles = rmfield(handles,'selectedRow'); else - % extract the row from where the user clicked on the table. + % extract the row(s) from where the user clicked on the table. handles.selectedRow = eventdata.Indices(:,1); end guidata(hObject,handles); @@ -805,46 +1198,9 @@ function InsertRowPushbutton_Callback(hObject, eventdata, handles) % hObject handle to InsertRowPushbutton (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) +insert_rows_below(hObject, eventdata, handles, 1) -data = get(handles.coords_table,'Data'); -% See if the selectedRow variable exists within the handles struct -% (doesn't if no selection performed before clicking or cell has been -% deselected) -if(isfield(handles,'selectedRow')) - if(handles.selectedRow(end) < 5) - errordlg('Cannot insert or delete Atlas Points','Error','modal'); - else - % insert above topmost selected row... - row = handles.selectedRow(end); - dataBelowSelectedRow = data(row+1:end,:); - % add new row by adding a single rowed cell array - data(row+1,:) = cell(1,size(data,2)); - % add back the data that was saved before by concatenating below where - % the new row has been added. - data = [data(1:row+1,:) ; dataBelowSelectedRow]; - - % check if have added row within where measurement has already been - % made - if(handles.selectedRow(end) < handles.point_count) - % increment point count to account for 1 extra point - handles.point_count = handles.point_count + 1; - end - - % Locations list has now been edited so change bool. - handles.editedLocationsList = true; - - end -else -% % insert empty row at the end -% data{end+1,1} = []; - - % Tell user to select a row before inserting - errordlg('Please select a row to insert below.','Insert Error','modal'); -end -% save the newly changed data to the table on the gui -set(handles.coords_table,'Data',data); -guidata(hObject,handles); % --- Executes on button press in DeleteRowPushbutton. function DeleteRowPushbutton_Callback(hObject, eventdata, handles) @@ -854,42 +1210,50 @@ function DeleteRowPushbutton_Callback(hObject, eventdata, handles) data = get(handles.coords_table,'Data'); -% See if the selectedRow variable exists within the handles struct -% (doesn't if no selection performed before clicking or cell has been +% See if the selectedRow variable exists within the handles struct +% (doesn't if no selection performed before clicking or cell has been % deselected) if(isfield(handles,'selectedRow')) - if(handles.selectedRow(1) <= 5) + if(handles.selectedRow(1) <= size(handles.AtlasLandmarks, 1)) errordlg('Cannot insert or delete Atlas Points','Edit Error','modal'); else % delete selected rows... data(handles.selectedRow,:) = []; - + % Locations list has now been edited so change bool. handles.editedLocationsList = true; - - % check if have deleted any rows where measurements have already - % been made - if(any(handles.selectedRow <= handles.point_count)) - - % find out how many of the selected rows are less than the - % current point_count - numToDecrement = nnz(handles.selectedRow <= handles.point_count); - - % decrement point count to account for number of fewer points - handles.point_count = handles.point_count - numToDecrement; - - % Remove point from graph... - delete(handles.pointhandle(handles.point_count)); - % and replot axes. + + % make sure selected rows are in descending order so we can remove + % them from the point count history (we decrement as necessary) + descendingSelectedRows = sort(handles.selectedRow, 'descend'); + + numToDecrement = 0; + for i=1:length(descendingSelectedRows) + selectedRow = descendingSelectedRows(i); + [isin, idx] = ismember(selectedRow, handles.point_count_history); + if any(isin) + % remove point and decrement point numbers above + handles.point_count_history(idx) = []; + to_decrement = handles.point_count_history > selectedRow; + handles.point_count_history(to_decrement) = handles.point_count_history(to_decrement) - 1; + % Remove point from graph + delete(handles.pointhandle(selectedRow)); + % will need to change point count... + numToDecrement = numToDecrement + 1; + end + end + if numToDecrement > 0 + % replot axes axis(handles.coord_plot,'equal'); end - end + handles.point_count = handles.point_count - numToDecrement; + end else % Tell user to select a row before inserting - errordlg('Please select a row to delete.','Delete Error','modal'); + errordlg('Please select a row to delete.','Delete Error','modal'); end % save the newly changed data to the table on the gui -set(handles.coords_table,'Data',data); +set(handles.coords_table,'Data',data); guidata(hObject,handles); @@ -911,13 +1275,13 @@ function ImportHeadpoints_Callback(hObject, eventdata, handles) %--------------------HEADPOINTS TO DIGITISE INPUT----------------------- if ~isdeployed - [filename,pathname] = ... + [filename,pathname] = ... uigetfile({'*.txt;*.dat;*.csv', ... 'Text Files (*.txt) (*.dat) (*.csv)'} ... ,['Select Location List File - Each Measurement Point Should be'... ' on a New Line']); else - [filename,pathname] = ... + [filename,pathname] = ... uigetfile({'*.txt;*.dat;*.csv', ... 'Text Files (*.txt) (*.dat) (*.csv)'} ... ,['Select Location List File - Each Measurement Point Should be'... @@ -938,7 +1302,7 @@ function ImportHeadpoints_Callback(hObject, eventdata, handles) % Warn user that this will reset all currently gathered data if any has % been collected. if(handles.point_count > 0) - + button = 'No'; button = questdlg({'Warning! Any existing data will be lost.';... @@ -948,32 +1312,43 @@ function ImportHeadpoints_Callback(hObject, eventdata, handles) if strcmp(button,'No') return end - + end +% Warn user that this will reset any loaded expected coordinates +if isfield(handles, 'expected_coords') || isfield(handles, 'expected_coords_tolerance') -disp(['User selected ', fullfile(pathname, filename)]) + button = questdlg({'Warning! Any loaded expected coordinates will be lost.';... + 'Do you wish to continue?'},'Expected Coordinates Warning','Yes','No','No'); -% Open File -FileID = fopen([pathname filename]); + % user selected cancel... + if strcmp(button,'No') + return + end +end + +if isfield(handles, 'expected_coords') + handles = rmfield(handles, 'expected_coords'); +end +if isfield(handles, 'expected_coords_tolerance') + handles = rmfield(handles, 'expected_coords_tolerance'); +end +handles = remove_expected_coords(handles); + +disp(['User selected ', fullfile(pathname, filename)]) + +% Open File +FileID = fopen([pathname filename]); % locations is a local variable that holds location data in this % function locations = textscan(FileID,'%s','delimiter','\n'); % append to list of reference points and convert to string array -locations = ['Nasion';'Inion';'Ar';'Al';'Cz'; ... - locations{1,1}]; +locations = [handles.AtlasLandmarkNames; locations{1,1}]; % Close file fclose(FileID); -% Save locations variable to be loaded next time -if ~isdeployed - save('savedLocationNames.mat','locations'); -else - save(fullfile(ctfroot,'savedLocationNames.mat'),'locations'); -end - % Reset points counter handles.point_count = 0; @@ -993,10 +1368,8 @@ function ImportHeadpoints_Callback(hObject, eventdata, handles) % and replot axes. axis(handles.coord_plot,'equal'); -% Locations list is now unedited (since it's just been imported) so reset -% both bools that deal with whether bits of the location list are edited. -handles.editedLocationsList = false; -handles.editedAtlasPoints = false; +handles.editedLocationsList = true; +handles.editedAtlasPoints = true; guidata(hObject,handles); @@ -1017,8 +1390,31 @@ function ExportHeadpoints_Callback(hObject, eventdata, handles) % ... and turn them off. set(InterfaceObj,'Enable','off'); -% Display warning dialogue before uiputfile if the atlas points have been -% editied... +% Check that user is happy to continue +button = questdlg('Warning! This only exports location names (excluding atlas location names). To save all data, including all coordinates and atlas location names, use "Save Data As...". Do you wish to continue?' ... + ,'Export Warning','Yes','No','No'); +if strcmp(button, 'No') + % Re-enable the interface objects. + set(InterfaceObj,'Enable','on'); + % re-enable measurements + handles.disable_measurements = false; + guidata(hObject,handles); + return +end + +% Set name to previous name prior to editing if user selects "no" +if(strcmp(button,'No')) + data = get(handles.coords_table,'Data'); + data{selectedRow,1} = PreviousData; + set(handles.coords_table,'Data',data); + NewData = PreviousData; +else + % Atlas points have been edited so update bool. + handles.editedAtlasPoints = true; +end + +% Display warning dialogue before uiputfile if the atlas points have been +% editied... if(handles.editedAtlasPoints) uiwait(warndlg({['Note: Atlas points are NOT included in location'... ' list files.'];... @@ -1029,12 +1425,12 @@ function ExportHeadpoints_Callback(hObject, eventdata, handles) % Open an "Export" Dialogue if ~isdeployed - [fileName,pathName,filterIndex] = ... + [fileName,pathName,filterIndex] = ... uiputfile({'*.txt;*.dat;*.csv', ... 'Text Files (*.txt) (*.dat) (*.csv)'} ... ,'Export Location List File ...'); else - [fileName,pathName,filterIndex] = ... + [fileName,pathName,filterIndex] = ... uiputfile({'*.txt;*.dat;*.csv', ... 'Text Files (*.txt) (*.dat) (*.csv)'} ... ,'Export Location List File ...',handles.userDir); @@ -1049,29 +1445,29 @@ function ExportHeadpoints_Callback(hObject, eventdata, handles) % Otherwise create a table from the cell array and output that to file. if(filterIndex ~= 0) % if == 0 then user selected "cancel" in save dialogue - + data = get(handles.coords_table,'Data'); - - % error if outputting only atlas points - if(size(data,1) <= 5) + + % error if outputting only atlas points + if(size(data,1) <= size(handles.AtlasLandmarks, 1)) errordlg({'Cannot export locations:';... 'Only atlas point locations have been found.';... 'Atlas points alone cannot be exported.'},... 'Export Error','modal'); else - + disp(['Locations saving to ' pathName fileName]); fileID = fopen([pathName fileName],'wt'); - %write from the 6th to the last data point - for i = 5+1:size(data,1) + %write from after the atlas points to the last data point + for i = size(handles.AtlasLandmarks, 1)+1:size(data,1) fprintf(fileID,'%s\n',data{i,1}); end fclose(fileID); clear fileID; - + end end @@ -1085,44 +1481,53 @@ function measureThisRowButton_Callback(hObject, eventdata, handles) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) -% See if the selectedRow variable exists within the handles struct -% (doesn't if no selection performed before clicking or cell has been +% See if the selectedRow variable exists within the handles struct +% (doesn't if no selection performed before clicking or cell has been % deselected) if(isfield(handles,'selectedRow')) - - if(length(handles.selectedRow) > 1) - % Multiple rows/row elements selected - errordlg({'Multiple rows or row elements have been selected.';... - 'Please select only a single cell.'},... - 'Selection Error','modal'); - elseif(handles.selectedRow <= 5) - % Atlas point selected - errordlg(['Atlas Points can only be measured in order'... - ' and cannot be changed after alignment.'],... - 'Selection Error','modal'); - elseif(handles.point_count >= 5 && ... - strcmp(get(handles.HeadAlign,'Enable'),'off')) - % (ie all atlas points collected and headalign clicked.) - % Point selected successfully! - - % Set point_count such that the selected row will be measured - handles.point_count = handles.selectedRow-1; - - % Update the "Point to Get" string - data = get(handles.coords_table,'Data'); - set(handles.infobox,'string',... - data(handles.selectedRow,1)) - - else - errordlg(['Please finish gathering atlas points then press '... - '"Align Atlas Points" before selecting individual locations to ',... - 'measure the position of.'],'Selection Error','modal'); + + % Set point_count such that the first of the selected rows will be + % measured + handles.point_count = handles.selectedRow(1)-1; + + % Update the "Point to Get" string + data = get(handles.coords_table,'Data'); + set(handles.infobox,'string',... + data(handles.selectedRow(1),1)) + + % delete the point data if there's data to delete + if size(data, 2) > 1 + % Remove plotted points from graph and replot axes + for i = 1:length(handles.selectedRow) + row_x_coord = data(handles.selectedRow(i), 2); + if ~isempty(row_x_coord{1}) + delete(handles.pointhandle(handles.selectedRow(i))); + axis(handles.coord_plot,'equal'); + [isin, idx] = ismember(handles.selectedRow(i), handles.point_count_history); + assert(all(isin)); + handles.point_count_history(idx) = []; + end + end + % delete data on table + data(handles.selectedRow,2:4) = cell(length(handles.selectedRow), 3); + if size(data, 2) > 4 + % Reset measurement status and distance if we have expected measurements + for i = 1:length(handles.selectedRow) + selectedRow = handles.selectedRow(i); + measurement_status = data(selectedRow, 8); + measurement_status = measurement_status{1}; + if ~isempty(measurement_status) + data(selectedRow, 8) = {'Unmeasured'}; + end + data(selectedRow, 10) = {[]}; + end + end + set(handles.coords_table,'Data',data); end + else % No point selected - - % Tell user to select a row to measure at - errordlg('Please select a row to gather location data.',... + errordlg('Please select rows to delete and gather location data.',... 'Selection Error','modal'); end guidata(hObject,handles); @@ -1147,22 +1552,22 @@ function coords_table_CellEditCallback(hObject, eventdata, handles) NewData = eventdata.NewData; PreviousData = eventdata.PreviousData; -% warn when editing of rows less than 5 (atlas points) -if(selectedRow <= 5) - +% warn when editing of rows less than number of atlas points +if(selectedRow <= size(handles.AtlasLandmarks, 1)) + % Check that user is happy to continue button = 'Yes'; button = questdlg({['Warning! About to rename Atlas Point "' ... PreviousData, '" to "', NewData, '".']; ... '';... 'Any changes will NOT be reflected in Exported Locations files';... - '(exported using the "Export Locations..." button) but WILL be';... + '(exported using the "Export Locations..." button) but WILL be';... ['reflected in saved data files (saved using the "Save Data'... ' As..." button.)'];... '';... 'Do you wish to continue?'} ... ,'Rename Warning','Yes','No','No'); - + % Set name to previous name prior to editing if user selects "no" if(strcmp(button,'No')) data = get(handles.coords_table,'Data'); @@ -1173,7 +1578,7 @@ function coords_table_CellEditCallback(hObject, eventdata, handles) % Atlas points have been edited so update bool. handles.editedAtlasPoints = true; end - + end % if the previous and the new data are different set editedLocationsList to % true. @@ -1182,3 +1587,777 @@ function coords_table_CellEditCallback(hObject, eventdata, handles) end guidata(hObject,handles); + + +% -------------------------------------------------------------------- +function menu_file_import_atlas_Callback(hObject, eventdata, handles) +% hObject handle to menu_file_import_atlas (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + +% disable measurements +handles.disable_measurements = true; +guidata(hObject,handles); + +% Find interface objects that are set to 'on' i.e. enabled... +InterfaceObj=findobj(handles.figure1,'Enable','on'); +% ... and turn them off. +set(InterfaceObj,'Enable','off'); + +%--------------------DIRECTORY INPUT----------------------- +atlas_dir = uigetdir(handles.userDir, ['Select atlas directory']); + +% Re-enable the interface objects. +set(InterfaceObj,'Enable','on'); + +% re-enable measurements +handles.disable_measurements = false; +guidata(hObject,handles); + +% user selected cancel... +if isequal(atlas_dir,0) + return +end + +% Warn user that this will reset all currently gathered data if any has +% been collected. +if(handles.point_count > 0) + + button = 'No'; + + button = questdlg({'Warning! Any existing data will be lost.';... + 'Do you wish to continue?'},'Data Warning','Yes','No','No'); + + % user selected cancel... + if strcmp(button,'No') + return + end + +end + +% Check atlas data is good +noatlas = true; +while noatlas + try + %load other data needed for headpoint plotting - these are the required + %names after getting atlas_dir from settings.mat + [landmarks, landmark_names, mesh] = load_atlas_data(atlas_dir); + if good_atlas_data(landmarks, landmark_names, mesh) + noatlas = false; + else + return + end + catch + errordlg('Bad atlas directory!', atlas_dir); + return + end +end + +% save our new mesh and landmarks +handles.AtlasLandmarks = landmarks; +handles.AtlasLandmarksNames = landmark_names; +handles.mesh = mesh; + +% Remove the old atlas directory from the path and add the new one to get +% necessary functions +rmpath(handles.atlas_dir) +handles.atlas_dir = atlas_dir; +addpath(handles.atlas_dir); + +% Reset points counter +handles.point_count = 0; + +% The landmark names are now the nes locations which we save +locations = landmark_names; + +% Display initial point to find on GUI +set(handles.infobox,'string',locations(1,1)); + +% display locations on table in gui +set(handles.coords_table,'Data',locations); + +% if head align button has been enabled set to disabled. +if(strcmp(get(handles.HeadAlign,'Enable'),'on')) + set(handles.HeadAlign,'Enable','off'); +end + +% clear previous measurements and headmap from plot... +cla(handles.coord_plot); +% and replot axes. +axis(handles.coord_plot,'equal'); + +guidata(hObject,handles); + + +% -------------------------------------------------------------------- +function menu_file_Callback(hObject, eventdata, handles) +% hObject handle to menu_file (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + +% -------------------------------------------------------------------- +function menu_file_import_locations_Callback(hObject, eventdata, handles) +% hObject handle to menu_file_import_locations (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +ImportHeadpoints_Callback(hObject, eventdata, handles) + +% -------------------------------------------------------------------- +function menu_file_export_locations_Callback(hObject, eventdata, handles) +% hObject handle to menu_file_export_locations (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +ExportHeadpoints_Callback(hObject, eventdata, handles) + + +% -------------------------------------------------------------------- +function menu_options_Callback(hObject, eventdata, handles) +% hObject handle to menu_options (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + +% -------------------------------------------------------------------- +function menu_options_double_tap_Callback(hObject, eventdata, handles) +% hObject handle to menu_options_double_tap (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + +% disable measurements +handles.disable_measurements = true; +guidata(hObject,handles); + +% Find interface objects that are set to 'on' i.e. enabled... +InterfaceObj=findobj(handles.figure1,'Enable','on'); +% ... and turn them off. +set(InterfaceObj,'Enable','off'); + +%--------------------DIALOGUE BOX----------------------- +[handles.error_distance, handles.double_tap_error_enabled] = ... + doubletapdialog(handles.error_distance, handles.double_tap_error_enabled); + +% Re-enable the interface objects. +set(InterfaceObj,'Enable','on'); + +% re-enable measurements +handles.disable_measurements = false; +guidata(hObject,handles); + + +% -------------------------------------------------------------------- +function menu_edit_Callback(hObject, eventdata, handles) +% hObject handle to menu_edit (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + +% -------------------------------------------------------------------- +function menu_edit_undo_Callback(hObject, eventdata, handles) +% hObject handle to menu_edit_undo (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +remove_last_pt_Callback(hObject, eventdata, handles) + + +% -------------------------------------------------------------------- +function menu_file_saveas_Callback(hObject, eventdata, handles) +% hObject handle to menu_file_saveas (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +save_Callback(hObject, eventdata, handles) + + +% -------------------------------------------------------------------- +function menu_file_import_expected_coordinates_Callback(hObject, eventdata, handles) +% hObject handle to menu_file_import_expected_coordinates (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + +% disable measurements +handles.disable_measurements = true; +guidata(hObject,handles); + +% Find interface objects that are set to 'on' i.e. enabled... +InterfaceObj=findobj(handles.figure1,'Enable','on'); +% ... and turn them off. +set(InterfaceObj,'Enable','off'); + +%--------------------HEADPOINTS TO DIGITISE INPUT----------------------- +if ~isdeployed + [filename,pathname] = ... + uigetfile({'*.txt;*.dat;*.csv', ... + 'Text Files (*.txt) (*.dat) (*.csv)'} ... + ,['Select expected coordinates list file - each location and coordinates should be'... + ' on a new line. Tolerance is taken as the X coordinate of the "Tolerance" location.']); +else + [filename,pathname] = ... + uigetfile({'*.txt;*.dat;*.csv', ... + 'Text Files (*.txt) (*.dat) (*.csv)'} ... + ,['Select expected coordinates list file - each location and coordinates should be'... + ' on a new line. Tolerance is taken as the X coordinate of the "Tolerance" location.'],handles.userDir); +end +% Re-enable the interface objects. +set(InterfaceObj,'Enable','on'); + +% re-enable measurements +handles.disable_measurements = false; +guidata(hObject,handles); + +% user selected cancel... +if isequal(filename,0) + return +end + +disp(['User selected ', fullfile(pathname, filename)]) + +expected_coords = readtable([pathname filename]); + +% Check file specification +if ~strcmp(expected_coords.Properties.VariableNames{1}, 'Location') + errordlg('Import Error: First entry in top line of coordinates list file should be "Location" (note capitalisation).', 'Import Error'); + return +end +if ~strcmp(expected_coords.Properties.VariableNames{2}, 'X') + errordlg('Import Error: Second entry in top line of coordinates list file should be "X" (note capitalisation).', 'Import Error'); + return +end +if ~strcmp(expected_coords.Properties.VariableNames{3}, 'Y') + errordlg('Import Error: Third entry in top line of coordinates list file should be "Y" (note capitalisation).', 'Import Error'); + return +end +if ~strcmp(expected_coords.Properties.VariableNames{4}, 'Z') + errordlg('Import Error: Fourth entry in top line of coordinates list file should be "Z" (note capitalisation).', 'Import Error'); + return +end + +% Find tolerance +tolrow = find(strcmp(expected_coords.Location, 'Tolerance')); +if isempty(tolrow) + errordlg('Tolerance not found in expected coordinates file! It should be an entry in the "Location" column.', 'Import Error'); + return +end +if length(tolrow) > 1 + errordlg('Multiple tolerance entries found in expected coordinates file! There should only be one.', 'Import Error'); + return +end +try + expected_coords_tolerance = expected_coords.X(tolrow); + assert(isnumeric(expected_coords_tolerance)); + assert(expected_coords_tolerance > 0); +catch + errordlg('Could not get tolerance from expected coordinates file! It should be a nonnegative number in the "X" column next to the "Tolerance" location.', 'Import Error'); + return +end + +% Delete tolerance from location names +expected_coords.Properties.RowNames = expected_coords.Location; +expected_coords.Location = []; +expected_coords('Tolerance', :) = []; + +handles = load_expected_coords(handles, expected_coords, expected_coords_tolerance); + +% save +handles.expected_coords = expected_coords; +handles.expected_coords_tolerance = expected_coords_tolerance; + +% Save locations variable to be loaded next time +if ~isdeployed + saved_expected_coords_loc = fullfile(pwd, 'saved_expected_coords.mat'); +else + saved_expected_coords_loc = fullfile(ctfroot, 'DIGIGUI', 'saved_expected_coords.mat'); +end +disp(['Saving expected_coords to: ', saved_expected_coords_loc]); +disp(['Saving expected_coords_tolerance to: ', saved_expected_coords_loc]); +save(saved_expected_coords_loc,'expected_coords','expected_coords_tolerance'); + +guidata(hObject,handles); + + +function handles = load_expected_coords(handles, expected_coords, expected_coords_tolerance) +% handles structure with handles and user data (see GUIDATA) +% expected_coords loaded expected coordinates table with tolerance removed + +% Search for expected coordinates names in location names +data = get(handles.coords_table,'Data'); +locations = data(:, 1); +locations_with_names = locations(~cellfun('isempty',locations)); +[is_match, match_rows] = ismember(expected_coords.Properties.RowNames, locations_with_names); +if ~all(is_match) + errordlg('Not all expected coordinate locations found in currently loaded coordinate locations! Check the expected coordinates file and the list of coordinates.', 'Import Error'); + return +end + +% Add as new rows to data in new columns +if size(data, 2) == 1 + % no data recorded yet + data(:, 2:4) = cell(size(data,1), 3); +end +data(:, 5:7) = cell(size(data,1), 3); +data(match_rows, 5:7) = table2cell(expected_coords); + +% Add measurement status too +data(:, 8) = cell(size(data,1), 1); +data(match_rows, 8) = {'Unmeasured'}; + +% Display tolerance +data(match_rows, 9) = {expected_coords_tolerance}; + +% Clear any already displayed distance +data(match_rows, 10) = {[]}; + +% display new data with new headings +set(handles.coords_table,'Data', data); +handles.coords_table.ColumnName(5:10) = {'X expected', 'Y expected', 'Z expected', 'Is expected', 'Tolerance', 'Distance'}; +handles.coords_table.ColumnWidth(5:7) = handles.coords_table.ColumnWidth(4); +handles.coords_table.ColumnWidth(8) = {'auto'}; +handles.coords_table.ColumnWidth(9:10) = handles.coords_table.ColumnWidth(4); + + + +function handles = remove_expected_coords(handles) +% handles structure with handles and user data (see GUIDATA) +if size(handles.coords_table.Data, 2) > 1 + handles.coords_table.Data = handles.coords_table.Data(:, 1:4); +end +handles.coords_table.ColumnName = handles.coords_table.ColumnName(1:4); + + + +% -------------------------------------------------------------------- +function menu_options_expected_coords_Callback(hObject, eventdata, handles) +% hObject handle to menu_options_expected_coords (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +% disable measurements +handles.disable_measurements = true; +guidata(hObject,handles); + +% Find interface objects that are set to 'on' i.e. enabled... +InterfaceObj=findobj(handles.figure1,'Enable','on'); +% ... and turn them off. +set(InterfaceObj,'Enable','off'); + +%--------------------DIALOGUE BOX----------------------- +choice = questdlg('OPTION 1/2: Save expected coordinates between sessions?', ... + 'Expected coordinates option 1/2', ... + 'Yes','No','No'); +switch choice + case 'Yes' + handles.save_expected_coords = true; + if ~isfield(handles, 'expected_coords') + uiwait(warndlg('No expected coordinates have been loaded yet. If the program is restarted without loading expected coordinates, a warning will be emitted unless "No" is selected.', 'Warning')) + end + case 'No' + handles.save_expected_coords = false; +end +choice = questdlg('OPTION 2/2: Disallow measurements until expected coordinates loaded?', ... + 'Expected coordinates option 2/2', ... + 'Yes','No','No'); +switch choice + case 'Yes' + handles.only_measure_with_expected_coords = true; + case 'No' + handles.only_measure_with_expected_coords = false; +end + +% Re-enable the interface objects. +set(InterfaceObj,'Enable','on'); + +% re-enable measurements +handles.disable_measurements = false; +guidata(hObject,handles); + + +% --- Executes on button press in pushbutton_import_expected_coordinates. +function pushbutton_import_expected_coordinates_Callback(hObject, eventdata, handles) +% hObject handle to pushbutton_import_expected_coordinates (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +menu_file_import_expected_coordinates_Callback(hObject, eventdata, handles) + + +% --- Executes on button press in pushbutton_insert_rows_below. +function pushbutton_insert_rows_below_Callback(hObject, eventdata, handles) +% hObject handle to pushbutton_insert_rows_below (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + +% disable measurements +handles.disable_measurements = true; +guidata(hObject,handles); + +% Find interface objects that are set to 'on' i.e. enabled... +InterfaceObj=findobj(handles.figure1,'Enable','on'); +% ... and turn them off. +set(InterfaceObj,'Enable','off'); + +if strcmp(handles.menu_options_insert_by_number.Checked, 'on') + assert(strcmp(handles.menu_options_insert_by_location_name.Checked, 'off')); + prompt = {'Number of rows:'}; + dlg_title = 'Insert Rows Below...'; + num_lines = 1; + if isfield(handles,'previousInsertRowsVal') + defaultans = {num2str(handles.previousInsertRowsVal)}; + else + defaultans = {'1'}; + end + answer = inputdlg(prompt,dlg_title,num_lines,defaultans); + if isempty(answer) + % cancel selected + + % Re-enable the interface objects. + set(InterfaceObj,'Enable','on'); + + % re-enable measurements + handles.disable_measurements = false; + guidata(hObject,handles); + return + end + try + nrows = str2num(answer{1}); + assert(nrows > 0); + assert(floor(nrows) == nrows); + handles.previousInsertRowsVal = nrows; + catch + h = errordlg('Number of rows to insert must be a positive integer.', 'Error', 'modal'); + uiwait(h) + % Re-enable the interface objects. + set(InterfaceObj,'Enable','on'); + % re-enable measurements + handles.disable_measurements = false; + guidata(hObject,handles); + return + end +else + assert(strcmp(handles.menu_options_insert_by_location_name.Checked, 'on')) + assert(strcmp(handles.menu_options_insert_by_number.Checked, 'off')) + % Open pre-populate rows dialogue + valid_entries = false; + while ~valid_entries + prompt = {'Start number (leave blank to not use):', 'End number (leave blank to not use):', 'Start letter (leave blank to not use):', 'End letter (leave blank to not use):'}; + dlg_title = 'Names to use (e.g. 1a, 1b, 2a, 2b...)'; + num_lines = 1; + defaultans = {'', '', '', ''}; + if isfield(handles,'previousStartNumber') + defaultans{1} = num2str(handles.previousStartNumber); + end + if isfield(handles,'previousEndNumber') + defaultans{2} = num2str(handles.previousEndNumber); + end + if isfield(handles,'previousStartLetter') + defaultans{3} = num2str(handles.previousStartLetter); + end + if isfield(handles,'previousEndLetter') + defaultans{4} = num2str(handles.previousEndLetter); + end + answer = inputdlg(prompt,dlg_title,num_lines,defaultans); + if isempty(answer) + % cancel selected - don't save handles, just return + + % Re-enable the interface objects. + set(InterfaceObj,'Enable','on'); + + % re-enable measurements + handles.disable_measurements = false; + + return + end + try + start_number = str2num(answer{1}); + if ~isempty(answer{1}) + assert(~isempty(start_number)); + end + if ~isempty(start_number) + assert(floor(start_number) == start_number); + end + handles.previousStartNumber = start_number; + catch + h = errordlg('Start number must be an integer!', 'Error', 'modal'); + uiwait(h) + continue + end + try + end_number = str2num(answer{2}); + if ~isempty(answer{2}) + assert(~isempty(end_number)); + end + if ~isempty(end_number) + assert(floor(end_number) == end_number); + end + handles.previousEndNumber = end_number; + catch + h = errordlg('End number must be an integer!', 'Error', 'modal'); + uiwait(h) + continue + end + if isempty(start_number) && ~isempty(end_number) + h = errordlg('If start number is unspecified, end number must also be unspecified.', 'Error', 'modal'); + uiwait(h) + continue + end + if ~isempty(start_number) && isempty(end_number) + h = errordlg('If start number is specified, end number must also be specified.', 'Error', 'modal'); + uiwait(h) + continue + end + if end_number < start_number + h = errordlg('End number must be greater than or equal to the start number!', 'Error', 'modal'); + uiwait(h) + continue + end + try + start_letter = char(answer{3}); + assert(length(start_letter) < 2); + handles.previousStartLetter = start_letter; + catch + h = errordlg('Only one start letter can be specified.', 'Error', 'modal'); + uiwait(h) + continue + end + try + end_letter = char(answer{4}); + assert(length(end_letter) < 2); + handles.previousEndLetter = end_letter; + catch + errordlg('Only one end letter can be specified.', 'Error', 'modal'); + continue + end + if isempty(start_number) && ~isempty(end_number) + h = errordlg('If start letter is unspecified, end letter must also be unspecified.', 'Error', 'modal'); + uiwait(h) + continue + end + if ~isempty(start_number) && isempty(end_number) + h = errordlg('If start letter is specified, end letter must also be specified.', 'Error', 'modal'); + uiwait(h) + continue + end + if end_letter < start_letter + h = errordlg('End letter must be after or the same as the start letter!', 'Error', 'modal'); + uiwait(h) + continue + end + valid_entries = true; + % work out nrows to add + if isempty(start_number) && ~isempty(start_letter) + nrows = end_letter - start_letter; + elseif ~isempty(start_number) && isempty(start_letter) + nrows = end_number - start_number; + else + nrows = (1 + end_letter - start_letter)*(1 + end_number - start_number); + end + end +end + +if isempty(nrows) + h = warndlg('No rows have been specified!', 'Insert Rows Below...'); + uiwait(h) + % Re-enable the interface objects. + set(InterfaceObj,'Enable','on'); + % re-enable measurements + handles.disable_measurements = false; + guidata(hObject,handles); + return +end + +[error, handles] = insert_rows_below(hObject, eventdata, handles, nrows); + +if strcmp(handles.menu_options_insert_by_location_name.Checked, 'on') && ~error + assert(strcmp(handles.menu_options_insert_by_number.Checked, 'off')) + % Fill in names + assert(isfield(handles,'selectedRow')) + data = get(handles.coords_table,'Data'); + start_row = handles.selectedRow(end) + 1; + end_row = start_row + nrows - 1; + current_number = start_number; + current_letter = start_letter; + for i = start_row:end_row + % loop through letters, then through numbers + name = strcat(num2str(current_number), current_letter); + reset_letter = current_letter >= end_letter; + if ~isempty(reset_letter) && reset_letter + current_letter = start_letter; + reset_number = current_number >= end_number; + if ~isempty(reset_number) && reset_number + current_number = start_number; + else + current_number = current_number + 1; + end + else + current_letter = char(current_letter + 1); + end + data{i,1} = name; + end + set(handles.coords_table,'Data', data) +end + +% Re-enable the interface objects. +set(InterfaceObj,'Enable','on'); + +% re-enable measurements +handles.disable_measurements = false; +guidata(hObject,handles); + + +function [error, handles] = insert_rows_below(hObject, eventdata, handles, nrows) + +error = false; + +data = get(handles.coords_table,'Data'); + +% See if the selectedRow variable exists within the handles struct +% (doesn't if no selection performed before clicking or cell has been +% deselected) +if(isfield(handles,'selectedRow')) + if(handles.selectedRow(end) < size(handles.AtlasLandmarks, 1)) + errordlg('Cannot insert or delete Atlas Points','Error','modal'); + error = true; + return + else + row = handles.selectedRow(end); + % insert new rows + data = [data(1:row,:); cell(nrows,size(data,2)); data(row+1:end,:)]; + + % check if have added rows within where measurement has already been + % made + if(handles.selectedRow(end) < handles.point_count) + % increment point count to account for extra points + handles.point_count = handles.point_count + nrows; + end + + % Locations list has now been edited so change bool. + handles.editedLocationsList = true; + + end +else +% % insert empty row at the end +% data{end+1,1} = []; + + % Tell user to select a row before inserting + errordlg('Please select a row to insert below.','Insert Error','modal'); + error = true; + return +end +% save the newly changed data to the table on the gui +set(handles.coords_table,'Data',data); +guidata(hObject,handles); + + +% -------------------------------------------------------------------- +function menu_edit_insert_Callback(hObject, eventdata, handles) +% hObject handle to menu_edit_insert (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + +% -------------------------------------------------------------------- +function menu_edit_delete_rows_Callback(hObject, eventdata, handles) +% hObject handle to menu_edit_delete_rows (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +DeleteRowPushbutton_Callback(hObject, eventdata, handles) + + +% -------------------------------------------------------------------- +function menu_edit_insert_rows_below_Callback(hObject, eventdata, handles) +% hObject handle to menu_edit_insert_rows_below (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +pushbutton_insert_rows_below_Callback(hObject, eventdata, handles) + + +% -------------------------------------------------------------------- +function menu_edit_insert_1_row_below_Callback(hObject, eventdata, handles) +% hObject handle to menu_edit_insert_1_row_below (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +InsertRowPushbutton_Callback(hObject, eventdata, handles) + + +% -------------------------------------------------------------------- +function menu_file_reset_all_Callback(hObject, eventdata, handles) +% hObject handle to menu_file_reset_all (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +choice = questdlg('Are you sure you want to reset? Any unsaved measurements will be lost! Settings will be retained.', ... + 'Are you sure?', ... + 'Yes','No','No'); +switch choice + case 'Yes' + % clear previous measurements and headmap from plot... + cla(handles.coord_plot); + % and replot axes. + axis(handles.coord_plot,'equal'); + save_settings(handles); + save_locations(handles); + handles = close_serial_port(handles); + DIGIGUI_OutputFcn(hObject, eventdata, handles); + case 'No' + return; +end + + +% -------------------------------------------------------------------- +function menu_file_reset_expected_coords_Callback(hObject, eventdata, handles) +% hObject handle to menu_file_reset_expected_coords (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +if isfield(handles, 'expected_coords') + handles = rmfield(handles, 'expected_coords'); +end +if isfield(handles, 'expected_coords_tolerance') + handles = rmfield(handles, 'expected_coords_tolerance'); +end +handles = remove_expected_coords(handles); +guidata(hObject,handles); + + +% -------------------------------------------------------------------- +function menu_options_location_prepopulation_Callback(hObject, eventdata, handles) +% hObject handle to menu_options_location_prepopulation (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +% disable measurements +if strcmp(handles.menu_options_location_prepopulation.Checked, 'on') + handles.menu_options_location_prepopulation.Checked = 'off'; +else + handles.menu_options_location_prepopulation.Checked = 'on'; +end +guidata(hObject,handles); + + +% -------------------------------------------------------------------- +function menu_options_insert_Callback(hObject, eventdata, handles) +% hObject handle to menu_options_insert (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + +% -------------------------------------------------------------------- +function menu_options_insert_by_number_Callback(hObject, eventdata, handles) +% hObject handle to menu_options_insert_by_number (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +if strcmp(handles.menu_options_insert_by_number.Checked, 'on') + handles.menu_options_insert_by_number.Checked = 'off'; + handles.menu_options_insert_by_location_name.Checked = 'on'; +else + handles.menu_options_insert_by_number.Checked = 'on'; + handles.menu_options_insert_by_location_name.Checked = 'off'; +end +guidata(hObject,handles); + +% -------------------------------------------------------------------- +function menu_options_insert_by_location_name_Callback(hObject, eventdata, handles) +% hObject handle to menu_options_insert_by_location_name (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +if strcmp(handles.menu_options_insert_by_location_name.Checked, 'on') + handles.menu_options_insert_by_number.Checked = 'on'; + handles.menu_options_insert_by_location_name.Checked = 'off'; +else + handles.menu_options_insert_by_number.Checked = 'off'; + handles.menu_options_insert_by_location_name.Checked = 'on'; +end +guidata(hObject,handles); diff --git a/DIGIGUI.prj b/DIGIGUI.prj index 0b713a3..43a347a 100644 --- a/DIGIGUI.prj +++ b/DIGIGUI.prj @@ -1,5 +1,5 @@ - + DIGIGUI ${PROJECT_ROOT}\DIGIGUI_resources\icon.ico @@ -8,12 +8,12 @@ ${PROJECT_ROOT}\DIGIGUI_resources\icon_24.png ${PROJECT_ROOT}\DIGIGUI_resources\icon_16.png - 1.2 + 1.3 Reuben Nixon-Hill reuben.w.hill@gmail.com A graphical user interface for use with the Polhemus Patriot Digitiser at a baud rate of 115200. - Version 1.2.1. + Version 1.3.0-gamma2. Copyright © 2023 Reuben Nixon-Hill, MATLAB® © 1984 - 2016 The MathWorks, Inc. This package is licenced under the MIT Licence (see LICENCE.txt) and was created using MATLAB® compiler. @@ -38,9 +38,9 @@ This package is licenced under the MIT Licence (see LICENCE.txt) and was created DIGIGUI_web DIGIGUI_mcr MyAppInstaller_app - true - false - log + false + true + log.txt @@ -61,18 +61,20 @@ This package is licenced under the MIT Licence (see LICENCE.txt) and was created - - ${PROJECT_ROOT}\DIGIGUI.m - ${PROJECT_ROOT}\DIGIGUI.fig - ${PROJECT_ROOT}\refpts_landmarks.mat - ${PROJECT_ROOT}\scalpSurfaceMesh.mat + ${PROJECT_ROOT}\default_atlas\atlas_landmarks.mat + ${PROJECT_ROOT}\default_atlas\CoordinateTransform.m + ${PROJECT_ROOT}\default_atlas\mesh.mat + ${PROJECT_ROOT}\default_settings.mat + ${PROJECT_ROOT}\savedLocationNames.mat + ${PROJECT_ROOT}\default_atlas + ${PROJECT_ROOT}\expected_coordinates_example.txt ${PROJECT_ROOT}\LICENCE.txt ${PROJECT_ROOT}\Locations Example.txt ${PROJECT_ROOT}\Readme.txt @@ -80,6 +82,9 @@ This package is licenced under the MIT Licence (see LICENCE.txt) and was created ${PROJECT_ROOT}\affine_trans_RJC.m ${PROJECT_ROOT}\affinemap.m + ${PROJECT_ROOT}\checkmark.tif + ${PROJECT_ROOT}\DIGIGUI.fig + ${PROJECT_ROOT}\doubletapdialog.m ${PROJECT_ROOT}\FindPatriotSerial.m diff --git a/Polhemus_Headmesh_Process.m b/Polhemus_Headmesh_Process.m index 40ae7ac..0664713 100644 --- a/Polhemus_Headmesh_Process.m +++ b/Polhemus_Headmesh_Process.m @@ -1,6 +1,6 @@ % Head mesh transformation sequence for polhemus gui % landmarks = Inion, Nasion, Ar, Al, Cz measured with Polhemus -% pts = landmarks of atlas (from refpts_landmarks.mat) +% pts = landmarks of atlas (from default_atlas/atlas_landmarks.mat) % mesh = adult atlas scalp mesh [A,B] = affinemap(pts,landmarks); diff --git a/README.md b/README.md index 0d95d74..a264115 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,6 @@ For more information see the comments in `DIGIGUI.m`. This has been designed for use with MATLAB compiler using the `DIGIGUI.prj` file. -Copyright (C) 2022 Reuben W. Nixon-Hill (formerly Reuben W. Hill) +Copyright (C) 2023 Reuben W. Nixon-Hill (formerly Reuben W. Hill) This package is licenced under the MIT Licence (see LICENCE.txt). \ No newline at end of file diff --git a/checkmark.tif b/checkmark.tif new file mode 100644 index 0000000..35d529c Binary files /dev/null and b/checkmark.tif differ diff --git a/default_atlas/CoordinateTransform.m b/default_atlas/CoordinateTransform.m new file mode 100644 index 0000000..7649f5c --- /dev/null +++ b/default_atlas/CoordinateTransform.m @@ -0,0 +1,65 @@ +%This function outputs a vector to transform inion to origin +%also outputs rotation transformation matrix to allign the head model to +%intuitive coordinates. +%To apply to row vector, the vector should be multiplied by the transpose +%with the vector on the left +function [Matrix,vector] = CoordinateTransform(landmarks) + %calculate lengths between vectors + + %these are the untransformed reference points, defined here in case I want + %to use them + Nasion = landmarks(1,:); + Inion = landmarks(2,:); + Ar = landmarks(3,:); + Al = landmarks(4,:); + Cz = landmarks(5,:); + + %------TRANSLATION------- + + %translate inion to origion + vector = -Inion; + + %translate Al + Al = Al + vector; + + %------ROTATE AL TO Y AXIS------- + + %calculate rotation to y axis + AlToYAxisRot = vrrotvec(Al,[0,1,0]); + + %convert to rotation matrix + AlToYAxisMatrix = vrrotvec2mat(AlToYAxisRot); + + %repmat + % Apply translation and rotation to points + for k = 1:5 + landmarks(k,:) = landmarks(k,:)+vector; + landmarks(k,:) = landmarks(k,:)*AlToYAxisMatrix'; + end + + %------ROTATE AR TO XY PLANE ABOUT INION-AL AXIS------- + + % find angle to rotate nasion into XY plane about the new y axis + [ArToXYRotAngle,~] = cart2pol(landmarks(3,1),landmarks(3,3)); + + %Find second rotation matrix + ArToXYMatrix = vrrotvec2mat([0,1,0,ArToXYRotAngle]); + + % Apply second rotation to points + for k = 1:5 + landmarks(k,:) = landmarks(k,:)*ArToXYMatrix'; + end + + %------FINAL ROTATION ABOUT AL-AR AXIS TO ALLIGN INION AND NASION------- + + %find angle of nasion to xy plane + [~,NasionToXYRotAngle,~] = cart2sph(landmarks(1,1),landmarks(1,2),landmarks(1,3)); + %define vector to rotate around (the line joining AL and AR) + NasionRotVector = landmarks(4,:) - landmarks(3,:); + %find rotation matrix + NasionToXYRotMatrix = vrrotvec2mat([NasionRotVector, NasionToXYRotAngle]); + + %------OUTPUT FINAL MATRIX------- + Matrix = NasionToXYRotMatrix*ArToXYMatrix*AlToYAxisMatrix; + +end \ No newline at end of file diff --git a/default_atlas/atlas_landmarks.mat b/default_atlas/atlas_landmarks.mat new file mode 100644 index 0000000..dc4cb63 Binary files /dev/null and b/default_atlas/atlas_landmarks.mat differ diff --git a/scalpSurfaceMesh.mat b/default_atlas/mesh.mat similarity index 100% rename from scalpSurfaceMesh.mat rename to default_atlas/mesh.mat diff --git a/default_settings.mat b/default_settings.mat new file mode 100644 index 0000000..052d83f Binary files /dev/null and b/default_settings.mat differ diff --git a/doubletapdialog.m b/doubletapdialog.m new file mode 100644 index 0000000..70a7291 --- /dev/null +++ b/doubletapdialog.m @@ -0,0 +1,64 @@ +function [choice, enabled] = doubletapdialog(currentchoice, currentenabled) + + d = dialog('Position',[300 300 260 180],'Name','Double tap'); + + checkbox = uicontrol('Parent', d, ... + 'Style', 'checkbox', ... + 'String', 'Enable double tap error?', ... + 'Position', [68 70 200 170], ... + 'Value', currentenabled, ... + 'Callback', @checkbox_callback); + + slider = uicontrol('Parent',d,... + 'Style','slider',... + 'Position',[31 60 200 25],... + 'Min',0,... + 'Max',1,... + 'Value', currentchoice,... + 'Callback',@slider_callback); + + txt = uicontrol('Parent',d,... + 'Style','text',... + 'Position',[31 100 210 30],... + 'String', sprintf('Use slider to set error distance.\nCurrent value: %0.2g cm.', currentchoice)); + + fun = @(~,e)set(txt,'String',sprintf('Use slider to set error distance.\nCurrent value: %0.2g cm.', get(e.AffectedObject,'Value'))); + + addlistener(slider, 'Value', 'PostSet', fun); + + btn = uicontrol('Parent',d,... + 'Position',[99 20 70 25],... + 'String','Close',... + 'Callback','delete(gcf)'); + + choice = currentchoice; + enabled = currentenabled; + + if currentenabled + set(txt,'Enable','on'); + set(slider,'Enable','on'); + else + set(txt,'Enable','off'); + set(slider,'Enable','off'); + end + + % Wait for d to close before running to completion + uiwait(d); + + + function slider_callback(slider,event) + choice = slider.Value; + end + + function checkbox_callback(checkbox,event) + enabled = checkbox.Value; + if enabled + set(txt,'Enable','on'); + set(slider,'Enable','on'); + else + set(txt,'Enable','off'); + set(slider,'Enable','off'); + end + end + +end \ No newline at end of file diff --git a/expected_coordinates_example.txt b/expected_coordinates_example.txt new file mode 100644 index 0000000..615002f --- /dev/null +++ b/expected_coordinates_example.txt @@ -0,0 +1,5 @@ +Location,X,Y,Z +Nasion,15.0,0,0 +Ar,0,0,0 +Example Pt 1,20.0,0,0 +Tolerance,10,NaN,NaN \ No newline at end of file diff --git a/refpts_landmarks.mat b/refpts_landmarks.mat deleted file mode 100644 index a3e10b4..0000000 Binary files a/refpts_landmarks.mat and /dev/null differ diff --git a/savedLocationNames.mat b/savedLocationNames.mat index 407e570..c7ba975 100644 Binary files a/savedLocationNames.mat and b/savedLocationNames.mat differ