diff --git a/scripts/trackmateEdgesR21a.m b/scripts/trackmateEdgesR21a.m new file mode 100644 index 000000000..49db4e54a --- /dev/null +++ b/scripts/trackmateEdgesR21a.m @@ -0,0 +1,273 @@ +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. + + TRACK_ID_ATTRIBUTE = 'TRACK_ID'; + TRACK_NAME_ATTRIBUTE = 'name'; + SPOT_SOURCE_ID_ATTRIBUTE = 'SPOT_SOURCE_ID'; + SPOT_TARGET_ID_ATTRIBUTE = 'SPOT_TARGET_ID'; + + %% Retrieve edge feature list. + global isNotFirst docNode %#ok + if isNotFirst + % Being called by other function + willClear = false; + else + isNotFirst = true; + willClear = true; + end + % 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 + + rootObj = docNode.getDocumentElement; + modelNodes = rootObj.getElementsByTagName('Model'); + + % Take featureList from the first node + if nargin < 2 || isempty( featureList ) + % //Edge. Why not full path? + edgeList = rootObj.getElementsByTagName( 'Edge' ); + if edgeList.Length > 0 + attrMap = edgeList.node(1).getAttributes; + nFeatures = attrMap.Length; + featureList = cell(nFeatures, 1); + for k = 1 : nFeatures + featureList{k} = attrMap.item(k-1).Name; + end + else + featureList = {}; + end + end + + frontOfList = { SPOT_SOURCE_ID_ATTRIBUTE; SPOT_TARGET_ID_ATTRIBUTE }; + featureList = union(frontOfList, featureList, 'stable'); + nFeatures = numel(featureList); + + %% XPath to retrieve filtered track elements. + % Initialize map + trackMap = containers.Map('KeyType', 'char', 'ValueType', 'any'); + + % XPath: //Model/FilteredTracks/TrackID/@TrackID + fTracks = zeros(rootObj.getElementsByTagName('TrackID').Length, 1); + iTracks = 0; + for j = 1:modelNodes.Length + node = modelNodes.node(j); + % Model level + fTNode = node.getFirstElementChild; + while ~isempty(fTNode) + if strcmp('FilteredTracks', fTNode.TagName) + % FilteredTracks level + tIDNode = fTNode.getFirstElementChild; + while ~isempty(tIDNode) + if strcmp('TrackID', tIDNode.TagName) + % Found TrackID node + iTracks = iTracks + 1; + fTracks(iTracks) = ... + str2double(tIDNode.getAttribute(TRACK_ID_ATTRIBUTE)); + end + tIDNode = tIDNode.getNextElementSibling; + end + end + fTNode = fTNode.getNextElementSibling; + end + end + + fTracks(iTracks+1:end) = []; + + if isempty(fTracks) + % No selected track, return empty map + if willClear + clear global isNotFirst docNode docNodeFileName + end + return + end + + %% Read Tracks table + neTracks = rootObj.getElementsByTagName('Track').Length; + names = cell(neTracks, 1); + tracks = zeros(neTracks, 1); + iTracks = 0; + for j = 1 : modelNodes.Length + node = modelNodes.node(j); + % Model level + aTNode = node.getFirstElementChild; + while ~isempty(aTNode) + if strcmp('AllTracks', aTNode.TagName) + % AllTracks level + tNode = aTNode.getFirstElementChild; + while ~isempty(tNode) + if strcmp('Track', tNode.TagName) + % Track level + iTracks = iTracks + 1; + at = tNode.getAttributes; + names{iTracks} = at.getNamedItem(TRACK_NAME_ATTRIBUTE).Value; + tracks(iTracks) = str2double(at.getNamedItem(TRACK_ID_ATTRIBUTE).Value); + end + tNode = tNode.getNextElementSibling; + end + end + aTNode = aTNode.getNextElementSibling; + end + end + names(iTracks+1:neTracks) = []; + tracks(iTracks+1:neTracks) = []; + + % Find the selected Track IDs and cache the result. + whichSel = ismember( tracks, fTracks); + + %% Prepare a map: trackName -> edge table. + % 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 + % None of the selected tracks were found, return empty map. + if willClear + clear global isNotFirst docNode docNodeFileName + end + return + end + + % Current track No. + iTracks = 0; + + % XPath: (//Model/AllTracks/Track)/Edge + for j = 1 : modelNodes.Length + node = modelNodes.node(j); + % Model level + aTNode = node.getFirstElementChild; + while ~isempty(aTNode) + if strcmp('AllTracks', aTNode.TagName) + % AllTracks level + tNode = aTNode.getFirstElementChild; + while ~isempty(tNode) + if strcmp('Track', tNode.TagName) + % Track level + iTracks = iTracks + 1; + if whichSel(iTracks) + tbl = walkAndMakeTable(tNode); + + tbl.Properties.DimensionNames = { 'Edge', 'Feature' }; + tbl.Properties.VariableDescriptions = vDescriptions; + tbl.Properties.VariableUnits = vUnits; + + trackMap( names{iTracks} ) = tbl; + end + end + tNode = tNode.getNextElementSibling; + end + end + aTNode = aTNode.getNextElementSibling; + end + end + + if willClear + clear global isNotFirst docNode docNodeFileName + end + + %% Subfunction. + + function edgeTable = walkAndMakeTable(trackNode) + % Assuming every child element is an ... + neEdges = trackNode.getChildElementCount; + holders = cell(1, nFeatures); + for i = 1 : nFeatures + holders{i} = zeros(neEdges, 1); + end + nEdges = 0; + edgeNode = trackNode.getFirstElementChild; + while ~isempty(edgeNode) + if strcmp('Edge', edgeNode.TagName) + nEdges = nEdges+1; + attrs = edgeNode.getAttributes; + for i = 1 : nFeatures + holders{i}(nEdges) = str2double(attrs.getNamedItem(featureList{i}).Value); + end + end + edgeNode = edgeNode.getNextElementSibling; + end + + % And trim the end if that turns out to be false + if nEdges < neEdges + for i = 1 : nFeatures + holders{i}(nEdges+1:end) = []; + end + end + + edgeTable = table(holders{:}, 'VariableNames', featureList); + end +end + diff --git a/scripts/trackmateFeatureDeclarationsR21a.m b/scripts/trackmateFeatureDeclarationsR21a.m new file mode 100644 index 000000000..b58e23c06 --- /dev/null +++ b/scripts/trackmateFeatureDeclarationsR21a.m @@ -0,0 +1,221 @@ +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 + + + %% 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... + % Parsing a large file takes time. Cache the document until return. + global isNotFirst docNode docNodeFileName %#ok + if isNotFirst + % Being called by other function + willClear = false; + else + isNotFirst = true; + willClear = true; + end + + % Either being called by user, or being called by other functions and + % is the first run. Or somehow was used to work on another file. + if willClear || isempty(docNode) || ~strcmp(docNodeFileName, filePath) + try + docNode = matlab.io.xml.dom.Parser().parseFile( filePath ); + catch ME + switch ME.identifier + case 'MATLAB:UndefinedFunction' + error("Your MATLAB is too old (pre-R2021a) to run this script.") + otherwise + throw(ME) + end + end + docNodeFileName = filePath; + end + + rootNode = docNode.getDocumentElement; + if isempty(rootNode) || ~strcmp(TRACKMATE_ELEMENT, rootNode.TagName) + error('MATLAB:trackMateGraph:BadXMLFile', ... + 'File does not seem to be a proper TrackMate file.') + end + + %% And retrieve physical units. + modelNode = rootNode.getFirstElementChild; + modelFound = false; + while ~isempty(modelNode) + if strcmp('Model', modelNode.TagName) + modelFound = true; + break + end + modelNode = modelNode.getNextElementSibling; + end + + if ~modelFound + error('MATLAB:trackMateGraph:BadXMLFile', ... + 'File does not seem to contain a valid Model element.') + end + + spaceUnits = modelNode.getAttribute( SPATIAL_UNITS_ATTRIBUTE ); + timeUnits = modelNode.getAttribute( TIME_UNITS_ATTRIBUTE ) ; + + %% XPath to retrieve spot feature declarations. + % /TrackMate/Model/FeatureDeclarations/EdgeFeatures/Feature + sf = makeFeatureTable('SpotFeatures', modelNode); + sf = transformFeatureTable(sf, spaceUnits, timeUnits); + + %% XPath to retrieve edge feature declarations. + if nargout >= 2 + % /TrackMate/Model/FeatureDeclarations/EdgeFeatures/Feature + ef = makeFeatureTable('EdgeFeatures', modelNode); + ef = transformFeatureTable(ef, spaceUnits, timeUnits); + end + + %% XPath to retrieve track feature declarations. + if nargout >= 3 + % /TrackMate/Model/FeatureDeclarations/TrackFeatures/Feature + tf = makeFeatureTable('TrackFeatures', modelNode); + tf = transformFeatureTable(tf, spaceUnits, timeUnits); + end + + if willClear + clear global isNotFirst docNode docNodeFileName + end + + %% Subfunctions. + + function ft = makeFeatureTable(featName, modelNode) + prevW = warning('off', 'MATLAB:table:PreallocateCharWarning'); + ft = table( 'Size', [0 5], ... + 'VariableNames', {'key' 'name' 'shortName' 'dimension' 'isInt'}, ... + 'VariableTypes', {'char' 'char' 'char' 'char' 'logical'}); + + declNode = modelNode.getFirstElementChild; + while ~isempty(declNode) + if strcmp('FeatureDeclarations', declNode.TagName) + % FeatureDeclarations level + fDNode = declNode.getFirstElementChild; + while ~isempty(fDNode) + if strcmp(featName, fDNode.TagName) + % featName level + neFeatures = fDNode.getChildElementCount; + key = cell(neFeatures, 1); + name = cell(neFeatures, 1); + shortName = cell(neFeatures, 1); + dimension = cell(neFeatures, 1); + isInt = false(neFeatures, 1); + + iFeat = 0; + featNode = fDNode.getFirstElementChild; + while ~isempty(featNode) + if strcmp('Feature', featNode.TagName) + % Feature node + iFeat = iFeat+1; + attrs = featNode.getAttributes; + key{iFeat} = attrs.getNamedItem(FEATURE_KEY_ATTRIBUTE).Value; + name{iFeat} = attrs.getNamedItem(FEATURE_NAME_ATTRIBUTE).Value; + shortName{iFeat} = attrs.getNamedItem(FEATURE_SHORTNAME_ATTRIBUTE).Value; + dimension{iFeat} = attrs.getNamedItem(FEATURE_DIMENSION_ATTRIBUTE).Value; + isInt(iFeat) = strcmp('true', attrs.getNamedItem(FEATURE_ISINT_ATTRIBUTE).Value); + end + featNode = featNode.getNextElementSibling; + end + + t = table(key, name, shortName, dimension, isInt); + if iFeat < neFeatures + t(iFeat+1:end, :) = []; + end + ft = vertcat(ft, t); %#ok + end + fDNode = fDNode.getNextElementSibling; + end + end + declNode = declNode.getNextElementSibling; + end + + warning(prevW); + 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..d71877a9c --- /dev/null +++ b/scripts/trackmateGraphR21a.m @@ -0,0 +1,151 @@ +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 & contributors - 2026 + + + %% 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 + + global isNotFirst %#ok + if isNotFirst + % Being called by other function + willClear = false; + else + isNotFirst = true; + willClear = true; + 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 = values(trackMap); + 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 ]; + + edgeTable = addvars(edgeTable, EndNodes, 'Before', 1, 'NewVariableNames', 'EndNodes'); + + G = digraph( edgeTable, spotTable ); + + if verbose + fprintf('Done in %.1f s.\n', toc) + end + + if willClear + clear global isNotFirst docNode docNodeFileName + end +end diff --git a/scripts/trackmateImageCalibrationR21a.m b/scripts/trackmateImageCalibrationR21a.m new file mode 100644 index 000000000..a9ddc4c21 --- /dev/null +++ b/scripts/trackmateImageCalibrationR21a.m @@ -0,0 +1,93 @@ +function cal = trackmateImageCalibration(path) +%%TRACKMATEIMAGECALIBRATION Reads the image calibration from a TrackMate file. +% +% cal = TRACKMATEIMAGECALIBRATION(file_path) returns the physical image +% calibration from a TrackMate file. +% +% 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: +% +% Calibration is returned as a struct with four fields: x, y, z and t. +% Each of this field is a struct with the pixel size or frame interval +% in physical units + +% __ +% Jean-Yves Tinevez & contributors - 2026 + + + %% Open XML file + try + tree = matlab.io.xml.dom.Parser().parseFile(path); + root = tree.getDocumentElement; + catch ME + switch ME.identifier + case 'MATLAB:UndefinedFunction' + error("Your MATLAB is too old (pre-R2021a) to run this script.") + otherwise + throw(ME) + end + end + + %% Prepare dim strings + + dimensionNames = { 'x', 'y', 'z', 't' }; + calibrationNames = { 'pixelwidth', 'pixelheight', 'voxeldepth', 'timeinterval' }; + unitsNames = { 'spatialunits', 'spatialunits', 'spatialunits', 'timeunits' }; + sizeNames = { 'width', 'height', 'nslices', 'nframes' }; + + %% Collect basic settings. + % //Settings[1]//BasicSettings[1] + settings = root.getElementsByTagName('Settings'); + settings = settings.item(0); + bs = settings.getElementsByTagName('BasicSettings'); + bs = bs.item(0); + + %% Collect image settings. + % //ImageData[1] + id = root.getElementsByTagName('ImageData'); + id = id.item(0); + + %% Populate calibration structure with values. + + for i = 1 : numel(dimensionNames) + + dim = dimensionNames{i}; + + if ~isempty( bs ) + cal.(dim).start = str2double(bs.getAttribute([dim 'start'])); + cal.(dim).end = str2double(bs.getAttribute([dim 'end'])); + cal.(dim).size = str2double(id.getAttribute(sizeNames{i})); + end + cal.(dim).value = str2double(id.getAttribute(calibrationNames{i})); + + end + + %% Get physical units from model element. + % //Model[1] + model = root.getElementsByTagName('Model'); + model = model.item(0); + + %% Populate calibration structure with values. + + for i = 1 : numel(dimensionNames) + + dim = dimensionNames{i}; + + cal.(dim).units = model.getAttribute(unitsNames{i}); + + end + +end diff --git a/scripts/trackmateSpotsR21a.m b/scripts/trackmateSpotsR21a.m new file mode 100644 index 000000000..6044a8904 --- /dev/null +++ b/scripts/trackmateSpotsR21a.m @@ -0,0 +1,265 @@ +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. + + SPOT_ID_ATTRIBUTE = 'ID'; + SPOT_NAME_ATTRIBUTE = 'name'; + ROI_N_POINTS_ATTTRIBUTE = 'ROI_N_POINTS'; + + %% Retrieve spot feature list. + global isNotFirst docNode %#ok + if isNotFirst + % Being called by other function + willClear = false; + else + isNotFirst = true; + willClear = true; + end + + % 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 + rootObj = docNode.getDocumentElement; + modelNodes = rootObj.getElementsByTagName('Model'); + + % Take featureList from the first node + if nargin < 2 || isempty( featureList ) + spotFound = false; + % XPath: (TrackMate/Model/AllSpots/SpotsInFrame/Spot)[1] + for j = 1:modelNodes.Length + aSNode = modelNodes.node(j).getFirstElementChild; + while ~isempty(aSNode) + if strcmp ('AllSpots', aSNode.TagName) + % AllSpots level + sIFNode = aSNode.getFirstElementChild; + while ~isempty(sIFNode) + if strcmp('SpotsInFrame', sIFNode.TagName) + spotNode = sIFNode.getFirstElementChild; + while ~isempty(spotNode) + if strcmp('Spot', spotNode.TagName) + spotFound = true; + attrMap = spotNode.getAttributes; + nFeatures = attrMap.Length; + featureList = cell(attrMap.Length, 1); + for k = 1:nFeatures + featureList{k} = attrMap.item(k-1).Name; + end + end + spotNode = spotNode.getNextElementSibling; + end + end + sIFNode = sIFNode.getNextElementSibling; + end + end + aSNode = aSNode.getNextElementSibling; + end + end + if ~spotFound + featureList = {}; + end + end + + %% Create table. + % Remove ID and name, because we will get them anyway. + featureList = setdiff( featureList, SPOT_ID_ATTRIBUTE ); + featureList = setdiff( featureList, SPOT_NAME_ATTRIBUTE ); + featureList = [{SPOT_ID_ATTRIBUTE; SPOT_NAME_ATTRIBUTE}; featureList]; + + n_features = numel( featureList ); + + % Preallocate. Assuming every Spot node resides in the right place. + neSpots = 0; + for j = 1:modelNodes.Length + neSpots = neSpots + modelNodes.node(j).getElementsByTagName('Spot').Length; + end + + holder = cell(1, n_features); + holder{1} = zeros(neSpots, 1); + holder{2} = cell(neSpots, 1); + for k = 3:n_features + holder{k} = zeros(neSpots, 1); + end + + % Read ROI coords if it's requested + if nargout >= 3 + willReadROIs = true; + rois = cell(neSpots, 1); + else + willReadROIs = false; + end + + %% Read the table for real + nSpots = 0; + % XPath: //Model/AllSpots/SpotsInFrame/Spot + for j = 1:modelNodes.Length + aSNode = modelNodes.node(j).getFirstElementChild; + while ~isempty(aSNode) + if strcmp ('AllSpots', aSNode.TagName) + % AllSpots level + sIFNode = aSNode.getFirstElementChild; + while ~isempty(sIFNode) + if strcmp('SpotsInFrame', sIFNode.TagName) + % SpotsInFrame level. + spotNode = sIFNode.getFirstElementChild; + while ~isempty(spotNode) + if strcmp('Spot', spotNode.TagName) + nSpots = nSpots + 1; + attrMap = spotNode.getAttributes; + holder{1}(nSpots) = str2double(attrMap.getNamedItem(featureList{1}).Value); + holder{2}{nSpots} = attrMap.getNamedItem(featureList{2}).Value; + for k = 3:nFeatures + holder{k}(nSpots) = str2double(attrMap.getNamedItem(featureList{k}).Value); + end + + if willReadROIs + coords_str = spotNode.TextContent; + if ~isempty(coords_str) + A = sscanf( coords_str, '%f' ); + A = reshape( A, 2, [] ).'; + rois{nSpots} = A; + end + end + end + spotNode = spotNode.getNextElementSibling; + end + end + sIFNode = sIFNode.getNextElementSibling; + end + end + aSNode = aSNode.getNextElementSibling; + end + end + + if nSpots ~= neSpots + for k = 1 : nFeatures + holder{k}(nSpots+1:end) = []; + end + if willReadROIs + rois(nSpots+1:end) = []; + end + end + + spotTable = table(holder{:}, 'VariableNames', featureList); + spotTable.Properties.DimensionNames = { 'Spot', 'Feature' }; + + [vDescriptions,vUnits] = cellfun(@determineDescriptions, ... + featureList, '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 + + if willClear + clear global isNotFirst docNode docNodeFileName + end + + %% Subfunction. + + 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