图像配准系列之基于FFD形变与梯度下降法的图像配准

前面我们讲到梯度下降法时,就有提到:人们经常要求解一个问题的最优解,通常做法是对该问题进行数学建模,转换成一个目标函数,然后通过一定的算法寻求该函数的最小值,最终寻求到最小值时的输入参数就是问题的最优解。

当我们有两张图像A和B,图像A与图像B形状相似,但具有位置偏移:平移、旋转、缩放、局部扭曲形变等。如果以图像A为基准图像,以图像B为浮动图像,对图像B进行配准,使其与图像A位置对齐,那么我们可以使用FFD自由形变作为坐标变换模型,对图像B进行形变,并计算图像A与形变之后的图像B的相似度,通过求解FFD形变的最优控制参数,使得两者相似度达到最大,从而使用最优控制参数对图像B进行形变,实现其配准

我们把图像A与FFD形变之后图像B的相似度作为目标函数,然后使用优化算法来求解这个目标函数的最优解,本文我们使用的优化算法是梯度下降法。

目标函数的示意图所下图所示:

梯度下降法求最优控制参数的示意图如下图所示:

下面我们分点讲解所有的步骤,然后再上C++代码。

1. 梯度下降法原理。

其原理我们在之前的文章已经详细讲解,此处不再重复,读者可参考博主的以下博文:

梯度下降法详解

2. FFD自由变换原理。

FFD的原理我们在之前的博文中也讲解过:

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

3. 图像的相似度衡量与目标函数。

常见的图像相似度衡量指标有峰值信噪比(PSNR)、结构相似度(SSIM)、归一化互相关(NCC)、归一化互信息(NMI)、均方误差(MSE)等。由于归一化互相关系数计算相对简单,且具有的良好凹凸特性利于求解最优参数,因此经过综合考虑,本文选择归一化互相关系数作为相似度的衡量指标。假设图像A与图像B都是m行n列的图像,那么归一化互相关系数可以按照下式计算:

上式中,归一化互相关NCC越大,说明图像A与图像B越相似,反之则两者差异越大。由于梯度下降法为求解目标函数的最小值,所以我们需要把以上函数取个倒数,使得NCC越小,图像的相似度越高:

需要注意,计算NCC的图像B是经过FFD形变的,因此目标函数的数学表达式如下,其中X为FFD形变模型的所有控制参数。

很多时候,图像之间存在很多局部的形变差异,所以通过分块来求解的NCC'更能表现两图的相似度。因此我们在以上基础上,在对图像A与图像B进行相同的分块,计算两图中对应位置块的NCC',最后再取所有块的NCC'的平均值作为整张图像的NCC'。假设把图像的高平均分为r块,宽平均分为c块,那么最终的目标函数F的表达式如下:

4. C++代码实现。

(1) 分块计算归一化互相关代码

double cal_cc_block(Mat S1, Mat Si, int row, int col)
{
  int ksize_row = S1.rows/row;
  int ksize_col = S1.cols/col;


  Mat tmp1, tmpi;
  double sum = 0.0;
  for(int i = 0; i < row; i++)
  {
    int i_begin = i*ksize_row;
    for(int j = 0; j < col; j++)
    {
      double sum1 = 0.0;
      double sum2 = 0.0;
      double sum3 = 0.0;
      int j_begin = j*ksize_col;
      for (int t1 = i_begin; t1 < i_begin+ksize_row; t1++)
      {
        uchar *S1_data = S1.ptr<uchar>(t1);
        uchar *Si_data = Si.ptr<uchar>(t1);
        for (int t2 = j_begin; t2 < j_begin+ksize_col; t2++)
        {
          sum1 += S1_data[t2]*Si_data[t2];    
          sum2 += S1_data[t2]*S1_data[t2];  
          sum3 += Si_data[t2]*Si_data[t2];  
        }
      }
      sum += sqrt(sum2*sum3)/(sum1+0.0000001);


    }
  }


  sum /= (row*col);


  return sum;
}

(2) 初始化控制参数代码

#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--);
  }
}

(3) 基于C++与CUDA实现的FFD变换代码

__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);
}

(4) 目标函数代码

float F_fun_bpline(Mat S1, Mat Si, int row_block_num, int col_block_num, Mat grid_points)
{
  double result;
  Mat Si_tmp;
  
  Bspline_Ffd_cuda(Si, Si_tmp, row_block_num, col_block_num, grid_points);


  result = cal_cc_block(S1, Si_tmp, 5, 5);   //默认分为5*5块计算互相关


  return result;
}

