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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: '8'
java-version: '11'
distribution: 'zulu'
cache: 'maven'
- name: Set up CI environment
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ GeffNode node0 = new GeffNode.Builder()
.segmentId(0)
.color(new double[]{1.0, 0.0, 0.0, 1.0}) // Red color
.radius(2.5)
.covariance2d(new double[]{1.0, 0.2, 0.2, 1.5}) // 2x2 covariance matrix flattened
.covariance2d(new double[]{1.0, 0.2, 0.2, 1.5}) // 2x2 covariance matrix, row-major
.polygonX(new double[]{1.0, 2.0, 3.0, 4.0}) // Polygon X coordinates
.polygonY(new double[]{5.0, 6.0, 7.0, 8.0}) // Polygon Y coordinates
.build();
Expand Down Expand Up @@ -283,8 +283,8 @@ dataset.zarr/
│ │ ├── color/values # RGBA colors [N, 4] (optional)
│ │ ├── radius/values # Node radii [N] (optional)
│ │ ├── <tracklet>/values # Track IDs [N] (name from track_node_props, optional)
│ │ ├── covariance2d/values # Flattened 2D covariance [N, 4] (optional)
│ │ ├── covariance3d/values # Flattened 3D covariance [N, 6] (optional)
│ │ ├── covariance2d/values # 2D covariance matrices [N, 2, 2] (optional)
│ │ ├── covariance3d/values # 3D covariance matrices [N, 3, 3] (optional)
│ │ └── <varlength_prop>/ # Variable-length property, e.g. polygon (optional)
│ │ ├── data # Flattened values [V]
│ │ ├── values # Offsets and shapes [N, ndim+1]
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/mastodon/geff/Geff.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public static void main( String[] args )

