diff --git a/src/main/java/bdv/ij/ApplyBigwarpPlugin.java b/src/main/java/bdv/ij/ApplyBigwarpPlugin.java index 7052c03a..c71689a8 100644 --- a/src/main/java/bdv/ij/ApplyBigwarpPlugin.java +++ b/src/main/java/bdv/ij/ApplyBigwarpPlugin.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -56,6 +57,7 @@ import bigwarp.BigWarpData; import bigwarp.BigWarpExporter; import bigwarp.BigWarpInit; +import bigwarp.FieldOfView; import bigwarp.landmarks.LandmarkTableModel; import bigwarp.transforms.BigWarpTransform; import fiji.util.gui.GenericDialogPlus; @@ -66,6 +68,7 @@ import ij.plugin.PlugIn; import mpicbg.spim.data.sequence.VoxelDimensions; import net.imglib2.FinalInterval; +import net.imglib2.FinalRealInterval; import net.imglib2.Interval; import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealInterval; @@ -76,6 +79,7 @@ import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.BoundingBoxEstimation; import net.imglib2.realtransform.InvertibleRealTransform; +import net.imglib2.realtransform.RealTransform; import net.imglib2.realtransform.RealTransformSequence; import net.imglib2.realtransform.RealViews; import net.imglib2.type.NativeType; @@ -179,7 +183,7 @@ public static double[] getResolution( } else if( resolutionOption.equals( MOVING ) ) { - if( bwData.numTargetSources() <= 0 ) + if( bwData.numMovingSources() <= 0 ) return null; final Source< ? > spimSource = bwData.getMovingSource( 0 ).getSpimSource(); @@ -356,7 +360,6 @@ else if( fieldOfViewOption.equals( MOVING_WARPED )) seq.add( outputResolution2Pixel.inverse() ); return bboxEst.estimatePixelInterval(seq, interval); - // return BigWarpExporter.estimateBounds( seq, interval ); } else if( fieldOfViewOption.equals( UNION_TARGET_MOVING )) { @@ -368,6 +371,24 @@ else if( fieldOfViewOption.equals( UNION_TARGET_MOVING )) return null; } + public static RealInterval getPhysicalInterval(Source src, RealTransform transform) { + + return getPhysicalInterval(new BoundingBoxEstimation(), src, transform); + } + + public static RealInterval getPhysicalInterval(final BoundingBoxEstimation bbox, final Source src, final RealTransform transform) { + + final Interval interval = src.getSource(0, 0); + final AffineTransform3D srcTform = new AffineTransform3D(); + src.getSourceTransform(0, 0, srcTform); + + final RealTransformSequence seq = new RealTransformSequence(); + seq.add(srcTform); + seq.add(transform); + + return bbox.estimateInterval(seq, interval); + } + public static double[] getResolution( final Source< ? > source, final String resolutionOption, @@ -607,6 +628,234 @@ public static List getPixelInterval( return null; } + /** + * Returns a {@link RealInterval} in physical units of the output given input options + * + * @param bwData + * the BigWarpData + * @param landmarks + * the landmarks + * @param transform + * the transformation + * @param fieldOfViewOption + * the field of view option + * @param fieldOfViewPointFilter + * the regexp for filtering landmarks points by name + * @param bboxEst + * the bounding box estimator + * @param fovSpec + * the field of view specification + * @param offsetSpec + * the offset specification + * @param outputResolution + * the resolution of the output image + * @return the output interval + */ + public static List getPhysicalInterval( + final BigWarpData bwData, + final LandmarkTableModel landmarks, + final InvertibleRealTransform transform, + final String fieldOfViewOption, + final String fieldOfViewPointFilter, + final BoundingBoxEstimation bboxEst, + final double[] fovSpec, + final double[] offsetSpec, + final double[] outputResolution) { + + if (fieldOfViewOption.equals(TARGET)) { + if (bwData.numTargetSources() <= 0) { + System.err.println("Requested target fov but target image is missing."); + return null; + } + + return Collections.singletonList( + FieldOfView.getPhysicaInterval(bwData.getTargetSource(0).getSpimSource())); + + } else if (fieldOfViewOption.equals(MOVING_WARPED)) { + + // TODO + return null; + + } else if (fieldOfViewOption.equals(UNION_TARGET_MOVING)) { + + if (bwData.numTargetSources() <= 0) { + System.err.println("Requested union of moving and target FOV but target image is missing. Returning moving FOV."); + FieldOfView.getPhysicaInterval(bwData.getMovingSource(0).getSpimSource()); + } + + final RealInterval tgtItvl = FieldOfView.getPhysicaInterval(bwData.getTargetSource(0).getSpimSource()); + final RealInterval mvgItvl = FieldOfView.getPhysicaInterval(bwData.getMovingSource(0).getSpimSource()); + return Collections.singletonList(Intervals.union(tgtItvl, mvgItvl)); + + } else if (fieldOfViewOption.equals(SPECIFIED_PIXEL)) { + + if (fovSpec.length < 2 && fovSpec.length > 3) { + System.err.println("Invalid fov spec, length : " + fovSpec.length); + return null; + } + + final ArrayList out = new ArrayList<>(); + out.add(FieldOfView.fromPixelMinSize(offsetSpec, fovSpec, outputResolution)); + return out; + + } else if (fieldOfViewOption.equals(SPECIFIED_PHYSICAL)) { + + if (fovSpec.length < 2 && fovSpec.length > 3) { + System.err.println("Invalid fov spec, length : " + fovSpec.length); + return null; + } + + final ArrayList out = new ArrayList<>(); + out.add(FieldOfView.fromMinSize(offsetSpec, fovSpec)); + return out; + + } else if (fieldOfViewOption.equals(LANDMARK_POINTS)) { + + final List matchedLandmarks = getMatchedPoints(landmarks, fieldOfViewPointFilter); + final double[] min = new double[landmarks.getNumdims()]; + final double[] max = new double[landmarks.getNumdims()]; + + Arrays.fill(min, Double.MAX_VALUE); + Arrays.fill(max, Double.MIN_VALUE); + + int numPoints = 0; + for (int i = 0; i < matchedLandmarks.size(); i++) { + final Double[] pt = matchedLandmarks.get(i); + for (int d = 0; d < pt.length; d++) { + final double lo = pt[d] / outputResolution[d]; + final double hi = pt[d] / outputResolution[d]; + + if (lo < min[d]) + min[d] = lo; + + if (hi > max[d]) + max[d] = hi; + + } + numPoints++; + } + + System.out.println("Estimated field of view using " + numPoints + " landmarks."); + // Make sure something naughty didn't happen + for (int d = 0; d < min.length; d++) { + if (min[d] == Long.MAX_VALUE) { + System.err.println("Problem generating field of view from landmarks"); + return null; + } + + if (max[d] == Long.MIN_VALUE) { + System.err.println("Problem generating field of view from landmarks"); + return null; + } + } + + final ArrayList out = new ArrayList<>(); + out.add(new FinalRealInterval(min, max)); + return out; + + } else if (fieldOfViewOption.equals(LANDMARK_POINT_CUBE_PHYSICAL) + || fieldOfViewOption.equals(LANDMARK_POINT_CUBE_PIXEL)) { + final List matchedLandmarks = getMatchedPoints(landmarks, fieldOfViewPointFilter); + if (matchedLandmarks.isEmpty()) { + System.err.println("No matching point found"); + return null; + } + + final int nd = landmarks.getNumdims(); + final ArrayList out = new ArrayList<>(); + + for (int i = 0; i < matchedLandmarks.size(); i++) { + final Double[] pt = matchedLandmarks.get(i); + final double[] min = new double[nd]; + final double[] max = new double[nd]; + + if (fieldOfViewOption.equals(LANDMARK_POINT_CUBE_PHYSICAL)) { + for (int d = 0; d < nd; d++) { + min[d] = (pt[d] / outputResolution[d]) - (fovSpec[d] / outputResolution[d]); + max[d] = (pt[d] / outputResolution[d]) + (fovSpec[d] / outputResolution[d]); + } + } else { + for (int d = 0; d < nd; d++) { + min[d] = (pt[d] / outputResolution[d]) - (fovSpec[d]) + 1; + max[d] = (pt[d] / outputResolution[d]) + (fovSpec[d]) - 1; + } + } + + out.add(new FinalRealInterval(min, max)); + } + return out; + } + + System.err.println("Invalid field of view option: ( " + fieldOfViewOption + " )"); + return null; + } + + + public static RealInterval getPhysicalInterval( + final Source< ? > source, + final LandmarkTableModel landmarks, + final InvertibleRealTransform transform, + final String fieldOfViewOption, + final BoundingBoxEstimation bboxEst, + final double[] outputResolution ) + { + final RandomAccessibleInterval< ? > rai = source.getSource( 0, 0 ); + + if( fieldOfViewOption.equals( TARGET )) + { + final double[] inputres = resolutionFromSource( source ); + + final int N = outputResolution.length <= rai.numDimensions() ? outputResolution.length : rai.numDimensions(); + final long[] max = new long[ rai.numDimensions() ]; + for( int d = 0; d < N; d++ ) + { + max[ d ] = (long)Math.ceil( ( inputres[ d ] * rai.dimension( d )) / outputResolution[ d ]); + } + + return new FinalInterval( max ); + } + else if( fieldOfViewOption.equals( MOVING_WARPED )) + { + final RandomAccessibleInterval< ? > raiSrc = ((WarpedSource)source).getWrappedSource().getSource( 0, 0 ); + final FinalInterval interval = new FinalInterval( + Intervals.minAsLongArray( raiSrc ), + Intervals.maxAsLongArray( raiSrc )); + + if( transform == null ) + return interval; + + final double[] movingRes = resolutionFromSource( source ); + final int ndims = transform.numSourceDimensions(); + final AffineTransform movingPixelToPhysical = new AffineTransform( ndims ); + movingPixelToPhysical.set( movingRes[ 0 ], 0, 0 ); + movingPixelToPhysical.set( movingRes[ 1 ], 1, 1 ); + if( ndims > 2 ) + movingPixelToPhysical.set( movingRes[ 2 ], 2, 2 ); + + final AffineTransform outputResolution2Pixel = new AffineTransform( ndims ); + outputResolution2Pixel.set( outputResolution[ 0 ], 0, 0 ); + outputResolution2Pixel.set( outputResolution[ 1 ], 1, 1 ); + if( ndims > 2 ) + outputResolution2Pixel.set( outputResolution[ 2 ], 2, 2 ); + + final RealTransformSequence seq = new RealTransformSequence(); + seq.add( movingPixelToPhysical ); + seq.add( transform.inverse() ); + seq.add( outputResolution2Pixel.inverse() ); + + return bboxEst.estimatePixelInterval(seq, interval); + // return BigWarpExporter.estimateBounds( seq, interval ); + } + else if( fieldOfViewOption.equals( UNION_TARGET_MOVING )) + { + final Interval movingWarpedInterval = getPixelInterval( source, landmarks, transform, MOVING_WARPED, bboxEst, outputResolution ); + final Interval targetInterval = getPixelInterval( source, landmarks, transform, TARGET, bboxEst, outputResolution ); + return Intervals.union( movingWarpedInterval, targetInterval ); + } + + return null; + } + public static void fillMatchedPointNames( final List ptList, final LandmarkTableModel landmarks, @@ -668,6 +917,7 @@ public static List getMatchedPoints( * @param fieldOfViewOption the field of view option * @param offsetSpec the offset specification * @param ltm the {@link LandmarkTableModel} + * @param transform the transform using to warp images (used for warped moving source option) * @param fieldOfViewPointFilter the landmark name filter for FOV estimation * @param outputResolution the resolution of the output image * @param targetSource the target source @@ -677,8 +927,11 @@ public static double[] getPhysicalOffset( final String fieldOfViewOption, final double[] offsetSpec, final LandmarkTableModel ltm, + final InvertibleRealTransform transform, final String fieldOfViewPointFilter, + final BoundingBoxEstimation bboxEst, final double[] outputResolution, + final Source movingSource, final Source targetSource ) { final int nd = 3; // this okay even for 2D @@ -707,19 +960,33 @@ else if( fieldOfViewOption.equals( LANDMARK_POINTS ) ) } return offset; } + else if( fieldOfViewOption.equals( MOVING_WARPED ) ) + { + return getPhysicalInterval(bboxEst, movingSource, transform.inverse()).minAsDoubleArray(); + } + else if( fieldOfViewOption.equals( MOVING ) ) + { + return offsetFromSource( movingSource ); + } else { - final Interval outputInterval = targetSource.getSource(0, 0); - final AffineTransform3D tform = new AffineTransform3D(); - targetSource.getSourceTransform(0, 0, tform); - - // is using interval min overkill? - // or can I count on it always be at the origin? - tform.apply(outputInterval.minAsDoubleArray(), offset); - return offset; + return offsetFromSource( targetSource ); } } + private static double[] offsetFromSource(final Source src) { + + final double[] offset = new double[3]; + final Interval outputInterval = src.getSource(0, 0); + final AffineTransform3D tform = new AffineTransform3D(); + src.getSourceTransform(0, 0, tform); + + // is using interval min overkill? + // or can I count on it always be at the origin? + tform.apply(outputInterval.minAsDoubleArray(), offset); + return offset; + } + /** * Get the offset in pixels given the output resolution and interval * @@ -938,7 +1205,7 @@ public static List apply( { final SourceAndConverter< T > movingSource = bwData.getMovingSource( i ); final WarpedSource ws = ((WarpedSource)(movingSource.getSpimSource())); - if (ws.getTransform() == null) { + if (ws.getTransform() == null && invXfm != null ) { ws.updateTransform(invXfm); ws.setIsTransformed(true); } @@ -953,7 +1220,6 @@ public static List apply( } final ProgressWriter progressWriter = new ProgressWriterIJ(); - // Generate the properties needed to generate the transform from output pixel space // to physical space final double[] res = getResolution( bwData, resolutionOption, resolutionSpec ); @@ -964,8 +1230,8 @@ public static List apply( if( outputIntervalList.size() > 1 ) ApplyBigwarpPlugin.fillMatchedPointNames( matchedPtNames, landmarks, fieldOfViewPointFilter ); - final double[] offset = getPhysicalOffset( fieldOfViewOption, offsetSpec, landmarks, - fieldOfViewPointFilter, res, tgtSrc ); + final double[] offset = getPhysicalOffset( fieldOfViewOption, offsetSpec, landmarks, invXfm, + fieldOfViewPointFilter, bboxEst, res, bwData.getMovingSource(0).getSpimSource(), tgtSrc ); if( writeOpts != null && writeOpts.n5Dataset != null && !writeOpts.n5Dataset.isEmpty()) { @@ -981,8 +1247,6 @@ public static List apply( } else { - - return runExport( bwData, bwData.sources, fieldOfViewOption, outputIntervalList, matchedPtNames, interp, offset, res, isVirtual, nThreads, diff --git a/src/main/java/bdv/ij/BigWarpCommand.java b/src/main/java/bdv/ij/BigWarpCommand.java index 6fe1a074..8cc14eb4 100644 --- a/src/main/java/bdv/ij/BigWarpCommand.java +++ b/src/main/java/bdv/ij/BigWarpCommand.java @@ -42,13 +42,6 @@ public void run( String args ) BigWarpInitDialog.runMacro( macroOptions ); else { -// if( datasetService != null ) -// { -// System.out.println( "dset service exists"); -// for( final Dataset d : datasetService.getDatasets() ) -// System.out.println( d.getName()); -// } - final BigWarpInitDialog dialog = BigWarpInitDialog.createAndShow( datasetService ); // dialog sets recorder to its initial state on cancel or execution dialog.setInitialRecorderState( initialRecorderState ); diff --git a/src/main/java/bigwarp/BigWarp.java b/src/main/java/bigwarp/BigWarp.java index 8942eb9f..9247a78b 100755 --- a/src/main/java/bigwarp/BigWarp.java +++ b/src/main/java/bigwarp/BigWarp.java @@ -3336,7 +3336,6 @@ else if ( ke.getID() == KeyEvent.KEY_RELEASED ) } } - // TODO, // consider this // https://github.com/kwhat/jnativehook // for arbitrary modifiers diff --git a/src/main/java/bigwarp/BigWarpBatchTransform.java b/src/main/java/bigwarp/BigWarpBatchTransform.java index 8cfed337..cd0cf116 100644 --- a/src/main/java/bigwarp/BigWarpBatchTransform.java +++ b/src/main/java/bigwarp/BigWarpBatchTransform.java @@ -91,8 +91,7 @@ public static void main( String[] args ) throws IOException, FormatException public static final SpimDataMinimal createSpimData( IFormatReader reader ) { Hashtable< String, Object > gmeta = reader.getGlobalMetadata(); - System.out.println( gmeta ); // header stuff here TODO - + // get relevant metadata double pw = 1.0; double ph = 1.0; diff --git a/src/main/java/bigwarp/BigWarpInit.java b/src/main/java/bigwarp/BigWarpInit.java index ed8ad65d..187601e3 100644 --- a/src/main/java/bigwarp/BigWarpInit.java +++ b/src/main/java/bigwarp/BigWarpInit.java @@ -31,6 +31,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -129,7 +130,6 @@ import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedIntType; import net.imglib2.type.volatiles.VolatileARGBType; -import net.imglib2.util.Util; import net.imglib2.view.ExtendedRandomAccessibleInterval; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; @@ -258,8 +258,6 @@ public static < T extends RealType< T > > void initSourceReal( final Source< T > data.sourceInfos.put( setupId++, info ); } - data.wrapUp(); - if ( names != null ) { final ArrayList wrappedSources = wrapSourcesAsRenamable( data.sources, names ); @@ -415,23 +413,30 @@ public static < T > BigWarpData< T > add( BigWarpData bwdata, Source< T > src, i return bwdata; } + @SuppressWarnings("unchecked") public static < T > BigWarpData< T > add( BigWarpData< T > bwdata, LinkedHashMap< Source< T >, SourceInfo > sources, RealTransform transform, Supplier transformUriSupplier ) { - sources.forEach( ( source, info ) -> { - addSourceToListsGenericType( source, info.getId(), bwdata.converterSetups, bwdata.sources ); - final SourceAndConverter< T > addedSource = bwdata.sources.get( bwdata.sources.size() - 1 ); - info.setSourceAndConverter( addedSource ); + for( Entry, SourceInfo> entry : sources.entrySet() ) { + + final Source source = entry.getKey(); + final SourceInfo info = entry.getValue(); + + // some initializers set the SourceAndConverter, some do not + if( info.getSourceAndConverter() == null ) { + addSourceToListsGenericType( source, info.getId(), bwdata.converterSetups, bwdata.sources ); + final SourceAndConverter< T > addedSource = bwdata.sources.get( bwdata.sources.size() - 1 ); + info.setSourceAndConverter( addedSource ); + } if ( transform != null ) - { info.setTransform( transform, transformUriSupplier ); - } + bwdata.sourceInfos.put( info.getId(), info ); - } ); + } return bwdata; } - @SuppressWarnings( { "rawtypes" } ) + @SuppressWarnings( { "rawtypes", "unchecked" } ) public static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( BigWarpData bwdata, Dataset data, int baseId, final boolean isMoving ) { boolean first = true; @@ -468,9 +473,7 @@ public static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( BigW final IntervalView> channelRaw = Views.hyperSlice( data, channelIdx, c ); final IntervalView> channel = hasZ ? channelRaw : Views.addDimension( channelRaw, 0, 0 ); - @SuppressWarnings( "unchecked" ) final RandomAccessibleIntervalSource source = new RandomAccessibleIntervalSource( channel, data.getType(), res, data.getName() ); - final SourceInfo info = new SourceInfo( baseId + c, isMoving, data.getName(), () -> data.getSource() ); info.setSerializable( first ); if ( first ) @@ -483,9 +486,7 @@ public static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( BigW { final RandomAccessibleInterval> img = hasZ ? data : Views.addDimension( data, 0, 0 ); - @SuppressWarnings( "unchecked" ) final RandomAccessibleIntervalSource source = new RandomAccessibleIntervalSource( img, data.getType(), res, data.getName() ); - final SourceInfo info = new SourceInfo( baseId, isMoving, data.getName(), () -> data.getSource() ); info.setSerializable( true ); sourceInfoMap.put( source, info ); @@ -523,6 +524,7 @@ public static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( BigW int setupId = baseId; for ( final SourceAndConverter sac : tmpSources ) { + @SuppressWarnings("unchecked") final Source< T > source = sac.getSpimSource(); sourceInfoMap.put( source, new SourceInfo( setupId++, isMoving, source.getName() ) ); } @@ -542,6 +544,7 @@ private static String schemeSpecificPartWithoutQuery( URI uri ) return uri.getSchemeSpecificPart().replaceAll( "\\?" + uri.getQuery(), "" ).replaceAll( "//", "" ); } + @SuppressWarnings("unchecked") public static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( final BigWarpData< T > bwData, String uri, int setupId, boolean isMoving ) throws URISyntaxException, IOException, SpimDataException { final SharedQueue sharedQueue = BigWarpData.getSharedQueue(); @@ -569,8 +572,8 @@ public static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( fina throw new URISyntaxException( firstScheme, "Unsupported Top Level Protocol" ); } - final Source< T > source = (Source)loadN5Source( n5reader, n5URL.getGroupPath(), sharedQueue ); - sourceStateMap.put( source, new SourceInfo( setupId, isMoving, n5URL.getGroupPath() ) ); + final SourceInfo info = loadN5SourceInfo(bwData, n5reader, n5URL.getGroupPath(), sharedQueue, setupId, isMoving ); + sourceStateMap.put( (Source)info.getSourceAndConverter().getSpimSource(), info ); } else { @@ -579,14 +582,12 @@ public static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( fina { final String containerWithoutN5Scheme = n5URL.getContainerPath().replaceFirst( "^n5://", "" ); final N5Reader n5reader = new N5Factory().openReader( containerWithoutN5Scheme ); - final String group = n5URL.getGroupPath(); - final Source< T > source = (Source)loadN5Source( n5reader, group, sharedQueue ); - - if( source != null ) - sourceStateMap.put( source, new SourceInfo( setupId, isMoving, group ) ); + final SourceInfo info = loadN5SourceInfo(bwData, n5reader, n5URL.getGroupPath(), sharedQueue, setupId, isMoving ); + sourceStateMap.put( (Source)info.getSourceAndConverter().getSpimSource(), info ); } catch ( final Exception ignored ) {} + if ( sourceStateMap.isEmpty() ) { final String containerPath = n5URL.getContainerPath(); @@ -662,7 +663,7 @@ public static < T > Map< Source< T >, SourceInfo > createSources( final BigWarpD return createSources( bwdata, isMoving, setupId, rootPath, dataset, null ); } - private static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( final BigWarpData< T > bwdata, final boolean isMoving, final int setupId, final String rootPath, final String dataset, final AtomicReference< SpimData > returnMovingSpimData ) + private static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( final BigWarpData< T > bwdata, final boolean isMoving, final int setupId, final String rootPath, final String dataset, final AtomicReference< SpimData > returnMovingSpimData ) { final SharedQueue sharedQueue = new SharedQueue(Math.max(1, Runtime.getRuntime().availableProcessors() / 2)); if ( rootPath.endsWith( "xml" ) ) @@ -706,16 +707,57 @@ private static < T > LinkedHashMap< Source< T >, SourceInfo > createSources( fi } else { - final LinkedHashMap< Source< T >, SourceInfo > map = new LinkedHashMap<>(); - final Source< T > source = (Source)loadN5Source( rootPath, dataset, sharedQueue ); - final SourceInfo info = new SourceInfo( setupId, isMoving, dataset, () -> rootPath + "$" + dataset ); - info.setSerializable( true ); - map.put( source, info ); - return map; + return makeMap( loadN5SourceInfo(bwdata, rootPath, dataset, sharedQueue, setupId, isMoving)); + } + } + + @SuppressWarnings("unchecked") + private static LinkedHashMap, SourceInfo> makeMap(final SourceInfo info) { + + final LinkedHashMap, SourceInfo> map = new LinkedHashMap<>(); + info.setSerializable(true); + map.put((Source)info.getSourceAndConverter().getSpimSource(), info); + return map; + } + + public static < T extends NativeType > SourceInfo loadN5SourceInfo( final BigWarpData bwData, final String n5Root, final String n5Dataset, final SharedQueue queue, + final int sourceId, final boolean moving ) + { + final N5Reader n5; + try + { + n5 = new N5Factory().openReader( n5Root ); + } + catch ( final RuntimeException e ) { + e.printStackTrace(); + return null; } + return loadN5SourceInfo( bwData, n5, n5Dataset, queue, sourceId, moving ); } + @SuppressWarnings("unchecked") + public static < T extends NativeType> SourceInfo loadN5SourceInfo( final BigWarpData bwData, final N5Reader n5, final String n5Dataset, final SharedQueue queue, + final int sourceId, final boolean moving ) + { + + N5Metadata meta = null; + try + { + final N5DatasetDiscoverer discoverer = new N5DatasetDiscoverer( n5, N5DatasetDiscoverer.fromParsers( PARSERS ), N5DatasetDiscoverer.fromParsers( GROUP_PARSERS ) ); + final N5TreeNode node = discoverer.discoverAndParseRecursive(""); + meta = node.getDescendant(n5Dataset).map(N5TreeNode::getMetadata).orElse(null); + } + catch ( final IOException e ) + {} + + final SourceAndConverter sac = (SourceAndConverter)openN5VSourceAndConverter( bwData, n5, meta, queue); + final String uri = n5.getURI().toString() + "$" + n5Dataset; + final SourceInfo info = new SourceInfo(sourceId, moving, sac.getSpimSource().getName(), () -> uri ); + info.setSourceAndConverter(sac); + return info; + } + @Deprecated public static < T extends NativeType > Source< T > loadN5Source( final String n5Root, final String n5Dataset, final SharedQueue queue ) { final N5Reader n5; @@ -731,6 +773,7 @@ public static < T extends NativeType > Source< T > loadN5Source( final String } @SuppressWarnings("unchecked") + @Deprecated public static < T extends NativeType> Source< T > loadN5Source( final N5Reader n5, final String n5Dataset, final SharedQueue queue ) { @@ -798,15 +841,43 @@ public static < T extends NativeType, M extends N5Metadata > Source< T > open return null; } - public static < T extends NativeType & NumericType> Source< T > openN5V( final N5Reader n5, final MultiscaleMetadata< ? > multiMeta, final SharedQueue sharedQueue ) - { + /** + * Use openN5VSourceAndConverter instead + * + * @param source type + * @param n5 the n5 reader + * @param multiMeta the multiscale metadata + * @param sharedQueue the shared queue + * @return a source + */ + @SuppressWarnings("unchecked") + @Deprecated + public static & NumericType> Source openN5V( final N5Reader n5, final MultiscaleMetadata multiMeta, + final SharedQueue sharedQueue) { + + return (Source)openN5VSourceAndConverter(null, n5, multiMeta, sharedQueue).getSpimSource(); + } + + public static & NumericType> SourceAndConverter openN5VSourceAndConverter( + final BigWarpData bwData, + final N5Reader n5, + final N5Metadata multiMeta, + final SharedQueue sharedQueue) { + final List> sources = new ArrayList<>(); final List converterSetups = new ArrayList<>(); try { - N5Viewer.buildN5Sources(n5, new DataSelection(n5, Collections.singletonList(multiMeta)), sharedQueue, converterSetups, sources, BdvOptions.options()); - if( sources.size() > 0 ) - return (Source)sources.get(0).getSpimSource(); - } catch (final IOException e) { } + N5Viewer.buildN5Sources(n5, new DataSelection(n5, Collections.singletonList(multiMeta)), sharedQueue, converterSetups, sources, + BdvOptions.options()); + + if (sources.size() > 0) { + if( bwData != null ) { + bwData.sources.add((SourceAndConverter)sources.get(0)); + bwData.converterSetups.add(converterSetups.get(0)); + } + return sources.get(0); + } + } catch (final IOException e) {} return null; } diff --git a/src/main/java/bigwarp/BigWarpRealExporter.java b/src/main/java/bigwarp/BigWarpRealExporter.java index 59880743..73e556b7 100644 --- a/src/main/java/bigwarp/BigWarpRealExporter.java +++ b/src/main/java/bigwarp/BigWarpRealExporter.java @@ -160,6 +160,7 @@ public RandomAccessibleInterval exportRai(Source src) { src.getSourceTransform(0, 0, srcXfm); // in pixel space + @SuppressWarnings("unchecked") final RealRandomAccessible raiRaw = (RealRandomAccessible)src.getInterpolatedSource(0, 0, interp); // the transform from world to new pixel coordinates diff --git a/src/main/java/bigwarp/FieldOfView.java b/src/main/java/bigwarp/FieldOfView.java new file mode 100644 index 00000000..9a1c35e2 --- /dev/null +++ b/src/main/java/bigwarp/FieldOfView.java @@ -0,0 +1,68 @@ +package bigwarp; + +import bdv.viewer.Source; +import net.imglib2.FinalRealInterval; +import net.imglib2.RealInterval; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.realtransform.BoundingBoxEstimation; + +public class FieldOfView { + + public static RealInterval getPhysicaInterval(final Source source) { + + final AffineTransform3D tform = new AffineTransform3D(); + return BoundingBoxEstimation.corners(tform, source.getSource(0, 0)); + } + + public static RealInterval fromMinSize(final double[] min, final double[] size) { + + final int nd = size.length; + final double[] max = new double[nd]; + for (int i = 0; i < nd; i++) + max[i] = min[i] + size[i]; + + return new FinalRealInterval(min, max); + } + + public static RealInterval fromPixelMinSize(final double[] minPixel, final double[] sizePixel, final double[] resolution) { + + final int nd = sizePixel.length; + final double[] min = new double[nd]; + final double[] max = new double[nd]; + for (int i = 0; i < nd; i++) { + min[i] = resolution[i] * min[i]; + max[i] = resolution[i] * (min[i] + sizePixel[i]); + } + + return new FinalRealInterval(min, max); + } + + public static RealInterval computeInterval( + final double[] resolution, + final double[] offset, + final long[] dimensions) { + + final int nd = resolution.length; + final double[] max = new double[resolution.length]; + for (int i = 0; i < nd; i++) { + max[i] = offset[i] + resolution[i] * dimensions[i]; + } + return new FinalRealInterval(offset, max); + } + + public static RealInterval expand( + final RealInterval interval, + final double... amounts) { + + final int nd = interval.numDimensions(); + final double[] min = new double[nd]; + final double[] max = new double[nd]; + + for (int i = 0; i < nd; i++) { + min[i] = interval.realMin(i) - amounts[i]; + max[i] = interval.realMax(i) + amounts[i]; + } + return new FinalRealInterval(min, max); + } + +} diff --git a/src/main/java/bigwarp/landmarks/LandmarkTableModel.java b/src/main/java/bigwarp/landmarks/LandmarkTableModel.java index 65888bed..33a28956 100644 --- a/src/main/java/bigwarp/landmarks/LandmarkTableModel.java +++ b/src/main/java/bigwarp/landmarks/LandmarkTableModel.java @@ -1660,14 +1660,6 @@ public void setValueAt(Object value, int row, int col) return; } - if (DEBUG) - { - System.out.println("Setting value at " + row + "," + col - + " to " + value - + " (an instance of " - + value.getClass() + ")"); - } - if( col == NAMECOLUMN ) { names.set(row, (String)value ); diff --git a/src/main/java/bigwarp/scripts/ReadDisplacementField.java b/src/main/java/bigwarp/scripts/ReadDisplacementField.java index 292178d9..16b3d68d 100644 --- a/src/main/java/bigwarp/scripts/ReadDisplacementField.java +++ b/src/main/java/bigwarp/scripts/ReadDisplacementField.java @@ -1,31 +1,42 @@ package bigwarp.scripts; import java.util.concurrent.Callable; +import java.util.stream.IntStream; +import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.imglib2.N5DisplacementField; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.janelia.saalfeldlab.n5.universe.N5Factory; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.AxisUtils; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.CoordinateSystem; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v05.TransformUtils; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v05.transformations.CoordinateTransform; import org.scijava.command.Command; import org.scijava.log.LogService; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; import org.scijava.ui.UIService; +import bigwarp.transforms.NgffTransformations; +import ij.IJ; +import ij.ImagePlus; import net.imagej.Dataset; import net.imagej.DatasetService; import net.imagej.axis.CalibratedAxis; import net.imagej.axis.DefaultAxisType; import net.imagej.axis.DefaultLinearAxis; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.cache.img.CachedCellImg; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.realtransform.AffineGet; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; -import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.view.Views; -import net.imglib2.type.numeric.integer.ByteType; -import net.imglib2.type.numeric.integer.ShortType; @Plugin(type = Command.class, menuPath = "Plugins>Transform>Read Displacement Field") public class ReadDisplacementField implements Callable, Command { @@ -48,14 +59,13 @@ public class ReadDisplacementField implements Callable, Command { @Parameter private String n5Dataset; - @Parameter(label = "ResultType type", style = "listBox", choices = { "FLOAT64", "FLOAT32", "NATIVE" }) - private String resultType; - @Parameter(label = "Thread count", required = true, min = "1", max = "999") private int nThreads = 1; private CalibratedAxis[] axes; + private Axis[] csAxes; + @Override public void run() { @@ -78,35 +88,70 @@ public & NativeType> void process() { ui.show(dataset); } - @SuppressWarnings("unchecked") private & NativeType> RandomAccessibleInterval readDataAndMetadata() { - try (N5Reader n5 = new N5Factory().openReader(n5Root)) { - createAxes(n5, n5Dataset); - final RandomAccessibleInterval img; - if( resultType.equals(NATIVE)) - img = N5DisplacementField.openRaw(n5, n5Dataset, (T)getRawZero(n5, n5Dataset)); - else - img = N5DisplacementField.openRaw(n5, n5Dataset, (T)getTargetType()); - - final int nd = img.numDimensions(); - final RandomAccessibleInterval imgp; - if( nd == 4 ) - imgp = Views.moveAxis(img, nd-2, nd-1); - else if( nd == 3 ) - imgp = img; - else - throw new N5Exception("Dataset must be 3D or 4D, but had " + nd + " dimensions"); - - return imgp; - - }catch( Exception e ) { + try (N5Reader n5 = new N5Factory().gsonBuilder(NgffTransformations.gsonBuilder()).openReader(n5Root)) { + + if( isN5Field(n5, n5Dataset)) + return readN5(n5, n5Dataset); + else if( isNgffField(n5, n5Dataset)) + return readNgff(n5, n5Dataset); + + } catch (Exception e) { e.printStackTrace(); } throw new N5Exception("Could not read displacement field from: " + n5Root + " " + n5Dataset); } - private CalibratedAxis[] createAxes(N5Reader n5, final String n5Dataset) { + private boolean isN5Field(final N5Reader n5, final String n5Dataset) { + + return n5.getAttribute(n5Dataset, N5DisplacementField.SPACING_ATTR, double[].class) != null; + } + + @SuppressWarnings("unchecked") + private & NativeType, T extends RealType> RandomAccessibleInterval readN5(final N5Reader n5, final String n5Dataset) { + + createAxesN5(n5, n5Dataset); + + DataType type = n5.getDatasetAttributes(n5Dataset).getDataType(); + boolean isQuantized = !(type == DataType.FLOAT64 || type == DataType.FLOAT32); + + final RandomAccessibleInterval img; + if (isQuantized) + img = N5DisplacementField.openQuantized(n5, n5Dataset, (Q)N5Utils.type(type), (T)new FloatType()); + else + img = (RandomAccessibleInterval)N5DisplacementField.openRaw(n5, n5Dataset, new FloatType()); + + final int nd = img.numDimensions(); + final RandomAccessibleInterval imgp; + if (nd == 4) + imgp = Views.moveAxis(img, nd - 2, nd - 1); + else if (nd == 3) + imgp = img; + else + throw new N5Exception("Dataset must be 3D or 4D, but had " + nd + " dimensions"); + + return imgp; + } + + @SuppressWarnings("unchecked") + private & NativeType> RandomAccessibleInterval readNgff(final N5Reader n5, final String n5Dataset) { + + createAxesNgff(n5, n5Dataset); + final CachedCellImg img = N5Utils.open(n5, n5Dataset); + final int nd = img.numDimensions(); + + // need to permute the axes because we permute the image below + final int[] p = IntStream.range(0, nd).toArray(); + p[0] = 2; + p[2] = 0; + + AxisUtils.permute(axes, axes, p); + return (RandomAccessibleInterval)Views.moveAxis(img, 0, 2); + } + + private CalibratedAxis[] createAxesN5(final N5Reader n5, final String n5Dataset) { + final DatasetAttributes dsetAttrs = n5.getDatasetAttributes(n5Dataset); if (dsetAttrs == null) throw new N5Exception("No dataset at" + n5Dataset); @@ -130,38 +175,36 @@ private CalibratedAxis[] createAxes(N5Reader n5, final String n5Dataset) { return axes; } - @SuppressWarnings({ "incomplete-switch", "unchecked" }) - private static & NativeType> T getRawZero( N5Reader n5, final String n5Dataset ) { - - // The types enumerated here are the only allowed types for displacement fields - final DatasetAttributes dsetAttrs = n5.getDatasetAttributes(n5Dataset); - if( dsetAttrs == null) - throw new N5Exception("No dataset at" + n5Dataset); + private boolean isNgffField(final N5Reader n5, final String n5Dataset) { - switch( dsetAttrs.getDataType() ) { - case INT8: - return (T)new ByteType(); - case INT16: - return (T)new ShortType(); - case FLOAT32: - return (T)new FloatType(); - case FLOAT64: - return (T)new DoubleType(); - } - throw new N5Exception("Unexpected type: " + dsetAttrs.getDataType()); + return n5.getAttribute(n5Dataset, CoordinateTransform.KEY, CoordinateTransform[].class) != null; } + private CalibratedAxis[] createAxesNgff(final N5Reader n5, final String n5Dataset) { - @SuppressWarnings("unchecked") - private & NativeType> T getTargetType() { - switch (resultType) { - case WriteDisplacementField.FLOAT32: - return (T)new FloatType(); - case WriteDisplacementField.FLOAT64: - return (T)new DoubleType(); + final CoordinateTransform[] cts = n5.getAttribute(n5Dataset, CoordinateTransform.KEY, CoordinateTransform[].class); + final CoordinateSystem[] css = n5.getAttribute(n5Dataset, CoordinateSystem.KEY, CoordinateSystem[].class); + csAxes = css[0].getAxes(); + + final AffineGet affine = TransformUtils.toAffine(cts[0], csAxes.length); + + final int nd = csAxes.length; + axes = new CalibratedAxis[nd]; + + for (int i = 0; i < nd; i++) { + final Axis csAxis = csAxes[i]; + if (csAxis.getType().equals(Axis.DISPLACEMENT)) { + axes[i] = new DefaultLinearAxis(new DefaultAxisType("v", false), "px"); + } else { + axes[i] = new DefaultLinearAxis( + new DefaultAxisType(csAxis.getName(), true), + csAxis.getUnit(), + affine.get(i, i), + affine.get(i, nd)); + } } - return null; - } + return axes; + } -} +} \ No newline at end of file diff --git a/src/main/java/bigwarp/scripts/WriteDisplacementField.java b/src/main/java/bigwarp/scripts/WriteDisplacementField.java index d3b7b993..ac1862d6 100644 --- a/src/main/java/bigwarp/scripts/WriteDisplacementField.java +++ b/src/main/java/bigwarp/scripts/WriteDisplacementField.java @@ -9,12 +9,15 @@ import org.janelia.saalfeldlab.n5.ij.N5ScalePyramidExporter; import org.janelia.saalfeldlab.n5.imglib2.N5DisplacementField; import org.janelia.saalfeldlab.n5.universe.N5Factory; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v05.transformations.DisplacementFieldCoordinateTransform; import org.scijava.command.Command; import org.scijava.log.LogService; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; import org.scijava.ui.UIService; +import bdv.gui.ExportDisplacementFieldFrame; +import bigwarp.transforms.NgffTransformations; import net.imagej.Dataset; import net.imglib2.RandomAccessibleInterval; import net.imglib2.converter.Converter; @@ -77,15 +80,26 @@ public class WriteDisplacementField implements Callable, Command { }) private String outputType; + @Parameter(label = "Output format", style = "listBox", choices = { + ExportDisplacementFieldFrame.FMT_NGFF, + ExportDisplacementFieldFrame.FMT_N5, + ExportDisplacementFieldFrame.FMT_BIGWARP_TPS + }) + private String format = ExportDisplacementFieldFrame.FMT_NGFF; + @Parameter(label = "Thread count", required = true, min = "1", max = "999") private int nThreads = 1; + @Parameter(label = "Quantization Error", required = true, min = "0") + private double quantizationError = 0.01; + private int nd = -1; private int vectorDim = -1; private int vectorSize = -1; @SuppressWarnings({ "unchecked" }) public & NativeType, S extends RealType & NativeType, Q extends NativeType & IntegerType> void process() { + final AffineGet affine = null; final Compression compression = N5ScalePyramidExporter.getCompression(compressionArg); @@ -95,6 +109,8 @@ public & NativeType, S extends RealType & NativeTyp final double[] spacing = new double[nd]; Arrays.fill(spacing, 1.0); + final String unit = dataset.axis(0).unit(); + int j = 0; for (int i = 0; i < dataset.numDimensions(); i++) { @@ -107,9 +123,8 @@ public & NativeType, S extends RealType & NativeTyp vectorSize = (int) dataset.dimension(i); } } - - validateAndWarn(); + validateAndWarn(); final int[] chunkSizeSpatial = N5ScalePyramidExporter.parseBlockSize(chunkSizeArg, spatialDims); final int[] chunkSize = IntStream.concat( IntStream.of(vectorSize), @@ -118,20 +133,28 @@ public & NativeType, S extends RealType & NativeTyp final RandomAccessibleInterval vectorAxisFirst = (RandomAccessibleInterval) Views.moveAxis( (RandomAccessibleInterval)dataset, vectorDim, 0); try (N5Writer n5 = new N5Factory().openWriter(n5Root)) { - if (outputType.equals(FLOAT32) || outputType.equals(FLOAT64)) { - final RandomAccessibleInterval converted = convertIfNecessary(vectorAxisFirst, (S)getTargetType()); - N5DisplacementField.save(n5, n5Dataset, affine, converted, spacing, offset, chunkSize, compression); - } - else { - final Q quantizedType = (Q)getTargetType(); - N5DisplacementField.save(n5, n5Dataset, affine, vectorAxisFirst, spacing, offset, chunkSize, compression, quantizedType, 1e-6); + if (format.equals(ExportDisplacementFieldFrame.FMT_N5)) { + if (outputType.equals(FLOAT32) || outputType.equals(FLOAT64)) { + final RandomAccessibleInterval converted = convertIfNecessary(vectorAxisFirst, (S)getTargetType()); + N5DisplacementField.save(n5, n5Dataset, affine, converted, spacing, offset, chunkSize, compression); + } else { + final Q quantizedType = (Q)getTargetType(); + N5DisplacementField.save(n5, n5Dataset, affine, vectorAxisFirst, spacing, offset, chunkSize, compression, quantizedType, quantizationError); + } + } else if (format.equals(ExportDisplacementFieldFrame.FMT_NGFF)) { + + final DisplacementFieldCoordinateTransform dfieldTform = NgffTransformations.save( + n5, n5Dataset, vectorAxisFirst, + "input", "output", spacing, offset, + unit, chunkSize, compression, nThreads); + + NgffTransformations.addCoordinateTransformations(n5, "/", dfieldTform); } } catch (Exception e) { System.err.println("Failed to write displacement field at " + n5Root); e.printStackTrace(); } - } @SuppressWarnings("unchecked") diff --git a/src/main/java/bigwarp/transforms/NgffTransformations.java b/src/main/java/bigwarp/transforms/NgffTransformations.java index 58c9475b..fa1a2cc0 100644 --- a/src/main/java/bigwarp/transforms/NgffTransformations.java +++ b/src/main/java/bigwarp/transforms/NgffTransformations.java @@ -91,8 +91,6 @@ public static InvertibleRealTransform openInvertible(final String url) { public static RealTransform findFieldTransformFirst(final N5Reader n5, final String group) { final String normGrp = N5URI.normalizeGroupPath(group); - System.out.println( "nnrmGrp: " + normGrp ); - final CoordinateTransform[] transforms = n5.getAttribute(group, CoordinateTransform.KEY, CoordinateTransform[].class); if (transforms == null) return null; @@ -106,10 +104,8 @@ public static RealTransform findFieldTransformFirst(final N5Reader n5, final Str for (final CoordinateTransform ct : transforms) { System.out.println(ct); final String nrmInput = N5URI.normalizeGroupPath(ct.getInput()); - System.out.println( "nrmInput: " + nrmInput ); if (nrmInput.equals(normGrp)) { found = true; - System.out.println( "found: " + ct ); } } @@ -120,7 +116,6 @@ public static RealTransform findFieldTransformFirst(final N5Reader n5, final Str public static RealTransform findFieldTransformStrict(final N5Reader n5, final String group, final String output ) { final String normGrp = N5URI.normalizeGroupPath(group); - System.out.println( "nnrmGrp: " + normGrp ); final CoordinateTransform[] transforms = n5.getAttribute(group, CoordinateTransform.KEY, CoordinateTransform[].class); if (transforms == null) @@ -130,9 +125,7 @@ public static RealTransform findFieldTransformStrict(final N5Reader n5, final St for (final CoordinateTransform ct : transforms) { System.out.println(ct); final String nrmInput = N5URI.normalizeGroupPath(ct.getInput()); - System.out.println( "nrmInput: " + nrmInput ); if (nrmInput.equals(normGrp) && ct.getOutput().equals(output) ) { - System.out.println( "found: " + ct ); return ct.getTransform(n5); } } @@ -156,9 +149,6 @@ public static String detectTransforms( final String url ) return null; } -// final String grp = ( uri.getGroupPath() != null ) ? uri.getGroupPath() : ""; -// final String attr = ( uri.getAttributePath() != null && !uri.getAttributePath().equals("/")) ? uri.getAttributePath() : "coordinateTransformations[0]"; - if( isValidTransformUri( url )) return url; diff --git a/src/test/java/bigwarp/BBoxTests.java b/src/test/java/bigwarp/BBoxTests.java index e85ecded..33673e69 100644 --- a/src/test/java/bigwarp/BBoxTests.java +++ b/src/test/java/bigwarp/BBoxTests.java @@ -24,10 +24,17 @@ import static org.junit.Assert.*; import org.junit.Test; +import bdv.ij.ApplyBigwarpPlugin; +import bdv.util.RandomAccessibleIntervalSource; import net.imglib2.FinalInterval; import net.imglib2.Interval; +import net.imglib2.RealInterval; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.BoundingBoxEstimation; +import net.imglib2.realtransform.Scale3D; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.util.ConstantUtils; +import net.imglib2.util.Intervals; public class BBoxTests { @@ -62,4 +69,42 @@ public void testCornersFaces() assertEquals( "max z ", itvl.max(2) * 4, bbox.max(2) ); } + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void testPhysicalBoundingBoxEstimation() { + + final double EPS = 1e-9; + + final FinalInterval pixelInterval = Intervals.createMinMax(0, 0, 0, 32, 16, 8); + System.out.println(pixelInterval); + + final AffineTransform3D identity = new AffineTransform3D(); + final UnsignedByteType type = new UnsignedByteType(); + final RandomAccessibleIntervalSource src = new RandomAccessibleIntervalSource( + ConstantUtils.constantRandomAccessibleInterval(type, pixelInterval), type, identity, "test1"); + + RealInterval physicalInterval = ApplyBigwarpPlugin.getPhysicalInterval(src, new Scale3D(5, 7, 11)); + assertEquals("scale x min", 0, physicalInterval.realMin(0), EPS); + assertEquals("scale y min", 0, physicalInterval.realMin(1), EPS); + assertEquals("scale z min", 0, physicalInterval.realMin(2), EPS); + + assertEquals("scale x max", 32 * 5, physicalInterval.realMax(0), EPS); + assertEquals("scale y max", 16 * 7, physicalInterval.realMax(1), EPS); + assertEquals("scale z max", 8 * 11, physicalInterval.realMax(2), EPS); + + final AffineTransform3D scale = new AffineTransform3D(); + scale.scale(2, 3, 4); + final RandomAccessibleIntervalSource srcScaled = new RandomAccessibleIntervalSource( + ConstantUtils.constantRandomAccessibleInterval(type, pixelInterval), type, scale, "test2"); + + physicalInterval = ApplyBigwarpPlugin.getPhysicalInterval(srcScaled, new Scale3D(5, 7, 11)); + assertEquals("scale x min", 0, physicalInterval.realMin(0), EPS); + assertEquals("scale y min", 0, physicalInterval.realMin(1), EPS); + assertEquals("scale z min", 0, physicalInterval.realMin(2), EPS); + + assertEquals("scale x max", 32 * 5 * 2, physicalInterval.realMax(0), EPS); + assertEquals("scale y max", 16 * 7 * 3, physicalInterval.realMax(1), EPS); + assertEquals("scale z max", 8 * 11 * 4, physicalInterval.realMax(2), EPS); + } + } diff --git a/src/test/java/bigwarp/apply/BigWarpApplyTests.java b/src/test/java/bigwarp/apply/BigWarpApplyTests.java index 6539d6e8..dff683f2 100644 --- a/src/test/java/bigwarp/apply/BigWarpApplyTests.java +++ b/src/test/java/bigwarp/apply/BigWarpApplyTests.java @@ -19,9 +19,11 @@ import ij.ImagePlus; import net.imglib2.img.Img; import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.iterator.IntervalIterator; import net.imglib2.iterator.RealIntervalIterator; import net.imglib2.realtransform.BoundingBoxEstimation; import net.imglib2.realtransform.InvertibleRealTransform; +import net.imglib2.realtransform.Scale3D; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.util.Intervals; @@ -172,6 +174,39 @@ public void testExportPtsSimple() { assertEquals(0, img.getAt(0, 0, 0).get()); } + @Test + public void testExportMvgWarped() { + + final long[] pt = new long[]{16, 8, 4}; + final long[] size = new long[]{32, 16, 8}; + + final ImagePlus mvg = new BigWarpTestUtils.TestImagePlusBuilder().title("mvg") + .size(size) + .position(pt).build(); + + final BigWarpData bwData = BigWarpInit.initData(); + BigWarpInit.add(bwData, BigWarpInit.createSources(bwData, mvg, 0, 0, true)); + bwData.wrapMovingSources(); + + final Scale3D tform = new Scale3D(4, 3, 2); + final LandmarkTableModel ltm = BigWarpTestUtils.landmarks(new IntervalIterator(new int[]{2, 2, 2}), tform); + + final long[] expectedResultMax = new long[3]; + Arrays.setAll(expectedResultMax, i -> (long)((size[i] - 1) * tform.get(i, i))); + + final List resList = transformMvgWarped(mvg, ltm); + + assertEquals(1, resList.size()); + final ImagePlus result = resList.get(0); + assertResolutionsEqual(mvg, result); + assertOriginsEqual(mvg, result); + + final Img img = ImageJFunctions.wrapByte(result); + assertArrayEquals("result image the wrong size", expectedResultMax, Intervals.maxAsLongArray(img)); + assertEquals(1, img.getAt(16 * 4, 8 * 3, 4 * 2).get()); + assertEquals(0, img.getAt(0, 0, 0).get()); + } + private static void assertResolutionsEqual(ImagePlus expected, ImagePlus actual) { assertEquals("width", expected.getCalibration().pixelWidth, actual.getCalibration().pixelWidth, EPS); @@ -288,4 +323,33 @@ private static List transformToPtsSpec(final ImagePlus mvg, false); } + private static List transformMvgWarped(final ImagePlus mvg, + final LandmarkTableModel ltm) { + + ImagePlus tgt = null; + final BigWarpData bwData = BigWarpInit.createBigWarpDataFromImages(mvg, tgt); + bwData.wrapMovingSources(); + final BoundingBoxEstimation bboxEst = new BoundingBoxEstimation(BoundingBoxEstimation.Method.CORNERS); + final InvertibleRealTransform invXfm = new BigWarpTransform( ltm, BigWarpTransform.AFFINE ).getTransformation(); + + return ApplyBigwarpPlugin.apply( + bwData, + ltm, + invXfm, + BigWarpTransform.AFFINE, + ApplyBigwarpPlugin.MOVING_WARPED, + "", // fov pt filter + bboxEst, + ApplyBigwarpPlugin.MOVING, + null, // res option + null, // fov spec + null, // offset spac + Interpolation.NEARESTNEIGHBOR, + false, // virtual + 1, // nThreads + true, + null, // writeOpts + false); + } + } diff --git a/src/test/java/bigwarp/dfield/DfieldExportTest.java b/src/test/java/bigwarp/dfield/DfieldExportTest.java index 61b73402..90d441ce 100644 --- a/src/test/java/bigwarp/dfield/DfieldExportTest.java +++ b/src/test/java/bigwarp/dfield/DfieldExportTest.java @@ -25,12 +25,15 @@ import bdv.viewer.Source; import bigwarp.BigWarpData; import bigwarp.BigWarpInit; +import bigwarp.FieldOfView; import bigwarp.landmarks.LandmarkTableModel; import bigwarp.source.SourceInfo; import bigwarp.transforms.BigWarpTransform; import ij.ImagePlus; +import net.imglib2.Dimensions; import net.imglib2.FinalRealInterval; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealInterval; import net.imglib2.RealPoint; import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.img.imageplus.ImagePlusImgs; @@ -45,162 +48,159 @@ import net.imglib2.util.Util; import net.imglib2.view.Views; -public class DfieldExportTest -{ +public class DfieldExportTest { + private BigWarpData data; private BigWarpData dataWithTransform; private LandmarkTableModel ltm; @Before - public void setup() - { - final ImagePlus imp = ImagePlusImgs.bytes( 64, 64, 16 ).getImagePlus(); - data = makeData( imp, null ); - dataWithTransform = makeData( imp, new Scale3D( 0.5, 0.5, 0.5 )); - - ltm = new LandmarkTableModel( 3 ); - try - { - ltm.load( new File( "src/test/resources/mr_landmarks_p2p2p4-111.csv" )); - } - catch ( final IOException e ) - { + public void setup() { + + final ImagePlus imp = ImagePlusImgs.bytes(64, 64, 16).getImagePlus(); + data = makeData(imp, null); + dataWithTransform = makeData(imp, new Scale3D(0.5, 0.5, 0.5)); + + ltm = new LandmarkTableModel(3); + try { + ltm.load(new File("src/test/resources/mr_landmarks_p2p2p4-111.csv")); + } catch (final IOException e) { e.printStackTrace(); fail(); } } - private static < T extends RealType< T > > BigWarpData< T > makeData( ImagePlus imp, RealTransform tform ) - { + private static > BigWarpData makeData(ImagePlus imp, RealTransform tform) { + final int id = 0; final boolean isMoving = true; final BigWarpData data = BigWarpInit.initData(); - final LinkedHashMap< Source< T >, SourceInfo > infos = BigWarpInit.createSources( data, imp, id, 0, isMoving ); - BigWarpInit.add( data, infos, tform, null ); + final LinkedHashMap, SourceInfo> infos = BigWarpInit.createSources(data, imp, id, 0, isMoving); + BigWarpInit.add(data, infos, tform, null); return data; } @Test - public void dfieldExportTest() - { - final BigWarpTransform bwTransform = new BigWarpTransform( ltm ); - bwTransform.setInverseTolerance( 0.05 ); - bwTransform.setInverseMaxIterations( 200 ); + public void dfieldExportTest() { + + final BigWarpTransform bwTransform = new BigWarpTransform(ltm); + bwTransform.setInverseTolerance(0.05); + bwTransform.setInverseMaxIterations(200); final boolean ignoreAffine = false; final boolean flatten = true; final boolean virtual = false; - final long[] dims = new long[] { 47, 56, 7 }; - final double[] spacing = new double[] { 0.8, 0.8, 1.6 }; - final double[] offset = new double[] { 0, 0, 0 }; + final long[] dims = new long[]{47, 56, 7}; + final double[] spacing = new double[]{0.8, 0.8, 1.6}; + final double[] offset = new double[]{0, 0, 0}; final int nThreads = 1; final FinalRealInterval testItvl = new FinalRealInterval( - new double[]{ 3.6, 3.6, 1.6 }, - new double[]{ 32.0, 40.0, 9.6 }); + new double[]{3.6, 3.6, 1.6}, + new double[]{32.0, 40.0, 9.6}); - final RealIntervalIterator it = new RealIntervalIterator( testItvl, spacing ); + final RealIntervalIterator it = new RealIntervalIterator(testItvl, spacing); final ImagePlus dfieldImp = BigWarpToDeformationFieldPlugIn.toImagePlus( data, ltm, bwTransform, ignoreAffine, flatten, false, virtual, dims, spacing, offset, - nThreads ); + nThreads); final InvertibleRealTransform tform = bwTransform.getTransformation(); - assertTrue( "forward", compare( tform, dfieldImp, it, 1e-3 )); + assertTrue("forward", compare(tform, dfieldImp, it, 1e-3)); final ImagePlus dfieldInvImp = BigWarpToDeformationFieldPlugIn.toImagePlus( data, ltm, bwTransform, ignoreAffine, flatten, true, virtual, dims, spacing, offset, - nThreads ); + nThreads); it.reset(); - assertTrue( "inverse", compare( tform.inverse(), dfieldInvImp, it, 0.25 )); + assertTrue("inverse", compare(tform.inverse(), dfieldInvImp, it, 0.25)); } @Test - public void dfieldIgnoreAffineExportTest() - { + public void dfieldIgnoreAffineExportTest() { + final boolean ignoreAffine = true; - final BigWarpTransform bwTransform = new BigWarpTransform( ltm ); + final BigWarpTransform bwTransform = new BigWarpTransform(ltm); // constant parameters final boolean flatten = true; final boolean virtual = false; - final long[] dims = new long[] { 47, 56, 7 }; - final double[] spacing = new double[] { 0.8, 0.8, 1.6 }; - final double[] offset = new double[] { 0, 0, 0 }; + final long[] dims = new long[]{47, 56, 7}; + final double[] spacing = new double[]{0.8, 0.8, 1.6}; + final double[] offset = new double[]{0, 0, 0}; final int nThreads = 1; final FinalRealInterval testItvl = new FinalRealInterval( - new double[]{ 3.6, 3.6, 1.6 }, - new double[]{ 32.0, 40.0, 9.6 }); + new double[]{3.6, 3.6, 1.6}, + new double[]{32.0, 40.0, 9.6}); final ImagePlus dfieldImp = BigWarpToDeformationFieldPlugIn.toImagePlus( data, ltm, bwTransform, ignoreAffine, flatten, false, virtual, dims, spacing, offset, - nThreads ); + nThreads); final RealTransformSequence total = new RealTransformSequence(); - total.add( toDfield( dfieldImp ) ); - total.add( bwTransform.affinePartOfTps() ); + total.add(toDfield(dfieldImp)); + total.add(bwTransform.affinePartOfTps()); - final RealIntervalIterator it = new RealIntervalIterator( testItvl, spacing ); + final RealIntervalIterator it = new RealIntervalIterator(testItvl, spacing); final InvertibleRealTransform tform = bwTransform.getTransformation(); - assertTrue( "split affine forward", compare( tform, total, it, 1e-3 )); + assertTrue("split affine forward", compare(tform, total, it, 1e-3)); } @Test - public void dfieldConcatExportTest() - { - final BigWarpTransform bwTransform = new BigWarpTransform( ltm ); + public void dfieldConcatExportTest() { + + final BigWarpTransform bwTransform = new BigWarpTransform(ltm); // constant parameters final boolean ignoreAffine = false; final boolean virtual = false; final boolean inverse = false; - final long[] dims = new long[] { 47, 56, 7 }; - final double[] spacing = new double[] { 0.8, 0.8, 1.6 }; - final double[] offset = new double[] { 0, 0, 0 }; + final long[] dims = new long[]{47, 56, 7}; + final double[] spacing = new double[]{0.8, 0.8, 1.6}; + final double[] offset = new double[]{0, 0, 0}; final int nThreads = 1; final FinalRealInterval testItvl = new FinalRealInterval( - new double[]{ 3.6, 3.6, 1.6 }, - new double[]{ 32.0, 40.0, 9.6 }); + new double[]{3.6, 3.6, 1.6}, + new double[]{32.0, 40.0, 9.6}); // flattened final ImagePlus dfieldImpFlat = BigWarpToDeformationFieldPlugIn.toImagePlus( dataWithTransform, ltm, bwTransform, ignoreAffine, true, inverse, virtual, dims, spacing, offset, - nThreads ); - final DisplacementFieldTransform dfieldFlat = toDfield( dfieldImpFlat ); + nThreads); + final DisplacementFieldTransform dfieldFlat = toDfield(dfieldImpFlat); final InvertibleRealTransform tform = bwTransform.getTransformation(); - final RealTransform preTransform = dataWithTransform.getSourceInfo( 0 ).getTransform(); + final RealTransform preTransform = dataWithTransform.getSourceInfo(0).getTransform(); final RealTransformSequence totalTrueTransform = new RealTransformSequence(); - totalTrueTransform.add( tform ); - totalTrueTransform.add( preTransform ); + totalTrueTransform.add(tform); + totalTrueTransform.add(preTransform); - final RealIntervalIterator it = new RealIntervalIterator( testItvl, spacing ); - assertTrue( "flatten forward", compare( totalTrueTransform, dfieldFlat, it, 1e-3 )); + final RealIntervalIterator it = new RealIntervalIterator(testItvl, spacing); + assertTrue("flatten forward", compare(totalTrueTransform, dfieldFlat, it, 1e-3)); // not flattened final ImagePlus dfieldImpUnFlat = BigWarpToDeformationFieldPlugIn.toImagePlus( dataWithTransform, ltm, bwTransform, ignoreAffine, false, inverse, virtual, dims, spacing, offset, - nThreads ); - final DisplacementFieldTransform dfieldUnflat = toDfield( dfieldImpUnFlat ); + nThreads); + final DisplacementFieldTransform dfieldUnflat = toDfield(dfieldImpUnFlat); it.reset(); - assertTrue( "un-flattened forward", compare( tform, dfieldUnflat, it, 1e-3 )); + assertTrue("un-flattened forward", compare(tform, dfieldUnflat, it, 1e-3)); } @Test @@ -232,8 +232,6 @@ public void dfieldQuantizationTest() throws IOException { new double[]{3.6, 3.6, 1.6}, new double[]{32.0, 40.0, 9.6}); - // final String n5BasePath = "/tmp/blah.n5"; - final File tmpFile = Files.createTempDirectory("bw-dfield-test-").toFile(); tmpFile.deleteOnExit(); final String n5BasePath = tmpFile.getCanonicalPath() + ".n5"; @@ -246,68 +244,108 @@ public void dfieldQuantizationTest() throws IOException { BigWarpToDeformationFieldPlugIn.writeN5(n5BasePath, sDset, ltm, bwTransform, data, dims, spacing, offset, "mm", blkSsize, compression, nThreads, format, false, DTYPE.SHORT, maxQuantizationError, inverse, inverseTolerance, inverseMaxIters); - final N5Writer n5 = new N5Factory().openWriter(n5BasePath); - assertTrue(n5.exists(fDset)); - final DatasetAttributes fAttrs = n5.getDatasetAttributes(fDset); - assertTrue(fAttrs.getDataType().equals(DataType.FLOAT32)); + try (final N5Writer n5 = new N5Factory().openWriter(n5BasePath)) { + assertTrue(n5.exists(fDset)); + final DatasetAttributes fAttrs = n5.getDatasetAttributes(fDset); + assertTrue(fAttrs.getDataType().equals(DataType.FLOAT32)); - assertTrue(n5.exists(sDset)); - final DatasetAttributes sAttrs = n5.getDatasetAttributes(sDset); - assertTrue(sAttrs.getDataType().equals(DataType.INT16)); + assertTrue(n5.exists(sDset)); + final DatasetAttributes sAttrs = n5.getDatasetAttributes(sDset); + assertTrue(sAttrs.getDataType().equals(DataType.INT16)); - try { - assertNotNull(n5.getAttribute(sDset, N5DisplacementField.MULTIPLIER_ATTR, double.class)); - } catch (final N5Exception ignore) {} + try { + assertNotNull(n5.getAttribute(sDset, N5DisplacementField.MULTIPLIER_ATTR, double.class)); + } catch (final N5Exception ignore) {} - final RealTransform dfieldF = N5DisplacementField.open(n5, fDset, false); - final RealTransform dfieldS = N5DisplacementField.open(n5, sDset, false); + final RealTransform dfieldF = N5DisplacementField.open(n5, fDset, false); + final RealTransform dfieldS = N5DisplacementField.open(n5, sDset, false); - final RealIntervalIterator it = new RealIntervalIterator(testItvl, spacing); - assertTrue("quantization error", compare(dfieldF, dfieldS, it, 2 * maxQuantizationError)); + final RealIntervalIterator it = new RealIntervalIterator(testItvl, spacing); + assertTrue("quantization error", compare(dfieldF, dfieldS, it, 2 * maxQuantizationError)); + + n5.remove(); + } + } + + @Test + public void dfieldFieldOfViewTest() throws IOException { + + final BigWarpTransform bwTransform = new BigWarpTransform(ltm); + final InvertibleRealTransform tform = bwTransform.getTransformation(); + + // constant parameters + final boolean inverse = false; + final double inverseTolerance = 0.01; + final int inverseMaxIters = 1; + + final long[] dims = new long[]{11, 13, 17}; + final double[] spacing = new double[]{0.5, 1.0, 2.0}; + final double[] offset = new double[]{100, 50, 25}; + + final int[] blkSsize = new int[]{64, 64, 64}; + final RawCompression compression = new RawCompression(); + + final String format = ExportDisplacementFieldFrame.FMT_N5; + final int nThreads = 1; + + final RealInterval totalItvl = FieldOfView.computeInterval( spacing, offset, dims ); + final RealInterval testItvl = FieldOfView.expand(totalItvl, -spacing[0], -spacing[1], -spacing[2]); + + final File tmpFile = Files.createTempDirectory("bw-dfield-test-").toFile(); + tmpFile.deleteOnExit(); + final String n5BasePath = tmpFile.getCanonicalPath() + ".n5"; + + final String fDset = "dfield"; + BigWarpToDeformationFieldPlugIn.writeN5(n5BasePath, fDset, ltm, bwTransform, data, dims, spacing, offset, "mm", + blkSsize, compression, nThreads, format, false, DTYPE.FLOAT, Double.MAX_VALUE, inverse, inverseTolerance, inverseMaxIters); - n5.remove(); + try (final N5Writer n5 = new N5Factory().openWriter(n5BasePath)) { + + assertTrue(n5.exists(fDset)); + final RealTransform dfieldF = N5DisplacementField.open(n5, fDset, false); + final RealIntervalIterator it = new RealIntervalIterator(testItvl, spacing); + assertTrue("test over offset field-of-view", compare(tform, dfieldF, it, 1e-3)); + + n5.remove(); + } } - public static DisplacementFieldTransform toDfield( final ImagePlus dfieldImp ) - { - final double[] spacing = new double[] { + public static DisplacementFieldTransform toDfield(final ImagePlus dfieldImp) { + + final double[] spacing = new double[]{ dfieldImp.getCalibration().pixelWidth, dfieldImp.getCalibration().pixelHeight, dfieldImp.getCalibration().pixelDepth }; - final double[] offset = new double[] { + final double[] offset = new double[]{ dfieldImp.getCalibration().xOrigin, dfieldImp.getCalibration().yOrigin, dfieldImp.getCalibration().zOrigin }; - final RandomAccessibleInterval< FloatType > img = ImageJFunctions.wrapRealNative( dfieldImp ); - final RandomAccessibleInterval< FloatType > dfimg = Views.moveAxis( img, 2, 0 ); - return new DisplacementFieldTransform( dfimg, spacing, offset ); + final RandomAccessibleInterval img = ImageJFunctions.wrapRealNative(dfieldImp); + final RandomAccessibleInterval dfimg = Views.moveAxis(img, 2, 0); + return new DisplacementFieldTransform(dfimg, spacing, offset); } - public static boolean compare( final RealTransform tform, final ImagePlus dfieldImp, final RealIntervalIterator it, final double tol ) - { - return compare( tform, toDfield( dfieldImp ), it, tol ); + public static boolean compare(final RealTransform tform, final ImagePlus dfieldImp, final RealIntervalIterator it, final double tol) { + + return compare(tform, toDfield(dfieldImp), it, tol); } - public static boolean compare( final RealTransform a, final RealTransform b, final RealIntervalIterator it, final double tol ) - { - final RealPoint gt = new RealPoint( 3 ); - final RealPoint df = new RealPoint( 3 ); - while( it.hasNext()) - { + public static boolean compare(final RealTransform a, final RealTransform b, final RealIntervalIterator it, final double tol) { + + final RealPoint gt = new RealPoint(3); + final RealPoint df = new RealPoint(3); + while (it.hasNext()) { it.fwd(); - a.apply( it, gt ); - b.apply( it, df ); - final double dist = Util.distance( gt, df ); - if( dist > tol ) - { - System.out.println( "it : " + it ); - System.out.println( "dist: " + dist); + a.apply(it, gt); + b.apply(it, df); + final double dist = Util.distance(gt, df); + if (dist > tol) { + System.out.println("it : " + it); + System.out.println("dist: " + dist); return false; } } return true; - } - } diff --git a/src/test/java/bigwarp/url/UrlParseHelper.java b/src/test/java/bigwarp/url/UrlParseHelper.java index 66b67852..029d6e0e 100644 --- a/src/test/java/bigwarp/url/UrlParseHelper.java +++ b/src/test/java/bigwarp/url/UrlParseHelper.java @@ -28,14 +28,17 @@ public static void main( String[] args ) throws IOException h5.setAttribute( "ant", "coordinateTransformations", data ); h5.createDataset( "img", new long[]{6, 8, 10}, new int[] {16, 16, 16}, DataType.UINT8, new GzipCompression() ); h5.createDataset( "img2", new long[]{12, 16, 20}, new int[] {20, 20, 20}, DataType.UINT8, new GzipCompression() ); + h5.close(); final N5FSWriter n5 = new N5FSWriter("src/test/resources/bigwarp/url/transformTest.n5" ); n5.createDataset( "img", new long[]{5, 8, 9}, new int[] {16, 16, 16}, DataType.UINT8, new GzipCompression() ); n5.createDataset( "img2", new long[]{10, 16, 18}, new int[] {18, 18, 18}, DataType.UINT8, new GzipCompression() ); + n5.close(); final N5ZarrWriter zarr = new N5ZarrWriter("src/test/resources/bigwarp/url/transformTest.zarr" ); zarr.createDataset( "/img", new long[]{4, 6, 8}, new int[] {16, 16, 16}, DataType.UINT8, new GzipCompression() ); zarr.createDataset( "/img2", new long[]{8, 12, 16}, new int[] {16, 16, 16}, DataType.UINT8, new GzipCompression() ); + zarr.close(); } }