当前位置:   article > 正文

TPS薄板样条变换计算原理及C++实现

tps算法 矩阵

TPS薄板样条变换属于一种非刚性形变,该形变算法的输入为两张图像中多组相同部位的匹配点对,输出为两张图像的相同部位的坐标映射,比如图A的点(x1,y1)对应图B的点(x1',y1'),图A的点(x2,y2)对应图B的点(x2',y2'),图A的点(x3,y3)对应图B的点(x3',y3')等等。举个简单例子,假设图B相对于图A的位置,相当于把图A从原来的位置整体向右平移一段距离d,那么经过TPS变换之后得到的坐标对应为:A的每一个像素点向右平移距离d,则是B上对应的点。当然,A与B之间往往还有更复杂的形变,这种情况下坐标对应关系则不是整体一样了,A上每个像素点对应B上每个像素点的情况各有不同,这正体现了TPS变换的非刚性。

TPS变换通常用于图像配准,由于其输入为多组匹配点对,TPS变换之前需要先获取两张图像的多组匹配点对,常见的获取匹配点对算法为Shift、Surf、Orb,以及光流跟踪等。本文主要讲TPS变换的原理,暂时把获取匹配点对的算法放在一边。我们假设已经获取到两张图像的n组匹配点对:(P1(x1,y1),P1’(x1’,y1’))、P2((x2,y2),P2’(x2’,y2’))、…、(Pn(xn,yn), Pn’(xn’,yn’))。那么使用TPS变换计算图A与图B的坐标对应关系的过程如下。

TPS形变的目标是求解一个函数f,使得f(Pi)=Pi’ (1≤i≤n),并且弯曲能量函数最小,同时图像上的其它点也可以通过插值得到很好的校正。可以把形变函数想象成弯折一块薄钢板,使这块钢板穿过给定的n个点,弯曲钢板所需能量可表示为:

可以证明薄板样条的插值函数就是弯曲能量最小的函数:

上式中只要求出a1,a2,a3和wi(1≤i≤n),就可以确定f(x,y),其中U为基函数:

记矩阵K、L、Y为:

由LW=Y解得W矩阵:

从而有A的任意坐标(xi,yi)到B的任意坐标(xi’,yi’)的映射:

讲完计算原理,下面我们上基于C++与Opencv的代码:

基函数的计算:

  1. //TPS基函数,r为两点距离的平方
  2. double tps_basis(double r)
  3. {
  4. if(r == 0.0)
  5. {
  6. return 0.0;
  7. }
  8. else
  9. {
  10. return (r*log(r));
  11. }
  12. }

K矩阵的计算:

  1. //n*n矩阵
  2. void cal_K(vector<Point2f> P, Mat &K)
  3. {
  4. if(K.empty())
  5. {
  6. K.create(P.size(), P.size(), CV_32FC1);
  7. }
  8. else if(K.rows != P.size() || K.cols != P.size())
  9. {
  10. resize(K, K, Size(P.size(), P.size()));
  11. }
  12. for(int i = 0; i < P.size(); i++)
  13. {
  14. for(int j = i; j < P.size(); j++)
  15. {
  16. Point2f diff_p = P[i] - P[j];
  17. double diff = diff_p.x*diff_p.x + diff_p.y*diff_p.y;
  18. float U = (float)tps_basis(diff);
  19. K.ptr<float>(i)[j] = U;
  20. K.ptr<float>(j)[i] = U;
  21. }
  22.   }
  23. }

L矩阵的计算:

  1. //(n+3)*(n+3)矩阵
  2. void cal_L(vector<Point2f> points1, Mat &L)
  3. {
  4. int N = points1.size();
  5. Mat L_tmp = Mat::zeros(Size(N+3, N+3), CV_32FC1);
  6. Mat P = Mat::ones(Size(3, N), CV_32FC1);
  7. for(int i = 0; i < N; i++)
  8. {
  9. P.ptr<float>(i)[1] = points1[i].x;
  10. P.ptr<float>(i)[2] = points1[i].y;
  11. }
  12. Mat P_T;
  13. transpose(P, P_T);
  14. Mat K;
  15. cal_K(points1, K);
  16. K.copyTo(L_tmp(Rect(0, 0, N, N)));
  17. P.copyTo(L_tmp(Rect(N, 0, 3, N)));
  18. P_T.copyTo(L_tmp(Rect(0, N, N, 3)));
  19. L = L_tmp.clone();
  20. }