// Demonstrate reading edges
System.out.println( "\n=== Reading Edges ===" );
List< GeffEdge > edges = GeffEdge.readFromZarr( zarrPath, metadata.getGeffVersion() );
List< GeffEdge > edges = GeffEdge.readFromZarr( zarrPath, metadata );
System.out.println( "Read " + edges.size() + " edges:" );
for ( int i = 0; i < Math.min( 5, edges.size() ); i++ )
{
Expand Down
41 changes: 29 additions & 12 deletions src/main/java/org/mastodon/geff/GeffEdge.java
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,26 @@ public static List< GeffEdge > readFromN5( final N5Reader reader, final String g
}
final int numEdges = edgeIds.size()[ 1 ];

// Determine the group names of the standard props from
// edge_props_metadata, falling back to the standard names for datasets
// that do not declare them.
final String distancePropName = metadata != null ? metadata.getEdgePropIdentifier( "distance" ) : "distance";
final String scorePropName = metadata != null ? metadata.getEdgePropIdentifier( "score" ) : "score";

// Read distances from chunks
final double[] distances = GeffUtils.readAsDoubleArray( reader, path + "/edges/props/distance/values", "distances" );
verifyLength( distances, numEdges, "/edges/props/distance/values" );
final String distancePropPath = path + "/edges/props/" + distancePropName + "/values";
final double[] distances = GeffUtils.readAsDoubleArray( reader, distancePropPath, "distances" );
verifyLength( distances, numEdges, distancePropPath );

// Read scores from chunks
final double[] scores = GeffUtils.readAsDoubleArray( reader, path + "/edges/props/score/values", "scores" );
verifyLength( scores, numEdges, "/edges/props/score/values" );
final String scorePropPath = path + "/edges/props/" + scorePropName + "/values";
final double[] scores = GeffUtils.readAsDoubleArray( reader, scorePropPath, "scores" );
verifyLength( scores, numEdges, scorePropPath );

// Read custom non-standard, non-varlength edge props from metadata
final Set< String > standardNames = new HashSet<>( STANDARD_EDGE_PROP_NAMES );
standardNames.add( distancePropName );
standardNames.add( scorePropName );
final Map< String, Object[] > customPropData = new HashMap<>();
final Map< String, VarlengthProperty[] > varlengthPropsMap = new HashMap<>();
if ( metadata != null && metadata.getEdgePropsMetadata() != null )
Expand All @@ -330,7 +341,7 @@ public static List< GeffEdge > readFromN5( final N5Reader reader, final String g
{
final String propName = entry.getKey();
final PropMetadata propMeta = entry.getValue();
if ( STANDARD_EDGE_PROP_NAMES.contains( propName ) )
if ( standardNames.contains( propName ) )
continue;
if ( GeffUtils.shouldSkipProperty( propName, propMeta ) )
continue;
Expand Down Expand Up @@ -375,7 +386,7 @@ public static List< GeffEdge > readFromN5( final N5Reader reader, final String g
}
customPropData.put( propName, edgeVals );
}
else if ( ndim == 2 )
else if ( ndim >= 2 )
{
if ( isFloat )
{
Expand Down Expand Up @@ -515,22 +526,28 @@ public static void writeToN5(
// there are no edge properties (mirrors what Python geff writes).
writer.createGroup( path + "/edges/props" );

// Determine the group names of the standard props from
// edge_props_metadata, falling back to the standard names for datasets
// that do not declare them.
final String distancePropName = metadata != null ? metadata.getEdgePropIdentifier( "distance" ) : "distance";
final String scorePropName = metadata != null ? metadata.getEdgePropIdentifier( "score" ) : "score";

// Write distances
if ( writeAllProps || edgePropsMetadata.containsKey( "distance" ) )
GeffUtils.writeDoubleArray( edges, GeffEdge::getDistance, writer, path + "/edges/props/distance/values", chunkSize );
if ( writeAllProps || metadata.getEdgePropMetadata( "distance" ) != null )
GeffUtils.writeDoubleArray( edges, GeffEdge::getDistance, writer, path + "/edges/props/" + distancePropName + "/values", chunkSize );

// Write scores
if ( writeAllProps || edgePropsMetadata.containsKey( "score" ) )
GeffUtils.writeDoubleArray( edges, GeffEdge::getScore, writer, path + "/edges/props/score/values", chunkSize );
if ( writeAllProps || metadata.getEdgePropMetadata( "score" ) != null )
GeffUtils.writeDoubleArray( edges, GeffEdge::getScore, writer, path + "/edges/props/" + scorePropName + "/values", chunkSize );

// When writeAllProps=true (no edgePropsMetadata provided), populate metadata
// with the standard props so the output zarr passes Python structural
// validation (edge_props_metadata is a required field in the Python spec).
if ( writeAllProps && metadata != null )
{
final Map< String, PropMetadata > edgePropsMap = new HashMap<>();
edgePropsMap.put( "distance", new PropMetadata( "distance", "float64", false, null, null, null ) );
edgePropsMap.put( "score", new PropMetadata( "score", "float64", false, null, null, null ) );
edgePropsMap.put( distancePropName, new PropMetadata( distancePropName, "float64", false, null, null, null ) );
edgePropsMap.put( scorePropName, new PropMetadata( scorePropName, "float64", false, null, null, null ) );
metadata.setEdgePropsMetadata( edgePropsMap );
}

Expand Down
159 changes: 159 additions & 0 deletions src/main/java/org/mastodon/geff/GeffMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,165 @@ private String resolveSpaceAxisName( final String hint, final String standardNam
return ( spaceAxes.length == 0 || indexFromLast < 2 ) ? standardName : null;
}

/**
* Get the metadata of a standard node property, as declared in
* {@code node_props_metadata}.
*
* @param standardName
* the conventional name of the property, e.g. {@code "radius"}
* or {@code "covariance3d"}
* @return the property metadata, or null if the property is not declared
* @see #findPropMetadata(Map, String)
*/
public PropMetadata getNodePropMetadata( final String standardName )
{
return findPropMetadata( nodePropsMetadata, standardName );
}

/**
* Get the metadata of a standard edge property, as declared in
* {@code edge_props_metadata}.
*
* @param standardName
* the conventional name of the property, e.g. {@code "distance"}
* or {@code "score"}
* @return the property metadata, or null if the property is not declared
* @see #findPropMetadata(Map, String)
*/
public PropMetadata getEdgePropMetadata( final String standardName )
{
return findPropMetadata( edgePropsMetadata, standardName );
}

/**
* Get the identifier of a standard node property, i.e. the name of the
* group holding it in {@code nodes/props}.
*
* @param standardName
* the conventional name of the property, e.g. {@code "radius"}
* or {@code "covariance3d"}
* @return the property identifier, never null
* @see #resolvePropIdentifier(Map, String)
*/
public String getNodePropIdentifier( final String standardName )
{
return resolvePropIdentifier( nodePropsMetadata, standardName );
}

/**
* Get the identifier of a standard edge property, i.e. the name of the
* group holding it in {@code edges/props}.
*
* @param standardName
* the conventional name of the property, e.g. {@code "distance"}
* or {@code "score"}
* @return the property identifier, never null
* @see #resolvePropIdentifier(Map, String)
*/
public String getEdgePropIdentifier( final String standardName )
{
return resolvePropIdentifier( edgePropsMetadata, standardName );
}

/**
* Look up a property in a props metadata map. The map is keyed by property
* identifier, so a property is matched by the {@code identifier} of its
* metadata first, and by the map key second, which covers maps built
* without setting the identifier.
*
* @param propsMetadata
* {@code node_props_metadata} or {@code edge_props_metadata},
* may be null
* @param standardName
* the conventional name of the property
* @return the property metadata, or null if the property is not declared
*/
private static PropMetadata findPropMetadata( final Map< String, PropMetadata > propsMetadata, final String standardName )
{
if ( propsMetadata == null )
return null;

for ( final PropMetadata propMetadata : propsMetadata.values() )
if ( propMetadata != null && standardName.equals( propMetadata.getIdentifier() ) )
return propMetadata;

return propsMetadata.get( standardName );
}

/**
* Resolve the identifier naming the group that holds a property, by looking
* the property up in a props metadata map.
*
* @param propsMetadata
* {@code node_props_metadata} or {@code edge_props_metadata},
* may be null
* @param standardName
* the conventional name of the property
* @return the declared identifier, falling back to {@code standardName} for
* properties that are not declared, e.g. in datasets written before
* props metadata existed
*/
private static String resolvePropIdentifier( final Map< String, PropMetadata > propsMetadata, final String standardName )
{
final PropMetadata propMetadata = findPropMetadata( propsMetadata, standardName );
final String identifier = propMetadata != null ? propMetadata.getIdentifier() : null;
if ( identifier != null && !identifier.trim().isEmpty() )
return identifier;

LOG.debug( "no props metadata for '{}', assuming it is stored under that name", standardName );
return standardName;
}

/**
* Get the positions that the x, y (and z) axes occupy in a property whose
* dimensions follow the declaration order of the spatial axes, such as a
* covariance matrix: the spec stores those "in the same coordinate system
* as the {@code space} type properties", and axes are declared slowest to
* fastest, i.e. typically {@code z, y, x}.
* <p>
* For axes declared as {@code z, y, x} this returns {@code { 2, 1, 0 }},
* i.e. the x axis is the last dimension of the stored matrix. For axes
* declared as {@code x, y, z} it returns the identity {@code { 0, 1, 2 }}.
*
* @param numDimensions
* 2 for a 2D property (x, y), 3 for a 3D one (x, y, z)
* @return the positions of the x, y (and z) axes in the stored order, or
* null if the dataset does not declare the spatial axes needed to
* tell, in which case the stored order is assumed to be x, y, z
*/
public int[] getSpaceAxisPositions( final int numDimensions )
{
final String[] spaceAxes = getAxisNamesByType( GeffAxis.TYPE_SPACE );
if ( spaceAxes.length < numDimensions )
return null;

final String[] axisNames = numDimensions == 3
? new String[] { getHorizontalAxisName(), getVerticalAxisName(), getDepthAxisName() }
: new String[] { getHorizontalAxisName(), getVerticalAxisName() };

final int[] declarationIndices = new int[ numDimensions ];
for ( int j = 0; j < numDimensions; ++j )
{
declarationIndices[ j ] = Arrays.asList( spaceAxes ).indexOf( axisNames[ j ] );
if ( declarationIndices[ j ] < 0 )
{
LOG.debug( "axis '{}' is not among the spatial axes {}, assuming the x, y, z order",
axisNames[ j ], Arrays.toString( spaceAxes ) );
return null;
}
}

// The position of an axis is its rank among the axes taken into
// account, so that a covariance2d of a z, y, x dataset is read as
// y, x rather than as the declaration indices 1, 2.
final int[] positions = new int[ numDimensions ];
for ( int j = 0; j < numDimensions; ++j )
for ( int k = 0; k < numDimensions; ++k )
if ( declarationIndices[ k ] < declarationIndices[ j ] )
++positions[ j ];
return positions;
}

public RelatedObjects getRelatedObjects()
{
return relatedObjects;
Expand Down
Loading
Loading