1 //:
2 // \file
3 // \author Tim Cootes
4 // \brief Calculate and apply 2D affine transformations
5
6 #include <iostream>
7 #include <cstddef>
8 #include "msm_affine_aligner.h"
9 #include "vnl/vnl_vector.h"
10 #include "vsl/vsl_binary_loader.h"
11 #include <vnl/algo/vnl_svd.h>
12 #include <vnl/algo/vnl_cholesky.h>
13 #ifdef _MSC_VER
14 # include "vcl_msvc_warnings.h"
15 #endif
16 #include <cassert>
17
18 //=======================================================================
19
20
21 //: Compute parameters for inverse transformation
inverse(const vnl_vector<double> & t) const22 vnl_vector<double> msm_affine_aligner::inverse(const vnl_vector<double>& t) const
23 {
24 double a = t[0]+1, b = t[1], tx = t[2];
25 double c = t[3], d = t[4]+1, ty = t[5];
26 vnl_vector<double> q(6);
27
28 double det = a*d-b*c;
29 if (det==0) det=1;
30 q[0] = d/det -1;
31 q[1] = -b/det;
32 q[2] = (b*ty-d*tx)/det;
33 q[3] = -c/det;
34 q[4] = a/det -1;
35 q[5] = (c*tx-a*ty)/det;
36
37 return q;
38 }
39
40 //: Apply the transformation to the given points
apply_transform(const msm_points & points,const vnl_vector<double> & t,msm_points & new_points) const41 void msm_affine_aligner::apply_transform(const msm_points& points,
42 const vnl_vector<double>& t,
43 msm_points& new_points) const
44 {
45 new_points.vector().set_size(points.vector().size());
46 double a = t[0]+1, b = t[1], tx = t[2];
47 double c = t[3], d = t[4]+1, ty = t[5];
48
49 const double* v1=points.vector().data_block();
50 const double* end_v=points.vector().end();
51 double* v2=new_points.vector().data_block();
52 for (;v1!=end_v;v1+=2,v2+=2)
53 {
54 double x=v1[0],y=v1[1];
55 v2[0]=a*x+b*y+tx;
56 v2[1]=c*x+d*y+ty;
57 }
58 }
59
60 //: Return scaling applied by the transform with given parameters.
scale(const vnl_vector<double> & t) const61 double msm_affine_aligner::scale(const vnl_vector<double>& t) const
62 {
63 assert(t.size()==6);
64 double a = t[0]+1, b = t[1];
65 double c = t[3], d = t[4]+1;
66 double s2 = std::fabs(a*d - b*c);
67 if (s2>0) return std::sqrt(s2);
68
69 return 0.0;
70 }
71
72
73 //: Estimate parameter which best map ref_points to points2
74 // Minimises ||points2-T(ref_points)||^2.
75 // Takes advantage of assumed properties of ref_points (eg CoG=origin,
76 // unit size etc) to perform efficiently.
77 //
78 // When used with a shape model of form ref_points+Pb, where the modes P
79 // have certain orthogonality properties with respect to the ref shape,
80 // this can give the optimal transformation into a tangent plane, independent
81 // of the current parameters. In this case a one-shot method can be used
82 // to compute the optimal shape and pose parameters, rather than an iterative
83 // method which is required where the orthogonality properties do not hold,
84 // or where weights are considered.
calc_transform_from_ref(const msm_points & ref_pts,const msm_points & pts2,vnl_vector<double> & trans) const85 void msm_affine_aligner::calc_transform_from_ref(const msm_points& ref_pts,
86 const msm_points& pts2,
87 vnl_vector<double>& trans) const
88 {
89 // Could be made more efficient.
90 calc_transform(ref_pts,pts2,trans);
91 }
92
93 // Calculate affine parameter vector (a-1,b,tx,c,d-1,ty)
msm_calc_affine_trans(vnl_vector<double> & trans,const vnl_matrix<double> & S,const vnl_vector<double> & rhs_x,const vnl_vector<double> & rhs_y)94 void msm_calc_affine_trans(vnl_vector<double>& trans,
95 const vnl_matrix<double>& S,
96 const vnl_vector<double>& rhs_x,
97 const vnl_vector<double>& rhs_y)
98 {
99 vnl_vector<double> tx(3),ty(3);
100 vnl_cholesky C(S,vnl_cholesky::estimate_condition);
101 if (C.rcond()>1e-6)
102 {
103 C.solve(rhs_x,&tx);
104 C.solve(rhs_y,&ty);
105 }
106 else
107 {
108 vnl_svd<double> svd(S);
109 tx = svd.solve(rhs_x);
110 ty = svd.solve(rhs_y);
111 }
112
113 trans.set_size(6);
114 trans[0]=tx[0]-1; trans[1]=tx[1] ; trans[2]=tx[2];
115 trans[3]=ty[0]; trans[4]=ty[1]-1; trans[5]=ty[2];
116 }
117
118
119 //: Estimate parameter which best map points1 to points2
120 // Minimises ||points2-T(points1)||^2
calc_transform(const msm_points & pts1,const msm_points & pts2,vnl_vector<double> & trans) const121 void msm_affine_aligner::calc_transform(const msm_points& pts1,
122 const msm_points& pts2,
123 vnl_vector<double>& trans) const
124 {
125 unsigned n=pts1.size();
126 const double* p1 = pts1.vector().begin();
127 const double* p2 = pts2.vector().begin();
128 const double* p1_end = pts1.vector().end();
129
130 vnl_matrix<double> S(3,3);
131 vnl_vector<double> rhs_x(3),rhs_y(3);
132 S.fill(0.0); rhs_x.fill(0); rhs_y.fill(0);
133
134 for (;p1!=p1_end;p1+=2,p2+=2)
135 {
136 double px = p1[0];
137 double py = p1[1];
138 double tx = p2[0];
139 double ty = p2[1];
140
141 S(0,0) += px*px;
142 S(0,1) += px*py;
143 S(0,2) += px;
144 S(1,1) += py*py;
145 S(1,2) += py;
146
147 rhs_x[0] += px*tx;
148 rhs_x[1] += py*tx;
149 rhs_x[2] += tx;
150
151 rhs_y[0] += px*ty;
152 rhs_y[1] += py*ty;
153 rhs_y[2] += ty;
154 }
155 S(1,0) = S(0,1);
156 S(2,0) = S(0,2);
157 S(2,1) = S(1,2);
158 S(2,2) = n;
159
160 msm_calc_affine_trans(trans,S,rhs_x,rhs_y);
161 }
162
163 //: Estimate parameters which map points1 to points2 allowing for weights
164 // Minimises sum of weighted squares error in frame of pts2,
165 // ie sum w_i * ||p2_i - T(p1_i)||
calc_transform_wt(const msm_points & pts1,const msm_points & pts2,const vnl_vector<double> & wts,vnl_vector<double> & trans) const166 void msm_affine_aligner::calc_transform_wt(const msm_points& pts1,
167 const msm_points& pts2,
168 const vnl_vector<double>& wts,
169 vnl_vector<double>& trans) const
170 {
171 const double* p1 = pts1.vector().begin();
172 const double* p2 = pts2.vector().begin();
173 const double* w=wts.data_block();
174 assert(wts.size()==pts1.size());
175 const double* p1_end = pts1.vector().end();
176
177 vnl_matrix<double> S(3,3);
178 vnl_vector<double> rhs_x(3),rhs_y(3);
179 S.fill(0.0); rhs_x.fill(0); rhs_y.fill(0);
180
181 for (;p1!=p1_end;p1+=2,p2+=2,++w)
182 {
183 double px = p1[0];
184 double py = p1[1];
185 double tx = p2[0];
186 double ty = p2[1];
187
188 S(0,0) += (*w)*px*px;
189 S(0,1) += (*w)*px*py;
190 S(0,2) += (*w)*px;
191 S(1,1) += (*w)*py*py;
192 S(1,2) += (*w)*py;
193 S(2,2) += (*w);
194
195 rhs_x[0] += (*w)*px*tx;
196 rhs_x[1] += (*w)*py*tx;
197 rhs_x[2] += (*w)*tx;
198
199 rhs_y[0] += (*w)*px*ty;
200 rhs_y[1] += (*w)*py*ty;
201 rhs_y[2] += (*w)*ty;
202 }
203 S(1,0) = S(0,1);
204 S(2,0) = S(0,2);
205 S(2,1) = S(1,2);
206
207 msm_calc_affine_trans(trans,S,rhs_x,rhs_y);
208 }
209
210 //: Estimate parameters which map points allowing for anisotropic wts
211 // Errors on point i are weighted by wt_mat[i] in pts2 frame.
212 // ie error is sum (p2_i-T(p1_i)'*wt_mat[i]*(p2_i-T(p1_i)
calc_transform_wt_mat(const msm_points & pts1,const msm_points & pts2,const std::vector<msm_wt_mat_2d> & wt_mat,vnl_vector<double> & trans) const213 void msm_affine_aligner::calc_transform_wt_mat(
214 const msm_points& pts1,
215 const msm_points& pts2,
216 const std::vector<msm_wt_mat_2d>& wt_mat,
217 vnl_vector<double>& trans) const
218 {
219 assert(pts2.size()==pts1.size());
220 assert(wt_mat.size()==pts1.size());
221
222 const double* p1 = pts1.vector().begin();
223 const double* p2 = pts2.vector().begin();
224 const msm_wt_mat_2d* w = &wt_mat[0];
225 const double* p1_end = pts1.vector().end();
226
227 vnl_matrix<double> S(6,6);
228 vnl_vector<double> rhs(6);
229 S.fill(0.0); rhs.fill(0);
230
231 for (;p1!=p1_end;p1+=2,p2+=2,++w)
232 {
233 double px = p1[0];
234 double py = p1[1];
235 double qx = p2[0];
236 double qy = p2[1];
237
238 S(0,0) += px*px*w->m11();
239 S(0,1) += px*py*w->m11();
240 S(0,2) += px*px*w->m12();
241 S(0,3) += px*py*w->m12();
242 S(0,4) += px*w->m11();
243 S(0,5) += px*w->m12();
244
245 // S(1,0) += py*px*w->m11(); Symmetric
246 S(1,1) += py*py*w->m11();
247 S(1,2) += py*px*w->m12();
248 S(1,3) += py*py*w->m12();
249 S(1,4) += py*w->m11();
250 S(1,5) += py*w->m12();
251
252 // S(2,0) += px*px*w->m12(); Symmetric
253 // S(2,1) += px*py*w->m12(); Symmetric
254 S(2,2) += px*px*w->m22();
255 S(2,3) += px*py*w->m22();
256 S(2,4) += px*w->m12();
257 S(2,5) += px*w->m22();
258
259 // S(3,0) += py*px*w->m12(); Symmetric
260 // S(3,1) += py*py*w->m12(); Symmetric
261 // S(3,2) += py*px*w->m22(); Symmetric
262 S(3,3) += py*py*w->m22();
263 S(3,4) += py*w->m12();
264 S(3,5) += py*w->m22();
265
266 S(4,4) += w->m11();
267 S(4,5) += w->m12();
268 S(5,5) += w->m22();
269
270 rhs[0] += px*qx*w->m11() + px*qy*w->m12();
271 rhs[1] += py*qx*w->m11() + py*qy*w->m12();
272 rhs[2] += px*qx*w->m12() + px*qy*w->m22();
273 rhs[3] += py*qx*w->m12() + py*qy*w->m22();
274 rhs[4] += w->m11()*qx + w->m12()*qy;
275 rhs[5] += w->m12()*qx + w->m22()*qy;
276 }
277 // Fill in symmetric elements
278 S(1,0)=S(0,1);
279 S(2,0)=S(0,2); S(2,1)=S(1,2);
280 S(3,0)=S(0,3); S(3,1)=S(1,3); S(3,2)=S(2,3);
281 S(4,0)=S(0,4); S(4,1)=S(1,4); S(4,2)=S(2,4); S(4,3)=S(3,4);
282 S(5,0)=S(0,5); S(5,1)=S(1,5); S(5,2)=S(2,5); S(5,3)=S(3,5); S(5,4)=S(4,5);
283
284 vnl_vector<double> t(6);
285 vnl_cholesky C(S,vnl_cholesky::estimate_condition);
286 if (C.rcond()>1e-6)
287 {
288 C.solve(rhs,&t);
289 }
290 else
291 {
292 vnl_svd<double> svd(S);
293 t = svd.solve(rhs);
294 }
295
296 trans.set_size(6);
297 trans[0]=t[0]-1; trans[1]=t[1] ; trans[2]=t[4];
298 trans[3]=t[2]; trans[4]=t[3]-1; trans[5]=t[5];
299
300 }
301
302
303 //: Apply transform to weight matrices (ie ignore translation component)
304 // W_new = A'WA
transform_wt_mat(const std::vector<msm_wt_mat_2d> & wt_mat,const vnl_vector<double> & t,std::vector<msm_wt_mat_2d> & new_wt_mat) const305 void msm_affine_aligner::transform_wt_mat(
306 const std::vector<msm_wt_mat_2d>& wt_mat,
307 const vnl_vector<double>& t,
308 std::vector<msm_wt_mat_2d>& new_wt_mat) const
309 {
310 double a = 1+t[0], b=t[1], c=t[3], d=t[4]+1;
311 new_wt_mat.resize(wt_mat.size());
312 for (unsigned i=0;i<wt_mat.size();++i)
313 {
314 double w1=wt_mat[i].m11(), w2=wt_mat[i].m12(), w3=wt_mat[i].m22();
315 new_wt_mat[i]=msm_wt_mat_2d(a*a*w1+ 2*a*c*w2+c*c*w3,
316 a*b*w1+(a*d+b*c)*w2+c*d*w3,
317 b*b*w1+ 2*b*d*w2+d*d*w3);
318 }
319
320 }
321
322 //: Returns params of pose such that pose(x) = pose1(pose2(x))
compose(const vnl_vector<double> & pose1,const vnl_vector<double> & pose2) const323 vnl_vector<double> msm_affine_aligner::compose(
324 const vnl_vector<double>& pose1,
325 const vnl_vector<double>& pose2) const
326 {
327 double a = pose1[0]+1, b = pose1[1], c = pose1[2];
328 double d = pose1[3], e = pose1[4]+1, f = pose1[5];
329 double da = pose2[0]+1, db = pose2[1], dc = pose2[2];
330 double dd = pose2[3], de = pose2[4]+1, df = pose2[5];
331
332 vnl_vector<double> p(6);
333 p[0] = a*da + b*dd -1.0;
334 p[1] = a*db + b*de;
335 p[2] = a*dc + b*df + c;
336 p[3] = d*da + e*dd;
337 p[4] = d*db + e*de -1.0;
338 p[5] = d*dc + e*df + f;
339 return p;
340 }
341
342 //: Apply transform to generate points in some reference frame
343 // For instance, depending on transform, may translate so the
344 // centre of gravity is at the origin and scale to a unit size.
normalise_shape(msm_points & points) const345 void msm_affine_aligner::normalise_shape(msm_points& points) const
346 {
347 vgl_point_2d<double> cog = points.cog();
348 points.translate_by(-cog.x(),-cog.y());
349 double L = points.vector().magnitude();
350 if (L>1e-6) points.scale_by(1.0/L);
351 }
352
353
354 //: Find poses which align a set of points
355 // On exit ref_mean_shape is the mean shape in the reference
356 // frame, pose_to_ref[i] maps points[i] into the reference
357 // frame (ie pose is the mapping from the reference frame to
358 // the target frames).
359 // \param pose_source defines how alignment of ref_mean_shape is calculated
360 // \param average_pose Average mapping from ref to target frame
align_set(const std::vector<msm_points> & points,msm_points & ref_mean_shape,std::vector<vnl_vector<double>> & pose_to_ref,vnl_vector<double> & average_pose,ref_pose_source pose_source) const361 void msm_affine_aligner::align_set(const std::vector<msm_points>& points,
362 msm_points& ref_mean_shape,
363 std::vector<vnl_vector<double> >& pose_to_ref,
364 vnl_vector<double>& average_pose,
365 ref_pose_source pose_source) const
366 {
367 std::size_t n_shapes = points.size();
368 assert(n_shapes>0);
369 pose_to_ref.resize(n_shapes);
370
371 // Use first shape as initial reference
372 ref_mean_shape = points[0];
373 normalise_shape(ref_mean_shape);
374
375 // Record normalised first shape to define initial orientation
376 msm_points base_shape = ref_mean_shape;
377
378 vnl_vector<double> pose_from_ref;
379 msm_points new_mean=ref_mean_shape;
380 average_pose.set_size(4);
381
382 double change=1.0;
383
384 unsigned n_its=0;
385
386 while (change>1e-6 && n_its<10)
387 {
388 ref_mean_shape = new_mean;
389 normalise_shape(ref_mean_shape);
390
391 average_pose.fill(0);
392
393 for (unsigned i=0;i<n_shapes;++i)
394 {
395 if (points[i].size()!=ref_mean_shape.size())
396 {
397 std::cerr<<"msm_affine_aligner::align_set() shape "<<i
398 <<" has different number of points to first shape"<<std::endl;
399 std::abort();
400 }
401 calc_transform_from_ref(ref_mean_shape,points[i],pose_from_ref);
402 pose_to_ref[i]=inverse(pose_from_ref);
403 average_pose+=pose_from_ref;
404 }
405
406 mean_of_transformed(points,pose_to_ref,new_mean);
407
408 // new_mean should be in the subspace orthogonal to
409 // the mean and to rotation defined by the mean.
410
411 change = (new_mean.vector()-ref_mean_shape.vector()).rms();
412
413 if (pose_source==mean_pose && n_its==0)
414 {
415 // Use the average pose to define the orientation of the mean in the ref frame
416 apply_transform(new_mean,average_pose/n_shapes,new_mean);
417 change=1.0; // Force at least one more iteration
418 }
419
420 n_its++;
421 }
422
423 // This will get translation right, but will not cope well
424 // with large rotation variations.
425 average_pose/=n_shapes;
426 }
427
428 //=======================================================================
429
is_a() const430 std::string msm_affine_aligner::is_a() const
431 {
432 return std::string("msm_affine_aligner");
433 }
434
435 //: Create a copy on the heap and return base class pointer
clone() const436 msm_aligner* msm_affine_aligner::clone() const
437 {
438 return new msm_affine_aligner(*this);
439 }
440