OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换

霍夫线变换

目标

在这个部分您将学习到:

  • 使用OpenCV的以下函数 HoughLines 和 HoughLinesP 来检测图像中的直线.

原理

Note

 

以下原理的说明来自书籍 学习OpenCV 作者Bradski和Kaehler.

霍夫线变换

  1. 霍夫线变换是一种用来寻找直线的方法.
  2. 是用霍夫线变换之前, 首先要对图像进行边缘检测的处理,也即霍夫线变换的直接输入只能是边缘二值图像.

它是如何实现的?

  1. 众所周知, 一条直线在图像二维空间可由两个变量表示. 例如:

    1. 在 笛卡尔坐标系: 可由参数:  斜率和截距表示.
    2. 在 极坐标系: 可由参数:  极径和极角表示
    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第1张图片

    对于霍夫变换, 我们将用 极坐标系 来表示直线. 因此, 直线的表达式可为:

    y = \left ( -\dfrac{\cos \theta}{\sin \theta} \right ) x + \left ( \dfrac{r}{\sin \theta} \right )

    化简得: r = x \cos \theta + y \sin \theta

  2. 一般来说对于点 (x_{0}, y_{0}), 我们可以将通过这个点的一族直线统一定义为:

    r_{\theta} = x_{0} \cdot \cos \theta  + y_{0} \cdot \sin \theta

    这就意味着每一对  代表一条通过点 (x_{0}, y_{0}) 的直线.

  3. 如果对于一个给定点 (x_{0}, y_{0}) 我们在极坐标对极径极角平面绘出所有通过它的直线, 将得到一条正弦曲线. 例如, 对于给定点x_{0} = 8 and y_{0} = 6 我们可以绘出下图 (在平面  - r):

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第2张图片

    只绘出满足下列条件的点  and 0< \theta < 2 \pi.

  4. 我们可以对图像中所有的点进行上述操作. 如果两个不同点进行上述操作后得到的曲线在平面  - r 相交, 这就意味着它们通过同一条直线. 例如, 接上面的例子我们继续对点: , y_{1} = 4 和点 x_{2} = 12,  绘图, 得到下图:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第3张图片

    这三条曲线在  - r 平面相交于点 (0.925, 9.6), 坐标表示的是参数对 () 或者是说点 (x_{0}, y_{0}), 点 (x_{1}, y_{1}) 和点 (x_{2}, y_{2}) 组成的平面内的的直线.

  5. 那么以上的材料要说明什么呢? 这意味着一般来说, 一条直线能够通过在平面  - r 寻找交于一点的曲线数量来 检测. 越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成. 一般来说我们可以通过设置直线上点的 阈值 来定义多少条曲线交于一点我们才认为 检测 到了一条直线.

  6. 这就是霍夫线变换要做的. 它追踪图像中每个点对应曲线间的交点. 如果交于一点的曲线的数量超过了 阈值, 那么可以认为这个交点所代表的参数对  在原图像中为一条直线.

标准霍夫线变换和统计概率霍夫线变换

OpenCV实现了以下两种霍夫线变换:

  1. 标准霍夫线变换
  • 原理在上面的部分已经说明了. 它能给我们提供一组参数对  的集合来表示检测到的直线
  • 在OpenCV 中通过函数 HoughLines 来实现
  1. 统计概率霍夫线变换
  • 这是执行起来效率更高的霍夫线变换. 它输出检测到的直线的端点 (x_{0}, y_{0}, x_{1}, y_{1})
  • 在OpenCV 中它通过函数 HoughLinesP 来实现

代码

  1. 这个程序是用来做什么的?
    • 加载一幅图片
    • 对图片进行 标准霍夫线变换 或是 统计概率霍夫线变换.
    • 分别在两个窗口显示原图像和绘出检测到直线的图像.
  2. 我们将要说明的例程能从 这里 下载。 一个更高级的版本 (能同时演示标准霍夫线变换和统计概率霍夫线变换并带有活动条来改变变换的阈值) 能从 这里 下载。
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"

#include <iostream>

using namespace cv;
using namespace std;

