/** * */ package lenscorrection; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import ij.IJ; 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.utils.Bureaucrat; import ini.trakem2.utils.IJError; import ini.trakem2.utils.Utils; import ini.trakem2.utils.Worker; import lenscorrection.Distortion_Correction.BasicParam; import lenscorrection.Distortion_Correction.PointMatchCollectionAndAffine; import mpicbg.ij.SIFT; import mpicbg.models.Point; import mpicbg.models.PointMatch; import mpicbg.models.Tile; import mpicbg.trakem2.align.AbstractAffineTile2D; import mpicbg.trakem2.align.Align; import mpicbg.trakem2.align.RegularizedAffineLayerAlignment.Param; import mpicbg.trakem2.transform.CoordinateTransform; /** * Methods collection to be called from the GUI for alignment tasks. * */ final public class DistortionCorrectionTask { static public class CorrectDistortionFromSelectionParam extends BasicParam { public int firstLayerIndex; public int lastLayerIndex; public boolean clearTransform = false; public boolean visualize = false; public boolean tilesAreInPlace = false; /** * Minimal absolute number of inliers */ public int minNumInliers = 20; public boolean multipleHypotheses = true; /** * Ignore identity transform up to a given tolerance */ public boolean rejectIdentity = true; public float identityTolerance = 5.0f; public int desiredModelIndex = 1; public int maxIterationsOptimize = 2000; public int maxPlateauwidthOptimize = 200; public int maxNumThreadsSift = Runtime.getRuntime().availableProcessors(); /** * Regularization for approximate alignment */ public boolean regularize = false; public int regularizerIndex = 0; public double lambdaRegularize = 0.01; public CorrectDistortionFromSelectionParam() { sift.fdSize = 4; expectedModelIndex = 0; desiredModelIndex = 0; minInlierRatio = 0; } public boolean setupSIFT( final String title ) { /* SIFT */ final GenericDialog gdSIFT = new GenericDialog( title + "SIFT parameters" ); SIFT.addFields( gdSIFT, sift ); gdSIFT.addMessage( "Local Descriptor Matching:" ); gdSIFT.addNumericField( "closest/next_closest_ratio :", rod, 2 ); gdSIFT.addMessage( "Miscellaneous:" ); gdSIFT.addNumericField( "feature_extraction_threads :", maxNumThreadsSift, 0 ); gdSIFT.showDialog(); if ( gdSIFT.wasCanceled() ) return false; SIFT.readFields( gdSIFT, sift ); rod = ( float )gdSIFT.getNextNumber(); maxNumThreadsSift = ( int )gdSIFT.getNextNumber(); return true; } public boolean setup( final Selection selection ) { if ( !setupSIFT( "Distortion Correction: " ) ) return false; /* Geometric filters */ final GenericDialog gd = new GenericDialog( "Distortion Correction: Geometric filters" ); 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 :", Param.modelStrings, Param.modelStrings[ expectedModelIndex ] ); gd.addCheckbox( "test_multiple_hypotheses", multipleHypotheses ); gd.addCheckbox( "ignore constant background", rejectIdentity ); gd.addNumericField( "tolerance :", identityTolerance, 2, 6, "px" ); gd.addCheckbox( "tiles are rougly in place", tilesAreInPlace ); gd.showDialog(); if ( gd.wasCanceled() ) return false; maxEpsilon = ( float )gd.getNextNumber(); minInlierRatio = ( float )gd.getNextNumber(); minNumInliers = ( int )gd.getNextNumber(); expectedModelIndex = gd.getNextChoiceIndex(); multipleHypotheses = gd.getNextBoolean(); rejectIdentity = gd.getNextBoolean(); identityTolerance = ( float )gd.getNextNumber(); tilesAreInPlace = gd.getNextBoolean(); final GenericDialog gdOptimize = new GenericDialog( "Distortion Correction: Montage Optimization" ); gdOptimize.addChoice( "desired_transformation :", modelStrings, modelStrings[ desiredModelIndex ] ); gdOptimize.addCheckbox( "regularize_model", regularize ); gdOptimize.addMessage( "Optimization:" ); gdOptimize.addNumericField( "maximal_iterations :", maxIterationsOptimize, 0 ); gdOptimize.addNumericField( "maximal_plateauwidth :", maxPlateauwidthOptimize, 0 ); gdOptimize.showDialog(); if ( gdOptimize.wasCanceled() ) return false; desiredModelIndex = gdOptimize.getNextChoiceIndex(); regularize = gdOptimize.getNextBoolean(); maxIterationsOptimize = ( int )gdOptimize.getNextNumber(); maxPlateauwidthOptimize = ( int )gdOptimize.getNextNumber(); if ( regularize ) { final GenericDialog gdRegularize = new GenericDialog( "Distortion Correction: Montage Regularization" ); gdRegularize.addChoice( "regularizer :", modelStrings, modelStrings[ regularizerIndex ] ); gdRegularize.addNumericField( "lambda :", lambdaRegularize, 2 ); gdRegularize.showDialog(); if ( gdRegularize.wasCanceled() ) return false; regularizerIndex = gdRegularize.getNextChoiceIndex(); lambdaRegularize = gdRegularize.getNextNumber(); } final GenericDialog gdLens = new GenericDialog( "Distortion Correction: Lens Distortion" ); gdLens.addMessage( "Lens Model :" ); gdLens.addNumericField( "power_of_polynomial_kernel :", dimension, 0 ); gdLens.addNumericField( "lambda :", lambda, 6 ); gdLens.addMessage( "Apply Distortion Correction :" ); Utils.addLayerRangeChoices( selection.getLayer(), gdLens ); gdLens.addCheckbox( "clear_present_transforms", clearTransform ); gdLens.addCheckbox( "visualize_distortion_model", visualize ); gdLens.showDialog(); if ( gdLens.wasCanceled() ) return false; dimension = ( int )gdLens.getNextNumber(); lambda = ( double )gdLens.getNextNumber(); firstLayerIndex = gdLens.getNextChoiceIndex(); lastLayerIndex = gdLens.getNextChoiceIndex(); clearTransform = gdLens.getNextBoolean(); visualize = gdLens.getNextBoolean(); return true; } @Override public CorrectDistortionFromSelectionParam clone() { final CorrectDistortionFromSelectionParam p = new CorrectDistortionFromSelectionParam(); p.sift.set( sift ); p.dimension = dimension; p.expectedModelIndex = expectedModelIndex; p.lambda = lambda; p.maxEpsilon = maxEpsilon; p.minInlierRatio = minInlierRatio; p.rod = rod; p.tilesAreInPlace = tilesAreInPlace; p.firstLayerIndex = firstLayerIndex; p.lastLayerIndex = lastLayerIndex; p.clearTransform = clearTransform; p.visualize = visualize; p.desiredModelIndex = desiredModelIndex; p.identityTolerance = identityTolerance; p.lambdaRegularize = lambdaRegularize; p.maxIterationsOptimize = maxIterationsOptimize; p.maxNumThreadsSift = maxNumThreadsSift; p.maxPlateauwidthOptimize = maxPlateauwidthOptimize; p.minNumInliers = minNumInliers; p.multipleHypotheses = multipleHypotheses; p.regularize = regularize; p.regularizerIndex = regularizerIndex; p.rejectIdentity = rejectIdentity; return p; } } /** * Sets a {@link CoordinateTransform} to a list of patches. */ final static protected class SetCoordinateTransformThread extends Thread { final protected List< Patch > patches; final protected CoordinateTransform transform; final protected AtomicInteger ai; public SetCoordinateTransformThread( final List< Patch > patches, final CoordinateTransform transform, final AtomicInteger ai ) { this.patches = patches; this.transform = transform; this.ai = ai; } @Override final public void run() { for ( int i = ai.getAndIncrement(); i < patches.size() && !isInterrupted(); i = ai.getAndIncrement() ) { final Patch patch = patches.get( i ); // Utils.log( "Setting transform \"" + transform + "\" for patch \"" + patch.getTitle() + "\"." ); patch.setCoordinateTransform( transform ); patch.updateMipMaps(); patch.getProject().getLoader().decacheImagePlus( patch.getId() ); IJ.showProgress( i, patches.size() ); } } } final static protected void setCoordinateTransform( final List< Patch > patches, final CoordinateTransform transform, final int numThreads ) { final AtomicInteger ai = new AtomicInteger( 0 ); final List< SetCoordinateTransformThread > threads = new ArrayList< SetCoordinateTransformThread >(); for ( int i = 0; i < numThreads; ++i ) { final SetCoordinateTransformThread thread = new SetCoordinateTransformThread( patches, transform, ai ); threads.add( thread ); thread.start(); } try { for ( final Thread thread : threads ) thread.join(); } catch ( final InterruptedException e ) { Utils.log( "Setting CoordinateTransform failed.\n" + e.getMessage() + "\n" + e.getStackTrace() ); } } /** * Appends a {@link CoordinateTransform} to a list of patches. */ final static protected class AppendCoordinateTransformThread extends Thread { final protected List< Patch > patches; final protected CoordinateTransform transform; final protected AtomicInteger ai; public AppendCoordinateTransformThread( final List< Patch > patches, final CoordinateTransform transform, final AtomicInteger ai ) { this.patches = patches; this.transform = transform; this.ai = ai; } @Override final public void run() { for ( int i = ai.getAndIncrement(); i < patches.size() && !isInterrupted(); i = ai.getAndIncrement() ) { final Patch patch = patches.get( i ); patch.appendCoordinateTransform( transform ); patch.updateMipMaps(); IJ.showProgress( i, patches.size() ); } } } final static protected void appendCoordinateTransform( final List< Patch > patches, final CoordinateTransform transform, final int numThreads ) { final AtomicInteger ai = new AtomicInteger( 0 ); final List< AppendCoordinateTransformThread > threads = new ArrayList< AppendCoordinateTransformThread >(); for ( int i = 0; i < numThreads; ++i ) { final AppendCoordinateTransformThread thread = new AppendCoordinateTransformThread( patches, transform, ai ); threads.add( thread ); thread.start(); } try { for ( final Thread thread : threads ) thread.join(); } catch ( final InterruptedException e ) { Utils.log( "Appending CoordinateTransform failed.\n" + e.getMessage() + "\n" + e.getStackTrace() ); } } final static public CorrectDistortionFromSelectionParam correctDistortionFromSelectionParam = new CorrectDistortionFromSelectionParam(); final static public Bureaucrat correctDistortionFromSelectionTask ( final Selection selection ) { final Worker worker = new Worker("Distortion Correction", false, true) { @Override public void run() { startedWorking(); try { correctDistortionFromSelection( selection ); Display.repaint(selection.getLayer()); } catch (final Throwable e) { IJError.print(e); } finally { finishedWorking(); } } @Override public void cleanup() { if (!selection.isEmpty()) selection.getLayer().getParent().undoOneStep(); } }; return Bureaucrat.createAndStart( worker, selection.getProject() ); } final static public void run( final CorrectDistortionFromSelectionParam p, final List< Patch > patches, final Displayable active, final Layer layer, final Worker worker ) { /* no multiple inheritance, so p cannot be an Align.ParamOptimize, working around legacy by copying data into one ... */ final Align.ParamOptimize ap = new Align.ParamOptimize(); ap.sift.set( p.sift ); ap.desiredModelIndex = p.desiredModelIndex; ap.expectedModelIndex = p.expectedModelIndex; ap.maxEpsilon = p.maxEpsilon; ap.minInlierRatio = p.minInlierRatio; ap.rod = p.rod; ap.identityTolerance = p.identityTolerance; ap.lambda = p.lambdaRegularize; ap.maxIterations = p.maxIterationsOptimize; ap.maxPlateauwidth = p.maxPlateauwidthOptimize; ap.minNumInliers = p.minNumInliers; ap.regularize = p.regularize; ap.regularizerModelIndex = p.regularizerIndex; ap.rejectIdentity = p.rejectIdentity; /** Get all patches that will be affected. */ final List< Patch > allPatches = new ArrayList< Patch >(); for ( final Layer l : layer.getParent().getLayers().subList( p.firstLayerIndex, p.lastLayerIndex + 1 ) ) for ( final Displayable d : l.getDisplayables( Patch.class ) ) allPatches.add( ( Patch )d ); /** Unset the coordinate transforms of all patches if desired. */ if ( p.clearTransform ) { if ( worker != null ) worker.setTaskName( "Clearing present transforms" ); setCoordinateTransform( allPatches, null, Runtime.getRuntime().availableProcessors() ); Display.repaint(); } if ( worker != null ) worker.setTaskName( "Establishing SIFT correspondences" ); final List< AbstractAffineTile2D< ? > > tiles = new ArrayList< AbstractAffineTile2D< ? > >(); final List< AbstractAffineTile2D< ? > > fixedTiles = new ArrayList< AbstractAffineTile2D< ? > > (); final List< Patch > fixedPatches = new ArrayList< Patch >(); if ( active != null && active instanceof Patch ) fixedPatches.add( ( Patch )active ); Align.tilesFromPatches( ap, patches, fixedPatches, tiles, fixedTiles ); final List< AbstractAffineTile2D< ? >[] > tilePairs = new ArrayList< AbstractAffineTile2D< ? >[] >(); if ( p.tilesAreInPlace ) AbstractAffineTile2D.pairOverlappingTiles( tiles, tilePairs ); else AbstractAffineTile2D.pairTiles( tiles, tilePairs ); AbstractAffineTile2D< ? > fixedTile = null; if ( fixedTiles.size() > 0 ) fixedTile = fixedTiles.get(0); else fixedTile = tiles.get(0); Align.connectTilePairs( ap, tiles, tilePairs, p.maxNumThreadsSift, p.multipleHypotheses ); /** Shift all local coordinates into the original image frame */ for ( final AbstractAffineTile2D< ? > tile : tiles ) { final Rectangle box = tile.getPatch().getCoordinateTransformBoundingBox(); for ( final PointMatch m : tile.getMatches() ) { final double[] l = m.getP1().getL(); final double[] w = m.getP1().getW(); l[ 0 ] += box.x; l[ 1 ] += box.y; w[ 0 ] = l[ 0 ]; w[ 1 ] = l[ 1 ]; } } if ( Thread.currentThread().isInterrupted() ) return; final List< Set< Tile< ? > > > graphs = AbstractAffineTile2D.identifyConnectedGraphs( tiles ); if ( graphs.size() > 1 ) Utils.log( "Could not interconnect all images with correspondences. " ); final List< AbstractAffineTile2D< ? > > interestingTiles; /** Find largest graph. */ Set< Tile< ? > > largestGraph = null; for ( final Set< Tile< ? > > graph : graphs ) if ( largestGraph == null || largestGraph.size() < graph.size() ) largestGraph = graph; interestingTiles = new ArrayList< AbstractAffineTile2D< ? > >(); for ( final Tile< ? > t : largestGraph ) interestingTiles.add( ( AbstractAffineTile2D< ? > )t ); if ( Thread.currentThread().isInterrupted() ) return; Utils.log( "Estimating lens model:" ); /* initialize with pure affine */ Align.optimizeTileConfiguration( ap, interestingTiles, fixedTiles ); /* measure the current error */ double e = 0; int n = 0; for ( final AbstractAffineTile2D< ? > t : interestingTiles ) for ( final PointMatch pm : t.getMatches() ) { e += pm.getDistance(); ++n; } e /= n; double dEpsilon_i = 0; double epsilon_i = e; double dEpsilon_0 = 0; NonLinearTransform lensModel = null; Utils.log( "0: epsilon = " + e ); /* Store original point locations */ final HashMap< Point, Point > originalPoints = new HashMap< Point, Point >(); for ( final AbstractAffineTile2D< ? > t : interestingTiles ) for ( final PointMatch pm : t.getMatches() ) originalPoints.put( pm.getP1(), pm.getP1().clone() ); /* ad hoc conditions to terminate iteration: * small improvement ( 1/1000) relative to first iteration * less than 20 iterations * at least 2 iterations */ for ( int i = 1; i < 20 && ( i < 2 || dEpsilon_i <= dEpsilon_0 / 1000 ); ++i ) { if ( Thread.currentThread().isInterrupted() ) return; /* Some data shuffling for the lens correction interface */ final List< PointMatchCollectionAndAffine > matches = new ArrayList< PointMatchCollectionAndAffine >(); for ( final AbstractAffineTile2D< ? >[] tilePair : tilePairs ) { final AffineTransform a = tilePair[ 0 ].createAffine(); a.preConcatenate( tilePair[ 1 ].getModel().createInverseAffine() ); final Collection< PointMatch > commonMatches = new ArrayList< PointMatch >(); tilePair[ 0 ].commonPointMatches( tilePair[ 1 ], commonMatches ); final Collection< PointMatch > originalCommonMatches = new ArrayList< PointMatch >(); for ( final PointMatch pm : commonMatches ) originalCommonMatches.add( new PointMatch( originalPoints.get( pm.getP1() ), originalPoints.get( pm.getP2() ) ) ); matches.add( new PointMatchCollectionAndAffine( a, originalCommonMatches ) ); } if ( worker != null ) worker.setTaskName( "Estimating lens distortion correction" ); lensModel = Distortion_Correction.createInverseDistortionModel( matches, p.dimension, p.lambda, ( int )fixedTile.getWidth(), ( int )fixedTile.getHeight() ); /* update local points */ for ( final AbstractAffineTile2D< ? > t : interestingTiles ) for ( final PointMatch pm : t.getMatches() ) { final Point currentPoint = pm.getP1(); final Point originalPoint = originalPoints.get( currentPoint ); final double[] l = currentPoint.getL(); final double[] lo = originalPoint.getL(); l[ 0 ] = lo[ 0 ]; l[ 1 ] = lo[ 1 ]; lensModel.applyInPlace( l ); } /* re-optimize */ Align.optimizeTileConfiguration( ap, interestingTiles, fixedTiles ); /* measure the current error */ e = 0; n = 0; for ( final AbstractAffineTile2D< ? > t : interestingTiles ) for ( final PointMatch pm : t.getMatches() ) { e += pm.getDistance(); ++n; } e /= n; dEpsilon_i = e - epsilon_i; epsilon_i = e; if ( i == 1 ) dEpsilon_0 = dEpsilon_i; Utils.log( i + ": epsilon = " + e ); Utils.log( i + ": delta epsilon = " + dEpsilon_i ); } if ( lensModel != null ) { if ( p.visualize ) { if ( Thread.currentThread().isInterrupted() ) return; if ( worker != null ) worker.setTaskName( "Visualizing lens distortion correction" ); lensModel.visualizeSmall( p.lambda ); } if ( worker != null ) worker.setTaskName( "Applying lens distortion correction" ); appendCoordinateTransform( allPatches, lensModel, Runtime.getRuntime().availableProcessors() ); Utils.log( "Done." ); } else Utils.log( "No lens model found." ); } final static public void run( final CorrectDistortionFromSelectionParam p, final List< Patch > patches, final Displayable active, final Layer layer ) { run( p, patches, active, layer, null ); } final static public Bureaucrat correctDistortionFromSelection( final Selection selection ) { final List< Patch > patches = new ArrayList< Patch >(); for ( final Displayable d : Display.getFront().getSelection().getSelected() ) if ( d instanceof Patch ) patches.add( ( Patch )d ); if ( patches.size() < 2 ) { Utils.log("No images in the selection."); return null; } final Worker worker = new Worker( "Lens correction" ) { @Override final public void run() { try { startedWorking(); if ( !correctDistortionFromSelectionParam.setup( selection ) ) return; DistortionCorrectionTask.run( correctDistortionFromSelectionParam.clone(), patches, selection.getActive(), selection.getLayer(), null ); Display.repaint(); } catch ( final Exception e ) { IJError.print( e ); } finally { finishedWorking(); } } }; return Bureaucrat.createAndStart( worker, selection.getProject() ); } }