Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions scripts/trackmateEdgesR21a.m
Original file line number Diff line number Diff line change
@@ -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:
% <?xml version="1.0" encoding="UTF-8"?>
% <TrackMate version="3.3.0">
% ...
% and has a Model element in it:
% <Model spatialunits="pixel" timeunits="sec">
%
% 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 <FeatureDelarations> instead of the first <Edge> node
if nargin < 2 || isempty( featureList )
featureList = keys(ef);
end

% % Take featureList from the first <Edge> 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
166 changes: 166 additions & 0 deletions scripts/trackmateFeatureDeclarationsR21a.m
Original file line number Diff line number Diff line change
@@ -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:
% <?xml version="1.0" encoding="UTF-8"?>
% <TrackMate version="3.3.0">
% ...
% and has a Model element in it:
% <Model spatialunits="pixel" timeunits="sec">
%
% 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

Loading