void help()
{
 cout << "\nThis program demonstrates line finding with the Hough transform.\n"
         "Usage:\n"
         "./houghlines <image_name>, Default is pic1.jpg\n" << endl;
}

int main(int argc, char** argv)
{
 const char* filename = argc >= 2 ? argv[1] : "pic1.jpg";

 Mat src = imread(filename, 0);
 if(src.empty())
 {
     help();
     cout << "can not open " << filename << endl;
     return -1;
 }

 Mat dst, cdst;
 Canny(src, dst, 50, 200, 3);
 cvtColor(dst, cdst, CV_GRAY2BGR);

 #if 0
  vector<Vec2f> lines;
  HoughLines(dst, lines, 1, CV_PI/180, 100, 0, 0 );

  for( size_t i = 0; i < lines.size(); i++ )
  {
     float rho = lines[i][0], theta = lines[i][1];
     Point pt1, pt2;
     double a = cos(theta), b = sin(theta);
     double x0 = a*rho, y0 = b*rho;
     pt1.x = cvRound(x0 + 1000*(-b));
     pt1.y = cvRound(y0 + 1000*(a));
     pt2.x = cvRound(x0 - 1000*(-b));
     pt2.y = cvRound(y0 - 1000*(a));
     line( cdst, pt1, pt2, Scalar(0,0,255), 3, CV_AA);
  }
 #else
  vector<Vec4i> lines;
  HoughLinesP(dst, lines, 1, CV_PI/180, 50, 50, 10 );
  for( size_t i = 0; i < lines.size(); i++ )
  {
    Vec4i l = lines[i];
    line( cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, CV_AA);
  }
 #endif
 imshow("source", src);
 imshow("detected lines", cdst);

 waitKey();

 return 0;
}

代码说明

  1. 加载图片

    Mat src = imread(filename, 0);
    if(src.empty())
    {
      help();
      cout << "can not open " << filename << endl;
      return -1;
    }
    
  2. 用Canny算子对图像进行边缘检测

    Canny(src, dst, 50, 200, 3);
    

    现在我们将要执行霍夫线变换. 我们将会说明怎样使用OpenCV的函数做到这一点:

  3. 标准霍夫线变换

    1. 首先, 你要执行变换:

      vector<Vec2f> lines;
      HoughLines(dst, lines, 1, CV_PI/180, 100, 0, 0 );
      

      带有以下自变量:

      • dst: 边缘检测的输出图像. 它应该是个灰度图 (但事实上是个二值化图)
      • lines: 储存着检测到的直线的参数对  的容器 * rho : 参数极径 r 以像素值为单位的分辨率. 我们使用 1像素.
      • theta: 参数极角  以弧度为单位的分辨率. 我们使用 1度 (即CV_PI/180)
      • threshold: 要”检测” 一条直线所需最少的的曲线交点
      • srn and stn: 参数默认为0. 查缺OpenCV参考文献来获取更多信息.
    2. 通过画出检测到的直线来显示结果.

      for( size_t i = 0; i < lines.size(); i++ )
      {
        float rho = lines[i][0], theta = lines[i][1];
        Point pt1, pt2;
        double a = cos(theta), b = sin(theta);
        double x0 = a*rho, y0 = b*rho;
        pt1.x = cvRound(x0 + 1000*(-b));
        pt1.y = cvRound(y0 + 1000*(a));
        pt2.x = cvRound(x0 - 1000*(-b));
        pt2.y = cvRound(y0 - 1000*(a));
        line( cdst, pt1, pt2, Scalar(0,0,255), 3, CV_AA);
      }
      
  4. 统计概率霍夫线变换

    1. 首先, 你要执行变换:

      vector<Vec4i> lines;
      HoughLinesP(dst, lines, 1, CV_PI/180, 50, 50, 10 );
      

      带有以下自变量:

      • dst: 边缘检测的输出图像. 它应该是个灰度图 (但事实上是个二值化图) * lines: 储存着检测到的直线的参数对 (x_{start}, y_{start}, x_{end}, y_{end}) 的容器
      • rho : 参数极径 r 以像素值为单位的分辨率. 我们使用 1 像素.
      • theta: 参数极角  以弧度为单位的分辨率. 我们使用 1度 (即CV_PI/180)
      • threshold: 要”检测” 一条直线所需最少的的曲线交点 * minLinLength: 能组成一条直线的最少点的数量. 点数量不足的直线将被抛弃.
      • maxLineGap: 能被认为在一条直线上的亮点的最大距离.
    2. 通过画出检测到的直线来显示结果.

      for( size_t i = 0; i < lines.size(); i++ )
      {
        Vec4i l = lines[i];
        line( cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, CV_AA);
      }
      
  5. 显示原始图像和检测到的直线:

    imshow("source", src);
    imshow("detected lines", cdst);
    
  6. 等待用户按键推出程序

    waitKey();
    

