/** * */ package mpicbg.trakem2.align; import java.awt.Color; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import ij.IJ; import ij.ImagePlus; import ij.gui.GenericDialog; import ini.trakem2.display.Display; import ini.trakem2.display.Displayable; import ini.trakem2.display.Layer; import ini.trakem2.display.Patch; import ini.trakem2.display.Selection; import ini.trakem2.persistence.FSLoader; import ini.trakem2.persistence.Loader; import ini.trakem2.utils.Filter; import ini.trakem2.utils.Utils; import mpicbg.ij.FeatureTransform; import mpicbg.ij.SIFT; import mpicbg.imagefeatures.Feature; import mpicbg.imagefeatures.FloatArray2DSIFT; import mpicbg.models.AbstractAffineModel2D; import mpicbg.models.AbstractModel; import mpicbg.models.AffineModel2D; import mpicbg.models.IllDefinedDataPointsException; import mpicbg.models.InterpolatedAffineModel2D; import mpicbg.models.Model; import mpicbg.models.NotEnoughDataPointsException; import mpicbg.models.Point; import mpicbg.models.PointMatch; import mpicbg.models.SimilarityModel2D; import mpicbg.models.Tile; import mpicbg.models.Transforms; import mpicbg.trakem2.transform.HomographyModel2D; import mpicbg.trakem2.transform.MovingLeastSquaresTransform2; import mpicbg.trakem2.transform.RigidModel2D; import mpicbg.trakem2.transform.TranslationModel2D; /** * A collection of methods regarding SIFT-based alignment * * TODO Bring the methods and tasks into a class for each method and clean up this mess. * * @author Stephan Saalfeld saalfeld@mpi-cbg.de */ public class Align { static public class Param implements Serializable { private static final long serialVersionUID = -6469820142091971052L; final public FloatArray2DSIFT.Param sift = new FloatArray2DSIFT.Param(); /** * Closest/next closest neighbour distance ratio */ public float rod = 0.92f; /** * Maximal allowed alignment error in px */ public float maxEpsilon = 100.0f; /** * Inlier/candidates ratio */ public float minInlierRatio = 0.2f; /** * Minimal absolute number of inliers */ public int minNumInliers = 7; /** * Implemeted transformation models for choice */ // final static public String[] modelStrings = new String[]{ "Translation", "Rigid", "Similarity", "Affine", "Homography" }; final static public String[] modelStrings = new String[]{ "Translation", "Rigid", "Similarity", "Affine" }; public int expectedModelIndex = 1; public int desiredModelIndex = 1; public int regularizerModelIndex = 1; /** * Use a regularized model instead of a pure one. */ public boolean regularize = false; /** * Regularization weight. */ public double lambda = 0.1; public float correspondenceWeight = 1; /** * Ignore identity transform up to a given tolerance */ public boolean rejectIdentity = false; public float identityTolerance = 0.5f; public Param() { sift.maxOctaveSize = 600; sift.fdSize = 8; } public void addSIFTFields( final GenericDialog gd ) { SIFT.addFields( gd, sift ); gd.addNumericField( "closest/next_closest_ratio :", rod, 2 ); } public void addGeometricConsensusFilterFields( final GenericDialog gd ) { gd.addNumericField( "maximal_alignment_error :", maxEpsilon, 2, 6, "px" ); gd.addNumericField( "minimal_inlier_ratio :", minInlierRatio, 2 ); gd.addNumericField( "minimal_number_of_inliers :", minNumInliers, 0 ); gd.addChoice( "expected_transformation :", modelStrings, modelStrings[ expectedModelIndex ] ); gd.addCheckbox( "ignore constant background", rejectIdentity ); gd.addNumericField( "tolerance :", identityTolerance, 2, 6, "px" ); } public void addAlignmentFields( final GenericDialog gd ) { gd.addChoice( "desired_transformation :", modelStrings, modelStrings[ desiredModelIndex ] ); gd.addNumericField( "correspondence weight :", correspondenceWeight, 2 ); gd.addCheckbox( "regularize", regularize ); } public void addRegularizationFields( final GenericDialog gd ) { gd.addChoice( "regularizer :", modelStrings, modelStrings[ regularizerModelIndex ] ); gd.addNumericField( "lambda :", lambda, 2 ); } public void addFields( final GenericDialog gd ) { addSIFTFields( gd ); gd.addMessage( "Geometric Consensus Filter:" ); addGeometricConsensusFilterFields( gd ); gd.addMessage( "Alignment:" ); addAlignmentFields( gd ); addRegularizationFields( gd ); } public boolean readSIFTFields( final GenericDialog gd ) { SIFT.readFields( gd, sift ); rod = ( float )gd.getNextNumber(); return !gd.invalidNumber(); } public boolean readGeometricConsensusFilterFields( final GenericDialog gd ) { maxEpsilon = ( float )gd.getNextNumber(); minInlierRatio = ( float )gd.getNextNumber(); minNumInliers = ( int )gd.getNextNumber(); expectedModelIndex = gd.getNextChoiceIndex(); rejectIdentity = gd.getNextBoolean(); identityTolerance = ( float )gd.getNextNumber(); return !gd.invalidNumber(); } public boolean readAlignmentFields( final GenericDialog gd ) { desiredModelIndex = gd.getNextChoiceIndex(); correspondenceWeight = ( float )gd.getNextNumber(); regularize = gd.getNextBoolean(); return !gd.invalidNumber(); } public boolean readRegularizationFields( final GenericDialog gd ) { regularizerModelIndex = gd.getNextChoiceIndex(); lambda = gd.getNextNumber(); return !gd.invalidNumber(); } public boolean readFields( final GenericDialog gd ) { boolean b = readSIFTFields( gd ); b &= readGeometricConsensusFilterFields( gd ); b &= readAlignmentFields( gd ); b &= readRegularizationFields( gd ); return b; } final public boolean setup( final String title ) { /* SIFT */ final GenericDialog gdSIFT = new GenericDialog( title + ": SIFT parameters" ); addSIFTFields( gdSIFT ); do { gdSIFT.showDialog(); if ( gdSIFT.wasCanceled() ) return false; } while ( !readSIFTFields( gdSIFT ) ); /* Geometric consensus */ final GenericDialog gdGeometricConsensusFilter = new GenericDialog( title + ": Geometric Consensus Filter" ); addGeometricConsensusFilterFields( gdGeometricConsensusFilter ); do { gdGeometricConsensusFilter.showDialog(); if ( gdGeometricConsensusFilter.wasCanceled() ) return false; } while ( !readGeometricConsensusFilterFields( gdGeometricConsensusFilter ) ); /* Alignment */ final GenericDialog gdAlignment = new GenericDialog( title + ": Alignment parameters" ); addAlignmentFields( gdAlignment ); do { gdAlignment.showDialog(); if ( gdAlignment.wasCanceled() ) return false; } while ( !readAlignmentFields( gdAlignment ) ); /* Regularization */ if ( regularize ) { final GenericDialog gdRegularization = new GenericDialog( title + ": Regularization parameters" ); addRegularizationFields( gdRegularization ); do { gdRegularization.showDialog(); if ( gdRegularization.wasCanceled() ) return false; } while ( !readRegularizationFields( gdRegularization ) ); } return true; } @Override public Param clone() { final Param p = new Param(); p.sift.initialSigma = this.sift.initialSigma; p.sift.steps = this.sift.steps; p.sift.minOctaveSize = this.sift.minOctaveSize; p.sift.maxOctaveSize = this.sift.maxOctaveSize; p.sift.fdSize = this.sift.fdSize; p.sift.fdBins = this.sift.fdBins; p.rod = rod; p.maxEpsilon = maxEpsilon; p.minInlierRatio = minInlierRatio; p.minNumInliers = minNumInliers; p.expectedModelIndex = expectedModelIndex; p.rejectIdentity = rejectIdentity; p.identityTolerance = identityTolerance; p.desiredModelIndex = desiredModelIndex; p.correspondenceWeight = correspondenceWeight; p.regularize = regularize; p.regularizerModelIndex = regularizerModelIndex; p.lambda = lambda; return p; } /** * Check if two parameter sets are equal. So far, this method ignores * the parameter {@link #desiredModelIndex} which defines the * transformation class to be used for {@link Tile} alignment. This * makes sense for the current use in {@link PointMatch} serialization * but might be misleading for other applications. * * TODO Think about this. * * @param p * @return */ public boolean equals( final Param p ) { return sift.equals( p.sift ) && ( rod == p.rod ) && ( maxEpsilon == p.maxEpsilon ) && ( minInlierRatio == p.minInlierRatio ) && ( minNumInliers == p.minNumInliers ) && ( expectedModelIndex == p.expectedModelIndex ) && ( rejectIdentity == p.rejectIdentity ) && ( identityTolerance == p.identityTolerance ); // && ( desiredModelIndex == p.desiredModelIndex ); } } final static public Param param = new Param(); static public class ParamOptimize extends Param { private static final long serialVersionUID = 2173278806083343006L; /** * Maximal number of iteration allowed for the optimizer. */ public int maxIterations = 2000; /** * Maximal number of iterations allowed to not change the parameter to * be optimized. */ public int maxPlateauwidth = 200; /** * Filter outliers */ public boolean filterOutliers = false; public float meanFactor = 3.0f; @Override public void addAlignmentFields( final GenericDialog gd ) { super.addAlignmentFields( gd ); gd.addMessage( "Optimization:" ); gd.addNumericField( "maximal_iterations :", maxIterations, 0 ); gd.addNumericField( "maximal_plateauwidth :", maxPlateauwidth, 0 ); gd.addCheckbox( "filter outliers", filterOutliers ); gd.addNumericField( "mean_factor :", meanFactor, 2 ); } @Override public boolean readAlignmentFields( final GenericDialog gd ) { super.readAlignmentFields( gd ); maxIterations = ( int )gd.getNextNumber(); maxPlateauwidth = ( int )gd.getNextNumber(); filterOutliers = gd.getNextBoolean(); meanFactor = ( float )gd.getNextNumber(); return !gd.invalidNumber(); } @Override public void addFields( final GenericDialog gd ) { super.addFields( gd ); gd.addNumericField( "maximal_iterations :", maxIterations, 0 ); gd.addNumericField( "maximal_plateauwidth :", maxPlateauwidth, 0 ); gd.addCheckbox( "filter outliers", filterOutliers ); gd.addNumericField( "mean_factor :", meanFactor, 2 ); } @Override public boolean readFields( final GenericDialog gd ) { super.readFields( gd ); maxIterations = ( int )gd.getNextNumber(); maxPlateauwidth = ( int )gd.getNextNumber(); filterOutliers = gd.getNextBoolean(); meanFactor = ( float )gd.getNextNumber(); return !gd.invalidNumber(); } @Override final public ParamOptimize clone() { final ParamOptimize p = new ParamOptimize(); p.sift.initialSigma = this.sift.initialSigma; p.sift.steps = this.sift.steps; p.sift.minOctaveSize = this.sift.minOctaveSize; p.sift.maxOctaveSize = this.sift.maxOctaveSize; p.sift.fdSize = this.sift.fdSize; p.sift.fdBins = this.sift.fdBins; p.rod = rod; p.maxEpsilon = maxEpsilon; p.minInlierRatio = minInlierRatio; p.minNumInliers = minNumInliers; p.expectedModelIndex = expectedModelIndex; p.rejectIdentity = rejectIdentity; p.identityTolerance = identityTolerance; p.desiredModelIndex = desiredModelIndex; p.regularize = regularize; p.regularizerModelIndex = regularizerModelIndex; p.lambda = lambda; p.maxIterations = maxIterations; p.maxPlateauwidth = maxPlateauwidth; p.filterOutliers = filterOutliers; p.meanFactor = meanFactor; return p; } public boolean equals( final ParamOptimize p ) { return super.equals( p ) && ( maxIterations == p.maxIterations ) && ( maxPlateauwidth == p.maxPlateauwidth ) && ( filterOutliers == p.filterOutliers ) && ( meanFactor == p.meanFactor ); } } final static public ParamOptimize paramOptimize = new ParamOptimize(); final static private class Features implements Serializable { private static final long serialVersionUID = 2689219384710526198L; FloatArray2DSIFT.Param p; ArrayList< Feature > features; Features( final FloatArray2DSIFT.Param p, final ArrayList< Feature > features ) { this.p = p; this.features = features; } } final static private class PointMatches implements Serializable { private static final long serialVersionUID = -2564147268101223484L; Param p; ArrayList< PointMatch > pointMatches; PointMatches( final Param p, final ArrayList< PointMatch > pointMatches ) { this.p = p; this.pointMatches = pointMatches; } } /** * Extracts {@link Feature SIFT-features} from a {@link List} of * {@link AbstractAffineTile2D Tiles} and saves them to disk. */ final static protected class ExtractFeaturesThread extends Thread { final protected Param p; final protected List< AbstractAffineTile2D< ? > > tiles; // final protected HashMap< AbstractAffineTile2D< ? >, Collection< Feature > > tileFeatures; final protected AtomicInteger ai; final protected AtomicInteger ap; final protected int steps; public ExtractFeaturesThread( final Param p, final List< AbstractAffineTile2D< ? > > tiles, // final HashMap< AbstractAffineTile2D< ? >, Collection< Feature > > tileFeatures, final AtomicInteger ai, final AtomicInteger ap, final int steps ) { this.p = p; this.tiles = tiles; this.ai = ai; this.ap = ap; this.steps = steps; } @Override final public void run() { final FloatArray2DSIFT sift = new FloatArray2DSIFT( p.sift ); final SIFT ijSIFT = new SIFT( sift ); for ( int i = ai.getAndIncrement(); i < tiles.size() && !isInterrupted(); i = ai.getAndIncrement() ) { if (isInterrupted()) return; final AbstractAffineTile2D< ? > tile = tiles.get( i ); Collection< Feature > features = deserializeFeatures( p, tile ); if ( features == null ) { /* extract features and, in case there is not enough memory available, try to free it and do again */ boolean memoryFlushed; do { try { features = new ArrayList< Feature >(); final long s = System.currentTimeMillis(); ijSIFT.extractFeatures( tile.createMaskedByteImage(), features ); Utils.log( features.size() + " features extracted in tile " + i + " \"" + tile.getPatch().getTitle() + "\" (took " + ( System.currentTimeMillis() - s ) + " ms)." ); if ( !serializeFeatures( p, tile, features ) ) Utils.log( "Saving features failed for tile \"" + tile.getPatch() + "\"" ); memoryFlushed = false; } catch ( final OutOfMemoryError e ) { Utils.log2( "Flushing memory for feature extraction" ); Loader.releaseAllCaches(); memoryFlushed = true; } } while ( memoryFlushed ); } else { Utils.log( features.size() + " features loaded for tile " + i + " \"" + tile.getPatch().getTitle() + "\"." ); } IJ.showProgress( ap.getAndIncrement(), steps ); } } } final static protected class MatchFeaturesAndFindModelThread extends Thread { final protected Param p; final protected List< AbstractAffineTile2D< ? > > tiles; //final protected HashMap< AbstractAffineTile2D< ? >, Collection< Feature > > tileFeatures; final protected List< AbstractAffineTile2D< ? >[] > tilePairs; final protected AtomicInteger ai; final protected AtomicInteger ap; final protected int steps; final protected boolean multipleHypotheses; public MatchFeaturesAndFindModelThread( final Param p, final List< AbstractAffineTile2D< ? > > tiles, final List< AbstractAffineTile2D< ? >[] > tilePairs, final AtomicInteger ai, final AtomicInteger ap, final int steps, final boolean multipleHypotheses ) { this.p = p; this.tiles = tiles; this.tilePairs = tilePairs; this.ai = ai; this.ap = ap; this.steps = steps; this.multipleHypotheses = multipleHypotheses; } public MatchFeaturesAndFindModelThread( final Param p, final List< AbstractAffineTile2D< ? > > tiles, final List< AbstractAffineTile2D< ? >[] > tilePairs, final AtomicInteger ai, final AtomicInteger ap, final int steps ) { this( p, tiles, tilePairs, ai, ap, steps, false ); } @Override final public void run() { final List< PointMatch > candidates = new ArrayList< PointMatch >(); for ( int i = ai.getAndIncrement(); i < tilePairs.size() && !isInterrupted(); i = ai.getAndIncrement() ) { if (isInterrupted()) return; candidates.clear(); final AbstractAffineTile2D< ? >[] tilePair = tilePairs.get( i ); Collection< PointMatch > inliers = deserializePointMatches( p, tilePair[ 0 ], tilePair[ 1 ] ); if ( inliers == null ) { inliers = new ArrayList< PointMatch >(); final long s = System.currentTimeMillis(); FeatureTransform.matchFeatures( fetchFeatures( p, tilePair[ 0 ] ), fetchFeatures( p, tilePair[ 1 ] ), candidates, p.rod ); /* find the model */ final AbstractModel< ? > model; switch ( p.expectedModelIndex ) { case 0: model = new TranslationModel2D(); break; case 1: model = new RigidModel2D(); break; case 2: model = new SimilarityModel2D(); break; case 3: model = new AffineModel2D(); break; case 4: model = new HomographyModel2D(); break; default: return; } final boolean modelFound = findModel( model, candidates, inliers, p.maxEpsilon, p.minInlierRatio, p.minNumInliers, p.rejectIdentity, p.identityTolerance, multipleHypotheses ); if ( modelFound ) Utils.log( "Model found for tiles \"" + tilePair[ 0 ].getPatch() + "\" and \"" + tilePair[ 1 ].getPatch() + "\":\n correspondences " + inliers.size() + " of " + candidates.size() + "\n average residual error " + model.getCost() + " px\n took " + ( System.currentTimeMillis() - s ) + " ms" ); else Utils.log( "No model found for tiles \"" + tilePair[ 0 ].getPatch() + "\" and \"" + tilePair[ 1 ].getPatch() + "\":\n correspondence candidates " + candidates.size() + "\n took " + ( System.currentTimeMillis() - s ) + " ms" ); if ( !serializePointMatches( p, tilePair[ 0 ], tilePair[ 1 ], inliers ) ) Utils.log( "Saving point matches failed for tiles \"" + tilePair[ 0 ].getPatch() + "\" and \"" + tilePair[ 1 ].getPatch() + "\"" ); } else Utils.log( "Point matches for tiles \"" + tilePair[ 0 ].getPatch().getTitle() + "\" and \"" + tilePair[ 1 ].getPatch().getTitle() + "\" fetched from disk cache" ); if ( inliers != null && inliers.size() > 0 ) { /* weight the inliers */ for ( final PointMatch pm : inliers ) pm.setWeights( new double[]{ p.correspondenceWeight } ); synchronized ( tilePair[ 0 ] ) { synchronized ( tilePair[ 1 ] ) { tilePair[ 0 ].connect( tilePair[ 1 ], inliers ); } tilePair[ 0 ].clearVirtualMatches(); } synchronized ( tilePair[ 1 ] ) { tilePair[ 1 ].clearVirtualMatches(); } } IJ.showProgress( ap.getAndIncrement(), steps ); } } } final static public boolean findModel( final Model< ? > model, final List< PointMatch > candidates, final Collection< PointMatch > inliers, final float maxEpsilon, final float minInlierRatio, final int minNumInliers, final boolean rejectIdentity, final float identityTolerance, final boolean multipleHypotheses ) { boolean again = false; int nHypotheses = 0; final ArrayList< PointMatch > hypothesisCandidates = new ArrayList< PointMatch >( candidates ); try { do { again = false; final ArrayList< PointMatch > inliers2 = new ArrayList< PointMatch >(); final boolean modelFound = model.filterRansac( hypothesisCandidates, inliers2, 1000, maxEpsilon, minInlierRatio, minNumInliers, 3 ); if ( modelFound ) { hypothesisCandidates.removeAll( inliers2 ); if ( rejectIdentity ) { final ArrayList< Point > points = new ArrayList< Point >(); PointMatch.sourcePoints( inliers2, points ); if ( Transforms.isIdentity( model, points, param.identityTolerance ) ) { Utils.log( "Identity transform for " + inliers2.size() + " matches rejected." ); again = true; } else { ++nHypotheses; inliers.addAll( inliers2 ); again = multipleHypotheses; } } else { ++nHypotheses; inliers.addAll( inliers2 ); again = multipleHypotheses; } } } while ( again ); } catch ( final NotEnoughDataPointsException e ) {} if ( nHypotheses > 0 && multipleHypotheses ) { try { model.fit( inliers ); PointMatch.apply( inliers, model ); model.setCost( PointMatch.meanDistance( inliers ) ); Utils.log( nHypotheses + " hypotheses" ); } catch ( final NotEnoughDataPointsException e ) {} catch ( final IllDefinedDataPointsException e ) { nHypotheses = 0; } } return nHypotheses > 0; } final static public boolean findModel( final Model< ? > model, final List< PointMatch > candidates, final Collection< PointMatch > inliers, final float maxEpsilon, final float minInlierRatio, final int minNumInliers, final boolean rejectIdentity, final float identityTolerance ) { return findModel( model, candidates, inliers, maxEpsilon, minInlierRatio, minNumInliers, rejectIdentity, identityTolerance, false ); } final static protected boolean serializeFeatures( final Param p, final AbstractAffineTile2D< ? > t, final Collection< Feature > f ) { final ArrayList< Feature > list = new ArrayList< Feature >(); list.addAll( f ); final Patch patch = t.getPatch(); final Loader loader = patch.getProject().getLoader(); final Features fe = new Features( p.sift, list ); return loader.serialize( fe, new StringBuilder( loader.getUNUIdFolder() ).append( "features.ser/" ) .append( FSLoader.createIdPath( Long.toString( patch.getId() ), "features", ".ser" ) ).toString() ); } /** * Retrieve the features only if saved with the exact same relevant SIFT parameters. */ final static protected Collection< Feature > deserializeFeatures( final Param p, final AbstractAffineTile2D< ? > t ) { final Patch patch = t.getPatch(); final Loader loader = patch.getProject().getLoader(); final Object ob = loader.deserialize( new StringBuilder( loader.getUNUIdFolder() ).append( "features.ser/" ) .append( FSLoader.createIdPath( Long.toString( patch.getId() ), "features", ".ser" ) ).toString() ); if ( null != ob ) { try { final Features fe = ( Features )ob; if ( p.sift.equals( fe.p ) && null != fe.p ) { return fe.features; } } catch ( final Exception e ) { e.printStackTrace(); } } return null; } final static protected Collection< Feature > fetchFeatures( final Param p, final AbstractAffineTile2D< ? > t ) { Collection< Feature > features = deserializeFeatures( p, t ); if ( features == null ) { final FloatArray2DSIFT sift = new FloatArray2DSIFT( p.sift ); final SIFT ijSIFT = new SIFT( sift ); features = new ArrayList< Feature >(); final long s = System.currentTimeMillis(); ijSIFT.extractFeatures( t.createMaskedByteImage(), features ); Utils.log( features.size() + " features extracted in tile \"" + t.getPatch().getTitle() + "\" (took " + ( System.currentTimeMillis() - s ) + " ms)." ); if ( !serializeFeatures( p, t, features ) ) Utils.log( "Saving features failed for tile: " + t.getPatch() ); } return features; } /** * Save a {@link Collection} of {@link PointMatch PointMatches} two-sided. * Creates two serialization files which is desperately required to clean * up properly invalid serializations on change of a {@link Patch}. * * @param p * @param t1 * @param t2 * @param m * @return */ final static protected boolean serializePointMatches( final Param p, final AbstractAffineTile2D< ? > t1, final AbstractAffineTile2D< ? > t2, final Collection< PointMatch > m ) { final ArrayList< PointMatch > list = new ArrayList< PointMatch >(); list.addAll( m ); final ArrayList< PointMatch > tsil = new ArrayList< PointMatch >(); PointMatch.flip( m, tsil ); final Patch p1 = t1.getPatch(); final Patch p2 = t2.getPatch(); final Loader loader = p1.getProject().getLoader(); return loader.serialize( new PointMatches( p, list ), new StringBuilder( loader.getUNUIdFolder() ).append( "pointmatches.ser/" ).append( FSLoader.createIdPath( Long.toString( p1.getId() ) + "_" + Long.toString( p2.getId() ), "pointmatches", ".ser" ) ).toString() ) && loader.serialize( new PointMatches( p, tsil ), new StringBuilder( loader.getUNUIdFolder() ).append( "pointmatches.ser/" ).append( FSLoader.createIdPath( Long.toString( p2.getId() ) + "_" + Long.toString( p1.getId() ), "pointmatches", ".ser" ) ).toString() ); } final static protected Collection< PointMatch > deserializePointMatches( final Param p, final AbstractAffineTile2D< ? > t1, final AbstractAffineTile2D< ? > t2 ) { final Patch p1 = t1.getPatch(); final Patch p2 = t2.getPatch(); final Loader loader = p1.getProject().getLoader(); final Object ob = loader.deserialize( new StringBuilder( loader.getUNUIdFolder() ).append( "pointmatches.ser/" ) .append( FSLoader.createIdPath( Long.toString( p1.getId() ) + "_" + Long.toString( p2.getId() ), "pointmatches", ".ser" ) ).toString() ); if ( null != ob ) { try { final PointMatches pm = ( PointMatches )ob; if ( p.equals( pm.p ) && null != pm.p ) { return pm.pointMatches; } } catch ( final Exception e ) { e.printStackTrace(); } } return null; } /** * Fetch a {@link Collection} of corresponding * {@link Feature SIFT-features}. Both {@link Feature SIFT-features} and * {@linkplain PointMatch corresponding points} are cached to disk. * * @param p * @param t1 * @param t2 * @return * <dl> * <dt>null</dt><dd>if matching failed for some reasons</dd> * <dt>empty {@link Collection}</dt><dd>if there was no consistent set * of {@link PointMatch matches}</dd> * <dt>{@link Collection} of {@link PointMatch PointMatches}</dt> * <dd>if there was a consistent set of {@link PointMatch * PointMatches}</dd> * </dl> */ final static protected Collection< PointMatch > fetchPointMatches( final Param p, final AbstractAffineTile2D< ? > t1, final AbstractAffineTile2D< ? > t2 ) { final Collection< PointMatch > pointMatches = deserializePointMatches( p, t1, t2 ); if ( pointMatches == null ) { final List< PointMatch > candidates = new ArrayList< PointMatch >(); final List< PointMatch > inliers = new ArrayList< PointMatch >(); final long s = System.currentTimeMillis(); FeatureTransform.matchFeatures( fetchFeatures( p, t1 ), fetchFeatures( p, t2 ), candidates, p.rod ); final AbstractAffineModel2D< ? > model; switch ( p.expectedModelIndex ) { case 0: model = new TranslationModel2D(); break; case 1: model = new RigidModel2D(); break; case 2: model = new SimilarityModel2D(); break; case 3: model = new AffineModel2D(); break; default: return null; } final boolean modelFound = findModel( model, candidates, inliers, p.maxEpsilon, p.minInlierRatio, p.minNumInliers, p.rejectIdentity, p.identityTolerance ); if ( modelFound ) Utils.log( "Model found for tiles \"" + t1.getPatch() + "\" and \"" + t2.getPatch() + "\":\n correspondences " + inliers.size() + " of " + candidates.size() + "\n average residual error " + model.getCost() + " px\n took " + ( System.currentTimeMillis() - s ) + " ms" ); else Utils.log( "No model found for tiles \"" + t1.getPatch() + "\" and \"" + t2.getPatch() + "\":\n correspondence candidates " + candidates.size() + "\n took " + ( System.currentTimeMillis() - s ) + " ms" ); if ( !serializePointMatches( p, t1, t2, pointMatches ) ) Utils.log( "Saving point matches failed for tile \"" + t1.getPatch() + "\" and tile \"" + t2.getPatch() + "\"" ); } return pointMatches; } /** * Align a set of {@link AbstractAffineTile2D tiles} using * the following procedure: * * <ol> * <li>Extract {@link Feature SIFT-features} from all * {@link AbstractAffineTile2D tiles}.</li> * <li>Establish {@link PointMatch point-correspondences} from * consistent sets of {@link Feature feature} matches among tile pairs, * optionally inspect only those that are already overlapping.</li> * <li>Globally align the tile configuration.</li> * </ol> * * Both * {@link SIFT#extractFeatures(ij.process.ImageProcessor, Collection) feature extraction} * and {@link FeatureTransform#matchFeatures(Collection, Collection, List, float) matching} * are executed in multiple {@link Thread Threads}, with the number of * {@link Thread Threads} being a parameter of the method. * * @param p * @param tiles * @param fixedTiles * @param tilesAreInPlace * @param numThreads */ final static public void alignTiles( final ParamOptimize p, final List< AbstractAffineTile2D< ? > > tiles, final List< AbstractAffineTile2D< ? > > fixedTiles, final boolean tilesAreInPlace, final int numThreads ) { final ArrayList< AbstractAffineTile2D< ? >[] > tilePairs = new ArrayList< AbstractAffineTile2D< ? >[] >(); if ( tilesAreInPlace ) AbstractAffineTile2D.pairOverlappingTiles( tiles, tilePairs ); else AbstractAffineTile2D.pairTiles( tiles, tilePairs ); connectTilePairs( p, tiles, tilePairs, numThreads ); optimizeTileConfiguration( p, tiles, fixedTiles ); } /** * Align a set of overlapping {@link AbstractAffineTile2D tiles} using * the following procedure: * * <ol> * <li>Extract {@link Feature SIFT-features} from all * {@link AbstractAffineTile2D tiles}.</li> * <li>Establish {@link PointMatch point-correspondences} from * consistent sets of {@link Feature feature} matches among overlapping * tiles.</li> * <li>Globally align the tile configuration.</li> * </ol> * * Both * {@link SIFT#extractFeatures(ij.process.ImageProcessor, Collection) feature extraction} * and {@link FeatureTransform#matchFeatures(Collection, Collection, List, float) matching} * are executed in multiple {@link Thread Threads}, with the number of * {@link Thread Threads} being a parameter of the method. * * @param p * @param tiles * @param numThreads */ final static public void alignTiles( final ParamOptimize p, final List< AbstractAffineTile2D< ? > > tiles, final List< AbstractAffineTile2D< ? > > fixedTiles, final int numThreads ) { alignTiles( p, tiles, fixedTiles, true, numThreads ); } /** * Align a set of {@link AbstractAffineTile2D tiles} that are * interconnected by {@link PointMatch point-correspondences}. */ final static public void optimizeTileConfiguration( final ParamOptimize p, final List< AbstractAffineTile2D< ? > > tiles, final List< AbstractAffineTile2D< ? > > fixedTiles ) { final TileConfiguration tc = new TileConfiguration(); for ( final AbstractAffineTile2D< ? > t : tiles ) if ( t.getConnectedTiles().size() > 0 ) tc.addTile( t ); // ArrayList< Set< Tile< ? > > > graphs = Tile.identifyConnectedGraphs( tiles ); // for ( Set< Tile< ? > > graph : graphs ) // { // boolean pleaseFix = true; // if ( fixedTiles != null ) // for ( final Tile< ? > t : fixedTiles ) // if ( graph.contains( t ) ) // { // pleaseFix = false; // break; // } // if ( pleaseFix ) // tc.fixTile( graph.iterator().next() ); // } for ( final Tile< ? > t : fixedTiles ) tc.fixTile( t ); try { if ( p.filterOutliers ) tc.optimizeAndFilter( p.maxEpsilon, p.maxIterations, p.maxPlateauwidth, p.meanFactor ); else tc.optimize( p.maxEpsilon, p.maxIterations, p.maxPlateauwidth ); } catch ( final Exception e ) { IJ.error( e.getMessage() + " " + e.getStackTrace() ); } } final static protected void pairwiseAlign( final AbstractAffineTile2D< ? > tile, final Set< AbstractAffineTile2D< ? > > visited ) { visited.add( tile ); for ( final Tile< ? > t : tile.getConnectedTiles() ) { if ( visited.contains( t ) ) continue; pairwiseAlign( ( AbstractAffineTile2D< ? > )t, visited ); // TODO Actually do it ... } } final static public void pairwiseAlignTileConfiguration( final List< AbstractAffineTile2D< ? > > tiles ) { // TODO Implement it } /** * Connect a {@link List} of {@link AbstractAffineTile2D Tiles} by * geometrically consistent {@link Feature SIFT-feature} correspondences. * * @param p * @param tiles * @param tilePairs * @param numThreads * @param multipleHypotheses */ final static public void connectTilePairs( final Param p, final List< AbstractAffineTile2D< ? > > tiles, final List< AbstractAffineTile2D< ? >[] > tilePairs, final int numThreads, final boolean multipleHypotheses ) { final AtomicInteger ai = new AtomicInteger( 0 ); final AtomicInteger ap = new AtomicInteger( 0 ); final int steps = tiles.size() + tilePairs.size(); final List< ExtractFeaturesThread > extractFeaturesThreads = new ArrayList< ExtractFeaturesThread >(); final List< MatchFeaturesAndFindModelThread > matchFeaturesAndFindModelThreads = new ArrayList< MatchFeaturesAndFindModelThread >(); /** Extract and save Features */ for ( int i = 0; i < numThreads; ++i ) { final ExtractFeaturesThread thread = new ExtractFeaturesThread( p.clone(), tiles, ai, ap, steps ); extractFeaturesThreads.add( thread ); thread.start(); } try { for ( final ExtractFeaturesThread thread : extractFeaturesThreads ) thread.join(); } catch ( final InterruptedException e ) { Utils.log( "Feature extraction interrupted." ); for ( final Thread thread : extractFeaturesThreads ) thread.interrupt(); try { for ( final Thread thread : extractFeaturesThreads ) thread.join(); } catch ( final InterruptedException f ) {} Thread.currentThread().interrupt(); IJ.showProgress( 1.0 ); return; } /** Establish correspondences */ ai.set( 0 ); for ( int i = 0; i < numThreads; ++i ) { final MatchFeaturesAndFindModelThread thread = new MatchFeaturesAndFindModelThread( p.clone(), tiles, tilePairs, ai, ap, steps, multipleHypotheses ); matchFeaturesAndFindModelThreads.add( thread ); thread.start(); } try { for ( final MatchFeaturesAndFindModelThread thread : matchFeaturesAndFindModelThreads ) thread.join(); } catch ( final InterruptedException e ) { Utils.log( "Establishing feature correspondences interrupted." ); for ( final Thread thread : matchFeaturesAndFindModelThreads ) thread.interrupt(); try { for ( final Thread thread : matchFeaturesAndFindModelThreads ) thread.join(); } catch ( final InterruptedException f ) {} Thread.currentThread().interrupt(); IJ.showProgress( 1.0 ); } } /** * Connect a {@link List} of {@link AbstractAffineTile2D Tiles} by * geometrically consistent {@link Feature SIFT-feature} correspondences. * * @param p * @param tiles * @param tilePairs * @param numThreads */ final static public void connectTilePairs( final Param p, final List< AbstractAffineTile2D< ? > > tiles, final List< AbstractAffineTile2D< ? >[] > tilePairs, final int numThreads ) { connectTilePairs(p, tiles, tilePairs, numThreads, false); } /** * If a Patch is locked or in fixedPatches, its corresponding Tile is added to fixedTiles. * * @param p * @param patches * @param fixedPatches * @param tiles will contain the generated * {@link AbstractAffineTile2D Tiles} * @param fixedTiles will contain the {@link AbstractAffineTile2D Tiles} * corresponding to the {@link Patch Patches} in fixedPatches */ @SuppressWarnings( { "unchecked", "rawtypes" } ) final static public void tilesFromPatches( final Param p, final List< ? extends Patch > patches, final Collection< ? extends Patch > fixedPatches, final List< AbstractAffineTile2D< ? > > tiles, final Collection< AbstractAffineTile2D< ? > > fixedTiles ) { for ( final Patch patch : patches ) { final AbstractAffineTile2D< ? > t; if ( p.regularize ) { /* can only be affine per convention */ final AbstractAffineModel2D< ? > m = ( AbstractAffineModel2D< ? > )Util.createModel( p.desiredModelIndex ); final AbstractAffineModel2D< ? > r = ( AbstractAffineModel2D< ? > )Util.createModel( p.regularizerModelIndex ); /* for type safety one would test both models as for the simple * case below but here I will go for the easy route and let * Java cast it to what is required and ignore the warning. */ @SuppressWarnings( { } ) final InterpolatedAffineModel2D< ?, ? > interpolatedModel = new InterpolatedAffineModel2D( m, r, p.lambda ); t = new GenericAffineTile2D( interpolatedModel, patch ); } else { switch ( p.desiredModelIndex ) { case 0: t = new TranslationTile2D( patch ); break; case 1: t = new RigidTile2D( patch ); break; case 2: t = new SimilarityTile2D( patch ); break; case 3: t = new AffineTile2D( patch ); break; default: return; } } tiles.add( t ); if ( ( fixedPatches != null && fixedPatches.contains( patch ) ) || patch.isLocked() ) fixedTiles.add( t ); } } /** * Align a selection of {@link Patch patches} in a Layer. * * @param selection * @param numThreads */ final static public void alignSelectedPatches( final Selection selection, final int numThreads ) { final List< Patch > patches = new ArrayList< Patch >(); for ( final Displayable d : selection.getSelected() ) if ( d instanceof Patch ) patches.add( ( Patch )d ); if ( patches.size() < 2 ) return; if ( !paramOptimize.setup( "Align selected patches" ) ) return; final List< AbstractAffineTile2D< ? > > tiles = new ArrayList< AbstractAffineTile2D< ? > >(); final List< AbstractAffineTile2D< ? > > fixedTiles = new ArrayList< AbstractAffineTile2D< ? > >(); final List< Patch > fixedPatches = new ArrayList< Patch >(); final Displayable active = selection.getActive(); if ( active != null && active instanceof Patch ) fixedPatches.add( ( Patch )active ); tilesFromPatches( paramOptimize, patches, fixedPatches, tiles, fixedTiles ); alignTiles( paramOptimize, tiles, fixedTiles, numThreads ); for ( final AbstractAffineTile2D< ? > t : tiles ) t.getPatch().setAffineTransform( t.getModel().createAffine() ); } /** * Align all {@link Patch patches} in a Layer. * * @param layer */ final static public void alignLayer( final Layer layer, final int numThreads ) { if ( !paramOptimize.setup( "Align patches in layer" ) ) return; final List< Displayable > displayables = layer.getDisplayables( Patch.class ); final List< Patch > patches = new ArrayList< Patch >(); for ( final Displayable d : displayables ) patches.add( ( Patch )d ); final List< AbstractAffineTile2D< ? > > tiles = new ArrayList< AbstractAffineTile2D< ? > >(); final List< AbstractAffineTile2D< ? > > fixedTiles = new ArrayList< AbstractAffineTile2D< ? > >(); tilesFromPatches( paramOptimize, patches, null, tiles, fixedTiles ); alignTiles( paramOptimize, tiles, fixedTiles, numThreads ); for ( final AbstractAffineTile2D< ? > t : tiles ) t.getPatch().setAffineTransform( t.getModel().createAffine() ); } /** * Align a range of layers by accumulating pairwise alignments of contiguous layers. * * @param layers The range of layers to align pairwise. * @param numThreads The number of threads to use. */ final static public void alignLayersLinearly( final List< Layer > layers, final int numThreads ) { alignLayersLinearly(layers, numThreads, null); } /** * Align a range of layers by accumulating pairwise alignments of contiguous layers. * * @param layers The range of layers to align pairwise. * @param numThreads The number of threads to use. * @param filter The {@link Filter} to decide which {@link Patch} instances to use in each {@link Layer}. Can be null. */ final static public void alignLayersLinearly( final List< Layer > layers, final int numThreads, final Filter<Patch> filter ) { param.sift.maxOctaveSize = 1600; if ( !param.setup( "Align layers linearly" ) ) return; final Rectangle box = layers.get( 0 ).getParent().getMinimalBoundingBox( Patch.class ); final double scale = Math.min( 1.0, Math.min( ( double )param.sift.maxOctaveSize / box.width, ( double )param.sift.maxOctaveSize / box.height ) ); final Param p = param.clone(); p.maxEpsilon *= scale; final FloatArray2DSIFT sift = new FloatArray2DSIFT( p.sift ); final SIFT ijSIFT = new SIFT( sift ); Rectangle box1 = null; Rectangle box2 = null; final Collection< Feature > features1 = new ArrayList< Feature >(); final Collection< Feature > features2 = new ArrayList< Feature >(); final List< PointMatch > candidates = new ArrayList< PointMatch >(); final List< PointMatch > inliers = new ArrayList< PointMatch >(); final AffineTransform a = new AffineTransform(); int i = 0; for ( final Layer l : layers ) { long s = System.currentTimeMillis(); features1.clear(); features1.addAll( features2 ); features2.clear(); final Rectangle box3 = l.getMinimalBoundingBox( Patch.class ); if ( box3 == null ) continue; box1 = box2; box2 = box3; final List<Patch> patches = l.getAll(Patch.class); if (null != filter) { for (final Iterator<Patch> it = patches.iterator(); it.hasNext(); ) { if (!filter.accept(it.next())) it.remove(); } } ijSIFT.extractFeatures( l.getProject().getLoader().getFlatImage( l, box2, scale, 0xffffffff, ImagePlus.GRAY8, Patch.class, patches, true ).getProcessor(), features2 ); Utils.log( features2.size() + " features extracted in layer \"" + l.getTitle() + "\" (took " + ( System.currentTimeMillis() - s ) + " ms)." ); if ( features1.size() > 0 ) { s = System.currentTimeMillis(); candidates.clear(); FeatureTransform.matchFeatures( features2, features1, candidates, p.rod ); final AbstractAffineModel2D< ? > model; switch ( p.expectedModelIndex ) { case 0: model = new TranslationModel2D(); break; case 1: model = new RigidModel2D(); break; case 2: model = new SimilarityModel2D(); break; case 3: model = new AffineModel2D(); break; default: return; } boolean modelFound; boolean again = false; try { do { again = false; modelFound = model.filterRansac( candidates, inliers, 1000, p.maxEpsilon, p.minInlierRatio, p.minNumInliers, 3 ); if ( modelFound && p.rejectIdentity ) { final ArrayList< Point > points = new ArrayList< Point >(); PointMatch.sourcePoints( inliers, points ); if ( Transforms.isIdentity( model, points, p.identityTolerance ) ) { Utils.log( "Identity transform for " + inliers.size() + " matches rejected." ); candidates.removeAll( inliers ); inliers.clear(); again = true; } } } while ( again ); } catch ( final NotEnoughDataPointsException e ) { modelFound = false; } if ( modelFound ) { Utils.log( "Model found for layer \"" + l.getTitle() + "\" and its predecessor:\n correspondences " + inliers.size() + " of " + candidates.size() + "\n average residual error " + ( model.getCost() / scale ) + " px\n took " + ( System.currentTimeMillis() - s ) + " ms" ); final AffineTransform b = new AffineTransform(); b.translate( box1.x, box1.y ); b.scale( 1.0f / scale, 1.0f / scale ); b.concatenate( model.createAffine() ); b.scale( scale, scale ); b.translate( -box2.x, -box2.y); a.concatenate( b ); l.apply( Displayable.class, a ); Display.repaint( l ); } else { Utils.log( "No model found for layer \"" + l.getTitle() + "\" and its predecessor:\n correspondence candidates " + candidates.size() + "\n took " + ( System.currentTimeMillis() - s ) + " ms" ); a.setToIdentity(); } } IJ.showProgress( ++i, layers.size() ); } } /** * Temporary helper method that creates * @param matches * @param alpha * @return * @throws Exception */ final static public MovingLeastSquaresTransform2 createMLST( final Collection< PointMatch > matches, final double alpha ) throws Exception { final MovingLeastSquaresTransform2 mlst = new MovingLeastSquaresTransform2(); mlst.setAlpha( alpha ); Class< ? extends AbstractAffineModel2D< ? > > c = AffineModel2D.class; switch ( matches.size() ) { case 1: c = TranslationModel2D.class; break; case 2: c = SimilarityModel2D.class; break; default: break; } mlst.setModel( c ); mlst.setMatches( matches ); return mlst; } /** * Align two collections of tiles * @param p * @param a * @param b */ final static public void alignTileCollections( final Param p, final Collection< AbstractAffineTile2D< ? > > a, final Collection< AbstractAffineTile2D< ? > > b ) { final ArrayList< Patch > pa = new ArrayList< Patch >(); final ArrayList< Patch > pb = new ArrayList< Patch >(); for ( final AbstractAffineTile2D< ? > t : a ) pa.add( t.getPatch() ); for ( final AbstractAffineTile2D< ? > t : b ) pb.add( t.getPatch() ); final Layer la = pa.iterator().next().getLayer(); final Layer lb = pb.iterator().next().getLayer(); final Rectangle boxA = Displayable.getBoundingBox( pa, null ); final Rectangle boxB = Displayable.getBoundingBox( pb, null ); final double scale = Math.min( 1.0, Math.min( Math.min( ( double )p.sift.maxOctaveSize / boxA.width, ( double )p.sift.maxOctaveSize / boxA.height ), Math.min( ( double )p.sift.maxOctaveSize / boxB.width, ( double )p.sift.maxOctaveSize / boxB.height ) ) ); final Param pp = p.clone(); pp.maxEpsilon *= scale; final FloatArray2DSIFT sift = new FloatArray2DSIFT( pp.sift ); final SIFT ijSIFT = new SIFT( sift ); final Collection< Feature > featuresA = new ArrayList< Feature >(); final Collection< Feature > featuresB = new ArrayList< Feature >(); final List< PointMatch > candidates = new ArrayList< PointMatch >(); final List< PointMatch > inliers = new ArrayList< PointMatch >(); long s = System.currentTimeMillis(); ijSIFT.extractFeatures( la.getProject().getLoader().getFlatImage( la, boxA, scale, 0xffffffff, ImagePlus.GRAY8, null, pa, true, Color.GRAY ).getProcessor(), featuresA ); Utils.log( featuresA.size() + " features extracted in graph A in layer \"" + la.getTitle() + "\" (took " + ( System.currentTimeMillis() - s ) + " ms)." ); s = System.currentTimeMillis(); ijSIFT.extractFeatures( lb.getProject().getLoader().getFlatImage( lb, boxB, scale, 0xffffffff, ImagePlus.GRAY8, null, pb, true, Color.GRAY ).getProcessor(), featuresB ); Utils.log( featuresB.size() + " features extracted in graph B in layer \"" + lb.getTitle() + "\" (took " + ( System.currentTimeMillis() - s ) + " ms)." ); if ( featuresA.size() > 0 && featuresB.size() > 0 ) { s = System.currentTimeMillis(); FeatureTransform.matchFeatures( featuresA, featuresB, candidates, pp.rod ); final AbstractAffineModel2D< ? > model; switch ( p.expectedModelIndex ) { case 0: model = new TranslationModel2D(); break; case 1: model = new RigidModel2D(); break; case 2: model = new SimilarityModel2D(); break; case 3: model = new AffineModel2D(); break; default: return; } boolean modelFound; boolean again = false; try { do { again = false; modelFound = model.filterRansac( candidates, inliers, 1000, p.maxEpsilon, p.minInlierRatio, p.minNumInliers, 3 ); if ( modelFound && p.rejectIdentity ) { final ArrayList< Point > points = new ArrayList< Point >(); PointMatch.sourcePoints( inliers, points ); if ( Transforms.isIdentity( model, points, p.identityTolerance ) ) { Utils.log( "Identity transform for " + inliers.size() + " matches rejected." ); candidates.removeAll( inliers ); inliers.clear(); again = true; } } } while ( again ); } catch ( final NotEnoughDataPointsException e ) { modelFound = false; } if ( modelFound ) { Utils.log( "Model found for graph A and B in layers \"" + la.getTitle() + "\" and \"" + lb.getTitle() + "\":\n correspondences " + inliers.size() + " of " + candidates.size() + "\n average residual error " + ( model.getCost() / scale ) + " px\n took " + ( System.currentTimeMillis() - s ) + " ms" ); final AffineTransform at = new AffineTransform(); at.translate( boxA.x, boxA.y ); at.scale( 1.0f / scale, 1.0f / scale ); at.concatenate( model.createAffine() ); at.scale( scale, scale ); at.translate( -boxB.x, -boxB.y); for ( final Patch t : pa ) t.preTransform( at, false ); Display.repaint( la ); } else Utils.log( "No model found for graph A and B in layers \"" + la.getTitle() + "\" and \"" + lb.getTitle() + "\":\n correspondence candidates " + candidates.size() + "\n took " + ( System.currentTimeMillis() - s ) + " ms" ); } } }