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..6e070dafd 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. ") + timer = 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(timer)) end %% Import edge table. if verbose - fprintf('Importing edge table. ') - tic + fprintf("Importing edge table. ") + timer = tic; end trackMap = trackmateEdges(filePath, edgeFeatureList); if verbose - fprintf('Done in %.1f s.\n', toc) + fprintf("Done in %.1f s.\n", toc(timer)) end tmp = trackMap.values; @@ -117,8 +125,8 @@ if verbose - fprintf('Building graph. ') - tic + fprintf("Building graph. ") + timer = 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(timer)) end + if willClear + clear global isNotFirst modelStruct xmlDocFileName + end -end +end \ No newline at end of file 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