结果

Note

 

得到的结果使用的是在上面 代码 部分提到的更高级版代码. 霍夫线变换的代码没有改变, 唯一不同的是在GUI的部分加入了活动条可动态改变阈值.输入图像为:

OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第4张图片

通过执行统计概率霍夫线变换我们能得到下面的结果:

当你使用滑动条来改变 阈值 的时候会观察到检测到线的数目的改变. 这是因为: 如果你设置了一个更大的阈值, 能检测到的线的数目将更少 (你需要更多的点来表示一条能检测到的直线).





霍夫圆变换

目标

在这个教程中你将学习如何:

  • 使用OpenCV函数 HoughCircles 在图像中检测圆.

原理

霍夫圆变换

  • 霍夫圆变换的基本原理和上个教程中提到的霍夫线变换类似, 只是点对应的二维极径极角空间被三维的圆心点x, y还有半径r空间取代.

  • 对直线来说, 一条直线能由参数极径极角 (r, \theta) 表示. 而对圆来说, 我们需要三个参数来表示一个圆, 如上文所说现在原图像的边缘图像的任意点对应的经过这个点的所有可能圆是在三维空间有下面这三个参数来表示了,其对应一条三维空间的曲线. 那么与二维的霍夫线变换同样的道理, 对于多个边缘点越多这些点对应的三维空间曲线交于一点那么他们经过的共同圆上的点就越多,类似的我们也就可以用同样的阈值的方法来判断一个圆是否被检测到, 这就是标准霍夫圆变换的原理, 但也正是在三维空间的计算量大大增加的原因, 标准霍夫圆变化很难被应用到实际中:

    C : ( x_{center}, y_{center}, r )

    这里的 (x_{center}, y_{center}) 表示圆心的位置 (下图中的绿点) 而 r 表示半径, 这样我们就能唯一的定义一个圆了, 见下图:

  • 出于上面提到的对运算效率的考虑, OpenCV实现的是一个比标准霍夫圆变换更为灵活的检测方法: 霍夫梯度法, 也叫2-1霍夫变换(21HT), 它的原理依据是圆心一定是在圆上的每个点的模向量上, 这些圆上点模向量的交点就是圆心, 霍夫梯度法的第一步就是找到这些圆心, 这样三维的累加平面就又转化为二维累加平面. 第二部根据所有候选中心的边缘非0像素对其的支持程度来确定半径. 21HT方法最早在Illingworth的论文The Adaptive Hough Transform中提出并详细描述, 也可参照Yuen在1990年发表的A Comparative Study of Hough Transform Methods for Circle Finding, Bradski的《学习OpenCV》一书则对OpenCV中具体对算法的具体实现有详细描述并讨论了霍夫梯度法的局限性.

例程

  1. 这个例程是用来干嘛的?
    • 加载一幅图像并对其模糊化以降噪
    • 对模糊化后的图像执行霍夫圆变换 .
    • 在窗体中显示检测到的圆.
  2. 下面要讲解的例程代码能从 这里 下载. 一个更高级的版本 (能同时演示标准霍夫圆变换和统计概率霍夫圆变换并带有可改变阈值的滑动条) 能从 这里 找到.
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>

using namespace cv;

