/** * License: GPL * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License 2 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package mpicbg.trakem2.align; import java.awt.Rectangle; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import ij.IJ; import ij.gui.GenericDialog; import ij.process.ByteProcessor; import ij.process.FloatProcessor; import ini.trakem2.Project; import ini.trakem2.display.Display; import ini.trakem2.display.Patch; import ini.trakem2.display.Patch.PatchImage; import ini.trakem2.utils.Utils; import mpicbg.ij.SIFT; import mpicbg.ij.blockmatching.BlockMatching; import mpicbg.imagefeatures.Feature; import mpicbg.imagefeatures.FloatArray2DSIFT; import mpicbg.models.AbstractAffineModel2D; import mpicbg.models.AbstractModel; import mpicbg.models.AffineModel2D; import mpicbg.models.ErrorStatistic; import mpicbg.models.InvertibleCoordinateTransform; import mpicbg.models.NotEnoughDataPointsException; import mpicbg.models.Point; import mpicbg.models.PointMatch; import mpicbg.models.RigidModel2D; import mpicbg.models.SimilarityModel2D; import mpicbg.models.Spring; import mpicbg.models.SpringMesh; import mpicbg.models.TranslationModel2D; import mpicbg.models.Vertex; import mpicbg.trakem2.align.Align.ParamOptimize; import mpicbg.trakem2.transform.ThinPlateSplineTransform; import mpicbg.trakem2.util.Triple; import mpicbg.util.Util; /** * @author Stephan Saalfeld saalfeld@mpi-cbg.de */ public class ElasticMontage { final static public class Param implements Serializable { private static final long serialVersionUID = 8017492269521223930L; public ParamOptimize po = new ParamOptimize(); { po.maxEpsilon = 25.0f; po.minInlierRatio = 0.0f; po.minNumInliers = 12; po.expectedModelIndex = 0; po.desiredModelIndex = 0; po.rejectIdentity = true; po.identityTolerance = 5.0f; } public boolean isAligned = false; public boolean tilesAreInPlace = true; /** * Block matching */ public double bmScale = 0.5f; public float bmMinR = 0.5f; public float bmMaxCurvatureR = 10f; public float bmRodR = 0.9f; public int bmSearchRadius = 25; public int bmBlockRadius = -1; public boolean bmUseLocalSmoothnessFilter = false; public int bmLocalModelIndex = 1; public float bmLocalRegionSigma = bmSearchRadius; public float bmMaxLocalEpsilon = bmSearchRadius / 2; public float bmMaxLocalTrust = 3; /** * Spring mesh */ public double springLengthSpringMesh = 100; public double stiffnessSpringMesh = 0.1f; public double dampSpringMesh = 0.9f; public double maxStretchSpringMesh = 2000.0f; public int maxIterationsSpringMesh = 1000; public int maxPlateauwidthSpringMesh = 200; public boolean useLegacyOptimizer = true; /** * Visualize spring mesh optimization */ public boolean visualize = false; /** * Change this in case you want to limit the number of parallel threads to a specific number. */ public int maxNumThreads = Runtime.getRuntime().availableProcessors(); public boolean setup() { /* Block Matching */ if ( bmBlockRadius < 0 ) { bmBlockRadius = Util.roundPos( springLengthSpringMesh / 2 ); } final GenericDialog gdBlockMatching = new GenericDialog( "Elastic montage: Block Matching and Spring Meshes" ); gdBlockMatching.addMessage( "Block Matching:" ); gdBlockMatching.addNumericField( "patch_scale :", bmScale, 2 ); gdBlockMatching.addNumericField( "search_radius :", bmSearchRadius, 0, 6, "px" ); gdBlockMatching.addNumericField( "block_radius :", bmBlockRadius, 0, 6, "px" ); gdBlockMatching.addMessage( "Correlation Filters:" ); gdBlockMatching.addNumericField( "minimal_PMCC_r :", bmMinR, 2 ); gdBlockMatching.addNumericField( "maximal_curvature_ratio :", bmMaxCurvatureR, 2 ); gdBlockMatching.addNumericField( "maximal_second_best_r/best_r :", bmRodR, 2 ); gdBlockMatching.addMessage( "Local Smoothness Filter:" ); gdBlockMatching.addCheckbox( "use_local_smoothness_filter", bmUseLocalSmoothnessFilter ); gdBlockMatching.addChoice( "approximate_local_transformation :", ParamOptimize.modelStrings, ParamOptimize.modelStrings[ bmLocalModelIndex ] ); gdBlockMatching.addNumericField( "local_region_sigma:", bmLocalRegionSigma, 2, 6, "px" ); gdBlockMatching.addNumericField( "maximal_local_displacement (absolute):", bmMaxLocalEpsilon, 2, 6, "px" ); gdBlockMatching.addNumericField( "maximal_local_displacement (relative):", bmMaxLocalTrust, 2 ); gdBlockMatching.addMessage( "Montage :" ); gdBlockMatching.addCheckbox( "tiles_are_pre-montaged", isAligned ); gdBlockMatching.showDialog(); if ( gdBlockMatching.wasCanceled() ) return false; bmScale = gdBlockMatching.getNextNumber(); bmSearchRadius = ( int )gdBlockMatching.getNextNumber(); bmBlockRadius = ( int )gdBlockMatching.getNextNumber(); bmMinR = ( float )gdBlockMatching.getNextNumber(); bmMaxCurvatureR = ( float )gdBlockMatching.getNextNumber(); bmRodR = ( float )gdBlockMatching.getNextNumber(); bmUseLocalSmoothnessFilter = gdBlockMatching.getNextBoolean(); bmLocalModelIndex = gdBlockMatching.getNextChoiceIndex(); bmLocalRegionSigma = ( float )gdBlockMatching.getNextNumber(); bmMaxLocalEpsilon = ( float )gdBlockMatching.getNextNumber(); bmMaxLocalTrust = ( float )gdBlockMatching.getNextNumber(); isAligned = gdBlockMatching.getNextBoolean(); final GenericDialog gdSpringMesh = new GenericDialog( "Elastic montage: Spring Meshes" ); /* TODO suggest a resolution that matches maxEpsilon */ gdSpringMesh.addNumericField( "spring_length :", springLengthSpringMesh, 2, 6, "px" ); gdSpringMesh.addNumericField( "stiffness :", stiffnessSpringMesh, 2 ); gdSpringMesh.addNumericField( "maximal_stretch :", maxStretchSpringMesh, 2, 6, "px" ); gdSpringMesh.addNumericField( "maximal_iterations :", maxIterationsSpringMesh, 0 ); gdSpringMesh.addNumericField( "maximal_plateauwidth :", maxPlateauwidthSpringMesh, 0 ); gdSpringMesh.addCheckbox( "use_legacy_optimizer :", useLegacyOptimizer ); gdSpringMesh.showDialog(); if ( gdSpringMesh.wasCanceled() ) return false; springLengthSpringMesh = gdSpringMesh.getNextNumber(); stiffnessSpringMesh = gdSpringMesh.getNextNumber(); maxStretchSpringMesh = gdSpringMesh.getNextNumber(); maxIterationsSpringMesh = ( int )gdSpringMesh.getNextNumber(); maxPlateauwidthSpringMesh = ( int )gdSpringMesh.getNextNumber(); useLegacyOptimizer = gdSpringMesh.getNextBoolean(); if ( isAligned ) po.desiredModelIndex = 3; else { if ( !po.setup( "Elastic montage : SIFT based pre-montage" ) ) return false; final GenericDialog gdSIFT = new GenericDialog( "Elastic montage : SIFT based pre-montage: Miscellaneous" ); gdSIFT.addCheckbox( "tiles_are_roughly_in_place", tilesAreInPlace ); gdSIFT.showDialog(); if ( gdSIFT.wasCanceled() ) return false; tilesAreInPlace = gdSIFT.getNextBoolean(); } return true; } @Override public Param clone() { final Param clone = new Param(); clone.po = po.clone(); clone.tilesAreInPlace = tilesAreInPlace; clone.isAligned = isAligned; clone.bmScale = bmScale; clone.bmMinR = bmMinR; clone.bmMaxCurvatureR = bmMaxCurvatureR; clone.bmRodR = bmRodR; clone.bmUseLocalSmoothnessFilter = bmUseLocalSmoothnessFilter; clone.bmLocalModelIndex = bmLocalModelIndex; clone.bmLocalRegionSigma = bmLocalRegionSigma; clone.bmMaxLocalEpsilon = bmMaxLocalEpsilon; clone.bmMaxLocalTrust = bmMaxLocalTrust; clone.springLengthSpringMesh = springLengthSpringMesh; clone.stiffnessSpringMesh = stiffnessSpringMesh; clone.dampSpringMesh = dampSpringMesh; clone.maxStretchSpringMesh = maxStretchSpringMesh; clone.maxIterationsSpringMesh = maxIterationsSpringMesh; clone.maxPlateauwidthSpringMesh = maxPlateauwidthSpringMesh; clone.useLegacyOptimizer = useLegacyOptimizer; clone.visualize = visualize; clone.maxNumThreads = maxNumThreads; return clone; } } final static Param p = new Param(); final static public Param setup() { return p.setup() ? p.clone() : null; } final static private String patchName( final Patch patch ) { return new StringBuffer( "patch `" ) .append( patch.getTitle() ) .append( "'" ) .toString(); } /** * Extract SIFT features and save them into the project folder. * * @param tiles * @param siftParam * @param clearCache * @throws Exception */ final static protected void extractAndSaveFeatures( final List< AbstractAffineTile2D< ? > > tiles, final FloatArray2DSIFT.Param siftParam, final boolean clearCache ) throws Exception { final ExecutorService exec = Executors.newFixedThreadPool( p.maxNumThreads ); /* extract features for all slices and store them to disk */ final AtomicInteger counter = new AtomicInteger( 0 ); final ArrayList< Future< ArrayList< Feature > > > siftTasks = new ArrayList< Future< ArrayList< Feature > > >(); for ( int i = 0; i < tiles.size(); ++i ) { final int tileIndex = i; siftTasks.add( exec.submit( new Callable< ArrayList< Feature > >() { @Override public ArrayList< Feature > call() { final AbstractAffineTile2D< ? > tile = tiles.get( tileIndex ); final String patchName = patchName( tile.getPatch() ); IJ.showProgress( counter.getAndIncrement(), tiles.size() - 1 ); ArrayList< Feature > fs = null; if ( !clearCache ) fs = mpicbg.trakem2.align.Util.deserializeFeatures( tile.getPatch().getProject(), siftParam, null, tile.getPatch().getId() ); if ( null == fs ) { final FloatArray2DSIFT sift = new FloatArray2DSIFT( siftParam ); final SIFT ijSIFT = new SIFT( sift ); fs = new ArrayList< Feature >(); final ByteProcessor ip = tile.createMaskedByteImage(); ijSIFT.extractFeatures( ip, fs ); Utils.log( fs.size() + " features extracted for " + patchName ); if ( !mpicbg.trakem2.align.Util.serializeFeatures( tile.getPatch().getProject(), siftParam, null, tile.getPatch().getId(), fs ) ) Utils.log( "FAILED to store serialized features for " + patchName ); } else Utils.log( fs.size() + " features loaded for " + patchName ); return fs; } } ) ); } /* join */ for ( final Future< ArrayList< Feature > > fu : siftTasks ) fu.get(); siftTasks.clear(); exec.shutdown(); } final static protected FloatProcessor scaleByte( final ByteProcessor bp ) { final FloatProcessor fp = new FloatProcessor( bp.getWidth(), bp.getHeight() ); final byte[] bytes = ( byte[] )bp.getPixels(); final float[] floats = ( float[] )fp.getPixels(); for ( int i = 0; i < bytes.length; ++i ) floats[ i ] = ( bytes[ i ] & 0xff ) / 255.0f; return fp; } final public void exec( final List< Patch > patches, final Set< Patch > fixedPatches ) throws Exception { /* make sure that passed patches are ok */ if ( patches.size() < 2 ) { Utils.log( "Elastic montage requires at least 2 patches to be montaged. You passed me " + patches.size() ); return; } final Project project = patches.get( 0 ).getProject(); for ( final Patch patch : patches ) { if ( patch.getProject() != project ) { Utils.log( "Elastic montage requires all patches to be member of a single project. You passed me patches from several projects." ); return; } } for ( final Patch patch : fixedPatches ) { if ( patch.getProject() != project ) { Utils.log( "Elastic montage requires all fixed patches to be member of a single project. You passed me fixed patches from several projects." ); return; } } final Param param = setup(); if ( param == null ) return; else exec( param, patches, fixedPatches ); } @SuppressWarnings( "deprecation" ) final public void exec( final Param param, final List< Patch > patches, final Set< Patch > fixedPatches ) throws Exception { /* free memory */ patches.get( 0 ).getProject().getLoader().releaseAll(); /* create tiles and models for all patches */ final ArrayList< AbstractAffineTile2D< ? > > tiles = new ArrayList< AbstractAffineTile2D< ? > >(); final ArrayList< AbstractAffineTile2D< ? > > fixedTiles = new ArrayList< AbstractAffineTile2D< ? > > (); Align.tilesFromPatches( param.po, patches, fixedPatches, tiles, fixedTiles ); if ( !param.isAligned ) { Align.alignTiles( param.po, tiles, fixedTiles, param.tilesAreInPlace, param.maxNumThreads ); /* Apply the estimated affine transform to patches */ for ( final AbstractAffineTile2D< ? > t : tiles ) t.getPatch().setAffineTransform( t.createAffine() ); Display.update(); } /* generate tile pairs for all by now overlapping tiles */ final ArrayList< AbstractAffineTile2D< ? >[] > tilePairs = new ArrayList< AbstractAffineTile2D<?>[] >(); AbstractAffineTile2D.pairOverlappingTiles( tiles, tilePairs ); /* check if there was any pair */ if ( tilePairs.size() == 0 ) { Utils.log( "Elastic montage could not find any overlapping patches after pre-montaging." ); return; } Utils.log( tilePairs.size() + " pairs of patches will be block-matched..." ); /* make pairwise global models local */ final ArrayList< Triple< AbstractAffineTile2D< ? >, AbstractAffineTile2D< ? >, InvertibleCoordinateTransform > > pairs = new ArrayList< Triple< AbstractAffineTile2D< ? >, AbstractAffineTile2D< ? >, InvertibleCoordinateTransform > >(); /* * The following casting madness is necessary to get this code compiled * with Sun/Oracle Java 6 which otherwise generates an inconvertible * type exception. * * TODO Remove as soon as this bug is fixed in Sun/Oracle javac. */ for ( final AbstractAffineTile2D< ? >[] pair : tilePairs ) { final AbstractAffineModel2D< ? > m; switch ( param.po.desiredModelIndex ) { case 0: final TranslationModel2D t = ( TranslationModel2D )( Object )pair[ 1 ].getModel().createInverse(); t.concatenate( ( TranslationModel2D )( Object )pair[ 0 ].getModel() ); m = t; break; case 1: final RigidModel2D r = ( RigidModel2D )( Object )pair[ 1 ].getModel().createInverse(); r.concatenate( ( RigidModel2D )( Object )pair[ 0 ].getModel() ); m = r; break; case 2: final SimilarityModel2D s = ( SimilarityModel2D )( Object )pair[ 1 ].getModel().createInverse(); s.concatenate( ( SimilarityModel2D )( Object )pair[ 0 ].getModel() ); m = s; break; case 3: final AffineModel2D a = ( AffineModel2D )( Object )pair[ 1 ].getModel().createInverse(); a.concatenate( ( AffineModel2D )( Object )pair[ 0 ].getModel() ); m = a; break; default: m = null; } pairs.add( new Triple< AbstractAffineTile2D< ? >, AbstractAffineTile2D< ? >, InvertibleCoordinateTransform >( pair[ 0 ], pair[ 1 ], m ) ); } /* Elastic alignment */ /* Initialization */ final double springTriangleHeightTwice = 2 * Math.sqrt( 0.75 * param.springLengthSpringMesh * param.springLengthSpringMesh ); final ArrayList< SpringMesh > meshes = new ArrayList< SpringMesh >( tiles.size() ); final HashMap< AbstractAffineTile2D< ? >, SpringMesh > tileMeshMap = new HashMap< AbstractAffineTile2D< ? >, SpringMesh >(); for ( final AbstractAffineTile2D< ? > tile : tiles ) { final double w = tile.getWidth(); final double h = tile.getHeight(); final int numX = Math.max( 2, ( int )Math.ceil( w / param.springLengthSpringMesh ) + 1 ); final int numY = Math.max( 2, ( int )Math.ceil( h / springTriangleHeightTwice ) + 1 ); final double wMesh = ( numX - 1 ) * param.springLengthSpringMesh; final double hMesh = ( numY - 1 ) * springTriangleHeightTwice; final SpringMesh mesh = new SpringMesh( numX, numY, wMesh, hMesh, param.stiffnessSpringMesh, param.maxStretchSpringMesh * param.bmScale, param.dampSpringMesh ); meshes.add( mesh ); tileMeshMap.put( tile, mesh ); } // final int blockRadius = Math.max( 32, Util.roundPos( param.springLengthSpringMesh / 2 ) ); final int blockRadius = Math.max( Util.roundPos( 16 / param.bmScale ), param.bmBlockRadius ); /** TODO set this something more than the largest error by the approximate model */ final int searchRadius = param.bmSearchRadius; final AbstractModel< ? > localSmoothnessFilterModel = mpicbg.trakem2.align.Util.createModel( param.bmLocalModelIndex ); for ( final Triple< AbstractAffineTile2D< ? >, AbstractAffineTile2D< ? >, InvertibleCoordinateTransform > pair : pairs ) { final AbstractAffineTile2D< ? > t1 = pair.a; final AbstractAffineTile2D< ? > t2 = pair.b; final SpringMesh m1 = tileMeshMap.get( t1 ); final SpringMesh m2 = tileMeshMap.get( t2 ); final ArrayList< PointMatch > pm12 = new ArrayList< PointMatch >(); final ArrayList< PointMatch > pm21 = new ArrayList< PointMatch >(); final ArrayList< Vertex > v1 = m1.getVertices(); final ArrayList< Vertex > v2 = m2.getVertices(); final String patchName1 = patchName( t1.getPatch() ); final String patchName2 = patchName( t2.getPatch() ); final PatchImage pi1 = t1.getPatch().createTransformedImage(); if ( pi1 == null ) { Utils.log( "Patch `" + patchName1 + "' failed generating a transformed image. Skipping..." ); continue; } final PatchImage pi2 = t2.getPatch().createTransformedImage(); if ( pi2 == null ) { Utils.log( "Patch `" + patchName2 + "' failed generating a transformed image. Skipping..." ); continue; } final FloatProcessor fp1 = ( FloatProcessor )pi1.target.convertToFloat(); final ByteProcessor mask1 = pi1.getMask(); final FloatProcessor fpMask1 = mask1 == null ? null : scaleByte( mask1 ); final FloatProcessor fp2 = ( FloatProcessor )pi2.target.convertToFloat(); final ByteProcessor mask2 = pi2.getMask(); final FloatProcessor fpMask2 = mask2 == null ? null : scaleByte( mask2 ); if ( !fixedTiles.contains( t1 ) ) { BlockMatching.matchByMaximalPMCC( fp1, fp2, fpMask1, fpMask2, param.bmScale, pair.c, blockRadius, blockRadius, searchRadius, searchRadius, param.bmMinR, param.bmRodR, param.bmMaxCurvatureR, v1, pm12, new ErrorStatistic( 1 ) ); if ( param.bmUseLocalSmoothnessFilter ) { Utils.log( "`" + patchName1 + "' > `" + patchName2 + "': found " + pm12.size() + " correspondence candidates." ); localSmoothnessFilterModel.localSmoothnessFilter( pm12, pm12, param.bmLocalRegionSigma, param.bmMaxLocalEpsilon, param.bmMaxLocalTrust ); Utils.log( "`" + patchName1 + "' > `" + patchName2 + "': " + pm12.size() + " candidates passed local smoothness filter." ); } else { Utils.log( "`" + patchName1 + "' > `" + patchName2 + "': found " + pm12.size() + " correspondences." ); } } else { Utils.log( "Skipping fixed patch `" + patchName1 + "'." ); } // /* <visualisation> */ // // final List< Point > s1 = new ArrayList< Point >(); // // PointMatch.sourcePoints( pm12, s1 ); // // final ImagePlus imp1 = new ImagePlus( i + " >", ip1 ); // // imp1.show(); // // imp1.setOverlay( BlockMatching.illustrateMatches( pm12 ), Color.yellow, null ); // // imp1.setRoi( Util.pointsToPointRoi( s1 ) ); // // imp1.updateAndDraw(); // /* </visualisation> */ if ( !fixedTiles.contains( t2 ) ) { BlockMatching.matchByMaximalPMCC( fp2, fp1, fpMask2, fpMask1, param.bmScale, pair.c.createInverse(), blockRadius, blockRadius, searchRadius, searchRadius, param.bmMinR, param.bmRodR, param.bmMaxCurvatureR, v2, pm21, new ErrorStatistic( 1 ) ); if ( param.bmUseLocalSmoothnessFilter ) { Utils.log( "`" + patchName1 + "' < `" + patchName2 + "': found " + pm21.size() + " correspondence candidates." ); localSmoothnessFilterModel.localSmoothnessFilter( pm21, pm21, param.bmLocalRegionSigma, param.bmMaxLocalEpsilon, param.bmMaxLocalTrust ); Utils.log( "`" + patchName1 + "' < `" + patchName2 + "': " + pm21.size() + " candidates passed local smoothness filter." ); } else { Utils.log( "`" + patchName1 + "' < `" + patchName2 + "': found " + pm21.size() + " correspondences." ); } } else { Utils.log( "Skipping fixed patch `" + patchName2 + "'." ); } /* <visualisation> */ // final List< Point > s2 = new ArrayList< Point >(); // PointMatch.sourcePoints( pm21, s2 ); // final ImagePlus imp2 = new ImagePlus( i + " <", ip2 ); // imp2.show(); // imp2.setOverlay( BlockMatching.illustrateMatches( pm21 ), Color.yellow, null ); // imp2.setRoi( Util.pointsToPointRoi( s2 ) ); // imp2.updateAndDraw(); /* </visualisation> */ for ( final PointMatch pm : pm12 ) { final Vertex p1 = ( Vertex )pm.getP1(); final Vertex p2 = new Vertex( pm.getP2() ); p1.addSpring( p2, new Spring( 0, 1.0f ) ); m2.addPassiveVertex( p2 ); } for ( final PointMatch pm : pm21 ) { final Vertex p1 = ( Vertex )pm.getP1(); final Vertex p2 = new Vertex( pm.getP2() ); p1.addSpring( p2, new Spring( 0, 1.0f ) ); m1.addPassiveVertex( p2 ); } } /* initialize */ for ( final Map.Entry< AbstractAffineTile2D< ? >, SpringMesh > entry : tileMeshMap.entrySet() ) entry.getValue().init( entry.getKey().getModel() ); /* optimize the meshes */ try { final long t0 = System.currentTimeMillis(); IJ.log( "Optimizing spring meshes..." ); if ( param.useLegacyOptimizer ) { Utils.log( " ...using legacy optimizer..."); SpringMesh.optimizeMeshes2( meshes, param.po.maxEpsilon, param.maxIterationsSpringMesh, param.maxPlateauwidthSpringMesh, param.visualize ); } else { SpringMesh.optimizeMeshes( meshes, param.po.maxEpsilon, param.maxIterationsSpringMesh, param.maxPlateauwidthSpringMesh, param.visualize ); } IJ.log( "Done optimizing spring meshes. Took " + ( System.currentTimeMillis() - t0 ) + " ms" ); } catch ( final NotEnoughDataPointsException e ) { Utils.log( "There were not enough data points to get the spring mesh optimizing." ); e.printStackTrace(); return; } /* apply */ for ( final Map.Entry< AbstractAffineTile2D< ? >, SpringMesh > entry : tileMeshMap.entrySet() ) { final AbstractAffineTile2D< ? > tile = entry.getKey(); if ( !fixedTiles.contains( tile ) ) { final Patch patch = tile.getPatch(); final SpringMesh mesh = entry.getValue(); final Set< PointMatch > matches = mesh.getVA().keySet(); Rectangle box = patch.getCoordinateTransformBoundingBox(); /* compensate for existing coordinate transform bounding box */ for ( final PointMatch pm : matches ) { final Point p1 = pm.getP1(); final double[] l = p1.getL(); l[ 0 ] += box.x; l[ 1 ] += box.y; } final ThinPlateSplineTransform mlt = ElasticLayerAlignment.makeTPS( matches ); patch.appendCoordinateTransform( mlt ); box = patch.getCoordinateTransformBoundingBox(); patch.getAffineTransform().setToTranslation( box.x, box.y ); patch.updateInDatabase( "transform" ); patch.updateBucket(); patch.updateMipMaps(); } } Utils.log( "Done." ); } }