图像配准系列之基于B样条的FFD自由变换原理与C++实现

基于B样条的FFD变换属于一种网格型的非刚性形变模型,它按照一定的间距在图像上分布一系列的网格点,使用网格点的位置来计算每个像素点的坐标偏移,最后根据坐标偏移对图像进行像素重采样,实现其非刚性形变。

在图像配准中,通常需要根据图像的形变特性选择一种合适的形变模型,如果图像整体运动比较大,则选择整体平移、仿射变换等整体变换模型,如果图像局部形变比较大,则选择薄板样条变换、FFD变换等具有局部特性的变换模型。如下图所示,FFD变换模型具有局部扭曲的特性:

1. FFD计算原理

假设有一张m行n列的图像,如下图中的紫色区域,使用一张r+3行c+3列的网格来覆盖在这张图像上面,其中网格节点称为控制点,每个控制点对应2个控制参数φx和φy,分别为x方向和y方向的控制参数,因此总共有(r+3)*(c+3)*2个控制参数。FFD变换的核心思想是使用每个像素点周围4*4个控制点的控制参数来计算它的位置偏移。之所以网格的行与列都加3,是为了确保图像的边缘点(如下图中的红点)也可以取到其周围4*4的网格节点。

对于m*n的FFD形变图像的任意像素点(x, y),现在我们来计算它相对于原图像的坐标变偏移量。

(1) 计算该像素点的浮点型网格坐标(x_block, y_block)、整型网格坐标(j, i),以及浮点型网格坐标的小数部分(u, v)。

如下式所示,其中的floor表示对浮点数向下取整,也即把其小数部分截断掉,只取整数部分,比如floor(1.25)的结果为1。

(2) 计算形变的权重系数。

权重系数分为x方向与y方向,各4个系数:pX0~pX3与pY0~pY3

上式中的BsplineBase函数为B样条的基函数:

(3) 计算坐标偏移并插值。

假设点(x, y)的坐标偏移量为(Tx, Ty),那么(Tx, Ty)按以下公式计算:

得到坐标偏移量之后,就得到形变之后的对应坐标(x', y'):

(x, y)为形变图像的坐标,(x', y')为对应的原图像上的坐标。由于(x', y')为浮点坐标,因此需要插值计算其像素值,然后再把插值计算得到的像素值赋值给形变图像的点(x, y)。考虑到最邻近插值容易出现边缘锯齿,因此我们使用双线性插值:

上式中,I为原图像,I'为形变图像。

2. 代码实现

核心代码如下:

void Bspline_Ffd(Mat srcimg, Mat &dstimg, int row_block_num, int col_block_num, Mat grid_points)
{
  dstimg.create(srcimg.size(), srcimg.type());


  float delta_x = srcimg.cols*1.0/col_block_num;
  float delta_y = srcimg.rows*1.0/row_block_num;
  int grid_rows = row_block_num + BPLINE_BOARD_SIZE;
  int grid_cols = col_block_num + BPLINE_BOARD_SIZE;
  int grid_size = grid_rows*grid_cols;


  float pX[4], pY[4];


  for (int y = 0; y < srcimg.rows; y++)   //B_spline 变形 
  {
    for (int x = 0; x < srcimg.cols; x++)
    {
      float y_block = y / delta_y;
      float x_block = x / delta_x;
      int i = floor(y_block);
      int j = floor(x_block);
      float u = x_block - j;
      float v = y_block - i;
      
      //使用基函数计算权重系数
      pX[0] = (1 - u*u*u + 3*u*u - 3*u) / 6.0;
      pX[1] = (4 + 3*u*u*u - 6*u*u) / 6.0;
      pX[2] = (1 - 3*u*u*u + 3*u*u + 3*u) / 6.0;
      pX[3] = u*u*u / 6.0;
      pY[0] = (1 - v*v*v + 3*v*v - 3*v) / 6.0;
      pY[1] = (4 + 3*v*v*v - 6*v*v) / 6.0;
      pY[2] = (1 - 3*v*v*v + 3*v*v + 3*v) / 6.0;
      pY[3] = v*v*v / 6.0;


      float Tx = 0;
      float Ty = 0;
      for (int m = 0; m < 4; m++)
      {
        for (int n = 0; n < 4; n++)
        {
          int control_point_x = j + n;
          int control_point_y = i + m;


          float temp = pY[m] * pX[n];
          Tx += temp*grid_points.at<float>(0, control_point_y*grid_cols+control_point_x);    //x
          Ty += temp*grid_points.at<float>(0, control_point_y*grid_cols+control_point_x+grid_size);    //y
        } 
      }
      
      float src_x = x + Tx;
      float src_y = y + Ty;
      int x1 = cvFloor(src_x);
      int y1 = cvFloor(src_y);
      if (x1 < 1 || x1 >= srcimg.cols-1 || y1 < 1 || y1 >= srcimg.rows-1)//越界
      {
        dstimg.at<uchar>(y, x) = 0;
      }
      else
      {
        //dstimg.at<uchar>(y, x) = srcimg.at<uchar>(y1, x1);    //最邻近插值
        int x2 = x1 + 1;
        int y2 = y1 + 1;
        uchar pointa = srcimg.at<uchar>(y1, x1);   
        uchar pointb = srcimg.at<uchar>(y1, x2);
        uchar pointc = srcimg.at<uchar>(y2, x1);
        uchar pointd = srcimg.at<uchar>(y2, x2);
        uchar gray = (uchar)((x2 - src_x)*(y2 - src_y)*pointa - (x1 - src_x)*(y2 - src_y)*pointb - (x2 - src_x)*(y1 - src_y)*pointc + (x1 - src_x)*(y1 - src_y)*pointd);
        dstimg.at<uchar>(y, x) = gray;
      }
    }
  }
}