/** @function main */
int main(int argc, char** argv)
{
  Mat src, src_gray;

  /// Read the image
  src = imread( argv[1], 1 );

  if( !src.data )
    { return -1; }

  /// Convert it to gray
  cvtColor( src, src_gray, CV_BGR2GRAY );

  /// Reduce the noise so we avoid false circle detection
  GaussianBlur( src_gray, src_gray, Size(9, 9), 2, 2 );

  vector<Vec3f> circles;

  /// Apply the Hough Transform to find the circles
  HoughCircles( src_gray, circles, CV_HOUGH_GRADIENT, 1, src_gray.rows/8, 200, 100, 0, 0 );

  /// Draw the circles detected
  for( size_t i = 0; i < circles.size(); i++ )
  {
      Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
      int radius = cvRound(circles[i][2]);
      // circle center
      circle( src, center, 3, Scalar(0,255,0), -1, 8, 0 );
      // circle outline
      circle( src, center, radius, Scalar(0,0,255), 3, 8, 0 );
   }

  /// Show your results
  namedWindow( "Hough Circle Transform Demo", CV_WINDOW_AUTOSIZE );
  imshow( "Hough Circle Transform Demo", src );

  waitKey(0);
  return 0;
}

说明

  1. 加载一幅图像

    src = imread( argv[1], 1 );
    
    if( !src.data )
      { return -1; }
    
  2. 转成灰度图:

    cvtColor( src, src_gray, CV_BGR2GRAY );
    
  3. 执行高斯模糊以降低噪声:

    GaussianBlur( src_gray, src_gray, Size(9, 9), 2, 2 );
    
  4. 执行霍夫圆变换:

    vector<Vec3f> circles;
    
    HoughCircles( src_gray, circles, CV_HOUGH_GRADIENT, 1, src_gray.rows/8, 200, 100, 0, 0 );
    

    函数带有以下自变量:

    • src_gray: 输入图像 (灰度图)
    • circles: 存储下面三个参数: x_{c}, y_{c}, r 集合的容器来表示每个检测到的圆.
    • CV_HOUGH_GRADIENT: 指定检测方法. 现在OpenCV中只有霍夫梯度法
    • dp = 1: 累加器图像的反比分辨率
    • min_dist = src_gray.rows/8: 检测到圆心之间的最小距离
    • param_1 = 200: Canny边缘函数的高阈值
    • param_2 = 100: 圆心检测阈值.
    • min_radius = 0: 能检测到的最小圆半径, 默认为0.
    • max_radius = 0: 能检测到的最大圆半径, 默认为0
  5. 绘出检测到的圆:

    for( size_t i = 0; i < circles.size(); i++ )
    {
       Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
       int radius = cvRound(circles[i][2]);
       // circle center
       circle( src, center, 3, Scalar(0,255,0), -1, 8, 0 );
       // circle outline
       circle( src, center, radius, Scalar(0,0,255), 3, 8, 0 );
     }
    

    你将会看到圆用红色绘出而圆心用小绿点表示

  6. 显示检测到的圆:

    namedWindow( "Hough Circle Transform Demo", CV_WINDOW_AUTOSIZE );
    imshow( "Hough Circle Transform Demo", src );
    
  7. 等待用户按键结束程序

    waitKey(0);
    

结果

上面例程输入一张图例得出的运行结果如下:

OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第5张图片




Remapping 重映射

目标

本教程向你展示如何使用OpenCV函数 remap 来实现简单重映射.

理论

重映射是什么意思?

  • 把一个图像中一个位置的像素放置到另一个图片指定位置的过程.

  • 为了完成映射过程, 有必要获得一些插值为非整数像素坐标,因为源图像与目标图像的像素坐标不是一一对应的.

  • 我们通过重映射来表达每个像素的位置 (x,y) :

    这里  是目标图像, f() 是源图像, h(x,y) 是作用于 (x,y) 的映射方法函数.

  • 让我们来思考一个快速的例子. 想象一下我们有一个图像 I , 我们想满足下面的条件作重映射:

    h(x,y) = (I.cols - x, y )

    会发生什么? 图像会按照 x 轴方向发生翻转. 例如, 源图像如下:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第6张图片

    看到红色圈关于 x 的位置改变( x 轴水平翻转):

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第7张图片
  • 通过 OpenCV 的函数 remap 提供一个简单的重映射实现.

