基本绘图
目的
本节你将学到:
OpenCV 原理
Scalar
-
表示了具有4个元素的数组。次类型在OpenCV中被大量用于传递像素值。
-
本节中,我们将进一步用它来表示RGB颜色值(三个参数)。如果用不到第四个参数,则无需定义。
-
我们来看个例子,如果给出以下颜色参数表达式:
那么定义的RGB颜色值为:Red = c,Green = bandBlue = a
代码分析
-
我们打算画两个例子(原子和赌棍), 所以必须创建两个图像和对应的窗口以显示。
-
创建用来画不同几何形状的函数。比如用MyEllipse和MyFilledCircle来画原子。
/// 1. 画一个简单的原子。 /// 1.a. 创建椭圆 MyEllipse( atom_image,80)">90 ); MyEllipse( atom_image,80)">0 ); MyEllipse( atom_image,80)">45 ); MyEllipse( atom_image, -45 ); /// 1.b. 创建圆 MyFilledCircle( atom_image, Point( w/2.0, w2.0) ); -
接下来用MyLine*,*rectangle和 aMyPolygon来画赌棍:
/// 2. 画一个赌棍 /// 2.a. 创建一个凸多边形 MyPolygon( rook_image ); /// 2.b. 创建矩形 rectangle( rook_image, Point( 0,80)">7*w8.0 ), Point( w, w), Scalar( 255,80)">255 ), 1, 8 ); /// 2.c. 画几条直线 MyLine( rook_image, Point( 15/16 ), Point( w,80)">16 ) ); MyLine( rook_image,80)">4,80)">8 ), w ) ); MyLine( rook_image,80)">2,80)">3w ) ); -
现在来看看每个函数内部如何定义:
-
MyLine
- 画一条从点start到点end的直线段
- 此线段将被画到图像img上
- 线的颜色由Scalar( 0,0)来定义,在此其相应RGB值为黑色
- 线的粗细由thickness设定(此处设为 2)
- 此线为8联通 (lineType= 8)
-
MyEllipse
void MyEllipse( Mat img, double angle ) { 8; ellipse( img, Point( w2.0 ), Size( w4.0,80)">16.0 ), angle,80)">360, thickness, lineType ); }根据以上代码,我们可看到函数ellipse按照以下规则绘制椭圆:
- 椭圆将被画到图像img上
- 椭圆中心为点(w/2.0,w/2.0)并且大小位于矩形(w/4.0,w/16.0)内
- 椭圆旋转角度为angle
- 椭圆扩展的弧度从0度到360度
- 图形颜色为Scalar( 255,0),既蓝色
- 绘椭圆的线粗为thickness,此处是2
-
MyFilledCircle
void MyFilledCircle( Mat img, Point center ) { = 1; 8; circle( img, center, w32.0, Scalar( thickness, lineType ); }类似于椭圆函数,我们可以看到circle函数的参数意义如下:
- 圆将被画到图像 (img)上
- 圆心由点center定义
- 圆的半径为:w/32.0
- 圆的颜色为:Scalar(0,255),按BGR的格式为红色
- 线粗定义为thickness= -1,因此次圆将被填充
-
MyPolygon
- 多边形将被画到图像img上
- 多边形的顶点集为ppt
- 要绘制的多边形顶点数目为npt
- 要绘制的多边形数量仅为1
- 多边形的颜色定义为Scalar( 255,255),既BGR值为白色
-
rectangle
rectangle( rook_image,80)">8 );最后是函数:rectangle:rectangle <>(我们并没有为这家伙创建特定函数)。请注意:
- 矩形将被画到图像rook_image上
- 矩形两个对角顶点为Point( 0,7*w/8.0 )和Point( w,w)
- 矩形的颜色为Scalar(0,255),既BGR格式下的黄色
- 由于线粗为-1,此矩形将被填充
-
结果
编译并运行例程,你将看到如下结果:
随机数发生器&绘制文字
说明
-
让我们检视main函数。我们发现第一步是实例化一个Random Number Generator(随机数发生器对象)(RNG):
RNG rng( 0xFFFFFFFF );RNG的实现了一个随机数发生器。 在上面的例子中,rng是用数值0xFFFFFFFF来实例化的一个RNG对象。
-
然后我们初始化一个0矩阵(代表一个全黑的图像),并且指定它的宽度,高度,和像素格式:
/// 初始化一个0矩阵 Mat image ::zeros( window_height, window_width, CV_8UC3 ); /// 把它会知道一个窗口中 imshow( window_name, image ); -
然后我们开始疯狂的绘制。看过代码时候你会发现它主要分八个部分,正如函数定义的一样:
/// 现在我们先画线 c = Drawing_Random_Lines(image, window_name, rng); if( c != 0 ) return 0; /// 继续,这次是一些矩形 c = Drawing_Random_Rectangles(image,144); font-style:italic">/// 画一些弧线 c = Drawing_Random_Ellipses( image, rng ); /// 画一些折线 c = Drawing_Random_Polylines( image,144); font-style:italic">/// 画被填充的多边形 c = Drawing_Random_Filled_Polygons( image,144); font-style:italic">/// 画圆 c = Drawing_Random_Circles( image,144); font-style:italic">/// 在随机的地方绘制文字 c = Displaying_Random_Text( image,144); font-style:italic">/// Displaying the big end! c = Displaying_Big_End( image, rng );所有这些范数都遵循相同的模式,所以我们只分析其中的一组,因为这适用于所有。
-
查看函数Drawing_Random_Lines:
int Drawing_Random_Lines( Mat image,0)">char* window_name, RNG rng ) { 8; Point pt1, pt2; for( int i 0; i < NUMBER; i++ ) { pt1.x = rng.uniform( x_1, x_2 ); pt1.y = rng.uniform( y_1, y_2 ); pt2.x x_2 ); pt2.y y_2 ); line( image, pt1, pt2, randomColor(rng), rng.uniform(10),80)">8 ); imshow( window_name, image ); if( waitKey( DELAY ) >= 0 ) { return 1; } } 0; }我们可以看到:
-
线段的两个端点分别是pt1和pt2. 对于pt1我们看到:
-
我们知道rng是一个随机数生成器对象。在上面的代码中我们调用了rng.uniform(a,b)。这指定了一个在a和b之间的均匀分布(包含a,但不含b)。
-
由上面的说明,我们可以推断出pt1和pt2将会是随机的数值,因此产生的线段是变幻不定的,这会产生一个很好的视觉效果(从下面绘制的图片可以看出)。
-
我们还可以发现,在line的参数设置中,对于color的设置我们用了:
让我们来看看函数的实现:
static Scalar randomColor( RNG& rng ) { int icolor = (unsigned) rng; return Scalar( icolor&(icolor>>8)16)255 ); }正如我们看到的,函数的返回值是一个用三个随机数初始化的Scalar对象,这三个随机数代表了颜色的R,G,B分量。所以,线段的颜色也是随机的!
-
-
上面的解释同样适用于其它的几何图形,比如说参数center(圆心)和vertices(顶点)也是随机的。
-
在结束之前,我们还应该看看函数Display_Random_Text和Displaying_Big_End,因为它们有一些有趣的特征:
-
Display_Random_Text:
int Displaying_Random_Text( Mat image,80)">8; for ( 1; i ++ ) { Point org; org.x = rng.uniform(x_1, x_2); org.y = rng.uniform(y_1, y_2); putText( image, "Testing text rendering", org,8), rng.uniform(100)*0.05+0.1, lineType); imshow( window_name, image ); if( waitKey(DELAY) 0 ) { 1; } } 0; }这些看起来都很熟悉,但是这一句:
putText( image, rng.uniform(lineType); -
Displaying_Big_End
int Displaying_Big_End( Mat image, RNG rng ) { Size textsize = getTextSize("OpenCV forever!", CV_FONT_HERSHEY_COMPLEX,80)">3,80)">5,80)">0); Point org((window_width - textsize.width)(window_height - textsize.height)2); 8; Mat image2; < 255; i += 2 ) { image2 = image - Scalar::all(i); putText( image2, Scalar(i, i,80)">255), lineType ); imshow( window_name, image2 ); 0; }除了getTextSize(用于获取文字的大小参数),我们可以发现在for循环里的新操作:
我们还要知道,减法操作 总是保证是 合理的操作,这表明结果总是在合理的范围内 (这个例子里结果不会为负数,并且保证在 0~255的合理范围内)。
结果
离散傅立叶变换 目标
本文档尝试解答如下问题:
- 什么是傅立叶变换及其应用?
- 如何使用OpenCV提供的傅立叶变换?
- 相关函数的使用,如:copyMakeBorder(),merge(),dft(),getOptimalDFTSize(),log()和normalize().
源码
你可以从此处下载源码或者通过OpenCV源码库文件samples/cpp/tutorial_code/core/discrete_fourier_transform/discrete_fourier_transform.cpp查看.
|
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
int main(int argc,0)">char ** argv)
{
const * filename = argc >=2 ? argv[1] : "lena.jpg";
Mat I = imread(filename, CV_LOAD_IMAGE_GRAYSCALE);
if( I.empty())
1;
Mat padded; //expand input image to optimal size
int m = getOptimalDFTSize( I.rows );
int n = getOptimalDFTSize( I.cols ); // on the border add zero values
copyMakeBorder(I, padded, m - I.rows, n - I.cols, BORDER_CONSTANT, Scalar::all(0));
Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)};
Mat complexI;
merge(planes, complexI); // Add to the expanded another plane with zeros
dft(complexI, complexI); // this way the result may fit in the source matrix
// compute the magnitude and switch to logarithmic scale
// => log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
split(complexI, planes); // planes[0] = Re(DFT(I),planes[1] = Im(DFT(I))
magnitude(planes[0], planes[1],80)">0]);// planes[0] = magnitude
Mat magI = planes[0];
magI += Scalar1); // switch to logarithmic scale
log(magI, magI);
// crop the spectrum,if it has an odd number of rows or columns
magI = magI(Rect(magI.cols & magI.rows 2));
// rearrange the quadrants of Fourier image so that the origin is at the image center
int cx = magI.cols2;
int cy = magI.rows2;
Mat q0(magI, Rect(cx, cy)); // Top-Left - Create a ROI per quadrant
Mat q1(magI, Rect(cx, cy)); // Top-Right
Mat q2(magI, cy,144); font-style:italic">// Bottom-Left
Mat q3(magI, cy)); // Bottom-Right
Mat tmp; // swap quadrants (Top-Left with Bottom-Right)
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp); // swap quadrant (Top-Right with Bottom-Left)
q2.copyTo(q1);
tmp.copyTo(q2);
normalize(magI, magI, CV_MINMAX); // Transform the matrix with float values into a
// viewable image form (float between values 0 and 1).
imshow("Input Image" , I ); // Show the result
imshow("spectrum magnitude", magI);
waitKey();
0;
}
|
原理
对一张图像使用傅立叶变换就是将它分解成正弦和余弦两部分。也就是将图像从空间域(spatial domain)转换到频域(frequency domain)。 这一转换的理论基础来自于以下事实:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅立叶变换就是一个用来将函数分解的工具。 2维图像的傅立叶变换可以用以下数学公式表达:
式中 f 是空间域(spatial domain)值, F 则是频域(frequency domain)值。 转换之后的频域值是复数, 因此,显示傅立叶变换之后的结果需要使用实数图像(real image) 加虚数图像(complex image),或者幅度图像(magitude image)加相位图像(phase image)。 在实际的图像处理过程中,仅仅使用了幅度图像,因为幅度图像包含了原图像的几乎所有我们需要的几何信息。 然而,如果你想通过修改幅度图像或者相位图像的方法来间接修改原空间图像,你需要使用逆傅立叶变换得到修改后的空间图像,这样你就必须同时保留幅度图像和相位图像了。
在此示例中,我将展示如何计算以及显示傅立叶变换后的幅度图像。由于数字图像的离散性,像素值的取值范围也是有限的。比如在一张灰度图像中,像素灰度值一般在0到255之间。 因此,我们这里讨论的也仅仅是离散傅立叶变换(DFT)。 如果你需要得到图像中的几何结构信息,那你就要用到它了。请参考以下步骤(假设输入图像为单通道的灰度图像I):
-
将图像延扩到最佳尺寸. 离散傅立叶变换的运行速度与图片的尺寸息息相关。当图像的尺寸是2, 3,5的整数倍时,计算速度最快。 因此,为了达到快速计算的目的,经常通过添凑新的边缘像素的方法获取最佳图像尺寸。函数getOptimalDFTSize()返回最佳尺寸,而函数copyMakeBorder()填充边缘像素:
添加的像素初始化为0.
-
为傅立叶变换的结果(实部和虚部)分配存储空间. 傅立叶变换的结果是复数,这就是说对于每个原图像值,结果是两个图像值。 此外,频域值范围远远超过空间值范围, 因此至少要将频域储存在float格式中。 结果我们将输入图像转换成浮点类型,并多加一个额外通道来储存复数部分:
Mat planes[] CV_32F)}; Mat complexI; merge(planes,144); font-style:italic">// 为延扩后的图像增添一个初始化为0的通道 -
进行离散傅立叶变换. 支持图像原地计算 (输入输出为同一图像):
dft(complexI,144); font-style:italic">// 变换结果很好的保存在原始矩阵中 -
将复数转换为幅度.复数包含实数部分(Re)和复数部分 (imaginary -Im)。 离散傅立叶变换的结果是复数,对应的幅度可以表示为:
@H_270_3010@转化为OpenCV代码:
split(complexI,planes[1] = Im(DFT(I)) magnitude(planes[// planes[0] = magnitude Mat magI 0];
对数尺度(logarithmic scale)缩放. 傅立叶变换的幅度值范围大到不适合在屏幕上显示。高值在屏幕上显示为白点,而低值为黑点,高低值的变化无法有效分辨。为了在屏幕上凸显出高低变化的连续性,我们可以用对数尺度来替换线性尺度:
转化为OpenCV代码:
剪切和重分布幅度图象限. 还记得我们在第一步时延扩了图像吗? 那现在是时候将新添加的像素剔除了。为了方便显示,我们也可以重新分布幅度图象限位置(注:将第五步得到的幅度图从中间划开得到四张1/4子图像,将每张子图像看成幅度图的一个象限,重新分布即将四个角点重叠到图片中心)。 这样的话原点(0,0)就位移到图像中心。
归一化. 这一步的目的仍然是为了显示。 现在我们有了重分布后的幅度图,但是幅度值仍然超过可显示范围[0,1] 。我们使用normalize()函数将幅度归一化到可显示范围。
结果
离散傅立叶变换的一个应用是决定图片中物体的几何方向.比如,在文字识别中首先要搞清楚文字是不是水平排列的? 看一些文字,你就会注意到文本行一般是水平的而字母则有些垂直分布。文本段的这两个主要方向也是可以从傅立叶变换之后的图像看出来。我们使用这个水平文本图像以及旋转文本图像来展示离散傅立叶变换的结果 。
水平文本图像:
旋转文本图像:
观察这两张幅度图你会发现频域的主要内容(幅度图中的亮点)是和空间图像中物体的几何方向相关的。 通过这点我们可以计算旋转角度并修正偏差。