From 9e5c3e0c8588cfd115f78f1a630ebe1a01513b16 Mon Sep 17 00:00:00 2001 From: Jerry1144 <6694895+Jerry1144@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:28:16 +0800 Subject: [PATCH 1/2] Rewrite with readstruct() Rewritten MATLAB import functions with readstruct(), available since R2020b Shifting to MATLAB's own capabilities helps alleviate Java heap size problems. MATLAB's memory use will skyrocket during run, but memory pressure will not. Caveat: readstruct() automatically converts imported data into "appropriate" forms, and there's no control over that. --- scripts/importTrackMateTracks.m | 120 +++++------ scripts/trackmateEdges.m | 281 ++++++++++++++---------- scripts/trackmateFeatureDeclarations.m | 154 ++++++------- scripts/trackmateGraph.m | 40 ++-- scripts/trackmateSpots.m | 285 ++++++++++++++++--------- 5 files changed, 519 insertions(+), 361 deletions(-) diff --git a/scripts/importTrackMateTracks.m b/scripts/importTrackMateTracks.m index fdbf643cf..737feb751 100644 --- a/scripts/importTrackMateTracks.m +++ b/scripts/importTrackMateTracks.m @@ -63,9 +63,14 @@ % % ... % -% -% -% Jean-Yves Tinevez - 2013 + + +% __ +% Jean-Yves Tinevez & contributors - 2026 + + %% Constants definition. + + ATTRIBUTE_SUFFIX = "__"; %% Input @@ -81,80 +86,71 @@ %% Load and Test compliance try - doc = xmlread(file); - catch %#ok - error('Failed to read XML file %s.',file); - end - - root = doc.getDocumentElement; - - if ~strcmp(root.getTagName, 'Tracks') - error('MATLAB:importTrackMateTracks:BadXMLFile', ... - 'File does not seem to be a proper track file.') + rootStruct = readstruct(file, "FileType", "xml", "StructSelector", "/Tracks", ... + "ImportAttributes", true, "AttributeSuffix", ATTRIBUTE_SUFFIX); + catch ME + switch ME.identifier + case 'MATLAB:UndefinedFunction' + error("Your MATLAB is too old (pre-R2021a) to run this script."); + case 'MATLAB:nonExistentField' + error('MATLAB:importTrackMateTracks:BadXMLFile', ... + "File does not seem to be a proper track file."); + otherwise + error(ME.identifier, "Failed to read XML file %s.", file); + end end %% Get metadata - metadata.spaceUnits = char( root.getAttribute('spaceUnits') ); - metadata.timeUnits = char( root.getAttribute('timeUnits') ); - metadata.frameInterval = str2double( root.getAttribute('frameInterval') ); - metadata.date = char( root.getAttribute('generationDateTime') ); - metadata.source = char( root.getAttribute('from') ); + metadata.spaceUnits = char(rootStruct.("spaceUnits"+ATTRIBUTE_SUFFIX)); + metadata.timeUnits = char(rootStruct.("timeUnits"+ATTRIBUTE_SUFFIX)); + metadata.frameInterval = rootStruct.("frameInterval"+ATTRIBUTE_SUFFIX); + if ~isa(metadata.frameInterval, "double") + metadata.frameInterval = double(metadata.frameInterval); end + % readstruct() recognizes date and transforms into Datetime. Probably need + % a datefmt argument here + metadata.date = char(rootStruct.("generationDateTime"+ATTRIBUTE_SUFFIX)); + metadata.source = char(rootStruct.("from"+ATTRIBUTE_SUFFIX)); + %% Scale time using physical units if required + + % NaN is not greater than zero + willScaleT = scalet && metadata.frameInterval > 0; + %% Parse - nTracks = str2double( root.getAttribute('nTracks') ); + nTracks = rootStruct.("nTracks"+ATTRIBUTE_SUFFIX); + if ~isa(nTracks, "double"); nTracks = double(nTracks); end tracks = cell(nTracks, 1); - trackNodes = root.getElementsByTagName('particle'); + trackNodes = rootStruct.particle(1:nTracks); for i = 1 : nTracks - - trackNode = trackNodes.item(i-1); - detectionNodes = trackNode.getElementsByTagName('detection'); - nSpots = str2double( trackNode.getAttribute('nSpots') ); - nSpots = min( nSpots, detectionNodes.getLength() ); + trackNode = trackNodes(i); + detectionNodes = trackNode.detection; - A = NaN( nSpots, 4); % T, X, Y, Z + nSpots = double(trackNode.("nSpots"+ATTRIBUTE_SUFFIX)); + nSpots = min( nSpots, numel(detectionNodes) ); + detectionNodes = detectionNodes(1:nSpots); - for j = 1 : nSpots - - detectionNode = detectionNodes.item(j-1); - t = str2double(detectionNode.getAttribute('t')); - x = str2double(detectionNode.getAttribute('x')); - y = str2double(detectionNode.getAttribute('y')); - z = str2double(detectionNode.getAttribute('z')); - A(j, :) = [ t x y z ]; - + t = [detectionNodes.("t"+ATTRIBUTE_SUFFIX)]; + if ~isa(t, "double"); t = double(t); end + x = [detectionNodes.("x"+ATTRIBUTE_SUFFIX)]; + if ~isa(x, "double"); x = double(x); end + y = [detectionNodes.("y"+ATTRIBUTE_SUFFIX)]; + if ~isa(y, "double"); y = double(y); end + z = [detectionNodes.("z"+ATTRIBUTE_SUFFIX)]; + if ~isa(z, "double"); z = double(z); end + + if willScaleT + t = t * metadata.frameInterval; end - - tracks{i} = A; - - end - - %% Clip Z dimension if possible and asked - - if clipz - - if all(cellfun(@(X) all( X(:,4) == 0), tracks)) + + if clipz && all(z == 0) % Remove the z coordinates since it is 0 everywhere - for i = 1 : nTracks - tracks{i} = tracks{i}(:, 1:3); - end - end - - end - - %% Scale time using physical units if required - - if scalet - if ~isnan(metadata.frameInterval) && metadata.frameInterval > 0 - - % Scale time so that it is in physical units - for i = 1 : nTracks - tracks{i}(:, 1) = tracks{i}(:, 1) * metadata.frameInterval; - end - + tracks{i} = [t(:) x(:) y(:)]; + else + tracks{i} = [t(:) x(:) y(:) z(:)]; end end diff --git a/scripts/trackmateEdges.m b/scripts/trackmateEdges.m index 22044adf7..de41f4c24 100644 --- a/scripts/trackmateEdges.m +++ b/scripts/trackmateEdges.m @@ -60,136 +60,201 @@ % __ -% Jean-Yves Tinevez - 2016 +% Jean-Yves Tinevez & contributors - 2026 -%% Import the XPath classes. - import javax.xml.xpath.* - %% 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'; + TRACK_ID_ATTRIBUTE = "TRACK_ID"; + TRACK_NAME_ATTRIBUTE = "name"; + SPOT_SOURCE_ID_ATTRIBUTE = "SPOT_SOURCE_ID"; + SPOT_TARGET_ID_ATTRIBUTE = "SPOT_TARGET_ID"; + ATTRIBUTE_SUFFIX = "__"; %% Open file - try - xmlDoc = xmlread( filePath ); - catch - error('Failed to read XML file %s.',filePath); - end - xmlRoot = xmlDoc.getFirstChild(); - - if ~strcmp(xmlRoot.getTagName, TRACKMATE_ELEMENT) - error('MATLAB:trackMateGraph:BadXMLFile', ... - 'File does not seem to be a proper TrackMate file.') + % We'll call trackmateFeatureDeclarations() to fill in table properties + % no matter what, so let's reuse that one's validation function. + global isNotFirst modelStruct %#ok + if isNotFirst + % Being called by other function + willClear = false; + else + isNotFirst = true; + willClear = true; end - - %% XPath initialization. - factory = XPathFactory.newInstance; - xPath = factory.newXPath; - - %% Retrieve edge feature list - if nargin < 2 || isempty( featureList ) - xPathEdgeFilter = xPath.compile('//Edge'); - edgeNode = xPathEdgeFilter.evaluate(xmlDoc, XPathConstants.NODE ); - featureList = getEdgeFeatureList( edgeNode ); + try + [ ~, ef ] = trackmateFeatureDeclarations( filePath ); + catch ME + rethrow(ME) end - % Add spot source and target, whether they are here or not. - featureList = union( SPOT_TARGET_ID_ATTRIBUTE, featureList, 'stable' ); - featureList = union( SPOT_SOURCE_ID_ATTRIBUTE, featureList, 'stable' ); - nFeatures = numel( featureList ); - %% XPath to retrieve filtered track IDs. - xPathFTrackFilter = xPath.compile('//Model/FilteredTracks/TrackID'); - fTrackNodeList = xPathFTrackFilter.evaluate(xmlDoc, XPathConstants.NODESET); - nFTracks = fTrackNodeList.getLength(); + % Prepare a map: trackName -> edge table. + trackMap = containers.Map("KeyType", "char", "ValueType", "any"); - fTrackIDs = NaN( nFTracks, 1); - for i = 1 : nFTracks - fTrackIDs( i ) = str2double( fTrackNodeList.item( i-1 ).getAttribute( TRACK_ID_ATTRIBUTE ) ); + try + filteredIDStruct = [modelStruct.FilteredTracks]; + filteredIDStruct = [filteredIDStruct.TrackID]; + fTrackIDs = [filteredIDStruct.( TRACK_ID_ATTRIBUTE+ATTRIBUTE_SUFFIX )]; + if ~isa(fTrackIDs, "double"); fTrackIDs = double(fTrackIDs); end + catch ME + switch ME.identifier + case 'MATLAB:nonExistentField' + % XPath points to 0 nodes + fTrackIDs = []; + otherwise + rethrow(ME) + end end - %% XPath to retrieve filtered track elements. - - xPathTrackFilter = xPath.compile('//Model/AllTracks/Track'); - trackNodeList = xPathTrackFilter.evaluate(xmlDoc, XPathConstants.NODESET); - nTracks = trackNodeList.getLength(); - - % Prepare a map: trackName -> edge table. - trackMap = containers.Map('KeyType', 'char', 'ValueType', 'any'); + if isempty(fTrackIDs) + % No selected track, return empty map + if willClear + clear global isNotFirst modelStruct xmlDocFileName + end + return + end - xPathEdgeFilter = xPath.compile('./Edge'); - for i = 1 : nTracks - - trackNode = trackNodeList.item( i-1 ); - trackID = str2double( trackNode.getAttribute( TRACK_ID_ATTRIBUTE ) ); - trackName = char( trackNode.getAttribute( TRACK_NAME_ATTRIBUTE ) ); - - if any( trackID == fTrackIDs ) - - edgeNodeList = xPathEdgeFilter.evaluate( trackNode, XPathConstants.NODESET ); - nEdges = edgeNodeList.getLength(); - features = NaN( nEdges, nFeatures ); - - % Read all edge nodes. - for k = 1 : nEdges - node = edgeNodeList.item( k-1 ); - for j = 1 : nFeatures - features( k, j ) = str2double( node.getAttribute( featureList{ j } ) ); + %% XPath to retrieve track elements. + + try + tracksStruct = [modelStruct.AllTracks]; % Can be multiple? + tracksStruct = [tracksStruct.Track]; % Likely multiple + catch ME + switch ME.identifier + case 'MATLAB:nonExistentField' + % XPath points to 0 nodes + if willClear + clear global isNotFirst modelStruct xmlDocFileName + end + return + otherwise + rethrow(ME) + end + end + + %% Retrieve edge feature list + % Guess the attribute name from struct names + % Valid XML name is a superset of MATLAB variable name, so MATLAB + % may have modified them when importing into struct fields + + % Combine knowledge from FeatureDeclarations and user input + if exist("featureList", "var") + fList = union( featureList, keys(ef)); + else + fList = keys(ef); + end + + [fList_mod, havemodd1] = matlab.lang.makeValidName(fList); + [fList_mod, havemodd2] = matlab.lang.makeUniqueStrings(fList_mod); + fList_mod = append(fList_mod, ATTRIBUTE_SUFFIX); + whichModified = havemodd1 | havemodd2; + + if nargin < 2 || isempty( featureList ) + % List of feature is all edge attributes. Look up the original name + % if it's *known* to be non-trivially renamed. + % Still, we will lose the original attribute name if it doesn't + % appear in + + featureList_mod = fieldnames(tracksStruct(1).Edge(1)); + % May contain a Text field for the node's text + featureList_mod = featureList_mod(endsWith(featureList_mod, ATTRIBUTE_SUFFIX)); + % Add spot source and target, whether they are here or not. + frontOfList = append([SPOT_SOURCE_ID_ATTRIBUTE; SPOT_TARGET_ID_ATTRIBUTE], ATTRIBUTE_SUFFIX); + featureList_mod = union(frontOfList, featureList_mod, "stable"); + featureList = strings(size(featureList_mod)); + + if any(whichModified) + renameMap = containers.Map(fList_mod(whichModified), fList(whichModified)); + willLookup = iskey(renameMap, cellstr(featureList_mod)); + for k = 1:numel(featureList_mod) + feature_mod = featureList_mod{k}; + if willLookup(k) + featureList{k} = renameMap(feature_mod); + else + featureList{k} = extractBefore(feature_mod, ... + ATTRIBUTE_SUFFIX+textBoundary("end")); end end - - % Create table. - edgeTable = table(); - for j = 1 : nFeatures - edgeTable.( featureList{ j } ) = features( :, j ); - end - - % Set table metadata. - edgeTable.Properties.DimensionNames = { 'Edge', 'Feature' }; - - vNames = edgeTable.Properties.VariableNames; - nVNames = numel( vNames ); - vDescriptions = cell( nVNames, 1); - vUnits = cell( nVNames, 1); - - [ ~, ef ] = trackmateFeatureDeclarations( filePath ); - for l = 1 : nVNames - vn = vNames{ l }; - vDescriptions{ l } = ef( vn ).name; - vUnits{ l } = ef( vn ).units; + else + featureList = extractBefore(featureList_mod, ... + ATTRIBUTE_SUFFIX+textBoundary("end")); + end + + else + % List of feature is the input list. Still, the renaming is done + % according to real attributes, so we look up the modified names. + featureList = string(featureList(:)); + + % Add spot source and target, whether they are here or not. + frontOfList = [SPOT_SOURCE_ID_ATTRIBUTE; SPOT_TARGET_ID_ATTRIBUTE]; + featureList = union( frontOfList, featureList, "stable" ); + + if any(whiwhModified) + renameMapRev = containers.Map(fList(whichModified), fList_mod(whichModified)); + willLookup = iskey(renameMapRev, cellstr(featureList)); + featureList_mod = strings(size(featureList)); + for k = 1:numel(featureList) + feature = featureList{k}; + if willLookup(k) + featureList_mod{k} = renameMapRev(feature); + else + featureList_mod{k} = append(feature, ATTRIBUTE_SUFFIX); + end end - edgeTable.Properties.VariableDescriptions = vDescriptions; - edgeTable.Properties.VariableUnits = vUnits; - - trackMap( trackName ) = edgeTable; - + else + featureList_mod = append(featureList, ATTRIBUTE_SUFFIX); end - end - - %% Subfunction. - - function featureList = getEdgeFeatureList(node) - - attribute_map = node.getAttributes; - n_attributes = attribute_map.getLength; - - featureList = cell(n_attributes, 1); - index = 1; - for ii = 1 : n_attributes - - namel = node.getAttributes.item(ii-1).getName; - featureList{index} = char(namel); - index = index + 1; - + + %% XPath to retrieve filtered track elements. + + tracksID = [tracksStruct.(TRACK_ID_ATTRIBUTE+ATTRIBUTE_SUFFIX)]; + if ~isa(tracksID, "double"); tracksID = double(tracksID); end + + % Find the selected Track IDs and cache the result. + whichSel = ismember(tracksID, fTrackIDs); + + % Prepare metadata once + if ~isempty(whichSel) + nVNames = numel( featureList ); + vDescriptions = strings( nVNames, 1); + vUnits = strings( 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 modelStruct xmlDocFileName end + return + end + + trackNames = [tracksStruct.(TRACK_NAME_ATTRIBUTE+ATTRIBUTE_SUFFIX)]; + if ~isstring(trackNames); trackNames = string(trackNames); end + + for iTracks = reshape(find(whichSel), 1, []) + edgeTable = struct2table([tracksStruct(iTracks).Edge], ... + "AsArray", true, "DimensionNames", {'Edge', 'Feature'}); + + [~,iAdd,iRemove] = setxor(featureList_mod, edgeTable.Properties.VariableNames); + edgeTable = removevars(edgeTable, iRemove); + edgeTable{:, featureList_mod(iAdd)} = NaN; + edgeTable = convertvars(edgeTable, @isstring, "double"); + + edgeTable = edgeTable(:, featureList_mod); + edgeTable = renamevars(edgeTable, featureList_mod, featureList); + % Set table metadata. + edgeTable.Properties.VariableDescriptions = vDescriptions; + edgeTable.Properties.VariableUnits = vUnits; + + trackMap( trackNames{iTracks} ) = edgeTable; end end \ No newline at end of file diff --git a/scripts/trackmateFeatureDeclarations.m b/scripts/trackmateFeatureDeclarations.m index b066b547a..e6025b6bb 100644 --- a/scripts/trackmateFeatureDeclarations.m +++ b/scripts/trackmateFeatureDeclarations.m @@ -39,105 +39,115 @@ % units: 'pixels' % __ -% Jean-Yves Tinevez - 2016 +% Jean-Yves Tinevez & contributors - 2026 - %% Import the XPath classes. - import javax.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'; + 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"; + ATTRIBUTE_SUFFIX = "__"; %% Open and check XML. - - try - xmlDoc = xmlread(filePath); - catch - error('Failed to read XML file %s.',filePath); + global isNotFirst modelStruct xmlDocFileName %#ok + if isNotFirst + % Being called by other function + willClear = false; + else + isNotFirst = true; + willClear = true; end - xmlRoot = xmlDoc.getFirstChild(); - - if ~strcmp(xmlRoot.getTagName, TRACKMATE_ELEMENT) - error('MATLAB:trackMateGraph:BadXMLFile', ... - 'File does not seem to be a proper TrackMate file.') + + % 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(modelStruct) || ~strcmp(xmlDocFileName, filePath) + xPathExp = "/" + TRACKMATE_ELEMENT + "/Model"; + try + modelStruct = readstruct(filePath, "FileType", "xml", ... + "StructSelector", xPathExp, "ImportAttributes", true, ... + "AttributeSuffix", ATTRIBUTE_SUFFIX); + catch ME + switch ME.identifier + case 'MATLAB:UndefinedFunction' + error("Your MATLAB is too old (pre-R2020b) to run this script.") + case 'MATLAB:io:xml:readstruct:NonexistentStructSelector' + % / not found + error('MATLAB:trackMateGraph:BadXMLFile', ... + "File does not seem to be a proper TrackMate file."); + otherwise + % case 'MATLAB:io:xml:common:InvalidXMLFile', etc. + error(ME.identifier, "Failed to read XML file %s.", filePath); + end + end + xmlDocFileName = filePath; end - factory = XPathFactory.newInstance; - xpath = factory.newXPath; - %% Retrieve physical units. - modelPath = xpath.compile('/TrackMate/Model'); - modelNode = modelPath.evaluate(xmlRoot, XPathConstants.NODESET).item(0); - spaceUnits = char( modelNode.getAttribute( SPATIAL_UNITS_ATTRIBUTE ) ); - timeUnits = char( modelNode.getAttribute( TIME_UNITS_ATTRIBUTE ) ); + spaceUnits = char( modelStruct(1).(SPATIAL_UNITS_ATTRIBUTE + ATTRIBUTE_SUFFIX) ); + timeUnits = char( modelStruct(1).(TIME_UNITS_ATTRIBUTE + ATTRIBUTE_SUFFIX) ); %% XPath to retrieve spot feature declarations. - spotFeatureFilter = xpath.compile('/TrackMate/Model/FeatureDeclarations/SpotFeatures/Feature'); - spotFeatureNodes = spotFeatureFilter.evaluate(xmlDoc, XPathConstants.NODESET); - nSpotFeatureNodes = spotFeatureNodes.getLength(); - - sf = containers.Map(); - for i = 1 : nSpotFeatureNodes - f = readFeature( spotFeatureNodes.item( i-1 ), spaceUnits, timeUnits ); - sf( f.key ) = f; - end + % /TrackMate/Model/FeatureDeclarations/SpotFeatures/Feature + sf = makeFeatureMap("SpotFeatures", modelStruct); %% XPath to retrieve edge feature declarations. - edgeFeatureFilter = xpath.compile('/TrackMate/Model/FeatureDeclarations/EdgeFeatures/Feature'); - edgeFeatureNodes = edgeFeatureFilter.evaluate(xmlDoc, XPathConstants.NODESET); - nEdgeFeatureNodes = edgeFeatureNodes.getLength(); - - ef = containers.Map(); - for i = 1 : nEdgeFeatureNodes - f = readFeature( edgeFeatureNodes.item( i-1 ), spaceUnits, timeUnits ); - ef( f.key ) = f; + if nargout >= 2 + % /TrackMate/Model/FeatureDeclarations/EdgeFeatures/Feature + ef = makeFeatureMap("EdgeFeatures", modelStruct); end %% XPath to retrieve track feature declarations. - trackFeatureFilter = xpath.compile('/TrackMate/Model/FeatureDeclarations/TrackFeatures/Feature'); - trackFeatureNodes = trackFeatureFilter.evaluate(xmlDoc, XPathConstants.NODESET); - nTrackFeatureNodes = trackFeatureNodes.getLength(); + if nargout >= 3 + % /TrackMate/Model/FeatureDeclarations/TrackFeatures/Feature + tf = makeFeatureMap("TrackFeatures", modelStruct); + end - tf = containers.Map(); - for i = 1 : nTrackFeatureNodes - f = readFeature( trackFeatureNodes.item( i-1 ), spaceUnits, timeUnits ); - tf( f.key ) = f; + if willClear + clear global isNotFirst modelStruct xmlDocFileName end %% Subfunctions. - function f = readFeature(featureNode, spaceUnits, timeUnits) - - key = char( featureNode.getAttribute( FEATURE_KEY_ATTRIBUTE ) ); - name = char( featureNode.getAttribute( FEATURE_NAME_ATTRIBUTE ) ); - shortName = char( featureNode.getAttribute( FEATURE_SHORTNAME_ATTRIBUTE ) ); - dimension = char( featureNode.getAttribute( FEATURE_DIMENSION_ATTRIBUTE ) ); - isInt = strcmp( 'true', char( featureNode.getAttribute( FEATURE_ISINT_ATTRIBUTE ) ) ); - units = determineUnits( dimension, spaceUnits, timeUnits ); - - f = struct(); - f.key = key; - f.name = name; - f.shortName = shortName; - f.dimension = dimension; - f.isInt = isInt; - f.units = units; - + function featureMap = makeFeatureMap(featName, modelStruct) + attrs = append([FEATURE_KEY_ATTRIBUTE, FEATURE_NAME_ATTRIBUTE, ... + FEATURE_SHORTNAME_ATTRIBUTE FEATURE_DIMENSION_ATTRIBUTE, ... + FEATURE_ISINT_ATTRIBUTE], ... + ATTRIBUTE_SUFFIX); + fields = cell(size(attrs)+1); + fields(1,:) = {'feature' 'name' 'shortName' 'dimension' 'isInt' 'units'}; + try + featureStruct = [modelStruct.FeatureDeclarations]; + featureStruct = [featureStruct.(featName)]; + featureStruct = [featureStruct.Feature]; + for k = 1 : (numel(attrs)-1) + fields{2,k} = cellstr(vertcat(featureStruct.(attrs{k}))); + end + % isInt + fields{2,k+1} = strcmp("true", vertcat(featureStruct.(attrs{k+1}))); + % units + fields{2,k+2} = cellfun(@(str)determineUnits(str, spaceUnits, timeUnits), ... + fields{2,4}, "UniformOutput", false); + featureMap = containers.Map(fields{2,1}, num2cell(struct(fields{:})) ); + catch ME + switch ME.identifier + case 'MATLAB:nonExistentField' + featureMap = containers.Map("KeyType", "char", "ValueType", "any"); + otherwise + rethrow(ME) + end + end end function units = determineUnits( dimension, spaceUnits, timeUnits ) @@ -166,4 +176,4 @@ units = 'no unit'; end end -end \ No newline at end of file +end diff --git a/scripts/trackmateGraph.m b/scripts/trackmateGraph.m index 5f4b2a77e..6271a2f21 100644 --- a/scripts/trackmateGraph.m +++ b/scripts/trackmateGraph.m @@ -58,13 +58,13 @@ % >> axis equal % __ -% Jean-Yves Tinevez - 2016 - 2024 +% Jean-Yves Tinevez & contributors - 2026 %% Constants definition. - SPOT_SOURCE_ID_ATTRIBUTE = 'SPOT_SOURCE_ID'; - SPOT_TARGET_ID_ATTRIBUTE = 'SPOT_TARGET_ID'; + SPOT_SOURCE_ID_ATTRIBUTE = "SPOT_SOURCE_ID"; + SPOT_TARGET_ID_ATTRIBUTE = "SPOT_TARGET_ID"; %% Deal with inputs. @@ -78,11 +78,19 @@ end end + global isNotFirst %#ok + if isNotFirst + willClear = false; + else + isNotFirst = true; + willClear = true; + end + %% Import spot table. if verbose - fprintf('Importing spot table. ') - tic + fprintf("Importing spot table. ") + t = tic; end if nargout >= 2 @@ -93,21 +101,21 @@ if verbose - fprintf('Done in %.1f s.\n', toc) + fprintf("Done in %.1f s.\n", toc(t)) end %% Import edge table. if verbose - fprintf('Importing edge table. ') - tic + fprintf("Importing edge table. ") + t = tic; end trackMap = trackmateEdges(filePath, edgeFeatureList); if verbose - fprintf('Done in %.1f s.\n', toc) + fprintf("Done in %.1f s.\n", toc(t)) end tmp = trackMap.values; @@ -117,8 +125,8 @@ if verbose - fprintf('Building graph. ') - tic + fprintf("Building graph. ") + t = tic; end sourceID = edgeTable.( SPOT_SOURCE_ID_ATTRIBUTE ); @@ -128,14 +136,16 @@ t = cell2mat( values( spotIDMap, num2cell(targetID) ) ); EndNodes = [ s t ]; - nodeTable = table( EndNodes ); - nt = horzcat( nodeTable, edgeTable ); + edgeTable = addvars(edgeTable, EndNodes, 'Before', 1, 'NewVariableNames', "EndNodes"); - G = digraph( nt, spotTable ); + G = digraph( edgeTable, spotTable ); if verbose - fprintf('Done in %.1f s.\n', toc) + fprintf("Done in %.1f s.\n", toc(t)) end + if willClear + clear global isNotFirst modelStruct xmlDocFileName + end end diff --git a/scripts/trackmateSpots.m b/scripts/trackmateSpots.m index 8a0ae5b31..b7e99a135 100644 --- a/scripts/trackmateSpots.m +++ b/scripts/trackmateSpots.m @@ -80,140 +80,217 @@ % __ -% Jean-Yves Tinevez - 2016 - 2024 +% Jean-Yves Tinevez & contributors - 2026 - %% Import the XPath classes. - import javax.xml.xpath.* - %% Constants definition. - TRACKMATE_ELEMENT = 'TrackMate'; - SPOT_ID_ATTRIBUTE = 'ID'; - SPOT_NAME_ATTRIBUTE = 'name'; - ROI_N_POINTS_ATTTRIBUTE = 'ROI_N_POINTS'; + SPOT_ID_ATTRIBUTE = "ID"; + SPOT_NAME_ATTRIBUTE = "name"; + ROI_N_POINTS_ATTTRIBUTE = "ROI_N_POINTS"; + ATTRIBUTE_SUFFIX = "__"; %% Open file. - - try - xmlDoc = xmlread(filePath); - catch - error('Failed to read XML file %s.',filePath); + % We'll call trackmateFeatureDeclarations() to fill in table properties + % no matter what, so let's reuse that one's validation function. + global isNotFirst modelStruct %#ok + if isNotFirst + % Being called by other function + willClear = false; + else + isNotFirst = true; + willClear = true; end - xmlRoot = xmlDoc.getFirstChild(); - if ~strcmp(xmlRoot.getTagName, TRACKMATE_ELEMENT) - error('MATLAB:trackMateGraph:BadXMLFile', ... - 'File does not seem to be a proper TrackMate file.') + try + fs = trackmateFeatureDeclarations( filePath ); + catch ME + rethrow(ME) end %% XPath to retrieve spot nodes. - - % Use XPath to retrieve all visible spots. - factory = XPathFactory.newInstance; - xPath = factory.newXPath; - xPathFilter = xPath.compile('//Model/AllSpots/SpotsInFrame/Spot[@VISIBILITY=1]'); - nodeList = xPathFilter.evaluate(xmlDoc, XPathConstants.NODESET); - + % Indexing into a field of an array returns a comma-separated list - + % concatenate to mimic XPath's behavior. Struct arrays are horizontal. + try + spotsStruct = [modelStruct.AllSpots]; % Can be multiple? + spotsStruct = [spotsStruct.SpotsInFrame]; % Likely multiple + spotsStruct = [spotsStruct.Spot]; % Usually multiple + + % Select the visible spots + spotsStruct = spotsStruct([spotsStruct.("VISIBILITY"+ATTRIBUTE_SUFFIX)] == 1); + catch ME + switch ME.identifier + case 'MATLAB:nonExistentField' + % XPath points to 0 nodes + spotsStruct = struct(SPOT_ID_ATTRIBUTE+ATTRIBUTE_SUFFIX, [], ... + SPOT_NAME_ATTRIBUTE+ATTRIBUTE_SUFFIX, []); + otherwise + rethrow(ME) + end + end + + nSpots = numel(spotsStruct); + %% Retrieve spot feature list. - - if nargin < 2 || isempty( featureList ) - featureList = getSpotFeatureList(nodeList.item(0)); + % Guess the attribute name from struct names + % Valid XML name is a superset of MATLAB variable name, so MATLAB + % may have modified them when importing into struct fields + + % Combine knowledge from FeatureDeclarations and user input + if exist("featureList", "var") + fList = union( featureList, keys(fs)); + else + fList = keys(fs); end - - % Remove ID and name, because we will get them anyway. - featureList = setdiff( featureList, SPOT_ID_ATTRIBUTE ); - featureList = setdiff( featureList, SPOT_NAME_ATTRIBUTE ); - n_features = numel( featureList ); - - %% Get filtered spot IDs. - - % Prepare holders. - nSpots = nodeList.getLength(); - ID = NaN( nSpots, 1 ); - name = cell( nSpots, 1); - features = NaN( nSpots, n_features ); - rois = cell( nSpots, 1); - - % Read all spot nodes. - for i = 1 : nSpots - node = nodeList.item( i-1 ); - ID( i ) = str2double( node.getAttribute( SPOT_ID_ATTRIBUTE ) ); - name{ i } = char( node.getAttribute( SPOT_NAME_ATTRIBUTE ) ); - for j = 1 : n_features - features( i, j ) = str2double( node.getAttribute( featureList{ j } ) ); + + [fList_mod, havemodd1] = matlab.lang.makeValidName(fList); + [fList_mod, havemodd2] = matlab.lang.makeUniqueStrings(fList_mod); + fList_mod = append(fList_mod, ATTRIBUTE_SUFFIX); + whichModified = havemodd1 | havemodd2; + + if nargin < 2 || isempty( featureList ) + % List of feature is all spot attributes. Look up the original name + % if it's *known* to be non-trivially renamed. + % Still, we will lose the original attribute name if it doesn't + % appear in + + featureList_mod = fieldnames(spotsStruct); + % May contain a Text field for the node's text + featureList_mod = featureList_mod(endsWith(featureList_mod, ATTRIBUTE_SUFFIX)); + frontOfList = append([SPOT_ID_ATTRIBUTE; SPOT_NAME_ATTRIBUTE], ATTRIBUTE_SUFFIX); + featureList_mod = union(frontOfList, featureList_mod, "stable"); + featureList = strings(size(featureList_mod)); + + if any(whichModified) + renameMap = containers.Map(fList_mod(whichModified), fList(whichModified)); + willLookup = iskey(renameMap, cellstr(featureList_mod)); + for k = 1:numel(featureList_mod) + feature_mod = featureList_mod{k}; + if willLookup(k) + featureList{k} = renameMap(feature_mod); + else + featureList{k} = extractBefore(feature_mod, ... + ATTRIBUTE_SUFFIX+textBoundary("end")); + end + end + else + featureList = extractBefore(featureList_mod, ... + ATTRIBUTE_SUFFIX+textBoundary("end")); end - - % Read ROI coords if it's there. - if nargout >= 3 - coords_str = node.getTextContent(); - if ~isempty( coords_str ) - A = sscanf(string(coords_str),'%f'); - n_points = numel(A) / 2; - A = reshape( A, 2, n_points )'; - rois{i} = A; + + else + % List of feature is the input list. Still, the renaming is done + % according to real attributes, so we look up the modified names. + featureList = string(featureList(:)); + + % Push ID and name to the front + frontOfList = [SPOT_ID_ATTRIBUTE; SPOT_NAME_ATTRIBUTE]; + featureList = union( frontOfList, featureList, "stable" ); + + if any(whiwhModified) + renameMapRev = containers.Map(fList(whichModified), fList_mod(whichModified)); + willLookup = iskey(renameMapRev, cellstr(featureList)); + featureList_mod = strings(size(featureList)); + for k = 1:numel(featureList) + feature = featureList{k}; + if willLookup(k) + featureList_mod{k} = renameMapRev(feature); + else + featureList_mod{k} = append(feature, ATTRIBUTE_SUFFIX); + end end + else + featureList_mod = append(featureList, ATTRIBUTE_SUFFIX); end end - - % Create table. - spotTable = table(); - spotTable.( SPOT_ID_ATTRIBUTE ) = ID; - spotTable.( SPOT_NAME_ATTRIBUTE ) = name; + + %% Create table + n_features = numel( featureList ); + % Assigning an entire variable changes its type + spotTable = table('Size', [nSpots n_features], 'VariableNames', featureList, ... + 'VariableTypes', repmat("double", size(featureList))); for j = 1 : n_features - spotTable.( featureList{ j } ) = features( :, j ); + featureID = featureList_mod{j}; + willBeChar = strcmp( SPOT_NAME_ATTRIBUTE + ATTRIBUTE_SUFFIX, featureID ); + + if isfield(spotsStruct, featureID) + % If *some* values are missing, they are read as missing() and + % automatically converted to corresponding missing values upon + % concatenation. + features = vertcat(spotsStruct.(featureID)); + + if willBeChar + if ~iscellstr(features) %#ok + if ~isstring(features) + features = string(features); + end + features = cellstr(features); + end + elseif ~isa(features, "double") + % Is it even possible that other features are accidentally + % read as a string? Deal with that anyway + features = double(features); + end + + else + % Asked for a non-existent attribute + if willBeChar + features = cellstr(strings(nSpots, 1)); + else + features = nan(nSpots, 1 , "double"); + end + end + + spotTable.( featureList{ j } ) = features; end % Set table metadata. spotTable.Properties.DimensionNames = { 'Spot', 'Feature' }; - vNames = spotTable.Properties.VariableNames; - nVNames = numel( vNames ); - vDescriptions = cell( nVNames, 1); - vUnits = cell( nVNames, 1); - - fs = trackmateFeatureDeclarations( filePath ); - for k = 1 : nVNames - vn = vNames{ k }; - if strcmp( SPOT_ID_ATTRIBUTE, vn ) - vDescriptions{ k } = 'Spot ID'; - vUnits{ k } = ''; - elseif strcmp( SPOT_NAME_ATTRIBUTE, vn ) - vDescriptions{ k } = 'Spot name'; - vUnits{ k } = ''; - elseif strcmp( ROI_N_POINTS_ATTTRIBUTE, vn ) - vDescriptions{ k } = 'ROI N points'; - vUnits{ k } = ''; - else - vDescriptions{ k } = fs( vn ).name; - vUnits{ k } = fs( vn ).units; - end - end + [vDescriptions, vUnits] = cellfun(@lookupDescriptionAndUnit, ... + featureList, "UniformOutput", true); spotTable.Properties.VariableDescriptions = vDescriptions; spotTable.Properties.VariableUnits = vUnits; % Generate map ID -> table row number. - spotIDMap = containers.Map( ID, 1 : nSpots, ... - 'UniformValues', true); + spotIDMap = containers.Map( spotTable.ID, 1 : nSpots, ... + "UniformValues", true); + + %% Read ROI coords if it's requested. + if nargout >= 3 + rois = cell(nSpots, 1); + if isfield(spotsStruct, "Text") + for i = 1 : nSpots + coords_str = spotsStruct(i).Text; + if ~isempty( coords_str ) + A = sscanf(coords_str, "%f"); + A = reshape(A, 2, []).'; + rois{i} = A; + end + end + end + end + + if willClear + clear global isNotFirst modelStruct xmlDocFileName + end %% Subfunction. - function featureList = getSpotFeatureList(node) - - attribute_map = node.getAttributes; - nAttributes = attribute_map.getLength; - - featureList = cell(nAttributes - 1, 1); % -1 for the spot name, which we do not take - index = 1; - for ii = 1 : nAttributes - - namel = node.getAttributes.item(ii-1).getName; - if strcmp(namel, SPOT_NAME_ATTRIBUTE) - continue; - end - featureList{index} = char(namel); - index = index + 1; - + function [description, unit] = lookupDescriptionAndUnit(featureName) + switch featureName + case SPOT_ID_ATTRIBUTE + description = "Spot ID"; + unit = ""; + case SPOT_NAME_ATTRIBUTE + description = "Spot ID"; + unit = ""; + case ROI_N_POINTS_ATTTRIBUTE + description = "ROI N points"; + unit = ""; + otherwise + description = string(fs( featureName ).name); + unit = string(fs( featureName ).units); end end From 8c41ad117499f1d0372a04a911cdfc5660a1d7e2 Mon Sep 17 00:00:00 2001 From: Jerry1144 <6694895+Jerry1144@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:53:40 +0800 Subject: [PATCH 2/2] Fix aliased timer variable --- scripts/trackmateGraph.m | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/trackmateGraph.m b/scripts/trackmateGraph.m index 6271a2f21..6e070dafd 100644 --- a/scripts/trackmateGraph.m +++ b/scripts/trackmateGraph.m @@ -90,7 +90,7 @@ if verbose fprintf("Importing spot table. ") - t = tic; + timer = tic; end if nargout >= 2 @@ -101,7 +101,7 @@ if verbose - fprintf("Done in %.1f s.\n", toc(t)) + fprintf("Done in %.1f s.\n", toc(timer)) end @@ -109,13 +109,13 @@ if verbose fprintf("Importing edge table. ") - t = tic; + timer = tic; end trackMap = trackmateEdges(filePath, edgeFeatureList); if verbose - fprintf("Done in %.1f s.\n", toc(t)) + fprintf("Done in %.1f s.\n", toc(timer)) end tmp = trackMap.values; @@ -126,7 +126,7 @@ if verbose fprintf("Building graph. ") - t = tic; + timer = tic; end sourceID = edgeTable.( SPOT_SOURCE_ID_ATTRIBUTE ); @@ -141,11 +141,11 @@ G = digraph( edgeTable, spotTable ); if verbose - fprintf("Done in %.1f s.\n", toc(t)) + fprintf("Done in %.1f s.\n", toc(timer)) end if willClear clear global isNotFirst modelStruct xmlDocFileName end -end +end \ No newline at end of file