代码

  1. 本程序做什么?
    • 装载一幅图像.
    • 程序按秒循环, 在一个窗口中顺序出现4种重映射过程对相同的图像.
    • 等待用户按 ‘ESC’ 键退出程序。
  2. 下面是本教程代码. 你也可以从 这里 下载。
 #include "opencv2/highgui/highgui.hpp"
 #include "opencv2/imgproc/imgproc.hpp"
 #include <iostream>
 #include <stdio.h>

 using namespace cv;

 /// Global variables
 Mat src, dst;
 Mat map_x, map_y;
 char* remap_window = "Remap demo";
 int ind = 0;

 /// Function Headers
 void update_map( void );

 /**
 * @function main
 */
 int main( int argc, char** argv )
 {
   /// Load the image
   src = imread( argv[1], 1 );

  /// Create dst, map_x and map_y with the same size as src:
  dst.create( src.size(), src.type() );
  map_x.create( src.size(), CV_32FC1 );
  map_y.create( src.size(), CV_32FC1 );

  /// Create window
  namedWindow( remap_window, CV_WINDOW_AUTOSIZE );

  /// Loop
  while( true )
  {
    /// Each 1 sec. Press ESC to exit the program
    int c = waitKey( 1000 );

    if( (char)c == 27 )
      { break; }

    /// Update map_x & map_y. Then apply remap
    update_map();
    remap( src, dst, map_x, map_y, CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0, 0) );

    /// Display results
    imshow( remap_window, dst );
  }
  return 0;
 }

 /**
 * @function update_map
 * @brief Fill the map_x and map_y matrices with 4 types of mappings
 */
 void update_map( void )
 {
   ind = ind%4;

   for( int j = 0; j < src.rows; j++ )
   { for( int i = 0; i < src.cols; i++ )
       {
         switch( ind )
         {
           case 0:
             if( i > src.cols*0.25 && i < src.cols*0.75 && j > src.rows*0.25 && j < src.rows*0.75 )
               {
                 map_x.at<float>(j,i) = 2*( i - src.cols*0.25 ) + 0.5 ;
                 map_y.at<float>(j,i) = 2*( j - src.rows*0.25 ) + 0.5 ;
                }
             else
               { map_x.at<float>(j,i) = 0 ;
                 map_y.at<float>(j,i) = 0 ;
               }
                 break;
           case 1:
                 map_x.at<float>(j,i) = i ;
                 map_y.at<float>(j,i) = src.rows - j ;
                 break;
           case 2:
                 map_x.at<float>(j,i) = src.cols - i ;
                 map_y.at<float>(j,i) = j ;
                 break;
           case 3:
                 map_x.at<float>(j,i) = src.cols - i ;
                 map_y.at<float>(j,i) = src.rows - j ;
                 break;
         } // end of switch
       }
    }
  ind++;
}

说明

  1. 首先准备程序用到的变量:

    Mat src, dst;
    Mat map_x, map_y;
    char* remap_window = "Remap demo";
    int ind = 0;
    
  2. 加载一幅图像:

    src = imread( argv[1], 1 );
    
  3. 创建目标图像和两个映射矩阵.( x 和 y )

    dst.create( src.size(), src.type() );
    map_x.create( src.size(), CV_32FC1 );
    map_y.create( src.size(), CV_32FC1 );
    
  4. 创建一个窗口用于展示结果.

    namedWindow( remap_window, CV_WINDOW_AUTOSIZE );
    
  5. 建立一个间隔1000毫秒的循环,每次循环执行更新映射矩阵参数并对源图像进行重映射处理(使用 mat_x 和 mat_y),然后把更新后的目标图像显示出来:

    while( true )
    {
      /// Each 1 sec. Press ESC to exit the program
      int c = waitKey( 1000 );
    
      if( (char)c == 27 )
        { break; }
    
      /// Update map_x & map_y. Then apply remap
      update_map();
      remap( src, dst, map_x, map_y, CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0, 0) );
    
      /// Display results
      imshow( remap_window, dst );
    }
    

    上面用到的重映射函数 remap. 参数说明:

    • src: 源图像
    • dst: 目标图像,与 src 相同大小
    • map_x: x方向的映射参数. 它相当于方法 h(i,j) 的第一个参数
    • map_y: y方向的映射参数. 注意 map_y 和 map_x 与 src 的大小一致。
    • CV_INTER_LINEAR: 非整数像素坐标插值标志. 这里给出的是默认值(双线性插值).
    • BORDER_CONSTANT: 默认

    如何更新重映射矩阵 mat_x 和 mat_y? 请继续看:

  6. 更新重映射矩阵: 我们将分别使用4种不同的映射:

    1. 图像宽高缩小一半,并显示在中间:

      所有成对的参数 (i,j) 处理后都符合: \dfrac{src.cols}{4}<i<\dfrac{3 \cdot src.cols}{4} 和 \dfrac{src.rows}{4}<j<\dfrac{3 \cdot src.rows}{4}

    2. 图像上下颠倒: h( i, j ) = (i, src.rows - j)

    3. 图像左右颠倒: h(i,j) = ( src.cols - i, j )

    4. 同时执行b和c的操作: h(i,j) = ( src.cols - i, src.rows - j )

