Skip to content

Commit c0b79b1

Browse files
Merge pull request #87 from JaneliaSciComp/fix-downsampled-resolutions
Fix the scale and translation transformations in OME-ZARR metadata
2 parents d02c8d6 + e2938c0 commit c0b79b1

6 files changed

Lines changed: 165 additions & 75 deletions

File tree

src/main/java/net/preibisch/mvrecon/fiji/plugin/resave/Resave_N5Api.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.concurrent.atomic.AtomicInteger;
4040
import java.util.stream.Collectors;
4141

42+
import mpicbg.spim.data.sequence.VoxelDimensions;
4243
import org.bigdataviewer.n5.N5CloudImageLoader;
4344
import org.janelia.saalfeldlab.n5.Compression;
4445
import org.janelia.saalfeldlab.n5.DataType;
@@ -188,13 +189,15 @@ else if ( n5Params.format == StorageFormat.HDF5 )
188189
}
189190
else
190191
{
192+
VoxelDimensions vx = data.getSequenceDescription().getViewDescription( viewId ).getViewSetup().getVoxelSize();
191193
// 5d OME-ZARR with dimension=1 in c and t
192194
mrInfo = N5ApiTools.setupBdvDatasetsOMEZARR(
193195
n5Writer,
194196
viewId,
195197
dataTypes.get( viewId.getViewSetupId() ),
196198
dimensions.get( viewId.getViewSetupId() ),
197-
//data.getSequenceDescription().getViewDescription( viewId ).getViewSetup().getVoxelSize().dimensionsAsDoubleArray(),
199+
vx.dimensionsAsDoubleArray(), // resolutionS0
200+
vx.unit(),
198201
compression,
199202
blockSize,
200203
downsamplings);

src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/AllenOMEZarrProperties.java

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@
2424

2525
import java.util.Arrays;
2626
import java.util.Map;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.concurrent.ConcurrentMap;
2729

2830
import org.janelia.saalfeldlab.n5.DataType;
2931
import org.janelia.saalfeldlab.n5.N5Reader;
3032
import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMultiScaleMetadata;
3133
import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMultiScaleMetadata.OmeNgffDataset;
3234
import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.CoordinateTransformation;
3335
import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.ScaleCoordinateTransformation;
36+
import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.TranslationCoordinateTransformation;
3437

3538
import bdv.img.n5.N5Properties;
3639
import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
@@ -42,25 +45,33 @@ public class AllenOMEZarrProperties implements N5Properties
4245
{
4346
private final AbstractSequenceDescription< ?, ?, ? > sequenceDescription;
4447

45-
private final Map< ViewId, OMEZARREntry > viewIdToPath;
48+
// mapping of viewIDs to corresponding OME-ZARRs
49+
private final Map< ViewId, OMEZARREntry > viewIdToOmeZarrPath;
50+
51+
// N5Properties.getDatasetPath should require an N5Reader so that the dataset path could always be retrieved correctly (e.g., "s0", "s1", "s2" or "0", "1", "2")
52+
// To work around this problem for now, first time we retrieve the view setup we cache it so that next time
53+
// the dataset path is needed - we use the cached value.
54+
// TODO: Remove this and the method that populates it, once the signature for N5Properties.getDatasetPath was updated to use the N5 Reader
55+
private final ConcurrentMap< ViewId, OmeNgffMultiScaleMetadata > viewIdToOmeMetadata = new ConcurrentHashMap<>();
4656

4757
public AllenOMEZarrProperties(
4858
final AbstractSequenceDescription< ?, ?, ? > sequenceDescription,
49-
final Map< ViewId, OMEZARREntry > viewIdToPath )
59+
final Map< ViewId, OMEZARREntry > viewIdToOmeZarrPath)
5060
{
5161
this.sequenceDescription = sequenceDescription;
52-
this.viewIdToPath = viewIdToPath;
62+
this.viewIdToOmeZarrPath = viewIdToOmeZarrPath;
5363
}
5464

5565
private String getPath( final int setupId, final int timepointId )
5666
{
57-
return viewIdToPath.get( new ViewId( timepointId, setupId ) ).getPath();
67+
return viewIdToOmeZarrPath.get( new ViewId( timepointId, setupId ) ).getPath();
5868
}
5969

6070
@Override
6171
public String getDatasetPath( final int setupId, final int timepointId, final int level )
6272
{
63-
return String.format( getPath( setupId, timepointId )+ "/%d", level );
73+
// Note: if the OME metadata has not been cached yet this method will return the default path, because the reader is not available
74+
return getMultiscaleDatasetPathOrDefault(null, timepointId, setupId, level);
6475
}
6576

6677
@Override
@@ -78,7 +89,7 @@ public double[][] getMipmapResolutions( final N5Reader n5, final int setupId )
7889
@Override
7990
public long[] getDimensions( final N5Reader n5, final int setupId, final int timepointId, final int level )
8091
{
81-
final String path = getDatasetPath( setupId, timepointId, level );
92+
final String path = getMultiscaleDatasetPathOrDefault(n5, timepointId, setupId, level);
8293
final long[] dimensions = n5.getDatasetAttributes( path ).getDimensions();
8394
// dataset dimensions is 5D, remove the channel and time dimensions
8495
return Arrays.copyOf( dimensions, 3 );
@@ -96,40 +107,29 @@ private static int getFirstAvailableTimepointId( final AbstractSequenceDescripti
96107
return tp.getId();
97108
}
98109

99-
throw new RuntimeException( "All timepoints for setupId " + setupId + " are declared missing. Stopping." );
110+
throw new IllegalStateException( "All timepoints for setupId " + setupId + " are declared missing. Stopping." );
100111
}
101112

102113
private static DataType getDataType( final AllenOMEZarrProperties n5properties, final N5Reader n5, final int setupId )
103114
{
104115
final int timePointId = getFirstAvailableTimepointId( n5properties.sequenceDescription, setupId );
105-
return n5.getDatasetAttributes( n5properties.getDatasetPath( setupId, timePointId, 0 ) ).getDataType();
116+
String datasetPath = n5properties.getMultiscaleDatasetPathOrDefault(n5, timePointId, setupId, 0);
117+
return n5.getDatasetAttributes( datasetPath ).getDataType();
106118
}
107119

108120
private static double[][] getMipMapResolutions( final AllenOMEZarrProperties n5properties, final N5Reader n5, final int setupId )
109121
{
110122
final int timePointId = getFirstAvailableTimepointId( n5properties.sequenceDescription, setupId );
111123

112124
// multiresolution pyramid
125+
OmeNgffMultiScaleMetadata multiScaleMetadata = n5properties.getViewSetupMultiscaleMetadata(n5, timePointId, setupId);
113126

114-
//org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadata
115-
// for this to work you need to register an adapter in the N5Factory class
116-
// final GsonBuilder builder = new GsonBuilder().registerTypeAdapter( CoordinateTransformation.class, new CoordinateTransformationAdapter() );
117-
final OmeNgffMultiScaleMetadata[] multiscales = n5.getAttribute( n5properties.getPath( setupId, timePointId ), "multiscales", OmeNgffMultiScaleMetadata[].class );
118-
119-
if ( multiscales == null || multiscales.length == 0 )
120-
throw new RuntimeException( "Could not parse OME-ZARR multiscales object. stopping." );
121-
122-
if ( multiscales.length != 1 )
123-
System.out.println( "This dataset has " + multiscales.length + " objects, we expected 1. Picking the first one." );
124-
125-
//System.out.println( "AllenOMEZarrLoader.getMipmapResolutions() for " + setupId + " using " + n5properties.getPath( setupId, timePointId ) + ": found " + multiscales[ 0 ].datasets.length + " multi-resolution levels." );
126-
127-
double[][] mipMapResolutions = new double[ multiscales[ 0 ].datasets.length ][ 3 ];
127+
double[][] mipMapResolutions = new double[ multiScaleMetadata.datasets.length ][ 3 ];
128128
double[] firstScale = null;
129129

130-
for ( int i = 0; i < multiscales[ 0 ].datasets.length; ++i )
130+
for ( int i = 0; i < multiScaleMetadata.datasets.length; ++i )
131131
{
132-
final OmeNgffDataset ds = multiscales[ 0 ].datasets[ i ];
132+
final OmeNgffDataset ds = multiScaleMetadata.datasets[ i ];
133133

134134
for ( final CoordinateTransformation< ? > c : ds.coordinateTransformations )
135135
{
@@ -145,11 +145,65 @@ private static double[][] getMipMapResolutions( final AllenOMEZarrProperties n5p
145145
mipMapResolutions[ i ][ d ] = s.getScale()[ d ] / firstScale[ d ];
146146
mipMapResolutions[ i ][ d ] = Math.round(mipMapResolutions[ i ][ d ]*10000)/10000d; // round to the 5th digit
147147
}
148-
//System.out.println( "AllenOMEZarrLoader.getMipmapResolutions(), level " + i + ": " + Arrays.toString( s.getScale() ) + " >> " + Arrays.toString( mipMapResolutions[ i ] ) );
148+
}
149+
if ( c instanceof TranslationCoordinateTransformation)
150+
{
151+
final TranslationCoordinateTransformation t = ( TranslationCoordinateTransformation ) c;
152+
153+
if (firstScale == null) {
154+
throw new IllegalStateException("Expected first scale to be set before the translation for level " + i + " dataset is processed");
155+
}
156+
157+
for ( int d = 0; d < mipMapResolutions[ i ].length; ++d )
158+
{
159+
// at this point firstScale should be available
160+
double pxTranslation = t.getTranslation()[ d ] / firstScale[ d ];
161+
double pxTranslationCorrection = (pxTranslation + 0.5) / mipMapResolutions[i][d] - 0.5;
162+
if (Math.abs(pxTranslationCorrection) >= 0.5) {
163+
System.out.printf("Pixel translation[%d][%d]=%f (=%fpx) and the pixel correction %f is more than 0.5px\n",
164+
i, d, t.getTranslation()[ d ], pxTranslation, pxTranslationCorrection);
165+
}
166+
}
149167
}
150168
}
151169
}
152170

153171
return mipMapResolutions;
154172
}
173+
174+
private String getMultiscaleDatasetPathOrDefault( N5Reader n5, int timepointId, int setupId, int level )
175+
{
176+
OmeNgffMultiScaleMetadata omeNgffMultiScaleMetadata = getViewSetupMultiscaleMetadata(n5, timepointId, setupId);
177+
178+
if (omeNgffMultiScaleMetadata == null) {
179+
throw new IllegalStateException("OME multiscale metadata could not be cached for (tp, setup) = (" +
180+
timepointId + "," + setupId + ") - current N5Reader is " + n5);
181+
}
182+
183+
String viewSetupPath = getPath( setupId, timepointId );
184+
// get the first scale path from the metadata
185+
String datasetPath = omeNgffMultiScaleMetadata.datasets[level].path;
186+
return String.format( "%s/%s", viewSetupPath, datasetPath);
187+
}
188+
189+
// retrieve and cache the multiscale metadata
190+
private OmeNgffMultiScaleMetadata getViewSetupMultiscaleMetadata(N5Reader n5, int timePointId, int setupId) {
191+
ViewId viewId = new ViewId(timePointId, setupId);
192+
193+
return viewIdToOmeMetadata.computeIfAbsent(viewId, k -> {
194+
if (n5 == null) {
195+
return null; // no mapping will be cached
196+
}
197+
198+
final OmeNgffMultiScaleMetadata[] multiscales = n5.getAttribute( getPath( setupId, timePointId ), "multiscales", OmeNgffMultiScaleMetadata[].class );
199+
200+
if ( multiscales == null || multiscales.length == 0 )
201+
throw new IllegalStateException( "Could not parse OME-ZARR multiscales object. stopping." );
202+
203+
if ( multiscales.length > 1 )
204+
System.out.println( "This dataset has " + multiscales.length + " objects, we expected 1. Picking the first one." );
205+
206+
return multiscales[0];
207+
});
208+
}
155209
}

