contrib/mul/msm/msm_similarity_aligner.cxx
Go to the documentation of this file.
00001 //:
00002 // \file
00003 // \author Tim Cootes
00004 // \brief Calculate and apply 2D similarity transformations
00005 
00006 #include "msm_similarity_aligner.h"
00007 #include <vnl/vnl_vector.h>
00008 #include <vsl/vsl_binary_loader.h>
00009 #include <vnl/algo/vnl_svd.h>
00010 #include <vnl/algo/vnl_cholesky.h>
00011 #include <vcl_cstddef.h> // for std::size_t
00012 #include <vcl_cassert.h>
00013 
00014 //=======================================================================
00015 
00016   //: Compute parameters for inverse transformation
00017 vnl_vector<double> msm_similarity_aligner::inverse(const vnl_vector<double>& t) const
00018 {
00019   vnl_vector<double> q(4);
00020   double a=t[0]+1.0, b=t[1];
00021   double s2 = a*a + b*b;
00022   double a1=a/s2, b1=-b/s2;
00023   q[0] = a1-1.0;
00024   q[1] = b1;
00025   q[2] = -a1 * t[2] + b1 * t[3];
00026   q[3] = -b1 * t[2] - a1 * t[3];
00027   return q;
00028 }
00029 
00030   //: Apply the transformation to the given points
00031 void msm_similarity_aligner::apply_transform(const msm_points& points,
00032                                              const vnl_vector<double>& trans,
00033                                              msm_points& new_points) const
00034 {
00035   new_points.vector().set_size(points.vector().size());
00036   double a=1.0+trans[0], b=trans[1], tx=trans[2], ty=trans[3];
00037 
00038   const double* v1=points.vector().data_block();
00039   const double* end_v=points.vector().end();
00040   double* v2=new_points.vector().data_block();
00041   for (;v1!=end_v;v1+=2,v2+=2)
00042   {
00043     double x=v1[0],y=v1[1];
00044     v2[0]=a*x-b*y+tx;
00045     v2[1]=b*x+a*y+ty;
00046   }
00047 }
00048 
00049 //: Return scaling applied by the transform with given parameters.
00050 double msm_similarity_aligner::scale(const vnl_vector<double>& trans) const
00051 {
00052   assert(trans.size()==4);
00053   double a=1.0+trans[0],b=trans[1];
00054   return vcl_sqrt(a*a+b*b);
00055 }
00056 
00057 
00058 //: Estimate parameter which best map ref_points to points2
00059 //  Minimises ||points2-T(ref_points)||^2.
00060 //  Takes advantage of assumed properties of ref_points (eg CoG=origin,
00061 //  unit size etc) to perform efficiently.
00062 //
00063 //  When used with a shape model of form ref_points+Pb, where the modes P
00064 //  have certain orthogonality properties with respect to the ref shape,
00065 //  this can give the optimal transformation into a tangent plane, independent
00066 //  of the current parameters.  In this case a one-shot method can be used
00067 //  to compute the optimal shape and pose parameters, rather than an iterative
00068 //  method which is required where the orthogonality properties do not hold,
00069 //  or where weights are considered.
00070 void msm_similarity_aligner::calc_transform_from_ref(const msm_points& ref_pts,
00071                                                      const msm_points& pts2,
00072                                                      vnl_vector<double>& trans) const
00073 {
00074   vgl_point_2d<double> cog2 = pts2.cog();
00075   const double* p1 = ref_pts.vector().begin();
00076   const double* p2 = pts2.vector().begin();
00077   const double* p1_end = ref_pts.vector().end();
00078 
00079   double xy_sum=0.0;
00080   double dot_sum=0.0;
00081 
00082   for (;p1!=p1_end;p1+=2,p2+=2)
00083   {
00084     double dpx = p1[0];
00085     double dpy = p1[1];
00086     double dtx = p2[0]-cog2.x();
00087     double dty = p2[1]-cog2.y();
00088 
00089     dot_sum += dpx*dtx + dpy*dty;
00090     xy_sum += dpx*dty - dpy*dtx;
00091   }
00092 
00093   trans.set_size(4);
00094   trans[0] = dot_sum - 1.0;
00095   trans[1] = xy_sum;
00096   trans[2] = cog2.x();
00097   trans[3] = cog2.y();
00098 }
00099 
00100   //: Estimate parameter which best map points1 to points2
00101   //  Minimises ||points2-T(points1)||^2
00102 void msm_similarity_aligner::calc_transform(const msm_points& pts1,
00103                                             const msm_points& pts2,
00104                                             vnl_vector<double>& trans) const
00105 {
00106   vgl_point_2d<double> cog1 = pts1.cog();
00107   vgl_point_2d<double> cog2 = pts2.cog();
00108   const double* p1 = pts1.vector().begin();
00109   const double* p2 = pts2.vector().begin();
00110   const double* p1_end = pts1.vector().end();
00111 
00112   double x2_sum=0.0;
00113   double xy_sum=0.0;
00114   double dot_sum=0.0;
00115 
00116   for (;p1!=p1_end;p1+=2,p2+=2)
00117   {
00118     double dpx = p1[0]-cog1.x();
00119     double dpy = p1[1]-cog1.y();
00120     double dtx = p2[0]-cog2.x();
00121     double dty = p2[1]-cog2.y();
00122 
00123     x2_sum += dpx*dpx + dpy*dpy;
00124     dot_sum += dpx*dtx + dpy*dty;
00125     xy_sum += dpx*dty - dpy*dtx;
00126   }
00127 
00128   trans.set_size(4);
00129   double a = dot_sum/x2_sum;
00130   double b = xy_sum/x2_sum;
00131   trans[0] = a - 1.0;
00132   trans[1] = b;
00133   trans[2] = cog2.x() - (a*cog1.x() - b*cog1.y());
00134   trans[3] = cog2.y() - (b*cog1.x() + a*cog1.y());
00135 }
00136 
00137 // Compute weighted CoG
00138 inline vgl_point_2d<double> msm_wtd_cog(const msm_points& pts,
00139                                         const vnl_vector<double>& wts)
00140 {
00141   const double* v=pts.vector().data_block();
00142   const double* w=wts.data_block();
00143   const unsigned int n=pts.size();
00144   const double* end_v=v+2*n;
00145   double cx=0.0,cy=0.0,w_sum=0.0;
00146   for (;v!=end_v;v+=2,++w)
00147   { cx+=w[0]*v[0]; cy+=w[0]*v[1]; w_sum+=w[0]; }
00148 
00149   if (w_sum>0) { cx/=w_sum; cy/=w_sum; }
00150   return vgl_point_2d<double>(cx,cy);
00151 }
00152 
00153   //: Estimate parameters which map points1 to points2 allowing for weights
00154   //  Minimises sum of weighted squares error in frame of pts2,
00155   //  ie sum w_i * ||p2_i - T(p1_i)||
00156 void msm_similarity_aligner::calc_transform_wt(const msm_points& pts1,
00157                                                const msm_points& pts2,
00158                                                const vnl_vector<double>& wts,
00159                                                vnl_vector<double>& trans) const
00160 {
00161   vgl_point_2d<double> cog1 = msm_wtd_cog(pts1,wts);
00162   vgl_point_2d<double> cog2 = msm_wtd_cog(pts2,wts);
00163   const double* p1 = pts1.vector().begin();
00164   const double* p2 = pts2.vector().begin();
00165   const double* w=wts.data_block();
00166   assert(wts.size()==pts1.size());
00167   const double* p1_end = pts1.vector().end();
00168 
00169   double x2_sum=0.0;
00170   double xy_sum=0.0;
00171   double dot_sum=0.0;
00172 
00173   for (;p1!=p1_end;p1+=2,p2+=2,++w)
00174   {
00175     double dpx = p1[0]-cog1.x();
00176     double dpy = p1[1]-cog1.y();
00177     double dtx = p2[0]-cog2.x();
00178     double dty = p2[1]-cog2.y();
00179 
00180     x2_sum  += w[0]*(dpx*dpx + dpy*dpy);
00181     dot_sum += w[0]*(dpx*dtx + dpy*dty);
00182     xy_sum  += w[0]*(dpx*dty - dpy*dtx);
00183   }
00184 
00185   trans.set_size(4);
00186   double a = dot_sum/x2_sum;
00187   double b = xy_sum/x2_sum;
00188   trans[0] = a - 1.0;
00189   trans[1] = b;
00190   trans[2] = cog2.x() - (a*cog1.x() - b*cog1.y());
00191   trans[3] = cog2.y() - (b*cog1.x() + a*cog1.y());
00192 }
00193 
00194 #if 0 // function commented out
00195 //: Estimate parameters which map points allowing for anisotropic wts
00196 //  Errors on point i are weighted by wt_mat[i] in pts2 frame.
00197 //  ie error is sum (p2_i-T(p1_i)'*wt_mat[i]*(p2_i-T(p1_i)
00198 *** Incorrect implementation - assumption about CoG incorrect ***
00199 void msm_similarity_aligner::calc_transform_wt_mat(
00200                               const msm_points& pts1,
00201                               const msm_points& pts2,
00202                               const vcl_vector<msm_wt_mat_2d>& wt_mat,
00203                               vnl_vector<double>& trans) const
00204 {
00205   assert(pts2.size()==pts1.size());
00206   assert(wt_mat.size()==pts1.size());
00207   // Compute weighted CoGs
00208   msm_wt_mat_2d w_sum(0,0,0);
00209   double x1=0.0,y1=0.0;
00210   double x2=0.0,y2=0.0;
00211   const double* p1 = pts1.vector().begin();
00212   const double* p2 = pts2.vector().begin();
00213   const double* p1_end = pts1.vector().end();
00214   vcl_vector<msm_wt_mat_2d>::const_iterator w=wt_mat.begin();
00215   for (;p1!=p1_end;p1+=2,p2+=2,++w)
00216   {
00217     double wa=w->m11(), wb=w->m12(), wc=w->m22();
00218     w_sum += (*w);
00219     x1 += wa*p1[0]+wb*p1[1];
00220     y1 += wb*p1[0]+wc*p1[1];
00221     x2 += wa*p2[0]+wb*p2[1];
00222     y2 += wb*p2[0]+wc*p2[1];
00223   }
00224   msm_wt_mat_2d w_inv = w_sum.inverse();
00225   double v11=w_inv.m11(), v12=w_inv.m12(), v22=w_inv.m22();
00226   vgl_point_2d<double> cog1(v11*x1+v12*y1,v12*x1+v22*y1);
00227   vgl_point_2d<double> cog2(v11*x2+v12*y2,v12*x2+v22*y2);
00228   vcl_cout<<"CoG1: "<<cog1<<'\n'
00229           <<"CoG2: "<<cog2<<vcl_endl;
00230 
00231   // Need to compute the a,b terms relative to these centres.
00232 
00233   msm_wt_mat_2d S(0,0,0); // Sum
00234   double txx_sum = 0;
00235   double txy_sum = 0;
00236 
00237   p1 = pts1.vector().begin();
00238   p2 = pts2.vector().begin();
00239   w=wt_mat.begin();
00240   for (;p1!=p1_end;p1+=2,p2+=2,++w)
00241   {
00242     double dpx = p1[0]-cog1.x();
00243     double dpy = p1[1]-cog1.y();
00244     double dtx = p2[0]-cog2.x();
00245     double dty = p2[1]-cog2.y();
00246 
00247     double wa=w->m11(), wb=w->m12(), wc=w->m22();
00248 
00249     double wx = wa*dpx + wb*dpy;
00250     double wy = wb*dpx + wc*dpy;
00251     double vx = wb*dpx - wa*dpy;
00252     double vy = wc*dpx - wb*dpy;
00253 
00254     S += msm_wt_mat_2d(dpx*wx + dpy*wy,
00255                        dpx*wy - dpy*wx,
00256                        dpx*vy - dpy*vx);
00257 
00258     txx_sum += dtx*wx + dty*wy;
00259     txy_sum += dtx*vx + dty*vy;
00260   }
00261 
00262   // Solve equation
00263   // ( S11 S12 )(a) = (txx_sum)
00264   // ( S12 S22 )(b)   (txy_sum)
00265 
00266   trans.set_size(4);
00267   msm_wt_mat_2d S_inv = S.inverse();
00268   double a = S_inv.m11()*txx_sum + S_inv.m12()*txy_sum;
00269   double b = S_inv.m21()*txx_sum + S_inv.m22()*txy_sum;
00270 
00271   trans[0] = a - 1.0;
00272   trans[1] = b;
00273   trans[2] = cog2.x() - (a*cog1.x() - b*cog1.y());
00274   trans[3] = cog2.y() - (b*cog1.x() + a*cog1.y());
00275 }
00276 #endif // 0
00277 
00278 //: Estimate parameters which map points allowing for anisotropic wts
00279 //  Errors on point i are weighted by wt_mat[i] in pts2 frame.
00280 //  ie error is sum (p2_i-T(p1_i)'*wt_mat[i]*(p2_i-T(p1_i)
00281 void msm_similarity_aligner::calc_transform_wt_mat(
00282                               const msm_points& pts1,
00283                               const msm_points& pts2,
00284                               const vcl_vector<msm_wt_mat_2d>& wt_mat,
00285                               vnl_vector<double>& trans) const
00286 {
00287   assert(pts2.size()==pts1.size());
00288   assert(wt_mat.size()==pts1.size());
00289 
00290   // Perform computations relative to CoG,
00291   // to minimise rounding errors
00292   vgl_point_2d<double> cog1 = pts1.cog();
00293   vgl_point_2d<double> cog2 = pts2.cog();
00294 
00295 
00296   // Compute weighted CoGs
00297   const double* p1 = pts1.vector().begin();
00298   const double* p2 = pts2.vector().begin();
00299   const double* p1_end = pts1.vector().end();
00300   vcl_vector<msm_wt_mat_2d>::const_iterator w=wt_mat.begin();
00301 
00302   msm_wt_mat_2d w_sum(0,0,0);
00303   msm_wt_mat_2d S(0,0,0); // Sum
00304   double tx_sum = 0, ty_sum=0;
00305   double txx_sum = 0, txy_sum = 0;
00306   double s31=0,s32=0,s41=0,s42=0;
00307 
00308   p1 = pts1.vector().begin();
00309   p2 = pts2.vector().begin();
00310   w=wt_mat.begin();
00311   for (;p1!=p1_end;p1+=2,p2+=2,++w)
00312   {
00313     double dpx = p1[0]-cog1.x();
00314     double dpy = p1[1]-cog1.y();
00315     double dtx = p2[0]-cog2.x();
00316     double dty = p2[1]-cog2.y();
00317 
00318     double wa=w->m11(), wb=w->m12(), wc=w->m22();
00319 
00320     w_sum += (*w);
00321 
00322     double wx = wa*dpx + wb*dpy;
00323     double wy = wb*dpx + wc*dpy;
00324     double vx = wb*dpx - wa*dpy;
00325     double vy = wc*dpx - wb*dpy;
00326     s31+=wx; s32+=vx;
00327     s41+=wy; s42+=vy;
00328 
00329     S += msm_wt_mat_2d(dpx*wx + dpy*wy,
00330                        dpx*wy - dpy*wx,
00331                        dpx*vy - dpy*vx);
00332 
00333     // Right hand side
00334     txx_sum += dtx*wx + dty*wy;
00335     txy_sum += dtx*vx + dty*vy;
00336     tx_sum += wa*dtx+wb*dty;
00337     ty_sum += wb*dtx+wc*dty;
00338   }
00339 
00340   vnl_matrix<double> M(4,4);
00341   M(0,0)=S.m11(); M(0,1)=S.m12(); M(0,2)=s31; M(0,3)=s41;
00342   M(1,0)=S.m21(); M(1,1)=S.m22(); M(1,2)=s32; M(1,3)=s42;
00343   M(2,0)=    s31; M(2,1)= s32; M(2,2)=w_sum.m11(); M(2,3)=w_sum.m12();
00344   M(3,0)=    s41; M(3,1)= s42; M(3,2)=w_sum.m21(); M(3,3)=w_sum.m22();
00345 
00346   vnl_vector<double> rhs(4);
00347   rhs[0]=txx_sum; rhs[1]=txy_sum; rhs[2]=tx_sum; rhs[3]=ty_sum;
00348 
00349   // M is symmetric, so attempt to use Cholesky
00350   vnl_cholesky chol(M,vnl_cholesky::estimate_condition);
00351   if (chol.rcond()>1e-6)
00352   {
00353     chol.solve(rhs,&trans);
00354   }
00355   else
00356   {
00357     vnl_svd<double> svd(M);
00358     trans = svd.solve(rhs);
00359   }
00360 
00361   double a = trans[0], b=trans[1];
00362 
00363   trans[0] = a - 1.0;
00364   // Include effects of translations by CoG
00365   trans[2] += cog2.x() - (a*cog1.x() - b*cog1.y());
00366   trans[3] += cog2.y() - (b*cog1.x() + a*cog1.y());
00367 }
00368 
00369 
00370   //: Apply transform to weight matrices (ie ignore translation component)
00371 void msm_similarity_aligner::transform_wt_mat(
00372                                 const vcl_vector<msm_wt_mat_2d>& wt_mat,
00373                                 const vnl_vector<double>& trans,
00374                                 vcl_vector<msm_wt_mat_2d>& new_wt_mat) const
00375 {
00376   double a = 1+trans[0], b=trans[1];
00377   new_wt_mat.resize(wt_mat.size());
00378   for (unsigned i=0;i<wt_mat.size();++i)
00379     new_wt_mat[i]=wt_mat[i].transform_by(a,b);
00380 }
00381 
00382 //: Returns params of pose such that pose(x) = pose1(pose2(x))
00383 vnl_vector<double> msm_similarity_aligner::compose(
00384                          const vnl_vector<double>& pose1,
00385                          const vnl_vector<double>& pose2) const
00386 {
00387   double a1=pose1[0]+1.0, b1=pose1[1];
00388   double a2=pose2[0]+1.0, b2=pose2[1];
00389 
00390   vnl_vector<double> p(4);
00391   double a3 = a1*a2-b1*b2;
00392   double b3 = b1*a2+a1*b2;
00393   p[0]= a3-1.0;
00394   p[1]= b3;
00395   p[2]= a1*pose2[2]-b1*pose2[3]+pose1[2];
00396   p[3]= b1*pose2[2]+a1*pose2[3]+pose1[3];
00397   return p;
00398 }
00399 
00400 //: Apply transform to generate points in some reference frame
00401 //  For instance, depending on transform, may translate so the
00402 //  centre of gravity is at the origin and scale to a unit size.
00403 void msm_similarity_aligner::normalise_shape(msm_points& points) const
00404 {
00405   vgl_point_2d<double> cog = points.cog();
00406   points.translate_by(-cog.x(),-cog.y());
00407   double L = points.vector().magnitude();
00408   if (L>1e-6) points.scale_by(1.0/L);
00409 }
00410 
00411 
00412 //: Find poses which align a set of points
00413 //  On exit ref_mean_shape is the mean shape in the reference
00414 //  frame, pose_to_ref[i] maps points[i] into the reference
00415 //  frame (ie pose is the mapping from the reference frame to
00416 //  the target frames).
00417 // \param average_pose Average mapping from ref to target frame
00418 void msm_similarity_aligner::align_set(const vcl_vector<msm_points>& points,
00419                                        msm_points& ref_mean_shape,
00420                                        vcl_vector<vnl_vector<double> >& pose_to_ref,
00421                                        vnl_vector<double>& average_pose) const
00422 {
00423   vcl_size_t n_shapes = points.size();
00424   assert(n_shapes>0);
00425   pose_to_ref.resize(n_shapes);
00426 
00427   // Use first shape as initial reference
00428   ref_mean_shape = points[0];
00429   normalise_shape(ref_mean_shape);
00430 
00431   // Record normalised first shape to define initial orientation
00432   msm_points base_shape = ref_mean_shape;
00433 
00434   vnl_vector<double> pose_from_ref;
00435   msm_points new_mean=ref_mean_shape;
00436   average_pose.set_size(4);
00437 
00438   double change=1.0;
00439 
00440   unsigned n_its=0;
00441 
00442   while (change>1e-6 && n_its<10)
00443   {
00444     ref_mean_shape = new_mean;
00445     normalise_shape(ref_mean_shape);
00446 
00447     average_pose.fill(0);
00448 
00449     for (unsigned i=0;i<n_shapes;++i)
00450     {
00451       calc_transform_from_ref(ref_mean_shape,points[i],pose_from_ref);
00452       pose_to_ref[i]=inverse(pose_from_ref);
00453       average_pose+=pose_from_ref;
00454     }
00455 
00456     mean_of_transformed(points,pose_to_ref,new_mean);
00457 
00458     // new_mean should be in the subspace orthogonal to
00459     // the mean and to rotation defined by the mean.
00460 
00461     change = (new_mean.vector()-ref_mean_shape.vector()).rms();
00462     n_its++;
00463   }
00464 
00465   // This will get translation right, but will not cope well
00466   // with large rotation variations.
00467   average_pose/=n_shapes;
00468 }
00469 
00470 //=======================================================================
00471 
00472 vcl_string msm_similarity_aligner::is_a() const
00473 {
00474   return vcl_string("msm_similarity_aligner");
00475 }
00476 
00477 //: Create a copy on the heap and return base class pointer
00478 msm_aligner* msm_similarity_aligner::clone() const
00479 {
00480   return new msm_similarity_aligner(*this);
00481 }
00482 
00483