本节你将学到:
本节中,我门将大量使用 Point 和 Scalar 这两个结构:
或者
表示了具有4个元素的数组。次类型在OpenCV中被大量用于传递像素值。
本节中,我们将进一步用它来表示RGB颜色值(三个参数)。如果用不到第四个参数,则无需定义。
我们来看个例子,如果给出以下颜色参数表达式:
那么定义的RGB颜色值为: Red = c, Green = b and Blue = a
我们打算画两个例子(原子和赌棍), 所以必须创建两个图像和对应的窗口以显示。
创建用来画不同几何形状的函数。比如用 MyEllipse 和 MyFilledCircle 来画原子。
接下来用 MyLine*,*rectangle 和 a MyPolygon 来画赌棍:
现在来看看每个函数内部如何定义:
MyLine
正如我们所见, MyLine 调用函数 line 来实现以下操作:
MyEllipse
根据以上代码,我们可看到函数 ellipse 按照以下规则绘制椭圆:
MyFilledCircle
类似于椭圆函数,我们可以看到 circle 函数的参数意义如下:
MyPolygon
rectangle
最后是函数:rectangle:rectangle <> (我们并没有为这家伙创建特定函数)。请注意:
本节你将学到:
让我们检视 main 函数。我们发现第一步是实例化一个 Random Number Generator(随机数发生器对象) (RNG):
RNG的实现了一个随机数发生器。 在上面的例子中, rng 是用数值 0xFFFFFFFF 来实例化的一个RNG对象。
然后我们初始化一个 0 矩阵(代表一个全黑的图像), 并且指定它的宽度,高度,和像素格式:
然后我们开始疯狂的绘制。看过代码时候你会发现它主要分八个部分,正如函数定义的一样:
所有这些范数都遵循相同的模式,所以我们只分析其中的一组,因为这适用于所有。
查看函数 Drawing_Random_Lines:
我们可以看到:
for 循环将重复 NUMBER 次。 并且函数 line 在循环中, 这意味着要生成 NUMBER 条线段。
线段的两个端点分别是 pt1 和 pt2. 对于 pt1 我们看到:
我们知道 rng 是一个 随机数生成器 对象。在上面的代码中我们调用了 rng.uniform(a,b) 。这指定了一个在 a和 b 之间的均匀分布(包含 a, 但不含 b)。
由上面的说明,我们可以推断出 pt1 和 pt2 将会是随机的数值,因此产生的线段是变幻不定的,这会产生一个很好的视觉效果(从下面绘制的图片可以看出)。
我们还可以发现, 在 line 的参数设置中,对于 color 的设置我们用了:
让我们来看看函数的实现:
正如我们看到的,函数的返回值是一个用三个随机数初始化的 Scalar 对象,这三个随机数代表了颜色的 R, G, B分量。所以,线段的颜色也是随机的!
上面的解释同样适用于其它的几何图形,比如说参数 center(圆心) 和 vertices(顶点) 也是随机的。
在结束之前,我们还应该看看函数 Display_Random_Text 和 Displaying_Big_End, 因为它们有一些有趣的特征:
Display_Random_Text:
这些看起来都很熟悉,但是这一句:
函数 putText 都做了些什么?在我们的例子中:
因此, 我们将绘制 (与其余函数类似) NUMBER 个文字到我们的图片上,以位置随机的方式。
Displaying_Big_End
除了 getTextSize (用于获取文字的大小参数), 我们可以发现在 for 循环里的新操作:
我们还要知道,减法操作 总是 保证是 合理 的操作, 这表明结果总是在合理的范围内 (这个例子里结果不会为负数,并且保证在 0~255的合理范围内)。
正如你在代码部分看到的, 程序将依次执行不同的绘图函数,这将:
本文档尝试解答如下问题:
你可以 从此处下载源码 或者通过OpenCV源码库文件samples/cpp/tutorial_code/core/discrete_fourier_transform/discrete_fourier_transform.cpp 查看.
以下为函数 dft() 使用范例:
|
|
对一张图像使用傅立叶变换就是将它分解成正弦和余弦两部分。也就是将图像从空间域(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 格式中。 结果我们将输入图像转换成浮点类型,并多加一个额外通道来储存复数部分:
进行离散傅立叶变换. 支持图像原地计算 (输入输出为同一图像):
将复数转换为幅度.复数包含实数部分(Re)和复数部分 (imaginary - Im)。 离散傅立叶变换的结果是复数,对应的幅度可以表示为:
转化为OpenCV代码:
对数尺度(logarithmic scale)缩放. 傅立叶变换的幅度值范围大到不适合在屏幕上显示。高值在屏幕上显示为白点,而低值为黑点,高低值的变化无法有效分辨。为了在屏幕上凸显出高低变化的连续性,我们可以用对数尺度来替换线性尺度:
转化为OpenCV代码:
剪切和重分布幅度图象限. 还记得我们在第一步时延扩了图像吗? 那现在是时候将新添加的像素剔除了。为了方便显示,我们也可以重新分布幅度图象限位置(注:将第五步得到的幅度图从中间划开得到四张1/4子图像,将每张子图像看成幅度图的一个象限,重新分布即将四个角点重叠到图片中心)。 这样的话原点(0,0)就位移到图像中心。
归一化. 这一步的目的仍然是为了显示。 现在我们有了重分布后的幅度图,但是幅度值仍然超过可显示范围[0,1] 。我们使用normalize() 函数将幅度归一化到可显示范围。
离散傅立叶变换的一个应用是决定图片中物体的几何方向.比如,在文字识别中首先要搞清楚文字是不是水平排列的? 看一些文字,你就会注意到文本行一般是水平的而字母则有些垂直分布。文本段的这两个主要方向也是可以从傅立叶变换之后的图像看出来。我们使用这个 水平文本图像 以及 旋转文本图像 来展示离散傅立叶变换的结果 。
水平文本图像:
旋转文本图像:
观察这两张幅度图你会发现频域的主要内容(幅度图中的亮点)是和空间图像中物体的几何方向相关的。 通过这点我们可以计算旋转角度并修正偏差。
你将得到以下几个问题的答案:
你可以 点击此处下载 或直接从OpenCV代码库中找到源文件。samples/cpp/tutorial_code/core/file_input_output/file_input_output.cpp 。
以下用简单的示例代码演示如何逐一实现所有目的.
|
|
这里我们仅讨论XML和YAML文件输入。你的输出(和相应的输入)文件可能仅具有其中一个扩展名以及对应的文件结构。XML和YAML的串行化分别采用两种不同的数据结构: mappings (就像STL map) 和 element sequence (比如 STL vector>。二者之间的区别在map中每个元素都有一个唯一的标识名供用户访问;而在sequences中你必须遍历所有的元素才能找到指定元素。
XML\YAML 文件的打开和关闭。 在你写入内容到此类文件中前,你必须先打开它,并在结束时关闭它。在OpenCV中标识XML和YAML的数据结构是 FileStorage 。要将此结构和硬盘上的文件绑定时,可使用其构造函数或者 open() 函数:
无论以哪种方式绑定,函数中的第二个参数都以常量形式指定你要对文件进行操作的类型,包括:WRITE, READ 或 APPEND。文件扩展名决定了你将采用的输出格式。如果你指定扩展名如 .xml.gz ,输出甚至可以是压缩文件。
当 FileStorage 对象被销毁时,文件将自动关闭。当然你也可以显示调用 release 函数:
输入\输出文本和数字。 数据结构使用与STL相同的 << 输出操作符。输出任何类型的数据结构时,首先都必须指定其标识符,这通过简单级联输出标识符即可实现。基本类型数据输出必须遵循此规则:
读入则通过简单的寻址(通过 [] 操作符)操作和强制转换或 >> 操作符实现:
输入\输出OpenCV数据结构。 其实和对基本类型的操作方法是相同的:
输入\输出 vectors(数组)和相应的maps. 之前提到我们也可以输出maps和序列(数组, vector)。同样,首先输出变量的标识符,接下来必须指定输出的是序列还是map。
对于序列,在第一个元素前输出”[“字符,并在最后一个元素后输出”]“字符:
对于maps使用相同的方法,但采用”{“和”}“作为分隔符。
对于数据读取,可使用 FileNode 和 FileNodeIterator 数据结构。 FileStorage 的[] 操作符将返回一个 FileNode 数据类型。如果这个节点是序列化的,我们可以使用 FileNodeIterator 来迭代遍历所有元素。
对于maps类型,可以用 [] 操作符访问指定的元素(或者 >> 操作符):
读写自定义数据类型。 假设你定义了如下数据类型:
添加内部和外部的读写函数,就可以使用OpenCV I/O XML/YAML接口对其进行序列化(就像对OpenCV数据结构进行序列化一样)。内部函数定义如下:
接下来在类的外部定义以下函数:
这儿可以看到,如果读取的节点不存在,我们返回默认值。更复杂一些的解决方案是返回一个对象ID为负值的实例。
一旦添加了这四个函数,就可以用 >> 操作符和 << 操作符分别进行读,写操作:
或试着读取不存在的值:
好的,大多情况下我们只输出定义过的成员。在控制台程序的屏幕上,你将看到:
然而, 在输出的xml文件中看到的结果将更加有趣:
或YAML文件:
你也可以看到动态实例: YouTube here .
对于OpenCV的开发团队来说,持续稳定地提高代码库非常重要。我们一直在思考如何在使其易用的同时保持灵活性。新的C++接口即为此而来。尽管如此,向下兼容仍然十分重要。我们并不想打断你基于早期OpenCV库的开发。因此,我们添加了一些函数来处理这种情况。在以下内容中你将学到:
在用新版本之前,你首先需要学习一些新的图像数据结构: Mat - 基本图像容器 ,它取代了旧的 CvMat 和 IplImage 。转换到新函数非常容易,你仅需记住几条新的原则。
OpenCV 2 接受按需定制。所有函数不再装入一个单一的库中。我们会提供许多模块,每个模块都包含了与其功能相关的数据结构和函数。这样一来,如果你仅仅需要使用OpenCV的一部分功能,你就不需要把整个巨大的OpenCV库都装入你的程序中。使用时,你仅需要包含用到的头文件,比如:
所有OpenCV用到的东西都被放入名字空间 cv 中以避免与其他库的数据结构和函数名称的命名冲突。因此,在使用OpenCV库中的任何定义和函数时,你必须在名称之前冠以 cv:: ,或者在包含头文件后,加上以下指令:
因为所有库中函数都已在此名字空间中,所以无需加 cv 作为前缀。据此所有新的C++兼容函数都无此前缀,并且遵循驼峰命名准则。也就是第一个字母为小写(除非是单个单词作为函数名,如 Canny)并且后续单词首字母大写(如 copyMakeBorder ).
接下来,请记住你需要将所有用到的模块链接到你的程序中。如果你在Windows下开发且用到了 动态链接库(DLL) ,你还需要将OpenCV对应动态链接库的路径加入程序执行路径中。关于Windows下开发的更多信息请阅读 How to build applications with OpenCV inside the Microsoft Visual Studio ;对于Linux用户,可参考 Using OpenCV with Eclipse (plugin CDT) 中的实例及说明。
你可以使用 IplImage 或 CvMat 操作符来转换 Mat 对象。在C接口中,你习惯于使用指针,但此处将不再需要。在C++接口中,我们大多数情况下都是用 Mat 对象。此对象可通过简单的赋值操作转换为 IplImage 和 CvMat 。示例如下:
现在,如果你想获取指针,转换就变得麻烦一点。编译器将不能自动识别你的意图,所以你需要明确指出你的目的。可以通过调用IplImage 和 CvMat 操作符来获取他们的指针。我们可以用 & 符号获取其指针如下:
来自C接口最大的抱怨是它将所有内存管理工作交给你来做。你需要知道何时可以安全释放不再使用的对象,并且确定在程序结束之前释放它,否则就会造成讨厌的内存泄露。为了绕开这一问题,OpenCV引进了一种智能指针。它将自动释放不再使用的对象。使用时,指针将被声明为 Ptr 模板的特化:
将C接口的数据结构转换为 Mat 时,可将其作为构造函数的参数传入,例如:
现在,你已经学习了最基本的知识。 这里 你将会看到一个混合使用C接口和C++接口的例子。你也可以在可以再OpenCV的代码库中的sample目录中找到此文件samples/cpp/tutorial_code/core/interoperability_with_OpenCV_1/interoperability_with_OpenCV_1.cpp 。为了进一步帮助你认清其中区别,程序支持两种模式:C和C++混合,以及纯C++。如果你宏定义了 DEMO_MIXED_API_USE ,程序将按第一种模式编译。程序的功能是划分颜色平面,对其进行改动并最终将其重新合并。
|
|
在此,你可一看到新的结构再无指针问题,哪怕使用旧的函数,并在最后结束时将结果转换为 Mat 对象。
|
|
因为我们打算搞乱图像的亮度通道,所以首先将图像由默认的RGB颜色空间转为YUV颜色空间,然后将其划分为独立颜色平面(Y,U,V)。第一个例子中,我们对每一个平面用OpenCV中三个主要图像扫描算法(C []操作符,迭代,单独元素访问)中的一个进行处理。在第二个例子中,我们给图像添加一些高斯噪声,然后依据一些准则融合所有通道。
运用扫描算法的代码如下:
|
|
此处可看到,我们可以以三种方式遍历图像的所有像素:迭代器,C指针和单独元素访问方式你可在 OpenCV如何扫描图像、利用查找表和计时 中获得更深入的了解。从旧的函数名转换新版本非常容易,仅需要删除 cv 前缀,并且使用 Mat 数据结构。下面的例子中使用了加权加法:
|
|
正如你所见,变量 planes 也是 Mat 类型的。无论如何,将 Mat 转换为 IplImage 都可通过简单的赋值操作符自动实现。
|
|
新的 imshow highgui函数可接受 Mat 和 IplImage 数据结构。 编译并运行例程,如果输入以下第一幅图像,程序将输出以下第二幅或者第三幅图像。
你可以在点击此处看到动态示例: YouTube here ,并可以 点击此处 下载源文件,或者在OpenCV源代码库中找到源文件:samples/cpp/tutorial_code/core/interoperability_with_OpenCV_1/interoperability_with_OpenCV_1.cpp 。