3939import mpicbg .spim .data .generic .sequence .AbstractSequenceDescription ;
4040import mpicbg .spim .data .sequence .TimePoint ;
4141import mpicbg .spim .data .sequence .ViewId ;
42+ import net .preibisch .legacy .io .IOFunctions ;
4243import net .preibisch .mvrecon .fiji .spimdata .imgloaders .AllenOMEZarrLoader .OMEZARREntry ;
4344
4445public 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