(5) 求取梯度代码

void cal_gradient(Mat S1, Mat Si, int row_block_num, int col_block_num, Mat grid_points, Mat &gradient)
{
  float EPS = 1;//1e-4f;


  gradient.create(grid_points.size(), CV_32FC1);
  
  float a1 = F_fun_bpline(S1, Si, row_block_num, col_block_num, grid_points);


  Mat grid_p = grid_points.clone();
  for(int i = 0; i < grid_points.cols; i++)
  {
    grid_p.at<float>(0, i) += EPS;
    float a2 = F_fun_bpline(S1, Si, row_block_num, col_block_num, grid_p);
    grid_p.at<float>(0, i) -= EPS;
    gradient.at<float>(0, i) = (a2 - a1) / EPS;   
  }
}

(6) 使用梯度来更新控制参数的代码

void update_grid_points(Mat &grid_points, Mat gradient, float alpha)
{
  for(int i = 0; i < grid_points.cols; i++)
  {
    grid_points.at<float>(0, i) = grid_points.at<float>(0, i) - gradient.at<float>(0, i)*alpha;
  }
}

(7) 梯度下降法代码

int bpline_match(Mat S1, Mat Si, Mat &M, int row_block_num, int col_block_num, Mat &grid_points)
{
  int max_iter = 5000;   //最多迭代次数
  Mat gradient, pre_gradient;
  Mat pre_grid_points;
  double e = 0.000005;//定义迭代精度
  float ret1 = 0.0;
  float ret2 = 0.0;
  int cnt = 0;
  float alpha = 8000000;
  //求梯度
  cal_gradient(S1, Si, row_block_num, col_block_num, grid_points, gradient);   //求梯度
  int out_cnt = 0;


  while (cnt < max_iter)
  {


    pre_grid_points = grid_points.clone();
    update_grid_points(grid_points, gradient, alpha);  //更新输入参数


    ret1 = F_fun_bpline(S1, Si, row_block_num, col_block_num, pre_grid_points);//F_fun(S1, Si, S1_entropy, delta_x, delta_y, pre_grid_points);
    ret2 = F_fun_bpline(S1, Si, row_block_num, col_block_num, grid_points);//F_fun(S1, Si, S1_entropy, delta_x, delta_y, grid_points);


    if (ret2 > ret1)  //如果当前轮迭代的目标函数值大于上一轮的函数值,则减小步长并重新计算梯度、重新更新参数
    {
      alpha *= 0.8;
      grid_points = pre_grid_points.clone();
      continue;
    }


    cout<<"f="<<ret2<<", alpha="<<alpha<<endl;


    if (abs(ret2 - ret1) < e)
    {
      out_cnt++;
      if(out_cnt >= 4)   //如果连续4次目标函数值不改变,则认为求到了最优解,停止迭代
      {
        Bspline_Ffd_cuda(Si, M, row_block_num, col_block_num, grid_points);
        return 0;
      }
    }
    else
    {
      out_cnt = 0;
    }
    gradient.copyTo(pre_gradient);
    //求梯度
    cal_gradient(S1, Si, row_block_num, col_block_num, grid_points, gradient);  //求梯度
    
    //如果梯度相比上一次迭代有所下降,则增大步长
    if(norm(gradient, NORM_L2) <= norm(pre_gradient, NORM_L2))
       alpha *= 2;
    
    cnt++;
  }


  return -1;


}

(8) 测试代码

void ffd_match_test(void)
{
  Mat img1 = imread("lena.jpg", CV_LOAD_IMAGE_GRAYSCALE);
  Mat img2 = imread("lena_out.jpg", CV_LOAD_IMAGE_GRAYSCALE);


  int row_block_num = 30;
  int col_block_num = 30;
  
  Mat grid_points;
  init_bpline_para(img1, row_block_num, col_block_num, grid_points, -0.001, 0.001);


  Mat out;
  bpline_match(img1, img2, out, row_block_num, col_block_num, grid_points);


  imshow("img1", img1);
  imshow("img2", img2);
  imshow("out", out);
  imshow("img1-img2", abs(img1-img2));
  imshow("img1-out", abs(img1-out));
  waitKey();
}

运行上述代码,对扭曲的Lena图像进行配准,结果如下图所示。可以看到,配准之后,配准图像没有那么扭曲了,也即与基准图像的位置更加对齐了。

基准图像

浮动图像

配准图像

基准图像与浮动图像的差值图

基准图像与配准图像的差值图

目标函数值的下降过程

微信公众号如下,欢迎关注:

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

萌萌哒程序猴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值