Added comment to prevent myself from doing something silly in the future.
[trakem2.git] / mpi / fruitfly / registration / TRModel2D.java
blob1c80d2b2d9f353f8cbb5f54fc34d06be361cde5b
1 package mpi.fruitfly.registration;
3 import ini.trakem2.utils.Utils;
5 import java.util.Iterator;
6 import java.util.ArrayList;
7 import java.util.Collection;
8 import java.util.List;
9 import java.util.Random;
10 import java.awt.geom.AffineTransform;
12 public class TRModel2D extends Model {
14 static final public int MIN_SET_SIZE = 2;
16 final private AffineTransform affine = new AffineTransform();
17 public AffineTransform getAffine() { return affine; }
19 @Override
20 public float[] apply( float[] point )
22 float[] transformed = new float[ 2 ];
23 affine.transform( point, 0, transformed, 0, 1 );
24 return transformed;
27 @Override
28 public void applyInPlace( float[] point )
30 affine.transform( point, 0, point, 0, 1 );
33 @Override
34 public float[] applyInverse( float[] point )
36 // the brilliant java.awt.geom.AffineTransform implements transform for float[] but inverseTransform for double[] only...
37 double[] double_point = new double[]{ point[ 0 ], point[ 1 ] };
38 double[] transformed = new double[ 2 ];
39 try
41 affine.inverseTransform( double_point, 0, transformed, 0, 1 );
43 catch ( Exception e )
45 System.err.println( "Noninvertible transformation." );
47 return new float[]{ ( float )transformed[ 0 ], ( float )transformed[ 1 ] };
50 @Override
51 public void applyInverseInPlace( float[] point )
53 float[] temp_point = applyInverse( point );
54 point[ 0 ] = temp_point[ 0 ];
55 point[ 1 ] = temp_point[ 1 ];
58 @Override
59 public boolean fit( PointMatch[] min_matches )
61 PointMatch m1 = min_matches[ 0 ];
62 PointMatch m2 = min_matches[ 1 ];
64 float[] m1_p1 = m1.getP1().getL();
65 float[] m2_p1 = m2.getP1().getL();
66 float[] m1_p2 = m1.getP2().getW();
67 float[] m2_p2 = m2.getP2().getW();
69 float x1 = m2_p1[ 0 ] - m1_p1[ 0 ];
70 float y1 = m2_p1[ 1 ] - m1_p1[ 1 ];
71 float x2 = m2_p2[ 0 ] - m1_p2[ 0 ];
72 float y2 = m2_p2[ 1 ] - m1_p2[ 1 ];
73 float l1 = ( float )Math.sqrt( x1 * x1 + y1 * y1 );
74 float l2 = ( float )Math.sqrt( x2 * x2 + y2 * y2 );
76 x1 /= l1;
77 x2 /= l2;
78 y1 /= l1;
79 y2 /= l2;
81 //! unrotate (x2,y2)^T to (x1,y1)^T = (1,0)^T getting the sinus and cosinus of the rotation angle
82 float cos = x1 * x2 + y1 * y2;
83 float sin = x1 * y2 - y1 * x2;
85 //m.alpha = atan2( y, x );
87 float tx = m1_p2[ 0 ] - cos * m1_p1[ 0 ] + sin * m1_p1[ 1 ];
88 float ty = m1_p2[ 1 ] - sin * m1_p1[ 0 ] - cos * m1_p1[ 1 ];
89 affine.setTransform( cos, sin, -sin, cos, tx, ty );
91 // System.out.println( this );
93 return true;
96 @Override
97 public String toString()
99 return ( "[3,3](" + affine + ") " + error );
102 public void minimize( Collection< PointMatch > matches )
104 // center of mass:
105 float xo1 = 0, yo1 = 0;
106 float xo2 = 0, yo2 = 0;
107 // Implementing Johannes Schindelin's squared error minimization formula
108 // tan(angle) = Sum(x1*y1 + x2y2) / Sum(x1*y2 - x2*y1)
109 int length = matches.size();
110 // 1 - compute centers of mass, for displacement and origin of rotation
112 if (0 == length) return;
114 for ( PointMatch m : matches )
116 float[] m_p1 = m.getP1().getL();
117 float[] m_p2 = m.getP2().getW();
119 xo1 += m_p1[ 0 ];
120 yo1 += m_p1[ 1 ];
121 xo2 += m_p2[ 0 ];
122 yo2 += m_p2[ 1 ];
124 xo1 /= length;
125 yo1 /= length;
126 xo2 /= length;
127 yo2 /= length;
129 float dx = xo1 - xo2; // reversed, because the second will be moved relative to the first
130 float dy = yo1 - yo2;
131 float sum1 = 0, sum2 = 0;
132 float x1, y1, x2, y2;
133 for ( PointMatch m : matches )
135 float[] m_p1 = m.getP1().getL();
136 float[] m_p2 = m.getP2().getW();
138 // make points local to the center of mass of the first landmark set
139 x1 = m_p1[ 0 ] - xo1; // x1
140 y1 = m_p1[ 1 ] - yo1; // x2
141 x2 = m_p2[ 0 ] - xo2 + dx; // y1
142 y2 = m_p2[ 1 ] - yo2 + dy; // y2
143 sum1 += x1 * y2 - y1 * x2; // x1 * y2 - x2 * y1 // assuming p1 is x1,x2, and p2 is y1,y2
144 sum2 += x1 * x2 + y1 * y2; // x1 * y1 + x2 * y2
146 float angle = ( float )Math.atan2( -sum1, sum2 );
148 affine.setToIdentity();
149 affine.rotate( -angle, xo2, yo2 );
150 affine.translate( -dx, -dy );
154 * change the model a bit
156 * estimates the necessary amount of shaking for each single dimensional
157 * distance in the set of matches
159 * @param matches point matches
160 * @param scale gives a multiplicative factor to each dimensional distance (increases the amount of shaking)
161 * @param center local pivot point
163 final public void shake(
164 Collection< PointMatch > matches,
165 float scale,
166 float[] center )
168 double xd = 0.0;
169 double yd = 0.0;
170 double rd = 0.0;
172 int num_matches = matches.size();
173 if ( num_matches > 0 )
175 for ( PointMatch m : matches )
177 float[] m_p1 = m.getP1().getW();
178 float[] m_p2 = m.getP2().getW();
180 xd += Math.abs( m_p1[ 0 ] - m_p2[ 0 ] );;
181 yd += Math.abs( m_p1[ 1 ] - m_p2[ 1 ] );;
183 // shift relative to the center
184 float x1 = m_p1[ 0 ] - center[ 0 ];
185 float y1 = m_p1[ 1 ] - center[ 1 ];
186 float x2 = m_p2[ 0 ] - center[ 0 ];
187 float y2 = m_p2[ 1 ] - center[ 1 ];
189 float l1 = ( float )Math.sqrt( x1 * x1 + y1 * y1 );
190 float l2 = ( float )Math.sqrt( x2 * x2 + y2 * y2 );
192 x1 /= l1;
193 x2 /= l2;
194 y1 /= l1;
195 y2 /= l2;
197 //! unrotate (x1,y1)^T to (x2,y2)^T = (1,0)^T getting the sinus and cosinus of the rotation angle
198 float cos = x1 * x2 + y1 * y2;
199 float sin = y1 * x2 - x1 * y2;
201 rd += Math.abs( Math.atan2( sin, cos ) );
203 xd /= matches.size();
204 yd /= matches.size();
205 rd /= matches.size();
207 //System.out.println( rd );
210 affine.rotate( rnd.nextGaussian() * ( float )rd * scale, center[ 0 ], center[ 1 ] );
214 * estimate the transformation model for a set of feature correspondences
215 * containing a high number of outliers using RANSAC
217 * @param candidates set of correspondence candidates
218 * @param inliers set ot correspondences that fit the finally estimated model if any
219 * @param iterations number of iterations
220 * @param epsilon maximally allowed displacement
221 * @param min_inlier_ratio minimal amount of inliers
223 * @return TRModel2D or null
226 * Bibtex reference:
227 * <pre>
228 * @article{FischlerB81,
229 * author = {Martin A. Fischler and Robert C. Bolles},
230 * title = {Random sample consensus: a paradigm for model fitting with applications to image analysis and automated cartography},
231 * journal = {Communications of the ACM},
232 * volume = {24},
233 * number = {6},
234 * year = {1981},
235 * pages = {381--395},
236 * publisher = {ACM Press},
237 * address = {New York, NY, USA},
238 * issn = {0001-0782},
239 * doi = {http://doi.acm.org/10.1145/358669.358692},
241 * </pre>
244 static public TRModel2D estimateModel(
245 List< PointMatch > candidates,
246 Collection< PointMatch > inliers,
247 int iterations,
248 float epsilon,
249 float min_inlier_ratio )
251 inliers.clear();
253 if ( candidates.size() < MIN_SET_SIZE )
255 System.err.println( candidates.size() + " correspondence candidates are not enough to estimate a model, at least " + TRModel2D.MIN_SET_SIZE + " required." );
256 return null;
259 TRModel2D model = new TRModel2D(); //!< the final model to be estimated
261 int i = 0;
262 while ( i < iterations )
264 // choose T::MIN_SET_SIZE disjunctive matches randomly
265 PointMatch[] min_matches = new PointMatch[ MIN_SET_SIZE ];
266 int[] keys = new int[ MIN_SET_SIZE ];
268 for ( int j = 0; j < MIN_SET_SIZE; ++j )
270 int key;
271 boolean in_set = false;
274 key = ( int )( rnd.nextDouble() * candidates.size() );
275 in_set = false;
277 // check if this key exists already
278 for ( int k = 0; k < j; ++k )
280 if ( key == keys[ k ] )
282 in_set = true;
283 break;
287 while ( in_set );
288 keys[ j ] = key;
289 min_matches[ j ] = candidates.get( key );
292 TRModel2D m = new TRModel2D();
293 final ArrayList< PointMatch > temp_inliers = new ArrayList< PointMatch >();
294 m.fit( min_matches );
295 int num_inliers = 0;
296 boolean is_good = m.test( candidates, temp_inliers, epsilon, min_inlier_ratio );
297 while ( is_good && num_inliers < temp_inliers.size() )
299 num_inliers = temp_inliers.size();
300 m.minimize( temp_inliers );
301 is_good = m.test( candidates, temp_inliers, epsilon, min_inlier_ratio );
303 if (
304 is_good &&
305 m.betterThan( model ) &&
306 temp_inliers.size() >= 3 * MIN_SET_SIZE ) // now at least 6 matches required
308 model = m.clone();
309 inliers.clear();
310 inliers.addAll( temp_inliers );
312 ++i;
314 if ( inliers.size() == 0 )
315 return null;
317 return model;
321 * estimate the transformation model for a set of feature correspondences
322 * containing a high number of outliers using RANSAC
324 * increase the error as long as not more inliers occur
326 static public TRModel2D estimateBestModel(
327 List< PointMatch > candidates,
328 Collection< PointMatch > inliers,
329 float min_epsilon,
330 float max_epsilon,
331 float min_inlier_ratio )
333 inliers.clear();
334 TRModel2D model = null;
335 float epsilon = 0.0f;
336 if ( candidates.size() > MIN_SET_SIZE )
338 int highest_num_inliers = 0;
339 int convergence_count = 0;
340 TRModel2D m = null;
343 final ArrayList< PointMatch > temp_inliers = new ArrayList< PointMatch >();
344 epsilon += min_epsilon;
345 // 1000 iterations lead to a probability of < 0.01% that only bad data values were found
346 m = estimateModel(
347 candidates, //!< point correspondence candidates
348 temp_inliers,
349 1000, //!< iterations
350 epsilon, //!< maximal alignment error for a good point pair when fitting the model
351 min_inlier_ratio ); //!< minimal partition (of 1.0) of inliers
353 if ( m != null )
355 int num_inliers = temp_inliers.size();
356 if ( num_inliers <= highest_num_inliers )
358 ++convergence_count;
360 else
362 model = m.clone();
363 inliers.clear();
364 inliers.addAll( temp_inliers );
365 convergence_count = 0;
366 highest_num_inliers = num_inliers;
370 while ( ( m == null || convergence_count < 4 ) && epsilon < max_epsilon );
372 if ( model == null )
374 Utils.log2( "No model found." );
376 else
378 Utils.log2( "Model with epsilon <= " + epsilon + " for " + inliers.size() + " inliers found." );
379 Utils.log2( " Affine transform: " + model.getAffine().toString() );
382 return model;
386 public TRModel2D clone()
388 TRModel2D trm = new TRModel2D();
389 trm.affine.setTransform( affine );
390 trm.error = error;
391 return trm;
394 public void preConcatenate(TRModel2D model) {
395 this.affine.preConcatenate(model.affine);
398 public void concatenate(TRModel2D model) {
399 this.affine.concatenate(model.affine);