Skip to content

Commit a34fcf6

Browse files
fixed translation handling when importing OME-ZARR's
1 parent c0b79b1 commit a34fcf6

1 file changed

Lines changed: 87 additions & 21 deletions

File tree

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

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
4040
import mpicbg.spim.data.sequence.TimePoint;
4141
import mpicbg.spim.data.sequence.ViewId;
42+
import net.preibisch.legacy.io.IOFunctions;
4243
import net.preibisch.mvrecon.fiji.spimdata.imgloaders.AllenOMEZarrLoader.OMEZARREntry;
4344

4445
public class AllenOMEZarrProperties implements N5Properties
@@ -54,6 +55,9 @@ public class AllenOMEZarrProperties implements N5Properties
5455
// TODO: Remove this and the method that populates it, once the signature for N5Properties.getDatasetPath was updated to use the N5 Reader
5556
private final ConcurrentMap< ViewId, OmeNgffMultiScaleMetadata > viewIdToOmeMetadata = new ConcurrentHashMap<>();
5657

58+
// only warn once per opened XML if TranslationCoordinateTransformation is missing
59+
private boolean warnedMissingTranslation = false;
60+
5761
public AllenOMEZarrProperties(
5862
final AbstractSequenceDescription< ?, ?, ? > sequenceDescription,
5963
final Map< ViewId, OMEZARREntry > viewIdToOmeZarrPath)
@@ -122,50 +126,112 @@ private static double[][] getMipMapResolutions( final AllenOMEZarrProperties n5p
122126
final int timePointId = getFirstAvailableTimepointId( n5properties.sequenceDescription, setupId );
123127

124128
// multiresolution pyramid
125-
OmeNgffMultiScaleMetadata multiScaleMetadata = n5properties.getViewSetupMultiscaleMetadata(n5, timePointId, setupId);
129+
final OmeNgffMultiScaleMetadata multiScaleMetadata = n5properties.getViewSetupMultiscaleMetadata(n5, timePointId, setupId);
126130

127-
double[][] mipMapResolutions = new double[ multiScaleMetadata.datasets.length ][ 3 ];
128-
double[] firstScale = null;
131+
final double[][] mipMapResolutions = new double[ multiScaleMetadata.datasets.length ][ 3 ];
132+
double[] scaleS0 = null;
129133

130-
for ( int i = 0; i < multiScaleMetadata.datasets.length; ++i )
134+
// iterate over all resolution levels for scale
135+
for ( int s = 0; s < multiScaleMetadata.datasets.length; ++s )
131136
{
132-
final OmeNgffDataset ds = multiScaleMetadata.datasets[ i ];
137+
final OmeNgffDataset ds = multiScaleMetadata.datasets[ s ];
133138

134139
for ( final CoordinateTransformation< ? > c : ds.coordinateTransformations )
135140
{
136141
if ( c instanceof ScaleCoordinateTransformation )
137142
{
138-
final ScaleCoordinateTransformation s = ( ScaleCoordinateTransformation ) c;
143+
final ScaleCoordinateTransformation scale = ( ScaleCoordinateTransformation ) c;
139144

140-
if ( firstScale == null )
141-
firstScale = s.getScale().clone();
145+
if ( scaleS0 == null )
146+
scaleS0 = scale.getScale().clone();
142147

143-
for ( int d = 0; d < mipMapResolutions[ i ].length; ++d )
148+
for ( int d = 0; d < mipMapResolutions[ s ].length; ++d )
144149
{
145-
mipMapResolutions[ i ][ d ] = s.getScale()[ d ] / firstScale[ d ];
146-
mipMapResolutions[ i ][ d ] = Math.round(mipMapResolutions[ i ][ d ]*10000)/10000d; // round to the 5th digit
150+
mipMapResolutions[ s ][ d ] = scale.getScale()[ d ] / scaleS0[ d ];
151+
mipMapResolutions[ s ][ d ] = Math.round(mipMapResolutions[ s ][ d ]*10000)/10000d; // round to the 5th digit
147152
}
148153
}
149-
if ( c instanceof TranslationCoordinateTransformation)
154+
}
155+
}
156+
157+
// iterate over all resolution levels for translation (to make sure scaleS0 is assigned if it existed)
158+
// OME-Zarr 0.4: physical = pixel * scale + translation
159+
// For averaging downsampling by factor r: expected relative shift = (r-1)/2 in s0 pixels
160+
// For non-averaging (strided) downsampling: expected relative shift = 0
161+
double[] translationS0 = null;
162+
Boolean isAveraging = null; // determined from first downsampled level with r>1, then verified for subsequent levels
163+
164+
//System.out.println( "\nsetup " + setupId );
165+
166+
for ( int s = 0; s < multiScaleMetadata.datasets.length; ++s )
167+
{
168+
final OmeNgffDataset ds = multiScaleMetadata.datasets[ s ];
169+
170+
boolean foundTranslation = false;
171+
172+
for ( final CoordinateTransformation< ? > c : ds.coordinateTransformations )
173+
{
174+
if ( c instanceof TranslationCoordinateTransformation )
150175
{
151176
final TranslationCoordinateTransformation t = ( TranslationCoordinateTransformation ) c;
177+
foundTranslation = true;
178+
179+
if ( scaleS0 == null )
180+
throw new IllegalStateException( "Expected first scale to be set before the translation for level " + s + " dataset is processed" );
152181

153-
if (firstScale == null) {
154-
throw new IllegalStateException("Expected first scale to be set before the translation for level " + i + " dataset is processed");
182+
//System.out.println( "s=" + s + ", translation: " + Arrays.toString( t.getTranslation() ));
183+
184+
// capture s0's translation
185+
if ( translationS0 == null )
186+
{
187+
translationS0 = t.getTranslation().clone();
188+
// s0: no validation needed, just capture
189+
break;
155190
}
156191

157-
for ( int d = 0; d < mipMapResolutions[ i ].length; ++d )
192+
for ( int d = 0; d < mipMapResolutions[ s ].length; ++d )
158193
{
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);
194+
final double r = mipMapResolutions[ s ][ d ];
195+
196+
// skip dimensions that are not downsampled (r=1), both modes have shift=0
197+
if ( Math.abs( r - 1.0 ) < 0.01 )
198+
continue;
199+
200+
// relative pixel translation: (translation_s - translationS0) / scaleS0
201+
final double relPxTranslation = ( t.getTranslation()[ d ] - translationS0[ d ] ) / scaleS0[ d ];
202+
final double expectedAveraging = ( r - 1.0 ) / 2.0; // 0.5, 1.5, 3.5, 7.5, ...
203+
204+
final boolean matchesAveraging = Math.abs( relPxTranslation - expectedAveraging ) < 0.01;
205+
final boolean matchesNonAveraging = Math.abs( relPxTranslation ) < 0.01;
206+
207+
//System.out.println( "s=" + s + ", d=" + d + ", relPxTranslation=" + relPxTranslation + " @ scale=" + r );
208+
209+
if ( isAveraging == null )
210+
{
211+
// determine mode from first downsampled dimension
212+
if ( matchesAveraging )
213+
isAveraging = true;
214+
else if ( matchesNonAveraging )
215+
throw new IllegalStateException( "Non-averaging downsampling detected (translation=0 for level " + s + " dim " + d + "), which is currently not supported." );
216+
else
217+
throw new IllegalStateException( "Unsupported translation for level " + s + " dim " + d + ": relative pixel translation=" + relPxTranslation + " (expected " + expectedAveraging + " for averaging or 0.0 for non-averaging)." );
218+
}
219+
else
220+
{
221+
// verify consistency with detected mode
222+
final double expected = isAveraging ? expectedAveraging : 0.0;
223+
if ( Math.abs( relPxTranslation - expected ) >= 0.01 )
224+
throw new IllegalStateException( "Inconsistent translation for level " + s + " dim " + d + ": relative pixel translation=" + relPxTranslation + ", expected " + expected + " based on detected " + ( isAveraging ? "averaging" : "non-averaging" ) + " downsampling." );
165225
}
166226
}
167227
}
168228
}
229+
230+
if ( !foundTranslation && s > 0 && !n5properties.warnedMissingTranslation )
231+
{
232+
IOFunctions.println( "WARNING: No TranslationCoordinateTransformation found, assuming half-pixel shifts for averaging-based downsampling." );
233+
n5properties.warnedMissingTranslation = true;
234+
}
169235
}
170236

171237
return mipMapResolutions;

0 commit comments

Comments
 (0)