下面的代码片段说明上述的映射过程. 在这里 map_x 代表第一个坐标 h(i,j) , map_y 是第二个坐标.

for( int j = 0; j < src.rows; j++ )
{ for( int i = 0; i < src.cols; i++ )
    {
      switch( ind )
      {
        case 0:
          if( i > src.cols*0.25 && i < src.cols*0.75 && j > src.rows*0.25 && j < src.rows*0.75 )
            {
              map_x.at<float>(j,i) = 2*( i - src.cols*0.25 ) + 0.5 ;
              map_y.at<float>(j,i) = 2*( j - src.rows*0.25 ) + 0.5 ;
             }
          else
            { map_x.at<float>(j,i) = 0 ;
              map_y.at<float>(j,i) = 0 ;
            }
              break;
        case 1:
              map_x.at<float>(j,i) = i ;
              map_y.at<float>(j,i) = src.rows - j ;
              break;
        case 2:
              map_x.at<float>(j,i) = src.cols - i ;
              map_y.at<float>(j,i) = j ;
              break;
        case 3:
              map_x.at<float>(j,i) = src.cols - i ;
              map_y.at<float>(j,i) = src.rows - j ;
              break;
      } // end of switch
    }
  }
 ind++;
}

结果

  1. 上面的代码编译后, 运行时给一个图片路径参数. 例如,使用下面的图片:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第8张图片
  2. 图像宽高缩小一半,并显示在中间:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第9张图片
  3. 图像上下颠倒:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第10张图片
  4. 图像左右颠倒:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第11张图片
  5. 两个方向同时颠倒:

OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第12张图片




仿射变换

目标

在这个教程中你将学习到如何:

  1. 使用OpenCV函数 warpAffine 来实现一些简单的重映射.
  2. 使用OpenCV函数 getRotationMatrix2D 来获得一个 2 \times 3 旋转矩阵

原理

什么是仿射变换?

  1. 一个任意的仿射变换都能表示为 乘以一个矩阵 (线性变换) 接着再 加上一个向量 (平移).

  2. 综上所述, 我们能够用仿射变换来表示:

    1. 旋转 (线性变换)
    2. 平移 (向量加)
    3. 缩放操作 (线性变换)

    你现在可以知道, 事实上, 仿射变换代表的是两幅图之间的 关系 .

  3. 我们通常使用 2 \times 3 矩阵来表示仿射变换.

    A = \begin{bmatrix}     a_{00} & a_{01} \\     a_{10} & a_{11}     \end{bmatrix}_{2 \times 2} B = \begin{bmatrix}     b_{00} \\     b_{10}     \end{bmatrix}_{2 \times 1} M = \begin{bmatrix}     A & B     \end{bmatrix} =\begin{bmatrix}     a_{00} & a_{01} & b_{00} \\     a_{10} & a_{11} & b_{10}\end{bmatrix}_{2 \times 3}

    考虑到我们要使用矩阵 A 和 B 对二维向量  做变换, 所以也能表示为下列形式:

    T = A \cdot \begin{bmatrix}x \\ y\end{bmatrix} + B or 

    T =  \begin{bmatrix}    a_{00}x + a_{01}y + b_{00} \\    a_{10}x + a_{11}y + b_{10}    \end{bmatrix}