以上代码中,row_block_num为网格中每行的控制点数,col_block_num为网格中每列的控制点数,grid_points为控制参数,它是一个2行(row_block_num+3)*(col_block_num+3)列的矩阵,第一行是x方向的控制参数,第2行是y方向的控制参数。为了测试以上实现的FFD形变,我们再来写一个初始化控制参数的函数,其功能是随机生成2*(row_block_num+3)*(col_block_num+3)个范围在min~max之间的随机数作为控制参数:



#define randf(a, b) (((rand()%10000+rand()%10000*10000)/100000000.0)*((b)-(a))+(a))


void init_bpline_para(Mat src, int row_block_num, int col_block_num, Mat &grid_points, float min, float max)
{
  int grid_rows = row_block_num + BPLINE_BOARD_SIZE;
  int grid_cols = col_block_num + BPLINE_BOARD_SIZE;
  
  int grid_size = grid_rows*grid_cols;
  grid_points.create(Size(2*grid_size, 1), CV_32FC1);


  float *grid_points_data = grid_points.ptr<float>(0);
  srand((unsigned int)time(NULL));
  for (int i = 0; i < grid_size; i++)
  {
    grid_points_data[i] = randf(min, max);     //x


    int cnt = 100000000;
    while(cnt--);
    
    grid_points_data[i+grid_size] = randf(min, max);    //y


    cnt = 100000000;
    while(cnt--);
  }
}

测试代码如下:

void ffd_test(void)
{
  Mat img = imread("lena.jpg", CV_LOAD_IMAGE_GRAYSCALE);
  
  int row_block_num = 30;
  int col_block_num = 30;
  
  Mat grid_points;
  init_bpline_para(img, row_block_num, col_block_num, grid_points, -10, 10);


  Mat out;
  Bspline_Ffd(img, out, row_block_num, col_block_num, grid_points);


  imshow("img", img);
  imshow("out", out);
  waitKey();
}

运行以上代码,对496*472的Lena图像进行FFD形变,得到结果如下。可以看到,图像被扭曲了,这是因为我们把控制参数在一定范围内随机化了。

原图像

FFD形变图像

以上实现的代码对496*472的Lena图像进行FFD形变,耗时约52毫秒,在图像配准中需要频繁计算FFD形变,因此总体会非常耗时,故我们使用CUDA来并行优化以上代码:

