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