怎样才能求得一个仿射变换?

  1. 好问题. 我们在上文有提到过仿射变换基本表示的就是两幅图片之间的 联系 . 关于这种联系的信息大致可从以下两种场景获得:

    1. 我们已知 X 和 T 而且我们知道他们是有联系的. 接下来我们的工作就是求出矩阵 M
    2. 我们已知 M and X. 要想求得 T. 我们只要应用算式 T = M \cdot X 即可. 对于这种联系的信息可以用矩阵 M 清晰的表达 (即给出明确的2×3矩阵) 或者也可以用两幅图片点之间几何关系来表达.
  2. 让我们形象地说明一下. 因为矩阵 M 联系着两幅图片, 我们以其表示两图中各三点直接的联系为例. 见下图:

    点1, 2 和 3 (在图一中形成一个三角形) 与图二中三个点一一映射, 仍然形成三角形, 但形状已经大大改变. 如果我们能通过这样两组三点求出仿射变换 (你能选择自己喜欢的点), 接下来我们就能把仿射变换应用到图像中所有的点.

例程

  1. 这个例程是做什么的?
    • 加载一幅图片
    • 对这幅图片应用仿射变换. 这个变换是从源图像和目标图像的两组三点之间的联系获得的. 这一步我们使用函数warpAffine 来实现.
    • 仿射变换结束后再对图像应用旋转. 这里的旋转绕图像中点
    • 等待用户退出程序
  2. 例程代码在下面给出. 当然你也可以从 这里 下载
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>

using namespace cv;
using namespace std;

/// 全局变量
char* source_window = "Source image";
char* warp_window = "Warp";
char* warp_rotate_window = "Warp + Rotate";

/** @function main */
 int main( int argc, char** argv )
 {
   Point2f srcTri[3];
   Point2f dstTri[3];

   Mat rot_mat( 2, 3, CV_32FC1 );
   Mat warp_mat( 2, 3, CV_32FC1 );
   Mat src, warp_dst, warp_rotate_dst;

   /// 加载源图像
   src = imread( argv[1], 1 );

   /// 设置目标图像的大小和类型与源图像一致
   warp_dst = Mat::zeros( src.rows, src.cols, src.type() );

   /// 设置源图像和目标图像上的三组点以计算仿射变换
   srcTri[0] = Point2f( 0,0 );
   srcTri[1] = Point2f( src.cols - 1, 0 );
   srcTri[2] = Point2f( 0, src.rows - 1 );

   dstTri[0] = Point2f( src.cols*0.0, src.rows*0.33 );
   dstTri[1] = Point2f( src.cols*0.85, src.rows*0.25 );
   dstTri[2] = Point2f( src.cols*0.15, src.rows*0.7 );

   /// 求得仿射变换
   warp_mat = getAffineTransform( srcTri, dstTri );

   /// 对源图像应用上面求得的仿射变换
   warpAffine( src, warp_dst, warp_mat, warp_dst.size() );

   /** 对图像扭曲后再旋转 */

   /// 计算绕图像中点顺时针旋转50度缩放因子为0.6的旋转矩阵
   Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
   double angle = -50.0;
   double scale = 0.6;

   /// 通过上面的旋转细节信息求得旋转矩阵
   rot_mat = getRotationMatrix2D( center, angle, scale );

   /// 旋转已扭曲图像
   warpAffine( warp_dst, warp_rotate_dst, rot_mat, warp_dst.size() );

   /// 显示结果
   namedWindow( source_window, CV_WINDOW_AUTOSIZE );
   imshow( source_window, src );

   namedWindow( warp_window, CV_WINDOW_AUTOSIZE );
   imshow( warp_window, warp_dst );

   namedWindow( warp_rotate_window, CV_WINDOW_AUTOSIZE );
   imshow( warp_rotate_window, warp_rotate_dst );

   /// 等待用户按任意按键退出程序
   waitKey(0);

   return 0;
  }