__global__ void Bspline_Ffd_kernel(uchar *srcimg, uchar *dstimg, int row_block_num, int col_block_num, float *grid_points, int row, int col)
{
  int x = threadIdx.x + blockDim.x * blockIdx.x;  //col
    int y = threadIdx.y + blockDim.y * blockIdx.y;  //row


  if(x < col && y < row)
  {
    int grid_rows = row_block_num + BPLINE_BOARD_SIZE;
    int grid_cols = col_block_num + BPLINE_BOARD_SIZE;
    int grid_size = grid_rows*grid_cols;
    float delta_x = col*1.0/col_block_num;
    float delta_y = row*1.0/row_block_num;
    float x_block = x / delta_x;
    float y_block = y / delta_y;
    
    int j = floor(x_block);
    int i = floor(y_block);
    float u = x_block - j;
    float v = y_block - i;


    float pX[4], pY[4];
    pX[0] = (1 - u*u*u + 3*u*u - 3*u) / 6.0;
    pX[1] = (4 + 3*u*u*u - 6*u*u) / 6.0;
    pX[2] = (1 - 3*u*u*u + 3*u*u + 3*u) / 6.0;
    pX[3] = u*u*u / 6.0;
    pY[0] = (1 - v*v*v + 3*v*v - 3*v) / 6.0;
    pY[1] = (4 + 3*v*v*v - 6*v*v) / 6.0;
    pY[2] = (1 - 3*v*v*v + 3*v*v + 3*v) / 6.0;
    pY[3] = v*v*v / 6.0;


    float Tx = 0;
    float Ty = 0;
    for (int m = 0; m < 4; m++)   //行
    {
      for (int n = 0; n < 4; n++)  //列
      {
        int control_point_x = j + n;
        int control_point_y = i + m;


        float temp = pY[m] * pX[n];
        
        Tx += temp*grid_points[control_point_y*grid_cols+control_point_x];    //x
        Ty += temp*grid_points[control_point_y*grid_cols+control_point_x+grid_size];    //y
      } 
    }
    
    float src_x = x + Tx;
    float src_y = y + Ty;
    int x1 = floor(src_x);
    int y1 = floor(src_y);
    
    if (x1 < 1 || x1 >= col-1 || y1 < 1 || y1 >= row-1)//越界
    {
      dstimg[y*col+x] = 0;
    }
    else
    {
      //dstimg[y*col+x] = srcimg[y1*col+x1];    //最邻近插值
      int x2 = x1 + 1;    //双线性插值
      int y2 = y1 + 1;
      uchar pointa = srcimg[y1*col+x1];   
      uchar pointb = srcimg[y1*col+x2];
      uchar pointc = srcimg[y2*col+x1];
      uchar pointd = srcimg[y2*col+x2];
      uchar gray = (uchar)((x2 - src_x)*(y2 - src_y)*pointa - (x1 - src_x)*(y2 - src_y)*pointb - (x2 - src_x)*(y1 - src_y)*pointc + (x1 - src_x)*(y1 - src_y)*pointd);
      dstimg[y*col+x] = gray;
    }
  }
}




void Bspline_Ffd_cuda(Mat srcimg, Mat &dstimg, int row_block_num, int col_block_num, Mat grid_points)
{
  
  dim3 Bpline_Block(16, 16);   //每个线程块有16*16个线程
  int M = (srcimg.cols+Bpline_Block.x-1)/Bpline_Block.x; 
  int N = (srcimg.rows+Bpline_Block.y-1)/Bpline_Block.y;
  dim3 Bpline_Grid(M, N);


  int grid_rows = row_block_num + BPLINE_BOARD_SIZE;
  int grid_cols = col_block_num + BPLINE_BOARD_SIZE;
  int grid_size = grid_rows*grid_cols;
  int img_size = srcimg.cols*srcimg.rows;


  uchar *srcimg_cuda;
  uchar *dstimg_cuda;
  float *grid_points_cuda;


  cudaMalloc((void**)&srcimg_cuda, img_size);
  cudaMalloc((void**)&dstimg_cuda, img_size);
  cudaMalloc((void**)&grid_points_cuda, 2*grid_size*sizeof(float));
  cudaMemcpy(srcimg_cuda, srcimg.data, img_size, cudaMemcpyHostToDevice);
  cudaMemcpy(grid_points_cuda, grid_points.data, 2*grid_size*sizeof(float), cudaMemcpyHostToDevice);


  Bspline_Ffd_kernel<< <Bpline_Grid, Bpline_Block >> >(srcimg_cuda, dstimg_cuda, row_block_num, col_block_num, grid_points_cuda, srcimg.rows, srcimg.cols);


  Mat tmp(srcimg.size(), CV_8UC1);
  cudaMemcpy(tmp.data, dstimg_cuda, img_size, cudaMemcpyDeviceToHost);
  tmp.copyTo(dstimg);


  cudaFree(srcimg_cuda);
  cudaFree(dstimg_cuda);
  cudaFree(grid_points_cuda);
}

cuda加速之后,对同样的Lena图像进行形变,耗时减少到约3.6毫秒,因此加速效果还是相当显著的。

微信公众号如下,欢迎扫码关注,欢迎私信技术交流:

  • 16
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萌萌哒程序猴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值