/*-
* #%L
* Fiji distribution of ImageJ for the life sciences.
* %%
* Copyright (C) 2007 - 2017 Fiji developers.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-2.0.html>.
* #L%
*/
package spim.process.fusion.boundingbox;
import ij.gui.GenericDialog;
import java.awt.Choice;
import java.awt.Label;
import java.awt.Scrollbar;
import java.awt.TextField;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.TextEvent;
import java.awt.event.TextListener;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.List;
import java.util.Vector;
import mpicbg.spim.data.registration.ViewRegistration;
import mpicbg.spim.data.sequence.ViewDescription;
import mpicbg.spim.data.sequence.ViewId;
import mpicbg.spim.io.IOFunctions;
import net.imglib2.Dimensions;
import net.imglib2.FinalRealInterval;
import net.imglib2.img.ImgFactory;
import net.imglib2.img.array.ArrayImgFactory;
import net.imglib2.img.cell.CellImgFactory;
import net.imglib2.img.imageplus.ImagePlusImgFactory;
import net.imglib2.type.NativeType;
import net.imglib2.type.numeric.ComplexType;
import spim.fiji.plugin.fusion.Fusion;
import spim.fiji.plugin.util.GUIHelper;
import spim.fiji.spimdata.SpimData2;
import spim.fiji.spimdata.ViewSetupUtils;
import spim.fiji.spimdata.boundingbox.BoundingBox;
import spim.process.fusion.export.ImgExport;
public class BoundingBoxGUI extends BoundingBox
{
public static int staticDownsampling = 1;
public static int defaultMin[] = { 0, 0, 0 };
public static int defaultMax[] = { 0, 0, 0 };
public static String[] pixelTypes = new String[]{ "32-bit floating point", "16-bit unsigned integer" };
public static int defaultPixelType = 0;
protected int pixelType = 0;
public static String[] imgTypes = new String[]{ "ArrayImg", "PlanarImg (large images, easy to display)", "CellImg (large images)" };
public static int defaultImgType = 1;
protected int imgtype = 1;
/**
* which viewIds to process, set in queryParameters
*/
protected final List< ViewId > viewIdsToProcess;
protected final SpimData2 spimData;
protected int downsampling = 1;
protected boolean changedSpimDataObject = false;
/**
* @param spimData
* @param viewIdsToProcess - which view ids to fuse
*/
public BoundingBoxGUI( final SpimData2 spimData, final List< ViewId > viewIdsToProcess )
{
// init bounding box with no values, means that we will use the default ones for the dialog
super( null, null );
this.spimData = spimData;
this.viewIdsToProcess = viewIdsToProcess;
}
public BoundingBoxGUI( final SpimData2 spimData, final List< ViewId > viewIdsToProcess, final BoundingBox bb )
{
// init bounding box with values from bounding box
super( bb.getMin(), bb.getMax() );
this.spimData = spimData;
this.viewIdsToProcess = viewIdsToProcess;
}
public boolean queryParameters( final Fusion fusion, final ImgExport imgExport )
{
return queryParameters( fusion, imgExport, true );
}
/**
* Query the necessary parameters for the bounding box
*
* @param fusion - the fusion for which the bounding box is computed, can be null
* @param imgExport - the export module used, can be null
* @return
*/
public boolean queryParameters( final Fusion fusion, final ImgExport imgExport, final boolean allowModifyDimensions )
{
final boolean compress = fusion == null ? false : fusion.compressBoundingBoxDialog();
final boolean supportsDownsampling = fusion == null ? false : fusion.supportsDownsampling();
final boolean supports16BitUnsigned = fusion == null ? false : fusion.supports16BitUnsigned();
final GenericDialog gd = getSimpleDialog( compress, allowModifyDimensions );
if ( !compress )
gd.addMessage( "" );
if ( supportsDownsampling )
gd.addSlider( "Downsample fused dataset", 1.0, 10.0, BoundingBoxGUI.staticDownsampling );
if ( supports16BitUnsigned )
gd.addChoice( "Pixel_type", pixelTypes, pixelTypes[ defaultPixelType ] );
if ( fusion != null && imgExport != null )
gd.addChoice( "ImgLib2_container", imgTypes, imgTypes[ defaultImgType ] );
if ( fusion != null )
fusion.queryAdditionalParameters( gd );
if ( imgExport != null )
imgExport.queryAdditionalParameters( gd, spimData );
gd.addMessage( "Estimated size: ", GUIHelper.largestatusfont, GUIHelper.good );
Label l1 = (Label)gd.getMessage();
gd.addMessage( "???x???x??? pixels", GUIHelper.smallStatusFont, GUIHelper.good );
Label l2 = (Label)gd.getMessage();
final ManageListeners m = new ManageListeners( gd, gd.getNumericFields(), gd.getChoices(), l1, l2, fusion, imgExport, supportsDownsampling, supports16BitUnsigned );
if ( fusion != null )
fusion.registerAdditionalListeners( m );
m.update();
gd.showDialog();
if ( gd.wasCanceled() )
return false;
if ( allowModifyDimensions )
{
this.min[ 0 ] = (int)Math.round( gd.getNextNumber() );
this.min[ 1 ] = (int)Math.round( gd.getNextNumber() );
this.min[ 2 ] = (int)Math.round( gd.getNextNumber() );
this.max[ 0 ] = (int)Math.round( gd.getNextNumber() );
this.max[ 1 ] = (int)Math.round( gd.getNextNumber() );
this.max[ 2 ] = (int)Math.round( gd.getNextNumber() );
}
else
{
setNFIndex( gd, 6 );
}
if ( supportsDownsampling )
this.downsampling = BoundingBoxGUI.staticDownsampling = (int)Math.round( gd.getNextNumber() );
else
this.downsampling = 1;
if ( supports16BitUnsigned )
this.pixelType = BoundingBoxGUI.defaultPixelType = gd.getNextChoiceIndex();
else
this.pixelType = BoundingBoxGUI.defaultPixelType = 0; //32-bit
if ( fusion != null && imgExport != null )
this.imgtype = BoundingBoxGUI.defaultImgType = gd.getNextChoiceIndex();
if ( min[ 0 ] > max[ 0 ] || min[ 1 ] > max[ 1 ] || min[ 2 ] > max[ 2 ] )
{
IOFunctions.println( "Invalid coordinates, min cannot be larger than max" );
return false;
}
if ( fusion != null )
if ( !fusion.parseAdditionalParameters( gd ) )
return false;
if ( imgExport != null )
if ( !imgExport.parseAdditionalParameters( gd, spimData ) )
return false;
BoundingBoxGUI.defaultMin[ 0 ] = min[ 0 ];
BoundingBoxGUI.defaultMin[ 1 ] = min[ 1 ];
BoundingBoxGUI.defaultMin[ 2 ] = min[ 2 ];
BoundingBoxGUI.defaultMax[ 0 ] = max[ 0 ];
BoundingBoxGUI.defaultMax[ 1 ] = max[ 1 ];
BoundingBoxGUI.defaultMax[ 2 ] = max[ 2 ];
return true;
}
protected GenericDialog getSimpleDialog( final boolean compress, final boolean allowModifyDimensions )
{
final int[] rangeMin = new int[ 3 ];
final int[] rangeMax = new int[ 3 ];
setUpDefaultValues( rangeMin, rangeMax );
final GenericDialog gd = new GenericDialog( "Manually define Bounding Box" );
gd.addMessage( "Note: Coordinates are in global coordinates as shown " +
"in Fiji status bar of a fused datasets", GUIHelper.smallStatusFont );
if ( !compress )
gd.addMessage( "", GUIHelper.smallStatusFont );
gd.addSlider( "Minimal_X", rangeMin[ 0 ], rangeMax[ 0 ], this.min[ 0 ] );
gd.addSlider( "Minimal_Y", rangeMin[ 1 ], rangeMax[ 1 ], this.min[ 1 ] );
gd.addSlider( "Minimal_Z", rangeMin[ 2 ], rangeMax[ 2 ], this.min[ 2 ] );
if ( !compress )
gd.addMessage( "" );
gd.addSlider( "Maximal_X", rangeMin[ 0 ], rangeMax[ 0 ], this.max[ 0 ] );
gd.addSlider( "Maximal_Y", rangeMin[ 1 ], rangeMax[ 1 ], this.max[ 1 ] );
gd.addSlider( "Maximal_Z", rangeMin[ 2 ], rangeMax[ 2 ], this.max[ 2 ] );
if ( !allowModifyDimensions )
{
for ( int i = gd.getSliders().size() - 6; i < gd.getSliders().size(); ++i )
((Scrollbar)gd.getSliders().get( i )).setEnabled( false );
for ( int i = gd.getNumericFields().size() - 6; i < gd.getNumericFields().size(); ++i )
((TextField)gd.getNumericFields().get( i )).setEnabled( false );
}
return gd;
}
/**
* populates this.min[] and this.max[] from the defaultMin and defaultMax
*
* @param rangeMin - will be populated with the maximal dimension that all views span
* @param rangeMax - will be populated with the maximal dimension that all views span
*/
protected void setUpDefaultValues( final int[] rangeMin, final int rangeMax[] )
{
final double[] minBB = new double[ rangeMin.length ];
final double[] maxBB = new double[ rangeMin.length ];
this.changedSpimDataObject = computeMaxBoundingBoxDimensions( spimData, viewIdsToProcess, minBB, maxBB );
for ( int d = 0; d < minBB.length; ++d )
{
rangeMin[ d ] = Math.round( (float)Math.floor( minBB[ d ] ) );
rangeMax[ d ] = Math.round( (float)Math.floor( maxBB[ d ] ) );
if ( rangeMin[ d ] < 0 )
--rangeMin[ d ];
if ( rangeMax[ d ] > 0 )
++rangeMax[ d ];
// first time called on this object
if ( this.min == null || this.max == null )
{
this.min = new int[ rangeMin.length ];
this.max = new int[ rangeMin.length ];
}
if ( this.min[ d ] == 0 && this.max[ d ] == 0 )
{
// not preselected
if ( BoundingBoxGUI.defaultMin[ d ] == 0 && BoundingBoxGUI.defaultMax[ d ] == 0 )
{
min[ d ] = rangeMin[ d ];
max[ d ] = rangeMax[ d ];
}
else
{
min[ d ] = BoundingBoxGUI.defaultMin[ d ];
max[ d ] = BoundingBoxGUI.defaultMax[ d ];
}
}
if ( min[ d ] > max[ d ] )
min[ d ] = max[ d ];
if ( min[ d ] < rangeMin[ d ] )
rangeMin[ d ] = min[ d ];
if ( max[ d ] > rangeMax[ d ] )
rangeMax[ d ] = max[ d ];
// test if the values are valid
//if ( min[ d ] < rangeMin[ d ] )
// min[ d ] = rangeMin[ d ];
//if ( max[ d ] > rangeMax[ d ] )
// max[ d ] = rangeMax[ d ];
}
}
/**
* @param spimData
* @param viewIdsToProcess - which view ids to fuse
* @return - a new instance without any special properties
*/
public BoundingBoxGUI newInstance( final SpimData2 spimData, final List< ViewId > viewIdsToProcess )
{
return new BoundingBoxGUI( spimData, viewIdsToProcess );
}
/**
* @return - to be displayed in the generic dialog
*/
public String getDescription() { return "Define manually"; }
/**
* Called before the XML is potentially saved
*
* @return - true if the spimdata was modified, otherwise false
*/
public boolean cleanUp() { return changedSpimDataObject; }
public int getDownSampling() { return downsampling; }
public int getPixelType() { return pixelType; }
public int getImgType() { return imgtype; }
public < T extends ComplexType< T > & NativeType < T > > ImgFactory< T > getImgFactory( final T type )
{
final ImgFactory< T > imgFactory;
if ( this.getImgType() == 0 )
imgFactory = new ArrayImgFactory< T >();
else if ( this.getImgType() == 1 )
imgFactory = new ImagePlusImgFactory< T >();
else
imgFactory = new CellImgFactory<T>( 256 );
return imgFactory;
}
/**
* @return - the final dimensions including downsampling of this bounding box (to instantiate an img)
*/
public long[] getDimensions()
{
final long[] dim = new long[ this.numDimensions() ];
this.dimensions( dim );
for ( int d = 0; d < this.numDimensions(); ++d )
dim[ d ] /= this.getDownSampling();
return dim;
}
/**
* @param spimData
* @param viewIdsToProcess
* @param minBB
* @param maxBB
* @return - true if the SpimData object was modified, otherwise false
*/
public static boolean computeMaxBoundingBoxDimensions( final SpimData2 spimData, final List< ViewId > viewIdsToProcess, final double[] minBB, final double[] maxBB )
{
for ( int d = 0; d < minBB.length; ++d )
{
minBB[ d ] = Double.MAX_VALUE;
maxBB[ d ] = -Double.MAX_VALUE;
}
boolean changed = false;
IOFunctions.println( new Date( System.currentTimeMillis() ) + ": Estimating Bounding Box for Fusion. If size of images is not known (they were never opened before), some of them need to be opened once to determine their size.");
for ( final ViewId viewId : viewIdsToProcess )
{
final ViewDescription viewDescription = spimData.getSequenceDescription().getViewDescription(
viewId.getTimePointId(), viewId.getViewSetupId() );
if ( !viewDescription.isPresent() )
continue;
if ( !viewDescription.getViewSetup().hasSize() )
changed = true;
final Dimensions size = ViewSetupUtils.getSizeOrLoad( viewDescription.getViewSetup(), viewDescription.getTimePoint(), spimData.getSequenceDescription().getImgLoader() );
final double[] min = new double[]{ 0, 0, 0 };
final double[] max = new double[]{
size.dimension( 0 ) - 1,
size.dimension( 1 ) - 1,
size.dimension( 2 ) - 1 };
final ViewRegistration r = spimData.getViewRegistrations().getViewRegistration( viewId );
r.updateModel();
final FinalRealInterval interval = r.getModel().estimateBounds( new FinalRealInterval( min, max ) );
for ( int d = 0; d < minBB.length; ++d )
{
minBB[ d ] = Math.min( minBB[ d ], interval.realMin( d ) );
maxBB[ d ] = Math.max( maxBB[ d ], interval.realMax( d ) );
}
}
return changed;
}
protected static long numPixels( final long[] min, final long[] max, final int downsampling )
{
long numpixels = 1;
for ( int d = 0; d < min.length; ++d )
numpixels *= (max[ d ] - min[ d ])/downsampling;
return numpixels;
}
public class ManageListeners
{
final GenericDialog gd;
final TextField minX, minY, minZ, maxX, maxY, maxZ, downsample;
final Choice pixelTypeChoice, imgTypeChoice;
final Label label1;
final Label label2;
final Fusion fusion;
final boolean supportsDownsampling;
final boolean supports16bit;
final long[] min = new long[ 3 ];
final long[] max = new long[ 3 ];
public ManageListeners(
final GenericDialog gd,
final Vector<?> tf,
final Vector<?> choices,
final Label label1,
final Label label2,
final Fusion fusion,
final ImgExport imgExport,
final boolean supportsDownsampling,
final boolean supports16bit )
{
this.gd = gd;
this.minX = (TextField)tf.get( 0 );
this.minY = (TextField)tf.get( 1 );
this.minZ = (TextField)tf.get( 2 );
this.maxX = (TextField)tf.get( 3 );
this.maxY = (TextField)tf.get( 4 );
this.maxZ = (TextField)tf.get( 5 );
if ( supports16bit )
{
pixelTypeChoice = (Choice)choices.get( 0 );
if ( fusion != null && imgExport != null )
imgTypeChoice = (Choice)choices.get( 1 );
else
imgTypeChoice = null;
}
else
{
pixelTypeChoice = null;
if ( fusion != null && imgExport != null )
imgTypeChoice = (Choice)choices.get( 0 );
else
imgTypeChoice = null;
}
if ( supportsDownsampling )
downsample = (TextField)tf.get( 6 );
else
downsample = null;
this.label1 = label1;
this.label2 = label2;
this.supportsDownsampling = supportsDownsampling;
this.supports16bit = supports16bit;
this.fusion = fusion;
this.addListeners( imgExport );
}
protected void addListeners( final ImgExport imgExport )
{
this.minX.addTextListener( new TextListener() { @Override
public void textValueChanged(TextEvent e) { update(); } });
this.minY.addTextListener( new TextListener() { @Override
public void textValueChanged(TextEvent e) { update(); } });
this.minZ.addTextListener( new TextListener() { @Override
public void textValueChanged(TextEvent e) { update(); } });
this.maxX.addTextListener( new TextListener() { @Override
public void textValueChanged(TextEvent e) { update(); } });
this.maxY.addTextListener( new TextListener() { @Override
public void textValueChanged(TextEvent e) { update(); } });
this.maxZ.addTextListener( new TextListener() { @Override
public void textValueChanged(TextEvent e) { update(); } });
if ( fusion != null && imgExport != null )
this.imgTypeChoice.addItemListener( new ItemListener() { @Override
public void itemStateChanged(ItemEvent e) { update(); } });
if ( supportsDownsampling )
this.downsample.addTextListener( new TextListener() { @Override
public void textValueChanged(TextEvent e) { update(); } });
if ( supports16bit )
this.pixelTypeChoice.addItemListener( new ItemListener() { @Override
public void itemStateChanged(ItemEvent e) { update(); } });
}
public void update()
{
try
{
min[ 0 ] = Long.parseLong( minX.getText() );
min[ 1 ] = Long.parseLong( minY.getText() );
min[ 2 ] = Long.parseLong( minZ.getText() );
max[ 0 ] = Long.parseLong( maxX.getText() );
max[ 1 ] = Long.parseLong( maxY.getText() );
max[ 2 ] = Long.parseLong( maxZ.getText() );
}
catch (Exception e ) {}
if ( supportsDownsampling )
downsampling = Integer.parseInt( downsample.getText() );
else
downsampling = 1;
if ( supports16bit )
pixelType = pixelTypeChoice.getSelectedIndex();
else
pixelType = 0;
if ( imgTypeChoice != null )
imgtype = imgTypeChoice.getSelectedIndex();
else
imgtype = 1;
final long numPixels = numPixels( min, max, downsampling );
final int bytePerPixel;
if ( pixelType == 1 )
bytePerPixel = 2;
else
bytePerPixel = 4;
final long megabytes = (numPixels * bytePerPixel) / (1024*1024);
if ( numPixels > Integer.MAX_VALUE && imgtype == 0 )
{
label1.setText( megabytes + " MB is too large for ArrayImg!" );
label1.setForeground( GUIHelper.error );
}
else
{
if ( fusion == null )
label1.setText( "Fused image: " + megabytes + " MB" );
else
label1.setText( "Fused image: " + megabytes + " MB, required total memory ~" + fusion.totalRAM( megabytes, bytePerPixel ) + " MB" );
label1.setForeground( GUIHelper.good );
}
label2.setText( "Dimensions: " +
(max[ 0 ] - min[ 0 ] + 1)/downsampling + " x " +
(max[ 1 ] - min[ 1 ] + 1)/downsampling + " x " +
(max[ 2 ] - min[ 2 ] + 1)/downsampling + " pixels @ " + BoundingBoxGUI.pixelTypes[ pixelType ] );
}
}
/**
* Increase the counter for GenericDialog.getNextNumber, so we can skip recording it
*
* @param gd
* @param nfIndex
*/
private static final void setNFIndex( final GenericDialog gd, final int nfIndex )
{
try
{
Class< ? > clazz = null;
boolean found = false;
do
{
if ( clazz == null )
clazz = gd.getClass();
else
clazz = clazz.getSuperclass();
if ( clazz != null )
for ( final Field field : clazz.getDeclaredFields() )
if ( field.getName().equals( "nfIndex" ) )
found = true;
}
while ( !found && clazz != null );
if ( !found )
{
System.out.println( "Failed to find GenericDialog.nfIndex field. Quiting." );
return;
}
final Field nfIndexField = clazz.getDeclaredField( "nfIndex" );
nfIndexField.setAccessible( true );
nfIndexField.setInt( gd, nfIndex );
}
catch ( Exception e ) { e.printStackTrace(); }
}
}