src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/OMEZarrAttibutes.java

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@
3939
import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.ScaleCoordinateTransformation;
4040
import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.TranslationCoordinateTransformation;
4141

42-
import mpicbg.spim.data.sequence.VoxelDimensions;
4342
import net.imglib2.realtransform.AffineTransform3D;
44-
import net.preibisch.mvrecon.process.interestpointregistration.TransformationTools;
4543
import util.URITools;
4644

4745
public class OMEZarrAttibutes
@@ -61,12 +59,6 @@ public static OmeNgffMultiScaleMetadata[] createOMEZarrMetadata(
6159
final Function<Integer, String> levelToName,
6260
final Function<Integer, AffineTransform3D > levelToMipmapTransform )
6361
{
64-
// TODO: make sure the unit is supported by OME-ZARR, if not replace it because otherwise readers will fail
65-
// TODO: e.g. um -> micrometer
66-
// TODO: etc.
67-
// TODO: can you find out what the correct unit for 'unit unknown' is, because that is what I would replace it with, otherwise micrometer
68-
// TOOD: then please also change in TransformationTools.computeCalibration
69-
7062
final OmeNgffMultiScaleMetadata[] meta = new OmeNgffMultiScaleMetadata[ 1 ];
7163

7264
// dataset name and co
@@ -85,9 +77,10 @@ public static OmeNgffMultiScaleMetadata[] createOMEZarrMetadata(
8577
if ( n >= 4 )
8678
axes[ index++ ] = new Axis( "channel", "c", null );
8779

88-
axes[ index++ ] = new Axis( "space", "z", unitXYZ );
89-
axes[ index++ ] = new Axis( "space", "y", unitXYZ );
90-
axes[ index++ ] = new Axis( "space", "x", unitXYZ );
80+
String unit = adaptSpatialUnit( unitXYZ );
81+
axes[ index ] = new Axis( "space", "z", unit );
82+
axes[ index + 1 ] = new Axis( "space", "y", unit );
83+
axes[ index + 2 ] = new Axis( "space", "x", unit );
9184

9285
// multiresolution-pyramid
9386
// TODO: seem to be in XYZCT order (but in the file it seems reversed)
@@ -106,8 +99,8 @@ public static OmeNgffMultiScaleMetadata[] createOMEZarrMetadata(
10699

107100
for ( int d = 0; d < 3; ++d )
108101
{
109-
translation[ d ] = m.getTranslation()[ d ];
110-
scale[ d ] = resolutionS0[ d ] * m.get( d, d );
102+
translation[ d ] = resolutionS0[d] * m.getTranslation()[ d ];
103+
scale[ d ] = resolutionS0[d] * m.get( d, d );
111104
}
112105

113106
// if 4d and 5d, add 1's for C and T
@@ -137,27 +130,74 @@ public static OmeNgffMultiScaleMetadata[] createOMEZarrMetadata(
137130
return meta;
138131
}
139132

140-
141-
// Note: TransformationTools.computeAverageCalibration does this reasonably correct
142-
/*
143-
public static double[] getResolutionS0( final VoxelDimensions vx, final double anisoF, final double downsamplingF )
133+
public static double[] getResolutionS0( final double[] cal, final double anisoF, final double downsamplingF )
144134
{
145-
final double[] resolutionS0 = vx.dimensionsAsDoubleArray();
135+
double[] resolutionS0 = Arrays.copyOf( cal, cal.length );
146136

147-
// not preserving anisotropy
148-
if ( Double.isNaN( anisoF ) )
149-
resolutionS0[ 2 ] = resolutionS0[ 0 ];
137+
if ( !Double.isNaN( anisoF ) ) {
138+
// preserving anisotropy
139+
resolutionS0[2] = cal[2] * anisoF;
140+
}
150141

151142
// downsampling
152143
if ( !Double.isNaN( downsamplingF ) )
153144
Arrays.setAll( resolutionS0, d -> resolutionS0[ d ] * downsamplingF );
154145

155-
// TODO: this is a hack so the export downsampling pyramid is working
156-
Arrays.setAll( resolutionS0, d -> 1 );
157-
158146
return resolutionS0;
159147
}
160-
*/
148+
149+
/**
150+
* Adapt various space unit namings to the units supported by Neuroglancer.
151+
* OME NGFF spec does not have any restrictions on units but Neuroglancer only supports the ones that end in meter or the US customary units.
152+
* @param unit
153+
* @return
154+
*/
155+
private static String adaptSpatialUnit(String unit)
156+
{
157+
if ( unit == null )
158+
return "micrometer";
159+
160+
switch ( unit.toLowerCase() ) {
161+
case "angstrom":
162+
case "ångström":
163+
case "ångströms":
164+
return "angstrom";
165+
case "nm":
166+
case "nanometers":
167+
case "nanometer":
168+
return "nanometer";
169+
case "mm":
170+
case "millimeters":
171+
case "millimeter":
172+
return "millimeter";
173+
case "m":
174+
case "meters":
175+
case "meter":
176+
return "meter";
177+
case "km":
178+
case "kilometer":
179+
case "kilometers":
180+
return "kilometer";
181+
case "inch":
182+
case "inches":
183+
return "inch";
184+
case "foot":
185+
case "feet":
186+
return "foot";
187+
case "yard":
188+
case "yards":
189+
return "yard";
190+
case "mile":
191+
case "miles":
192+
return "mile";
193+
case "um":
194+
case "μm":
195+
case "microns":
196+
case "micron":
197+
default:
198+
return "micrometer";
199+
}
200+
}
161201

162202
public static void loadOMEZarr( final N5Reader n5, final String dataset )
163203
{

src/main/java/net/preibisch/mvrecon/process/export/ExportN5Api.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -245,23 +245,23 @@ else if ( storageType == StorageFormat.N5 || storageType == StorageFormat.ZARR )
245245
final Function<Integer, AffineTransform3D> levelToMipmapTransform =
246246
(level) -> MipmapTransforms.getMipmapTransformDefault( mrInfoZarr[level].absoluteDownsamplingDouble() );
247247

248-
IOFunctions.println( "Resolution of level 0: " + Util.printCoordinates( cal ) + " " + unit ); //vx.unit() might not be OME-ZARR compatible
248+
// at this point the pixel size has already been set
249+
// - here we adjust the S0 resolution based on selected downsampling and anisotropy before exporting to N5
250+
double[] resolutionS0 = OMEZarrAttibutes.getResolutionS0( cal, anisoF, downsamplingF );
251+
252+
IOFunctions.println( "Calibration: " + Util.printCoordinates( cal ) + " micrometer; resolution at S0: " + Util.printCoordinates( resolutionS0 ) + " " + unit);
249253

250254
// create metadata
251255
final OmeNgffMultiScaleMetadata[] meta = OMEZarrAttibutes.createOMEZarrMetadata(
252256
5, // int n
253257
"/", // String name, I also saw "/"
254-
cal, // double[] resolutionS0,
258+
resolutionS0, // double[] resolutionS0,
255259
unit, //"micrometer", //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer
256260
mrInfoZarr.length, // int numResolutionLevels,
257261
levelToName,
258262
levelToMipmapTransform );
259263

260264
// save metadata
261-
262-
//org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadata
263-
// for this to work you need to register an adapter in the N5Factory class
264-
// final GsonBuilder builder = new GsonBuilder().registerTypeAdapter( CoordinateTransformation.class, new CoordinateTransformationAdapter() );
265265
driverVolumeWriter.setAttribute( "/", "multiscales", meta );
266266
}
267267
}
@@ -362,14 +362,16 @@ else if ( storageType == StorageFormat.ZARR ) // OME-Zarr export
362362
final Function<Integer, AffineTransform3D> levelToMipmapTransform =
363363
(level) -> MipmapTransforms.getMipmapTransformDefault( mrInfo[level].absoluteDownsamplingDouble() );
364364

365-
IOFunctions.println( "Resolution of level 0: " + Util.printCoordinates( cal ) + " micrometer" );
365+
double[] resolutionS0 = OMEZarrAttibutes.getResolutionS0( cal, anisoF, downsamplingF );
366+
367+
IOFunctions.println( "Calibration: " + Util.printCoordinates( cal ) + " micrometer; resolution at S0: " + Util.printCoordinates( resolutionS0 ) + " " + unit);
366368

367369
// create metadata
368370
final OmeNgffMultiScaleMetadata[] meta = OMEZarrAttibutes.createOMEZarrMetadata(
369371
3, // int n
370372
omeZarrSubContainer, // String name, I also saw "/"
371-
cal, // double[] resolutionS0,
372-
unit, //"micrometer", //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer
373+
resolutionS0, // double[] resolutionS0,
374+
unit, // might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer
373375
mrInfo.length, // int numResolutionLevels,
374376
(level) -> "/" + level,
375377
levelToMipmapTransform );

0 commit comments

Comments
 (0)