W矩阵的计算:

  1. //W = {w0, w1, w2, ..., a1, ax, ay}
  2. void cal_W(vector<Point2f> points1, vector<Point2f> points2, Mat &W)
  3. {
  4.   int N = points1.size();
  5. Mat L;
  6. cal_L(points1, L);
  7. Mat Y = Mat::zeros(Size(2, N+3), CV_32FC1); //row:N+3, col:2
  8. for(int i = 0; i < N; i++)
  9. {
  10. Y.ptr<float>(i)[0] = points2[i].x;
  11. Y.ptr<float>(i)[1] = points2[i].y;
  12. }
  13.   solve(L, Y, W, DECOMP_LU);   //LW = Y,求W
  14. }

坐标映射的计算,计算图A与图B中每一个点的坐标映射关系:

  1. Point2f Tps_Transformation(vector<Point2f> points1, Point2f point, Mat W)
  2. {
  3.     Point2f out;
  4.     float a1_x = W.at<float>(W.rows-30);
  5. float ax_x = W.at<float>(W.rows-2, 0);
  6. float ay_x = W.at<float>(W.rows-1, 0);
  7.     float a1_y = W.at<float>(W.rows-31);
  8. float ax_y = W.at<float>(W.rows-2, 1);
  9. float ay_y = W.at<float>(W.rows-1, 1);
  10. float affine_x = a1_x+ax_x*point.x + ay_x*point.y;
  11. float affine_y = a1_y+ax_y*point.x + ay_y*point.y;
  12. float nonrigid_x = 0;
  13. float nonrigid_y = 0;
  14. for (int j = 0; j < points1.size(); j++)
  15. {
  16. Point2f diff_p = points1[j] - point;
  17. double diff = diff_p.x*diff_p.x + diff_p.y*diff_p.y;
  18. double tps_diff = tps_basis(diff);
  19. nonrigid_x += W.at<float>(j, 0)*tps_diff;
  20. nonrigid_y += W.at<float>(j, 1)*tps_diff;
  21. }
  22. out.x = affine_x + nonrigid_x;
  23.    out.y = affine_y + nonrigid_y;
  24.   
  25. return out; //返回(x, y)对应的点坐标(x', y')
  26. }

由于每个点的坐标分为x坐标与y坐标,因此把获取到的点应该拆分为坐标与y坐标,所以最后得到Tx与Ty两个Mat矩阵,分别对应所有像素点的x坐标映射与y坐标映射。

  1. //获取图A与图B的每个点的映射坐标
  2. void Tps_TxTy(vector<Point2f> points1, vector<Point2f> points2, int row, int col, Mat &Tx, Mat &Ty)
  3. {
  4. Mat mapX(row, col, CV_32FC1);
  5. Mat mapY(row, col, CV_32FC1);
  6. Mat W;
  7. cal_W(points1, points2, W);
  8. for (int i = 0; i < row; i++)
  9. {
  10. for (int j = 0; j < col; j++)
  11.         {
  12. Point2f pt = Tps_Transformation(points1, Point2f(float(j), float(i)), W);
  13. mapX.at<float>(i, j) = pt.x;
  14. mapY.at<float>(i, j) = pt.y;
  15. }
  16. }
  17. mapX.copyTo(Tx);
  18.   mapY.copyTo(Ty);
  19. }

以上代码运行起来是非常慢的,因为其需要计算对数,每一个点的坐标映射大约需要计算n次(n组匹配点)对数与惩罚,耗时点主要在这里。在之后的文章中,我们再讲解使用CUDA来加速TPS变换的计算,敬请期待!

微信公众号:

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/128779
推荐阅读
相关标签
  

闽ICP备14008679号