diff --git a/scripts/trackmateEdgesR21a.m b/scripts/trackmateEdgesR21a.m new file mode 100644 index 000000000..012263dd1 --- /dev/null +++ b/scripts/trackmateEdgesR21a.m @@ -0,0 +1,206 @@ +function trackMap = trackmateEdges(filePath, featureList) +%%TRACKMATEEDGES Import edges from a TrackMate data file. +% +% trackMap = TRACKMATEEDGES(file_path) imports the edges - or links - +% contained in the TrackMate XML file file_path. TRACKMATEEDGES only +% imports the edges of visible tracks. +% +% trackMap = TRACKMATEEDGES(file_path, feature_list) where feature_list +% is a cell array of string only imports the edge features whose names +% are in the cell array. +% +% INPUT: +% +% file_path must be a path to a TrackMate file, containing the whole +% TrackMate data, and not the simplified XML file that contains only +% linear tracks. Such simplified tracks are imported using the +% importTrackMateTracks function. +% +% A TrackMate file is a XML file that starts with the following header: +% +% +% ... +% and has a Model element in it: +% +% +% OUTPUT: +% +% The output is a collection of tracks. trackMap is a Map that links +% track names to a MATLAB table containing the edges of this track. The +% columns of the table depend on the feature_list specified as second +% argument, but it always contains at least the SPOT_SOURCE_ID and +% SPOT_TARGET_ID features, that store the IDs of the source and target +% spots. +% +% EXAMPLE: +% +% >> trackMap = trackmateEdges(file_path); +% >> trackNames = trackMap.keys; +% >> trackNames{1} +% +% ans = +% Track_0 +% +% >> trackMap('Track_0') +% +% ans = +% SPOT_SOURCE_ID SPOT_TARGET_ID DISPLACEMENT LINK_COST VELOCITY +% ______________ ______________ ____________ _________ ________ +% +% 14580 16501 4.7503 1 4.7503 +% 12683 14580 2.8316 1 2.8316 +% 10813 12683 8.1622 1 8.1622 +% 5295 7123 3.193 1 3.193 +% 1715 3487 4.3063 1 4.3063 +% 7123 8953 3.0804 1 3.0804 +% 8953 10813 3.3689 1 3.3689 +% 0 1715 6.2733 1 6.2733 +% 3487 5295 5.9587 1 5.9587 +% + +% __ +% Jean-Yves Tinevez & contributors - 2026 + + %% Constants definition. + + % TRACKMATE_ELEMENT = 'TrackMate'; + TRACK_ID_ATTRIBUTE = 'TRACK_ID'; + TRACK_NAME_ATTRIBUTE = 'name'; + SPOT_SOURCE_ID_ATTRIBUTE = 'SPOT_SOURCE_ID'; + SPOT_TARGET_ID_ATTRIBUTE = 'SPOT_TARGET_ID'; + + %% Open file. + + % We'll call trackmateFeatureDeclarations() to fill in table properties + % no matter what, so let's reuse that one's validation function. + try + [ ~, ef ] = trackmateFeatureDeclarationsR21a( filePath ); + catch ME + throw(ME) + end + + %% Retrieve edge feature list. + % Difference from original code: for performance reasons, featureList + % is taken from instead of the first node + if nargin < 2 || isempty( featureList ) + featureList = keys(ef); + end + + % % Take featureList from the first node + % if nargin < 2 || isempty( featureList ) + % ATTRIBUTE_SUFFIX = '__'; + % try + % % Why not a full path, TrackMate/Model/AllTracks/Track/Edge? + % opt = detectImportOptions(filePath, 'FileType', 'xml', ... + % 'RowNodeName', 'Edge', ... + % 'ImportAttributes', true, 'AttributeSuffix', ATTRIBUTE_SUFFIX, ... + % 'VariableNamingRule', 'preserve'); + % featureList = opt.SelectedVariableNames; + % featureListSel = endsWith(featureList, ATTRIBUTE_SUFFIX); + % featureList = extractBefore(featureList(featureListSel), ... + % ATTRIBUTE_SUFFIX+textBoundary('end')); + % if isstring(featureList) + % featureList = cellstr(featureList); + % end + % catch ME + % switch ME.identifier + % % case 'MATLAB:io:xml:detection:RowSelectorInvalidSelection' + % case 'MATLAB:io:xml:common:NonexistentNode' + % % No edge in file + % featureList = {}; + % otherwise + % throw(ME) + % end + % end + % end + + frontOfList = { SPOT_SOURCE_ID_ATTRIBUTE; SPOT_TARGET_ID_ATTRIBUTE }; + featureList = union(frontOfList, featureList, 'stable'); + + %% XPath to retrieve filtered track elements. + % Initialize map + trackMap = containers.Map('KeyType', 'char', 'ValueType', 'any'); + + opt = xmlImportOptions('NumVariables', 1, 'VariableSelectors', ... + ['//Model/FilteredTracks/TrackID/@' TRACK_ID_ATTRIBUTE], ... + 'VariableTypes', 'double', 'MissingRule', 'omitrow'); + fTracks = readtable(filePath, opt); + fTracks = fTracks.(1); + if isempty(fTracks) + % No selected track, return empty map + return + end + + %% Read Tracks table + opt = makeXMLOptionsTrack(); + tracks = readtable( filePath, opt ); + names = tracks.(TRACK_NAME_ATTRIBUTE); + tracks = tracks.(TRACK_ID_ATTRIBUTE); + + [~, whichSel, ~] = intersect( tracks, fTracks); + + %% Prepare a map: trackName -> edge table. + % 'Track/Edge/../@TRACK_ID' selects n_track nodes, not n_edge, so + % children won't receive a correct "parent ID" column with XPath 1.0 + % We can't read every Edge into a single table and subdivide later + % Will have repeatedly call readtable() with + % //Model/AllTracks/Track[@TRACK_ID == 'num']/Edge + % On the bright side, we need one table for each ID anyway + + % Prepare metadata once + if ~isempty(whichSel) + nVNames = numel( featureList ); + vDescriptions = cell( nVNames, 1); + vUnits = cell( nVNames, 1); + + for l = 1 : nVNames + vn = featureList{ l }; + vDescriptions{ l } = ef( vn ).name; + vUnits{ l } = ef( vn ).units; + end + else + return + end + + for k = 1 : numel(whichSel) + rowNum = whichSel(k); + opt = makeXMLOptionsEdgesOfTrackID( tracks(rowNum) ); + edgetbl = readtable( filePath, opt ); + + % Set table metadata. + edgetbl.Properties.DimensionNames = { 'Edge', 'Feature' }; + edgetbl.Properties.VariableDescriptions = vDescriptions; + edgetbl.Properties.VariableUnits = vUnits; + + trackMap( names{rowNum} ) = edgetbl; + end + + %% Subfunction. + + function opt = makeXMLOptionsTrack() + nodePath = '//Model/AllTracks/Track'; + + varNames = { TRACK_ID_ATTRIBUTE; TRACK_NAME_ATTRIBUTE }; + varTypes = { 'double'; 'char' }; + varSelectors = append('(', nodePath, ')/@', varNames); + + opt = xmlImportOptions( 'NumVariables', 2, ... + 'VariableNames', varNames, 'VariableTypes', varTypes, ... + 'VariableSelectors', varSelectors, 'RowSelector', nodePath, ... + 'VariableNamingRule', 'preserve', 'MissingRule', 'fill' ); + end + + + function opt = makeXMLOptionsEdgesOfTrackID( trackID ) + nodePath = ['//Model/AllTracks/Track[@' TRACK_ID_ATTRIBUTE ' = ''' num2str(trackID) ''']/Edge']; + + n_features = numel( featureList ); + varTypes = repmat( {'double'}, n_features, 1 ); + varSelectors = append( '(', nodePath, ')/@', featureList ); + + opt = xmlImportOptions( 'NumVariables', n_features, ... + 'VariableNames', featureList, 'VariableTypes', varTypes, ... + 'VariableSelectors', varSelectors, 'RowSelector', nodePath, ... + 'VariableNamingRule', 'preserve', 'MissingRule', 'fill' ); + end +end diff --git a/scripts/trackmateFeatureDeclarationsR21a.m b/scripts/trackmateFeatureDeclarationsR21a.m new file mode 100644 index 000000000..c182fb5f0 --- /dev/null +++ b/scripts/trackmateFeatureDeclarationsR21a.m @@ -0,0 +1,166 @@ +function [ sf, ef, tf ] = trackmateFeatureDeclarations(filePath) +%%TRACKMATEFATUREDECLARATIONS Import feature declarations from a TrackMate file. +% +% [ sf, ef, tf ] = TRACKMATEFEATUREDECLARATIONS(file_path) imports the +% feature declarations stored in a TrackMate file file_path and returns +% them as three maps: +% - sf is the map for spot features; +% - ef is the map for edge features; +% - tf is the map for track features. +% Each map links the feature key to a struct containing the feature +% declaration. +% +% INPUT: +% +% file_path must be a path to a TrackMate file, containing the whole +% TrackMate data, and not the simplified XML file that contains only +% linear tracks. Such simplified tracks are imported using the +% importTrackMateTracks function. +% +% A TrackMate file is a XML file that starts with the following header: +% +% +% ... +% and has a Model element in it: +% +% +% EXAMPLE: +% +% >> [ sf, ef, tf ] = trackmateFeatureDeclarations(file_path); +% >> tf.keys +% >> tf('TRACK_DISPLACEMENT') +% +% ans = +% key: 'TRACK_DISPLACEMENT' +% name: 'Track displacement' +% shortName: 'Displacement' +% dimension: 'LENGTH' +% isInt: 0 +% units: 'pixels' + +% __ +% Jean-Yves Tinevez & contributors - 2026 + + + %% Import the XPath classes. + % import javax.xml.xpath.* + % import matlab.io.xml.dom.* + % import matlab.io.xml.xpath.* + + %% Constants definition. + TRACKMATE_ELEMENT = 'TrackMate'; + SPATIAL_UNITS_ATTRIBUTE = 'spatialunits'; + TIME_UNITS_ATTRIBUTE = 'timeunits'; + FEATURE_KEY_ATTRIBUTE = 'feature'; + FEATURE_NAME_ATTRIBUTE = 'name'; + FEATURE_SHORTNAME_ATTRIBUTE = 'shortname'; + FEATURE_DIMENSION_ATTRIBUTE = 'dimension'; + FEATURE_ISINT_ATTRIBUTE = 'isint'; + + + %% Open and check XML... + % detectImportOptions(filename) would return an XMLImportOptions, but + % takes (a lot of) time to scan through the file. Instead, we construct + % the option directly, and let readtable() throw. + varNames = {SPATIAL_UNITS_ATTRIBUTE TIME_UNITS_ATTRIBUTE}; + modelPath = ['/' TRACKMATE_ELEMENT '[1]/Model']; + try + opt_unit = xmlImportOptions('NumVariables', 2, ... + 'VariableNames', varNames, 'VariableTypes', {'char' 'char'}, ... + 'VariableNamingRule', 'preserve', 'RowSelector', modelPath, ... + 'VariableSelectors', append('(', modelPath, ')/@', varNames)); + unitTbl = readtable(filePath, opt_unit); + catch ME + switch ME.identifier + case 'MATLAB:UndefinedFunction' + % xmlImportOptions() Starts from R2021a + error('Your MATLAB is too old (pre-R2021a) to run this script.'); + otherwise + % Attach the error struct to facilitate diagnostics. + error(ME, 'Failed to read XML file %s.', filePath); + end + end + + if height(unitTbl) < 1 + % No attribute read from the model node + error('MATLAB:trackMateGraph:BadXMLFile', ... + 'File does not seem to be a proper TrackMate file.'); + end + + %% And retrieve physical units. + spaceUnits = unitTbl{1,SPATIAL_UNITS_ATTRIBUTE}{1}; + timeUnits = unitTbl{1,TIME_UNITS_ATTRIBUTE}{1}; + + %% XPath to retrieve spot feature declarations. + % /TrackMate[1]/Model/FeatureDeclarations/EdgeFeatures/Feature + opt_spot = makeXMLOptionsFeature('SpotFeatures'); + sf = transformFeatureTable(readtable(filePath, opt_spot), spaceUnits, timeUnits); + + %% XPath to retrieve edge feature declarations. + if nargout >= 2 + % /TrackMate[1]/Model/FeatureDeclarations/EdgeFeatures/Feature + opt_edge = makeXMLOptionsFeature('EdgeFeatures'); + ef = transformFeatureTable(readtable(filePath, opt_edge), spaceUnits, timeUnits); + end + + %% XPath to retrieve track feature declarations. + if nargout >= 3 + % /TrackMate[1]/Model/FeatureDeclarations/TrackFeatures/Feature + opt_track = makeXMLOptionsFeature('TrackFeatures'); + tf = transformFeatureTable(readtable(filePath, opt_track), spaceUnits, timeUnits); + end + %% Subfunctions. + + % It's predetermined that 5 attributes exist at a fixed xpath. Let's + % fill an XMLImportOptions directly. Also control the names and orders + % of variables here. + function opt = makeXMLOptionsFeature(nodeName) + nodePath = ['/' TRACKMATE_ELEMENT '[1]/Model/FeatureDeclarations/' nodeName '/Feature']; + nodeSelectors = append('(', nodePath, ')/@', {FEATURE_KEY_ATTRIBUTE ... + FEATURE_NAME_ATTRIBUTE FEATURE_SHORTNAME_ATTRIBUTE ... + FEATURE_DIMENSION_ATTRIBUTE FEATURE_ISINT_ATTRIBUTE}); + + opt = xmlImportOptions('NumVariables', 5, ... + 'VariableNames', {'key' 'name' 'shortName' 'dimension' 'isInt'}, ... + 'VariableTypes', {'char' 'char' 'char' 'char' 'logical'}, ... + 'RowSelector', nodePath, 'VariableSelectors', nodeSelectors); + end +end + + % Fill in the Units, and transform into Map + function featureMap = transformFeatureTable(featureTable, spaceUnits, timeUnits) + units = cellfun(@(dim)determineUnits(dim, spaceUnits, timeUnits), ... + featureTable.dimension, 'UniformOutput', false); + featureTable = addvars(featureTable, units, 'NewVariableNames', 'units'); + + featureStruct = table2struct(featureTable); + featureMap = containers.Map({featureStruct.key}.', num2cell(featureStruct)); + end + + function units = determineUnits( dimension, spaceUnits, timeUnits ) + switch ( dimension ) + case 'ANGLE' + units = 'Radians'; + case 'INTENSITY' + units = 'Counts'; + case 'INTENSITY_SQUARED' + units = 'Counts^2'; + case' NONE' + units = ''; + case { 'POSITION', 'LENGTH' } + units = spaceUnits; + case 'QUALITY' + units = 'Quality'; + case 'TIME' + units = timeUnits; + case 'VELOCITY' + units = [ spaceUnits '/' timeUnits]; + case 'RATE' + units = [ '/' timeUnits]; + case 'STRING' + units = ''; + otherwise + units = 'no unit'; + end + end + diff --git a/scripts/trackmateGraphR21a.m b/scripts/trackmateGraphR21a.m new file mode 100644 index 000000000..d9f2886c0 --- /dev/null +++ b/scripts/trackmateGraphR21a.m @@ -0,0 +1,150 @@ +function [G, rois] = trackmateGraph(filePath, spotFeatureList, edgeFeatureList, verbose) +%%TRACKMATEGRAPH Import a TrackMate data file as a MATLAB directed graph. +% +% G = TRACKMATEGRAPH(file_path) imports the TrackMate data stored in the +% file file_path and returns it as a MATLAB directed graph. +% +% G = TRACKMATEGRAPH(file_path, spot_feature_list, edge_feature_list) +% where spot_feature_list and edge_feature_list are two cell arrays of +% string only imports the spot and edge features whose names are in the +% cell arrays. If the cell arrays are empty, all available features are +% imported. +% +% G = TRACKMATEGRAPH(file_path, sfl, efl, true) generates output in the +% command window that log the current import progress. +% +% [ G, rois ] = TRACKMATEGRAPH( ... ) also returns rois, a cell array +% containing the 2D polygons of each spot, if there is one. +% +% INPUT: +% +% file_path must be a path to a TrackMate file, containing the whole +% TrackMate data, and not the simplified XML file that contains only +% linear tracks. Such simplified tracks are imported using the +% importTrackMateTracks function. +% +% A TrackMate file is a XML file that starts with the following header: +% +% +% ... +% and has a Model element in it: +% +% +% OUTPUT: +% +% The ouput G is a MATLAB directed graph, which allows for the +% representation of tracks with possible split and merge events. The full +% capability of MATLAB graph is listed in the digraph class +% documentation. +% +% G.Edges and G.Nodes are two MATLAB tables that list the spot and edges +% feature values. The G.Edges.EndNodes N x 2 matrix lists the source and +% target nodes row number in the G.Nodes table. +% +% The 'rois' output (2nd output) is a cell array. The ith element is a +% Nx2 array that contains the polygon vertices coordinates (X, Y) for the +% spot in the ith line of the Nodes table. These coordinates are +% respective to the (POSITION_X, POSITION_Y) spot center. If a spot does +% not have a ROI, the cell is empty. +% +% EXAMPLE: +% +% >> G = trackmateGraph(file_path, [], [], true); +% >> x = G.Nodes.POSITION_X; +% >> y = G.Nodes.POSITION_Y; +% >> z = G.Nodes.POSITION_Z; +% >> % MATLAB cannot plot graphs in 3D, so we ship gplot23D. +% >> gplot23D( adjacency(G), [ x y z ], 'k.-' ) +% >> axis equal + +% __ +% Jean-Yves Tinevez - 2016 - 2024 + + + %% Constants definition. + + SPOT_SOURCE_ID_ATTRIBUTE = 'SPOT_SOURCE_ID'; + SPOT_TARGET_ID_ATTRIBUTE = 'SPOT_TARGET_ID'; + + %% Deal with inputs. + + if nargin < 4 + verbose = true; + if nargin < 3 + edgeFeatureList = []; + if nargin < 2 + spotFeatureList = []; + end + end + end + + %% Import spot table. + + if verbose + fprintf('Importing spot table. ') + tic + end + + if nargout >= 2 + [ spotTable, spotIDMap, rois ] = trackmateSpotsR21a(filePath, spotFeatureList); + else + [ spotTable, spotIDMap ] = trackmateSpotsR21a(filePath, spotFeatureList); + end + + + if verbose + fprintf('Done in %.1f s.\n', toc) + end + + + %% Import edge table. + + if verbose + fprintf('Importing edge table. ') + tic + end + + trackMap = trackmateEdgesR21a(filePath, edgeFeatureList); + + if verbose + fprintf('Done in %.1f s.\n', toc) + end + + tmp = trackMap.values; + edgeTable = vertcat( tmp{:} ); + + %% Build graph. + + + if verbose + fprintf('Building graph. ') + tic + end + + sourceID = edgeTable.( SPOT_SOURCE_ID_ATTRIBUTE ); + targetID = edgeTable.( SPOT_TARGET_ID_ATTRIBUTE ); + + s = cell2mat( values( spotIDMap, num2cell(sourceID) ) ); + t = cell2mat( values( spotIDMap, num2cell(targetID) ) ); + + EndNodes = [ s t ]; + + % tic + % EndNodes = arrayfun(@(id)spotIDMap(id), [sourceID targetID]); + % toc; + + % tic; + % EndNodes = edgeTable{:,{SPOT_SOURCE_ID_ATTRIBUTE SPOT_TARGET_ID_ATTRIBUTE}}; + % EndNodes = cell2mat(values(spotIDMap, num2cell(EndNodes))); + % toc; + + edgeTable = addvars(edgeTable, EndNodes, 'Before', 1, 'NewVariableNames', 'EndNodes'); + + G = digraph( edgeTable, spotTable ); + + if verbose + fprintf('Done in %.1f s.\n', toc) + end + + +end diff --git a/scripts/trackmateSpotsR21a.m b/scripts/trackmateSpotsR21a.m new file mode 100644 index 000000000..51b66d043 --- /dev/null +++ b/scripts/trackmateSpotsR21a.m @@ -0,0 +1,244 @@ +function [ spotTable, spotIDMap, rois ] = trackmateSpots(filePath, featureList) +%%TRACKMATESPOTS Import spots from a TrackMate data file. +% +% S = TRACKMATESPOTS(file_path) imports the spots contained in the +% TrackMate XML file file_path as a MATLAB table. TRACKMATESPOTS only +% imports visible spots. +% +% S = TRACKMATESPOTS(file_path, feature_list) where feature_list is a +% cell array of string only imports the spot features whose names are in +% the cell array. +% +% [ S, idMap ] = TRACKMATESPOTS( ... ) also returns idMap, a Map from +% spot ID to row number in the table. idMap is such that idMap(10) the +% row at which the spot with ID 10 is listed. +% +% [ S, idMap, rois ] = TRACKMATESPOTS( ... ) also returns rois, a cell +% array containing the 2D polygons of each spot, if there is one. +% +% INPUT: +% +% file_path must be a path to a TrackMate file, containing the whole +% TrackMate data, and not the simplified XML file that contains only +% linear tracks. Such simplified tracks are imported using the +% importTrackMateTracks function. +% +% A TrackMate file is a XML file that starts with the following header: +% +% +% ... +% and has a Model element in it: +% +% +% OUTPUT: +% +% The first output is a MATLAB table with at least two columns, ID (the +% spot ID) and name (the spot name). Extra features listed in the +% specified feature_list input appear as supplemental column. +% +% The 'rois' output (3rd output) is a cell array. The ith element is a +% Nx2 array that contains the polygon vertices coordinates (X, Y) for the +% spot in the ith line of the table S. These coordinates are respective +% to the (POSITION_X, POSITION_Y) spot center. If a spot does not have a +% ROI, the cell is empty. +% +% EXAMPLES: +% +% >> [ spotTable, spotIDMap ] = trackmateSpots(file_path, {'POSITION_X', ... +% 'POSITION_Y', 'POSITION_Z' } ); +% >> spotTable(20:25, :) +% +% ans = +% ID name POSITION_X POSITION_Y POSITION_Z +% __ _________ __________ __________ __________ +% +% 18 '18 (18)' 309.04 937.77 713.72 +% 21 '21 (21)' 210.25 1023.7 955.36 +% 20 '20 (20)' 302.03 1271.2 1247.9 +% 23 '23 (23)' 1577.6 888.73 547.66 +% 22 '22 (22)' 253.45 1186.9 1179.4 +% 25 '25 (25)' 947.44 1565.2 1297.1 +% +% >> r = spotIDMap(20) +% +% r = +% 22 +% +% >> spotTable(22, :) +% +% ans = +% ID name POSITION_X POSITION_Y POSITION_Z +% __ _________ __________ __________ __________ +% +% 20 '20 (20)' 302.03 1271.2 1247.9 +% +% >> x = spotTable.POSITION_X; +% >> y = spotTable.POSITION_Y; +% >> z = spotTable.POSITION_Z; +% >> plot3(x, y, z, 'k.') +% >> axis equal + + +% __ +% Jean-Yves Tinevez & contributors - 2026 + + %% Constants definition. + + % TRACKMATE_ELEMENT = 'TrackMate'; + SPOT_ID_ATTRIBUTE = 'ID'; + SPOT_NAME_ATTRIBUTE = 'name'; + ROI_N_POINTS_ATTTRIBUTE = 'ROI_N_POINTS'; + + %% Open file. + % This alone takes as much time as calling + % trackmateFeatureDeclarations(filePath) (which has 4x readtable())! + + % xmlDoc = parseFile(matlab.io.xml.dom.Parser, filePath) + % xmlNode = xmlDoc.getFirstChildNode; + + %% XPath to retrieve spot nodes. + % Use XPath to retrieve all visible spots. + % Note evaluate() is called on a different objective than JavaX's. + % This takes ~50min to read through an 850 MB file (R25b). Unacceptable + + % evalObj = matlab.io.xml.xpath.Evaluator(); + % filterObj = evalObj.compileExpression('//Model/AllSpots/SpotsInFrame/Spot[@VISIBILITY=1]'); + % nodeList = evalObj.evaluate(filterObj,xmlDoc, EvalResultType.NodeSet); + + %% Retrieve spot feature list. + + % We'll call trackmateFeatureDeclarations() to fill in table properties + % no matter what, so let's reuse that one's validation function. + try + fs = trackmateFeatureDeclarationsR21a( filePath ); + catch ME + throw(ME) + end + + % Difference from original code: for performance reasons, featureList + % is taken from instead of the first node + if nargin < 2 || isempty( featureList ) + featureList = keys(fs); + end + + % % Take featureList from the first node + % if nargin < 2 || isempty( featureList ) + % ATTRIBUTE_SUFFIX = '__'; + % try + % opt = detectImportOptions(filePath, 'FileType', 'xml', ... + % 'RowSelector', '/TrackMate/Model/AllSpots/SpotsInFrame/Spot[1]', ... + % 'ImportAttributes', true, 'AttributeSuffix', ATTRIBUTE_SUFFIX, ... + % 'VariableNamingRule', 'preserve'); + % featureList = opt.SelectedVariableNames; + % featureListSel = endsWith(featureList, ATTRIBUTE_SUFFIX); + % featureList = extractBefore(featureList(featureListSel), ... + % ATTRIBUTE_SUFFIX+textBoundary('end')); + % if isstring(featureList) + % featureList = cellstr(featureList); + % end + % catch ME + % switch ME.identifier + % case 'MATLAB:io:xml:detection:RowSelectorInvalidSelection' + % % No spots in file + % featureList = {}; + % otherwise + % throw(ME) + % end + % end + % end + %% Create table. + opt = makeXMLOptionsSpot(featureList); + + % Read ROI coords if it's requested + if nargout >= 3 + willReadROIs = true; + opt.SelectedVariableNames = [cellstr(opt.SelectedVariableNames) {'rois'}]; + else + willReadROIs = false; + end + + spotTable = readtable(filePath, opt); + nSpots = height(spotTable); + + if willReadROIs + roistrs = spotTable.rois; + rois = cell( nSpots, 1 ); + for i = 1 : nSpots + coords_str = roistrs{i}; + if ~isempty( coords_str ) + A = sscanf( coords_str, '%f' ); + A = reshape( A, 2, [] ).'; + rois{i} = A; + end + end + spotTable = removevars(spotTable, 'rois'); + end + + % Set table metadata. + spotTable.Properties.DimensionNames = { 'Spot', 'Feature' }; + + [vDescriptions,vUnits] = cellfun(@determineDescriptions, ... + cellstr(spotTable.Properties.VariableNames), ... + "UniformOutput",false); + + spotTable.Properties.VariableDescriptions = vDescriptions; + spotTable.Properties.VariableUnits = vUnits; + + %% Generate map ID -> table row number. + if nargout >= 2 + spotIDMap = containers.Map( spotTable.ID, 1 : nSpots, ... + 'UniformValues', true); + end + + %% Subfunction. + + function opt = makeXMLOptionsSpot(featureList) + % Remove ID and name, because we will get them anyway. + featureList = setdiff( featureList, SPOT_ID_ATTRIBUTE ); + featureList = setdiff( featureList, SPOT_NAME_ATTRIBUTE ); + + nodePath = '/TrackMate/Model/AllSpots/SpotsInFrame/Spot[@VISIBILITY=1]'; + + n_features = numel( featureList ); + varNames = cell( n_features + 3, 1 ); % { ID; name; featureList; rois } + varTypes = repmat( {'double'}, n_features + 3, 1 ); + varNames{1} = SPOT_ID_ATTRIBUTE; + % ID is imported as double, mimicking the original code + varNames{2} = SPOT_NAME_ATTRIBUTE; + varTypes{2} = 'char'; + varNames(3:end-1) = featureList; + varNames{end} = 'rois'; + varTypes{end} = 'char'; + % The VISIBILITY attribute wasn't given special treatment (ignore, + % or read as logical), just like the original code + + varSelectors = append( '(', nodePath, ')/@', varNames ); + varSelectors{end} = nodePath; % Select the node itself for its text + + opt = xmlImportOptions( 'NumVariables', n_features+3, ... + 'VariableNames', varNames, 'VariableTypes', varTypes, ... + 'VariableSelectors', varSelectors, 'RowSelector', nodePath, ... + 'SelectedVariableNames', 1:n_features+2, ... + 'VariableNamingRule', 'preserve', 'MissingRule', 'fill' ); + + % Preserve whitespaces in name and rois + opt = setvaropts(opt, [2 n_features+3], 'WhitespaceRule', 'preserve'); + end + + function [desc, unit] = determineDescriptions( varName ) + switch ( varName ) + case SPOT_ID_ATTRIBUTE + desc = 'Spot ID'; + unit = ''; + case SPOT_NAME_ATTRIBUTE + desc = 'Spot name'; + unit = ''; + case ROI_N_POINTS_ATTTRIBUTE + desc = 'ROI N points'; + unit = ''; + otherwise + desc = fs(varName).name; + unit = fs(varName).units; + end + end +end