4 package mpicbg
.trakem2
.align
;
7 import java
.awt
.Rectangle
;
8 import java
.awt
.geom
.AffineTransform
;
9 import java
.io
.Serializable
;
10 import java
.util
.ArrayList
;
11 import java
.util
.Collection
;
12 import java
.util
.Iterator
;
13 import java
.util
.List
;
15 import java
.util
.concurrent
.atomic
.AtomicInteger
;
19 import ij
.gui
.GenericDialog
;
21 import ini
.trakem2
.display
.Display
;
22 import ini
.trakem2
.display
.Displayable
;
23 import ini
.trakem2
.display
.Layer
;
24 import ini
.trakem2
.display
.Patch
;
25 import ini
.trakem2
.display
.Selection
;
26 import ini
.trakem2
.persistence
.Loader
;
27 import ini
.trakem2
.persistence
.FSLoader
;
28 import ini
.trakem2
.utils
.Filter
;
29 import ini
.trakem2
.utils
.Utils
;
31 import mpicbg
.ij
.FeatureTransform
;
32 import mpicbg
.ij
.SIFT
;
33 import mpicbg
.imagefeatures
.Feature
;
34 import mpicbg
.imagefeatures
.FloatArray2DSIFT
;
35 import mpicbg
.models
.AbstractAffineModel2D
;
36 import mpicbg
.models
.AffineModel2D
;
37 import mpicbg
.models
.Model
;
38 import mpicbg
.models
.NotEnoughDataPointsException
;
39 import mpicbg
.models
.Point
;
40 import mpicbg
.models
.PointMatch
;
41 import mpicbg
.models
.SimilarityModel2D
;
42 import mpicbg
.models
.Tile
;
43 import mpicbg
.models
.Transforms
;
44 import mpicbg
.trakem2
.transform
.MovingLeastSquaresTransform2
;
45 import mpicbg
.trakem2
.transform
.RigidModel2D
;
46 import mpicbg
.trakem2
.transform
.TranslationModel2D
;
49 * A collection of methods regarding SIFT-based alignment
51 * TODO Bring the methods and tasks into a class for each method and clean up this mess.
53 * @author Stephan Saalfeld <saalfeld@mpi-cbg.de>
57 static public class Param
implements Serializable
59 private static final long serialVersionUID
= -2247163691721712461L;
61 final public FloatArray2DSIFT
.Param sift
= new FloatArray2DSIFT
.Param();
64 * Closest/next closest neighbour distance ratio
66 public float rod
= 0.92f
;
69 * Maximal allowed alignment error in px
71 public float maxEpsilon
= 100.0f
;
74 * Inlier/candidates ratio
76 public float minInlierRatio
= 0.2f
;
79 * Minimal absolute number of inliers
81 public int minNumInliers
= 7;
84 * Implemeted transformation models for choice
86 final static public String
[] modelStrings
= new String
[]{ "Translation", "Rigid", "Similarity", "Affine" };
87 public int expectedModelIndex
= 1;
88 public int desiredModelIndex
= 1;
90 public float correspondenceWeight
= 1;
93 * Ignore identity transform up to a given tolerance
95 public boolean rejectIdentity
= false;
96 public float identityTolerance
= 0.5f
;
100 sift
.maxOctaveSize
= 600;
104 public void addSIFTFields( final GenericDialog gd
)
106 SIFT
.addFields( gd
, sift
);
107 gd
.addNumericField( "closest/next_closest_ratio :", rod
, 2 );
110 public void addFields( final GenericDialog gd
)
114 gd
.addMessage( "Geometric Consensus Filter:" );
115 gd
.addNumericField( "maximal_alignment_error :", maxEpsilon
, 2, 6, "px" );
116 gd
.addNumericField( "minimal_inlier_ratio :", minInlierRatio
, 2 );
117 gd
.addNumericField( "minimal_number_of_inliers :", minNumInliers
, 0 );
118 gd
.addChoice( "expected_transformation :", modelStrings
, modelStrings
[ expectedModelIndex
] );
119 gd
.addCheckbox( "ignore constant background", rejectIdentity
);
120 gd
.addNumericField( "tolerance :", identityTolerance
, 2, 6, "px" );
122 gd
.addMessage( "Alignment:" );
123 gd
.addChoice( "desired_transformation :", modelStrings
, modelStrings
[ desiredModelIndex
] );
124 gd
.addNumericField( "correspondence weight :", correspondenceWeight
, 2 );
127 public boolean readFields( final GenericDialog gd
)
129 SIFT
.readFields( gd
, sift
);
131 rod
= ( float )gd
.getNextNumber();
133 maxEpsilon
= ( float )gd
.getNextNumber();
134 minInlierRatio
= ( float )gd
.getNextNumber();
135 minNumInliers
= ( int )gd
.getNextNumber();
136 expectedModelIndex
= gd
.getNextChoiceIndex();
138 rejectIdentity
= gd
.getNextBoolean();
139 identityTolerance
= ( float )gd
.getNextNumber();
141 desiredModelIndex
= gd
.getNextChoiceIndex();
143 correspondenceWeight
= ( float )gd
.getNextNumber();
145 return !gd
.invalidNumber();
148 public boolean setup( final String title
)
150 final GenericDialog gd
= new GenericDialog( title
);
157 if ( gd
.wasCanceled() ) return false;
159 while ( !readFields( gd
) );
166 Param p
= new Param();
168 p
.sift
.initialSigma
= this.sift
.initialSigma
;
169 p
.sift
.steps
= this.sift
.steps
;
170 p
.sift
.minOctaveSize
= this.sift
.minOctaveSize
;
171 p
.sift
.maxOctaveSize
= this.sift
.maxOctaveSize
;
172 p
.sift
.fdSize
= this.sift
.fdSize
;
173 p
.sift
.fdBins
= this.sift
.fdBins
;
176 p
.maxEpsilon
= maxEpsilon
;
177 p
.minInlierRatio
= minInlierRatio
;
178 p
.minNumInliers
= minNumInliers
;
179 p
.expectedModelIndex
= expectedModelIndex
;
180 p
.rejectIdentity
= rejectIdentity
;
181 p
.identityTolerance
= identityTolerance
;
183 p
.desiredModelIndex
= desiredModelIndex
;
185 p
.correspondenceWeight
= correspondenceWeight
;
191 * Check if two parameter sets are equal. So far, this method ignores
192 * the parameter {@link #desiredModelIndex} which defines the
193 * transformation class to be used for {@link Tile} alignment. This
194 * makes sense for the current use in {@link PointMatch} serialization
195 * but might be misleading for other applications.
197 * TODO Think about this.
202 public boolean equals( Param p
)
205 sift
.equals( p
.sift
) &&
207 ( maxEpsilon
== p
.maxEpsilon
) &&
208 ( minInlierRatio
== p
.minInlierRatio
) &&
209 ( minNumInliers
== p
.minNumInliers
) &&
210 ( expectedModelIndex
== p
.expectedModelIndex
) &&
211 ( rejectIdentity
== p
.rejectIdentity
) &&
212 ( identityTolerance
== p
.identityTolerance
);
213 // && ( desiredModelIndex == p.desiredModelIndex );
217 final static public Param param
= new Param();
219 static public class ParamOptimize
extends Param
221 private static final long serialVersionUID
= 970673723211054580L;
224 * Maximal number of iteration allowed for the optimizer.
226 public int maxIterations
= 2000;
229 * Maximal number of iterations allowed to not change the parameter to
232 public int maxPlateauwidth
= 200;
237 public boolean filterOutliers
= false;
238 public float meanFactor
= 3.0f
;
241 public void addFields( final GenericDialog gd
)
243 super.addFields( gd
);
245 gd
.addNumericField( "maximal_iterations :", maxIterations
, 0 );
246 gd
.addNumericField( "maximal_plateauwidth :", maxPlateauwidth
, 0 );
247 gd
.addCheckbox( "filter outliers", filterOutliers
);
248 gd
.addNumericField( "mean_factor :", meanFactor
, 2 );
252 public boolean readFields( final GenericDialog gd
)
254 super.readFields( gd
);
256 maxIterations
= ( int )gd
.getNextNumber();
257 maxPlateauwidth
= ( int )gd
.getNextNumber();
258 filterOutliers
= gd
.getNextBoolean();
259 meanFactor
= ( float )gd
.getNextNumber();
261 return !gd
.invalidNumber();
265 final public boolean setup( final String title
)
267 final GenericDialog gd
= new GenericDialog( title
);
274 if ( gd
.wasCanceled() ) return false;
276 while ( !readFields( gd
) );
282 final public ParamOptimize
clone()
284 ParamOptimize p
= new ParamOptimize();
286 p
.sift
.initialSigma
= this.sift
.initialSigma
;
287 p
.sift
.steps
= this.sift
.steps
;
288 p
.sift
.minOctaveSize
= this.sift
.minOctaveSize
;
289 p
.sift
.maxOctaveSize
= this.sift
.maxOctaveSize
;
290 p
.sift
.fdSize
= this.sift
.fdSize
;
291 p
.sift
.fdBins
= this.sift
.fdBins
;
294 p
.maxEpsilon
= maxEpsilon
;
295 p
.minInlierRatio
= minInlierRatio
;
296 p
.minNumInliers
= minNumInliers
;
297 p
.expectedModelIndex
= expectedModelIndex
;
298 p
.rejectIdentity
= rejectIdentity
;
299 p
.identityTolerance
= identityTolerance
;
301 p
.desiredModelIndex
= desiredModelIndex
;
302 p
.maxIterations
= maxIterations
;
303 p
.maxPlateauwidth
= maxPlateauwidth
;
304 p
.filterOutliers
= filterOutliers
;
305 p
.meanFactor
= meanFactor
;
310 public boolean equals( ParamOptimize p
)
314 ( maxIterations
== p
.maxIterations
) &&
315 ( maxPlateauwidth
== p
.maxPlateauwidth
) &&
316 ( filterOutliers
== p
.filterOutliers
) &&
317 ( meanFactor
== p
.meanFactor
);
321 final static public ParamOptimize paramOptimize
= new ParamOptimize();
323 final static private class Features
implements Serializable
325 private static final long serialVersionUID
= 2689219384710526198L;
327 FloatArray2DSIFT
.Param p
;
328 ArrayList
< Feature
> features
;
329 Features( final FloatArray2DSIFT
.Param p
, final ArrayList
< Feature
> features
)
332 this.features
= features
;
336 final static private class PointMatches
implements Serializable
338 private static final long serialVersionUID
= -2564147268101223484L;
341 ArrayList
< PointMatch
> pointMatches
;
342 PointMatches( final Param p
, final ArrayList
< PointMatch
> pointMatches
)
345 this.pointMatches
= pointMatches
;
350 * Extracts {@link Feature SIFT-features} from a {@link List} of
351 * {@link AbstractAffineTile2D Tiles} and saves them to disk.
353 final static protected class ExtractFeaturesThread
extends Thread
355 final protected Param p
;
356 final protected List
< AbstractAffineTile2D
< ?
> > tiles
;
357 // final protected HashMap< AbstractAffineTile2D< ? >, Collection< Feature > > tileFeatures;
358 final protected AtomicInteger ai
;
359 final protected AtomicInteger ap
;
360 final protected int steps
;
362 public ExtractFeaturesThread(
364 final List
< AbstractAffineTile2D
< ?
> > tiles
,
365 // final HashMap< AbstractAffineTile2D< ? >, Collection< Feature > > tileFeatures,
366 final AtomicInteger ai
,
367 final AtomicInteger ap
,
378 final public void run()
380 FloatArray2DSIFT sift
= new FloatArray2DSIFT( p
.sift
);
381 SIFT ijSIFT
= new SIFT( sift
);
383 for ( int i
= ai
.getAndIncrement(); i
< tiles
.size() && !isInterrupted(); i
= ai
.getAndIncrement() )
385 if (isInterrupted()) return;
386 AbstractAffineTile2D
< ?
> tile
= tiles
.get( i
);
387 Collection
< Feature
> features
= deserializeFeatures( p
, tile
);
388 if ( features
== null )
390 /* extract features and, in case there is not enough memory available, try to free it and do again */
391 boolean memoryFlushed
;
396 features
= new ArrayList
< Feature
>();
397 long s
= System
.currentTimeMillis();
398 ijSIFT
.extractFeatures( tile
.createMaskedByteImage(), features
);
399 IJ
.log( features
.size() + " features extracted in tile " + i
+ " \"" + tile
.getPatch().getTitle() + "\" (took " + ( System
.currentTimeMillis() - s
) + " ms)." );
400 if ( !serializeFeatures( p
, tile
, features
) )
401 IJ
.log( "Saving features failed for tile \"" + tile
.getPatch() + "\"" );
402 memoryFlushed
= false;
404 catch ( OutOfMemoryError e
)
406 Utils
.log2( "Flushing memory for feature extraction" );
407 Loader
.releaseAllCaches();
408 memoryFlushed
= true;
411 while ( memoryFlushed
);
415 IJ
.log( features
.size() + " features loaded for tile " + i
+ " \"" + tile
.getPatch().getTitle() + "\"." );
417 IJ
.showProgress( ap
.getAndIncrement(), steps
);
423 final static protected class MatchFeaturesAndFindModelThread
extends Thread
425 final protected Param p
;
426 final protected List
< AbstractAffineTile2D
< ?
> > tiles
;
427 //final protected HashMap< AbstractAffineTile2D< ? >, Collection< Feature > > tileFeatures;
428 final protected List
< AbstractAffineTile2D
< ?
>[] > tilePairs
;
429 final protected AtomicInteger ai
;
430 final protected AtomicInteger ap
;
431 final protected int steps
;
433 public MatchFeaturesAndFindModelThread(
435 final List
< AbstractAffineTile2D
< ?
> > tiles
,
436 final List
< AbstractAffineTile2D
< ?
>[] > tilePairs
,
437 final AtomicInteger ai
,
438 final AtomicInteger ap
,
443 this.tilePairs
= tilePairs
;
450 final public void run()
452 final List
< PointMatch
> candidates
= new ArrayList
< PointMatch
>();
454 for ( int i
= ai
.getAndIncrement(); i
< tilePairs
.size() && !isInterrupted(); i
= ai
.getAndIncrement() )
456 if (isInterrupted()) return;
458 final AbstractAffineTile2D
< ?
>[] tilePair
= tilePairs
.get( i
);
460 Collection
< PointMatch
> inliers
= deserializePointMatches( p
, tilePair
[ 0 ], tilePair
[ 1 ] );
462 if ( inliers
== null )
464 inliers
= new ArrayList
< PointMatch
>();
466 long s
= System
.currentTimeMillis();
468 FeatureTransform
.matchFeatures(
469 fetchFeatures( p
, tilePair
[ 0 ] ),
470 fetchFeatures( p
, tilePair
[ 1 ] ),
475 final AbstractAffineModel2D
< ?
> model
;
476 switch ( p
.expectedModelIndex
)
479 model
= new TranslationModel2D();
482 model
= new RigidModel2D();
485 model
= new SimilarityModel2D();
488 model
= new AffineModel2D();
494 boolean modelFound
= findModel(
502 p
.identityTolerance
);
505 IJ
.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" );
507 IJ
.log( "No model found for tiles \"" + tilePair
[ 0 ].getPatch() + "\" and \"" + tilePair
[ 1 ].getPatch() + "\":\n correspondence candidates " + candidates
.size() + "\n took " + ( System
.currentTimeMillis() - s
) + " ms" );
509 if ( !serializePointMatches( p
, tilePair
[ 0 ], tilePair
[ 1 ], inliers
) )
510 IJ
.log( "Saving point matches failed for tiles \"" + tilePair
[ 0 ].getPatch() + "\" and \"" + tilePair
[ 1 ].getPatch() + "\"" );
514 IJ
.log( "Point matches for tiles \"" + tilePair
[ 0 ].getPatch().getTitle() + "\" and \"" + tilePair
[ 1 ].getPatch().getTitle() + "\" fetched from disk cache" );
516 if ( inliers
!= null && inliers
.size() > 0 )
518 /* weight the inliers */
519 for ( final PointMatch pm
: inliers
)
520 pm
.setWeights( new float[]{ p
.correspondenceWeight
} );
522 synchronized ( tilePair
[ 0 ] )
524 synchronized ( tilePair
[ 1 ] ) { tilePair
[ 0 ].connect( tilePair
[ 1 ], inliers
); }
525 tilePair
[ 0 ].clearVirtualMatches();
527 synchronized ( tilePair
[ 1 ] ) { tilePair
[ 1 ].clearVirtualMatches(); }
530 IJ
.showProgress( ap
.getAndIncrement(), steps
);
536 final static public boolean findModel(
537 final Model
< ?
> model
,
538 final List
< PointMatch
> candidates
,
539 final Collection
< PointMatch
> inliers
,
540 final float maxEpsilon
,
541 final float minInlierRatio
,
542 final int minNumInliers
,
543 final boolean rejectIdentity
,
544 final float identityTolerance
)
547 boolean again
= false;
553 modelFound
= model
.filterRansac(
561 if ( modelFound
&& rejectIdentity
)
563 final ArrayList
< Point
> points
= new ArrayList
< Point
>();
564 PointMatch
.sourcePoints( inliers
, points
);
565 if ( Transforms
.isIdentity( model
, points
, identityTolerance
) )
567 Utils
.log( "Identity transform for " + inliers
.size() + " matches rejected." );
568 candidates
.removeAll( inliers
);
576 catch ( NotEnoughDataPointsException e
)
585 final static protected boolean serializeFeatures( final Param p
, AbstractAffineTile2D
< ?
> t
, final Collection
< Feature
> f
)
587 final ArrayList
< Feature
> list
= new ArrayList
< Feature
>();
589 final Patch patch
= t
.getPatch();
590 final Loader loader
= patch
.getProject().getLoader();
591 final Features fe
= new Features( p
.sift
, list
);
592 return loader
.serialize( fe
, new StringBuilder( loader
.getUNUIdFolder() ).append( "features.ser/" )
593 .append( FSLoader
.createIdPath( Long
.toString( patch
.getId() ), "features", ".ser" ) ).toString() );
597 * Retrieve the features only if saved with the exact same relevant SIFT parameters.
599 final static protected Collection
< Feature
> deserializeFeatures( final Param p
, final AbstractAffineTile2D
< ?
> t
)
601 final Patch patch
= t
.getPatch();
602 final Loader loader
= patch
.getProject().getLoader();
604 final Object ob
= loader
.deserialize( new StringBuilder( loader
.getUNUIdFolder() ).append( "features.ser/" )
605 .append( FSLoader
.createIdPath( Long
.toString( patch
.getId() ), "features", ".ser" ) ).toString() );
610 final Features fe
= ( Features
)ob
;
611 if ( p
.sift
.equals( fe
.p
) && null != fe
.p
)
616 catch ( Exception e
)
625 final static protected Collection
< Feature
> fetchFeatures(
627 final AbstractAffineTile2D
< ?
> t
)
629 Collection
< Feature
> features
= deserializeFeatures( p
, t
);
630 if ( features
== null )
632 final FloatArray2DSIFT sift
= new FloatArray2DSIFT( p
.sift
);
633 final SIFT ijSIFT
= new SIFT( sift
);
634 features
= new ArrayList
< Feature
>();
635 long s
= System
.currentTimeMillis();
636 ijSIFT
.extractFeatures( t
.createMaskedByteImage(), features
);
637 IJ
.log( features
.size() + " features extracted in tile \"" + t
.getPatch().getTitle() + "\" (took " + ( System
.currentTimeMillis() - s
) + " ms)." );
638 if ( !serializeFeatures( p
, t
, features
) )
639 IJ
.log( "Saving features failed for tile: " + t
.getPatch() );
646 * Save a {@link Collection} of {@link PointMatch PointMatches} two-sided.
647 * Creates two serialization files which is desperately required to clean
648 * up properly invalid serializations on change of a {@link Patch}.
656 final static protected boolean serializePointMatches(
658 final AbstractAffineTile2D
< ?
> t1
,
659 final AbstractAffineTile2D
< ?
> t2
,
660 final Collection
< PointMatch
> m
)
662 final ArrayList
< PointMatch
> list
= new ArrayList
< PointMatch
>();
664 final ArrayList
< PointMatch
> tsil
= new ArrayList
< PointMatch
>();
665 PointMatch
.flip( m
, tsil
);
666 final Patch p1
= t1
.getPatch();
667 final Patch p2
= t2
.getPatch();
668 final Loader loader
= p1
.getProject().getLoader();
671 new PointMatches( p
, list
),
672 new StringBuilder( loader
.getUNUIdFolder() ).append( "pointmatches.ser/" ).append( FSLoader
.createIdPath( Long
.toString( p1
.getId() ) + "_" + Long
.toString( p2
.getId() ), "pointmatches", ".ser" ) ).toString() ) &&
674 new PointMatches( p
, tsil
),
675 new StringBuilder( loader
.getUNUIdFolder() ).append( "pointmatches.ser/" ).append( FSLoader
.createIdPath( Long
.toString( p2
.getId() ) + "_" + Long
.toString( p1
.getId() ), "pointmatches", ".ser" ) ).toString() );
679 final static protected Collection
< PointMatch
> deserializePointMatches(
681 final AbstractAffineTile2D
< ?
> t1
,
682 final AbstractAffineTile2D
< ?
> t2
)
684 final Patch p1
= t1
.getPatch();
685 final Patch p2
= t2
.getPatch();
686 final Loader loader
= p1
.getProject().getLoader();
688 final Object ob
= loader
.deserialize( new StringBuilder( loader
.getUNUIdFolder() ).append( "pointmatches.ser/" )
689 .append( FSLoader
.createIdPath( Long
.toString( p1
.getId() ) + "_" + Long
.toString( p2
.getId() ), "pointmatches", ".ser" ) ).toString() );
695 final PointMatches pm
= ( PointMatches
)ob
;
696 if ( p
.equals( pm
.p
) && null != pm
.p
)
698 return pm
.pointMatches
;
701 catch ( Exception e
)
711 * Fetch a {@link Collection} of corresponding
712 * {@link Feature SIFT-features}. Both {@link Feature SIFT-features} and
713 * {@linkplain PointMatch corresponding points} are cached to disk.
720 * <dt>null</dt><dd>if matching failed for some reasons</dd>
721 * <dt>empty {@link Collection}</dt><dd>if there was no consistent set
722 * of {@link PointMatch matches}</dd>
723 * <dt>{@link Collection} of {@link PointMatch PointMatches}</dt>
724 * <dd>if there was a consistent set of {@link PointMatch
728 final static protected Collection
< PointMatch
> fetchPointMatches(
730 final AbstractAffineTile2D
< ?
> t1
,
731 final AbstractAffineTile2D
< ?
> t2
)
733 Collection
< PointMatch
> pointMatches
= deserializePointMatches( p
, t1
, t2
);
734 if ( pointMatches
== null )
736 final List
< PointMatch
> candidates
= new ArrayList
< PointMatch
>();
737 final List
< PointMatch
> inliers
= new ArrayList
< PointMatch
>();
739 long s
= System
.currentTimeMillis();
740 FeatureTransform
.matchFeatures(
741 fetchFeatures( p
, t1
),
742 fetchFeatures( p
, t2
),
746 final AbstractAffineModel2D
< ?
> model
;
747 switch ( p
.expectedModelIndex
)
750 model
= new TranslationModel2D();
753 model
= new RigidModel2D();
756 model
= new SimilarityModel2D();
759 model
= new AffineModel2D();
765 boolean modelFound
= findModel(
773 p
.identityTolerance
);
776 IJ
.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" );
778 IJ
.log( "No model found for tiles \"" + t1
.getPatch() + "\" and \"" + t2
.getPatch() + "\":\n correspondence candidates " + candidates
.size() + "\n took " + ( System
.currentTimeMillis() - s
) + " ms" );
780 if ( !serializePointMatches( p
, t1
, t2
, pointMatches
) )
781 IJ
.log( "Saving point matches failed for tile \"" + t1
.getPatch() + "\" and tile \"" + t2
.getPatch() + "\"" );
788 * Align a set of {@link AbstractAffineTile2D tiles} using
789 * the following procedure:
792 * <li>Extract {@link Feature SIFT-features} from all
793 * {@link AbstractAffineTile2D tiles}.</li>
794 * <li>Establish {@link PointMatch point-correspondences} from
795 * consistent sets of {@link Feature feature} matches among tile pairs,
796 * optionally inspect only those that are already overlapping.</li>
797 * <li>Globally align the tile configuration.</li>
801 * {@link SIFT#extractFeatures(ij.process.ImageProcessor, Collection) feature extraction}
802 * and {@link FeatureTransform#matchFeatures(Collection, Collection, List, float) matching}
803 * are executed in multiple {@link Thread Threads}, with the number of
804 * {@link Thread Threads} being a parameter of the method.
809 * @param tilesAreInPlace
812 final static public void alignTiles(
813 final ParamOptimize p
,
814 final List
< AbstractAffineTile2D
< ?
> > tiles
,
815 final List
< AbstractAffineTile2D
< ?
> > fixedTiles
,
816 final boolean tilesAreInPlace
,
817 final int numThreads
)
819 final ArrayList
< AbstractAffineTile2D
< ?
>[] > tilePairs
= new ArrayList
< AbstractAffineTile2D
< ?
>[] >();
820 if ( tilesAreInPlace
)
821 AbstractAffineTile2D
.pairOverlappingTiles( tiles
, tilePairs
);
823 AbstractAffineTile2D
.pairTiles( tiles
, tilePairs
);
824 connectTilePairs( p
, tiles
, tilePairs
, numThreads
);
825 optimizeTileConfiguration( p
, tiles
, fixedTiles
);
830 * Align a set of overlapping {@link AbstractAffineTile2D tiles} using
831 * the following procedure:
834 * <li>Extract {@link Feature SIFT-features} from all
835 * {@link AbstractAffineTile2D tiles}.</li>
836 * <li>Establish {@link PointMatch point-correspondences} from
837 * consistent sets of {@link Feature feature} matches among overlapping
839 * <li>Globally align the tile configuration.</li>
843 * {@link SIFT#extractFeatures(ij.process.ImageProcessor, Collection) feature extraction}
844 * and {@link FeatureTransform#matchFeatures(Collection, Collection, List, float) matching}
845 * are executed in multiple {@link Thread Threads}, with the number of
846 * {@link Thread Threads} being a parameter of the method.
852 final static public void alignTiles(
853 final ParamOptimize p
,
854 final List
< AbstractAffineTile2D
< ?
> > tiles
,
855 final List
< AbstractAffineTile2D
< ?
> > fixedTiles
,
856 final int numThreads
)
858 alignTiles( p
, tiles
, fixedTiles
, true, numThreads
);
864 * Align a set of {@link AbstractAffineTile2D tiles} that are
865 * interconnected by {@link PointMatch point-correspondences}.
867 final static public void optimizeTileConfiguration(
868 final ParamOptimize p
,
869 final List
< AbstractAffineTile2D
< ?
> > tiles
,
870 final List
< AbstractAffineTile2D
< ?
> > fixedTiles
)
872 final TileConfiguration tc
= new TileConfiguration();
873 tc
.addTiles( tiles
);
875 ArrayList
< Set
< Tile
< ?
> > > graphs
= Tile
.identifyConnectedGraphs( tiles
);
876 for ( Set
< Tile
< ?
> > graph
: graphs
)
878 boolean pleaseFix
= true;
879 if ( fixedTiles
!= null )
880 for ( final Tile
< ?
> t
: fixedTiles
)
881 if ( graph
.contains( t
) )
887 tc
.fixTile( graph
.iterator().next() );
889 for ( final Tile
< ?
> t
: fixedTiles
)
894 if ( p
.filterOutliers
)
895 tc
.optimizeAndFilter( p
.maxEpsilon
, p
.maxIterations
, p
.maxPlateauwidth
, p
.meanFactor
);
897 tc
.optimize( p
.maxEpsilon
, p
.maxIterations
, p
.maxPlateauwidth
);
899 catch ( Exception e
) { IJ
.error( e
.getMessage() + " " + e
.getStackTrace() ); }
903 final static protected void pairwiseAlign(
904 AbstractAffineTile2D
< ?
> tile
,
905 Set
< AbstractAffineTile2D
< ?
> > visited
)
908 for ( Tile
< ?
> t
: tile
.getConnectedTiles() )
910 if ( visited
.contains( t
) ) continue;
911 pairwiseAlign( ( AbstractAffineTile2D
< ?
> )t
, visited
);
912 // TODO Actually do it ...
917 final static public void pairwiseAlignTileConfiguration(
918 final List
< AbstractAffineTile2D
< ?
> > tiles
)
925 * Connect a {@link List} of {@link AbstractAffineTile2D Tiles} by
926 * geometrically consistent {@link Feature SIFT-feature} correspondences.
932 final static public void connectTilePairs(
934 final List
< AbstractAffineTile2D
< ?
> > tiles
,
935 final List
< AbstractAffineTile2D
< ?
>[] > tilePairs
,
936 final int numThreads
)
938 final AtomicInteger ai
= new AtomicInteger( 0 );
939 final AtomicInteger ap
= new AtomicInteger( 0 );
940 final int steps
= tiles
.size() + tilePairs
.size();
941 final List
< ExtractFeaturesThread
> extractFeaturesThreads
= new ArrayList
< ExtractFeaturesThread
>();
942 final List
< MatchFeaturesAndFindModelThread
> matchFeaturesAndFindModelThreads
= new ArrayList
< MatchFeaturesAndFindModelThread
>();
944 /** Extract and save Features */
945 for ( int i
= 0; i
< numThreads
; ++i
)
947 final ExtractFeaturesThread thread
= new ExtractFeaturesThread( p
.clone(), tiles
, ai
, ap
, steps
);
948 extractFeaturesThreads
.add( thread
);
954 for ( final ExtractFeaturesThread thread
: extractFeaturesThreads
)
957 catch ( InterruptedException e
)
959 Utils
.log( "Feature extraction interrupted." );
960 for ( final Thread thread
: extractFeaturesThreads
)
964 for ( final Thread thread
: extractFeaturesThreads
)
967 catch ( InterruptedException f
) {}
968 Thread
.currentThread().interrupt();
969 IJ
.showProgress( 1.0 );
973 /** Establish correspondences */
975 for ( int i
= 0; i
< numThreads
; ++i
)
977 MatchFeaturesAndFindModelThread thread
= new MatchFeaturesAndFindModelThread( p
.clone(), tiles
, tilePairs
, ai
, ap
, steps
);
978 matchFeaturesAndFindModelThreads
.add( thread
);
983 for ( final MatchFeaturesAndFindModelThread thread
: matchFeaturesAndFindModelThreads
)
986 catch ( InterruptedException e
)
988 Utils
.log( "Establishing feature correspondences interrupted." );
989 for ( final Thread thread
: matchFeaturesAndFindModelThreads
)
993 for ( final Thread thread
: matchFeaturesAndFindModelThreads
)
996 catch ( InterruptedException f
) {}
997 Thread
.currentThread().interrupt();
998 IJ
.showProgress( 1.0 );
1004 * If a Patch is locked or in fixedPatches, its corresponding Tile is added to fixedTiles.
1008 * @param fixedPatches
1009 * @param tiles will contain the generated
1010 * {@link AbstractAffineTile2D Tiles}
1011 * @param fixedTiles will contain the {@link AbstractAffineTile2D Tiles}
1012 * corresponding to the {@link Patch Patches} in fixedPatches
1014 final static public void tilesFromPatches(
1016 final List
< ?
extends Patch
> patches
,
1017 final Collection
< ?
extends Patch
> fixedPatches
,
1018 final List
< AbstractAffineTile2D
< ?
> > tiles
,
1019 final Collection
< AbstractAffineTile2D
< ?
> > fixedTiles
)
1021 for ( final Patch patch
: patches
)
1023 final AbstractAffineTile2D
< ?
> t
;
1024 switch ( p
.desiredModelIndex
)
1027 t
= new TranslationTile2D( patch
);
1030 t
= new RigidTile2D( patch
);
1033 t
= new SimilarityTile2D( patch
);
1036 t
= new AffineTile2D( patch
);
1042 if ( ( fixedPatches
!= null && fixedPatches
.contains( patch
) ) || patch
.isLocked() )
1043 fixedTiles
.add( t
);
1049 * Align a selection of {@link Patch patches} in a Layer.
1053 final static public void alignSelectedPatches( Selection selection
, final int numThreads
)
1055 final List
< Patch
> patches
= new ArrayList
< Patch
>();
1056 for ( final Displayable d
: selection
.getSelected() )
1057 if ( d
instanceof Patch
) patches
.add( ( Patch
)d
);
1059 if ( patches
.size() < 2 ) return;
1061 if ( !paramOptimize
.setup( "Align selected patches" ) ) return;
1063 List
< AbstractAffineTile2D
< ?
> > tiles
= new ArrayList
< AbstractAffineTile2D
< ?
> >();
1064 List
< AbstractAffineTile2D
< ?
> > fixedTiles
= new ArrayList
< AbstractAffineTile2D
< ?
> >();
1065 List
< Patch
> fixedPatches
= new ArrayList
< Patch
>();
1066 final Displayable active
= selection
.getActive();
1067 if ( active
!= null && active
instanceof Patch
)
1068 fixedPatches
.add( ( Patch
)active
);
1069 tilesFromPatches( paramOptimize
, patches
, fixedPatches
, tiles
, fixedTiles
);
1071 alignTiles( paramOptimize
, tiles
, fixedTiles
, numThreads
);
1073 for ( AbstractAffineTile2D
< ?
> t
: tiles
)
1074 t
.getPatch().setAffineTransform( t
.getModel().createAffine() );
1079 * Align all {@link Patch patches} in a Layer.
1083 final static public void alignLayer( final Layer layer
, final int numThreads
)
1085 if ( !paramOptimize
.setup( "Align patches in layer" ) ) return;
1087 List
< Displayable
> displayables
= layer
.getDisplayables( Patch
.class );
1088 List
< Patch
> patches
= new ArrayList
< Patch
>();
1089 for ( Displayable d
: displayables
)
1090 patches
.add( ( Patch
)d
);
1091 List
< AbstractAffineTile2D
< ?
> > tiles
= new ArrayList
< AbstractAffineTile2D
< ?
> >();
1092 List
< AbstractAffineTile2D
< ?
> > fixedTiles
= new ArrayList
< AbstractAffineTile2D
< ?
> >();
1093 tilesFromPatches( paramOptimize
, patches
, null, tiles
, fixedTiles
);
1095 alignTiles( paramOptimize
, tiles
, fixedTiles
, numThreads
);
1097 for ( AbstractAffineTile2D
< ?
> t
: tiles
)
1098 t
.getPatch().setAffineTransform( t
.getModel().createAffine() );
1102 * Align a range of layers by accumulating pairwise alignments of contiguous layers.
1104 * @param layers The range of layers to align pairwise.
1105 * @param numThreads The number of threads to use.
1107 final static public void alignLayersLinearly( final List
< Layer
> layers
, final int numThreads
)
1109 alignLayersLinearly(layers
, numThreads
, null);
1113 * Align a range of layers by accumulating pairwise alignments of contiguous layers.
1115 * @param layers The range of layers to align pairwise.
1116 * @param numThreads The number of threads to use.
1117 * @param filter The {@link Filter} to decide which {@link Patch} instances to use in each {@param Layer}. Can be null.
1119 final static public void alignLayersLinearly( final List
< Layer
> layers
, final int numThreads
, final Filter
<Patch
> filter
)
1121 param
.sift
.maxOctaveSize
= 1600;
1123 if ( !param
.setup( "Align layers linearly" ) ) return;
1125 final Rectangle box
= layers
.get( 0 ).getParent().getMinimalBoundingBox( Patch
.class );
1126 final float scale
= Math
.min( 1.0f
, Math
.min( ( float )param
.sift
.maxOctaveSize
/ ( float )box
.width
, ( float )param
.sift
.maxOctaveSize
/ ( float )box
.height
) );
1127 final Param p
= param
.clone();
1128 p
.maxEpsilon
*= scale
;
1130 final FloatArray2DSIFT sift
= new FloatArray2DSIFT( p
.sift
);
1131 final SIFT ijSIFT
= new SIFT( sift
);
1133 Rectangle box1
= null;
1134 Rectangle box2
= null;
1135 final Collection
< Feature
> features1
= new ArrayList
< Feature
>();
1136 final Collection
< Feature
> features2
= new ArrayList
< Feature
>();
1137 List
< PointMatch
> candidates
= new ArrayList
< PointMatch
>();
1138 List
< PointMatch
> inliers
= new ArrayList
< PointMatch
>();
1140 final AffineTransform a
= new AffineTransform();
1143 for ( Layer l
: layers
)
1145 long s
= System
.currentTimeMillis();
1148 features1
.addAll( features2
);
1151 final Rectangle box3
= l
.getMinimalBoundingBox( Patch
.class );
1153 if ( box3
== null ) continue;
1158 final List
<Patch
> patches
= l
.getAll(Patch
.class);
1159 if (null != filter
) {
1160 for (final Iterator
<Patch
> it
= patches
.iterator(); it
.hasNext(); ) {
1161 if (!filter
.accept(it
.next())) it
.remove();
1165 ijSIFT
.extractFeatures(
1166 l
.getProject().getLoader().getFlatImage( l
, box2
, scale
, 0xffffffff, ImagePlus
.GRAY8
, Patch
.class, patches
, true ).getProcessor(),
1168 IJ
.log( features2
.size() + " features extracted in layer \"" + l
.getTitle() + "\" (took " + ( System
.currentTimeMillis() - s
) + " ms)." );
1170 if ( features1
.size() > 0 )
1172 s
= System
.currentTimeMillis();
1176 FeatureTransform
.matchFeatures(
1182 final AbstractAffineModel2D
< ?
> model
;
1183 switch ( p
.expectedModelIndex
)
1186 model
= new TranslationModel2D();
1189 model
= new RigidModel2D();
1192 model
= new SimilarityModel2D();
1195 model
= new AffineModel2D();
1202 boolean again
= false;
1208 modelFound
= model
.filterRansac(
1216 if ( modelFound
&& p
.rejectIdentity
)
1218 final ArrayList
< Point
> points
= new ArrayList
< Point
>();
1219 PointMatch
.sourcePoints( inliers
, points
);
1220 if ( Transforms
.isIdentity( model
, points
, p
.identityTolerance
) )
1222 IJ
.log( "Identity transform for " + inliers
.size() + " matches rejected." );
1223 candidates
.removeAll( inliers
);
1231 catch ( NotEnoughDataPointsException e
)
1238 IJ
.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" );
1239 final AffineTransform b
= new AffineTransform();
1240 b
.translate( box1
.x
, box1
.y
);
1241 b
.scale( 1.0f
/ scale
, 1.0f
/ scale
);
1242 b
.concatenate( model
.createAffine() );
1243 b
.scale( scale
, scale
);
1244 b
.translate( -box2
.x
, -box2
.y
);
1247 l
.apply( Displayable
.class, a
);
1248 Display
.repaint( l
);
1252 IJ
.log( "No model found for layer \"" + l
.getTitle() + "\" and its predecessor:\n correspondence candidates " + candidates
.size() + "\n took " + ( System
.currentTimeMillis() - s
) + " ms" );
1256 IJ
.showProgress( ++i
, layers
.size() );
1261 * Temporary helper method that creates
1267 final static public MovingLeastSquaresTransform2
createMLST( final Collection
< PointMatch
> matches
, final float alpha
) throws Exception
1269 final MovingLeastSquaresTransform2 mlst
= new MovingLeastSquaresTransform2();
1270 mlst
.setAlpha( 1.0f
);
1271 Class
< ?
extends AbstractAffineModel2D
< ?
> > c
= AffineModel2D
.class;
1272 switch ( matches
.size() )
1275 c
= TranslationModel2D
.class;
1278 c
= SimilarityModel2D
.class;
1284 mlst
.setMatches( matches
);
1291 * Align two collections of tiles
1296 final static public void alignTileCollections( final Param p
, final Collection
< AbstractAffineTile2D
< ?
> > a
, final Collection
< AbstractAffineTile2D
< ?
> > b
)
1298 final ArrayList
< Patch
> pa
= new ArrayList
< Patch
>();
1299 final ArrayList
< Patch
> pb
= new ArrayList
< Patch
>();
1300 for ( final AbstractAffineTile2D
< ?
> t
: a
)
1301 pa
.add( t
.getPatch() );
1302 for ( final AbstractAffineTile2D
< ?
> t
: b
)
1303 pb
.add( t
.getPatch() );
1305 final Layer la
= pa
.iterator().next().getLayer();
1306 final Layer lb
= pb
.iterator().next().getLayer();
1308 final Rectangle boxA
= Displayable
.getBoundingBox( pa
, null );
1309 final Rectangle boxB
= Displayable
.getBoundingBox( pb
, null );
1311 final float scale
= Math
.min(
1315 ( float )p
.sift
.maxOctaveSize
/ ( float )boxA
.width
,
1316 ( float )p
.sift
.maxOctaveSize
/ ( float )boxA
.height
),
1318 ( float )p
.sift
.maxOctaveSize
/ ( float )boxB
.width
,
1319 ( float )p
.sift
.maxOctaveSize
/ ( float )boxB
.height
) ) );
1321 final Param pp
= p
.clone();
1322 pp
.maxEpsilon
*= scale
;
1324 final FloatArray2DSIFT sift
= new FloatArray2DSIFT( pp
.sift
);
1325 final SIFT ijSIFT
= new SIFT( sift
);
1327 final Collection
< Feature
> featuresA
= new ArrayList
< Feature
>();
1328 final Collection
< Feature
> featuresB
= new ArrayList
< Feature
>();
1329 List
< PointMatch
> candidates
= new ArrayList
< PointMatch
>();
1330 List
< PointMatch
> inliers
= new ArrayList
< PointMatch
>();
1332 long s
= System
.currentTimeMillis();
1333 ijSIFT
.extractFeatures(
1334 la
.getProject().getLoader().getFlatImage( la
, boxA
, scale
, 0xffffffff, ImagePlus
.GRAY8
, null, pa
, true, Color
.GRAY
).getProcessor(), featuresA
);
1335 Utils
.log( featuresA
.size() + " features extracted in graph A in layer \"" + la
.getTitle() + "\" (took " + ( System
.currentTimeMillis() - s
) + " ms)." );
1337 s
= System
.currentTimeMillis();
1338 ijSIFT
.extractFeatures(
1339 lb
.getProject().getLoader().getFlatImage( lb
, boxB
, scale
, 0xffffffff, ImagePlus
.GRAY8
, null, pb
, true, Color
.GRAY
).getProcessor(), featuresB
);
1340 Utils
.log( featuresB
.size() + " features extracted in graph B in layer \"" + lb
.getTitle() + "\" (took " + ( System
.currentTimeMillis() - s
) + " ms)." );
1342 if ( featuresA
.size() > 0 && featuresB
.size() > 0 )
1344 s
= System
.currentTimeMillis();
1345 FeatureTransform
.matchFeatures(
1351 final AbstractAffineModel2D
< ?
> model
;
1352 switch ( p
.expectedModelIndex
)
1355 model
= new TranslationModel2D();
1358 model
= new RigidModel2D();
1361 model
= new SimilarityModel2D();
1364 model
= new AffineModel2D();
1371 boolean again
= false;
1377 modelFound
= model
.filterRansac(
1385 if ( modelFound
&& p
.rejectIdentity
)
1387 final ArrayList
< Point
> points
= new ArrayList
< Point
>();
1388 PointMatch
.sourcePoints( inliers
, points
);
1389 if ( Transforms
.isIdentity( model
, points
, p
.identityTolerance
) )
1391 IJ
.log( "Identity transform for " + inliers
.size() + " matches rejected." );
1392 candidates
.removeAll( inliers
);
1400 catch ( NotEnoughDataPointsException e
)
1407 IJ
.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" );
1408 final AffineTransform at
= new AffineTransform();
1409 at
.translate( boxA
.x
, boxA
.y
);
1410 at
.scale( 1.0f
/ scale
, 1.0f
/ scale
);
1411 at
.concatenate( model
.createAffine() );
1412 at
.scale( scale
, scale
);
1413 at
.translate( -boxB
.x
, -boxB
.y
);
1415 for ( final Patch t
: pa
)
1416 t
.preTransform( at
, false );
1417 Display
.repaint( la
);
1420 IJ
.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" );