说明

  1. 定义一些需要用到的变量, 比如需要用来储存中间和目标图像的Mat和两个需要用来定义仿射变换的二维点数组.

    Point2f srcTri[3];
    Point2f dstTri[3];
    
    Mat rot_mat( 2, 3, CV_32FC1 );
    Mat warp_mat( 2, 3, CV_32FC1 );
    Mat src, warp_dst, warp_rotate_dst;
    
  2. 加载源图像:

    src = imread( argv[1], 1 );
    
  3. 以与源图像同样的类型和大小来对目标图像初始化:

    warp_dst = Mat::zeros( src.rows, src.cols, src.type() );
    
  4. 仿射变换: 正如上文所说, 我们需要源图像和目标图像上分别一一映射的三个点来定义仿射变换:

    srcTri[0] = Point2f( 0,0 );
    srcTri[1] = Point2f( src.cols - 1, 0 );
    srcTri[2] = Point2f( 0, src.rows - 1 );
    
    dstTri[0] = Point2f( src.cols*0.0, src.rows*0.33 );
    dstTri[1] = Point2f( src.cols*0.85, src.rows*0.25 );
    dstTri[2] = Point2f( src.cols*0.15, src.rows*0.7 );
    

    你可能想把这些点绘出来以获得对变换的更直观感受. 他们的位置大概就是在上面图例中的点的位置 (原理部分). 你会注意到由三点定义的三角形的大小和方向改变了.

  5. 通过这两组点, 我们能够使用OpenCV函数 getAffineTransform 来求出仿射变换:

    warp_mat = getAffineTransform( srcTri, dstTri );
    

    我们获得了用以描述仿射变换的 2 \times 3 矩阵 (在这里是 warp_mat)

  6. 将刚刚求得的仿射变换应用到源图像

    warpAffine( src, warp_dst, warp_mat, warp_dst.size() );
    

    函数有以下参数:

    • src: 输入源图像
    • warp_dst: 输出图像
    • warp_mat: 仿射变换矩阵
    • warp_dst.size(): 输出图像的尺寸

    这样我们就获得了变换后的图像! 我们将会把它显示出来. 在此之前, 我们还想要旋转它...

  7. 旋转: 想要旋转一幅图像, 你需要两个参数:

    1. 旋转图像所要围绕的中心
    2. 旋转的角度. 在OpenCV中正角度是逆时针的
    3. 可选择: 缩放因子

    我们通过下面的代码来定义这些参数:

    Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
    double angle = -50.0;
    double scale = 0.6;
    
  8. 我们利用OpenCV函数 getRotationMatrix2D 来获得旋转矩阵, 这个函数返回一个 2 \times 3 矩阵 (这里是 rot_mat)

    rot_mat = getRotationMatrix2D( center, angle, scale );
    
  9. 现在把旋转应用到仿射变换的输出.

    warpAffine( warp_dst, warp_rotate_dst, rot_mat, warp_dst.size() );
    
  10. 最后我们把仿射变换和旋转的结果绘制在窗体中,源图像也绘制出来以作参照:

    namedWindow( source_window, CV_WINDOW_AUTOSIZE );
    imshow( source_window, src );
    
    namedWindow( warp_window, CV_WINDOW_AUTOSIZE );
    imshow( warp_window, warp_dst );
    
    namedWindow( warp_rotate_window, CV_WINDOW_AUTOSIZE );
    imshow( warp_rotate_window, warp_rotate_dst );
    
  11. 等待用户退出程序

    waitKey(0);
    

结果

  1. 编译上述例程之后, 我们给出一个图像的路径作为参数. 比如这样一幅图片:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第13张图片

    仿射变换后我们获得:

    OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换_第14张图片

    最后, 应用了一个负角度旋转 (记住负角度指的是顺时针) 和基于一个缩放因子的缩放之后, 我们得到:





from: http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/table_of_content_imgproc/table_of_content_imgproc.html#table-of-content-imgproc

你可能感兴趣的:(模块,opencv,图像处理,霍夫变换,霍夫线变换,imgproc)