OpenCV学习笔记(1):入门、傅里叶变换与亮度对比度调整

记录一下学习《OpenCV3编程入门》的过程。由于有图像处理的基础,感觉很多内容都很亲切。

这几天学习的总结

  • OpenCV在VS下的配置,OpenCV头文件的引用关系(第一章)
  • OpenCV基础的操作(感受到变量是怎样在函数中传递的)(第一章)
  • OpenCV的常用函数(第三章)
  • Mat矩阵变量的初始化和输出(第四章)
  • 一些C++相关的基础知识(第二章)
    • printf()格式输出函数
    • 一些变量的命名规范
    • 通过宏来定义的常量,通常所有字母集体大写
  • 图像混合、亮度与对比度调整、傅里叶变换(第五章)(顺带复习了第四章的基础函数,Rect()Scalar()Szie()等)
  • 窗口程序相关:滑动条的创建(第三章与第五章)

《OpenCV3编程入门》这本书一个特点就是前后知识会有相关和重合,这样学起来感觉不错。

傅里叶变换

有个之前接触过,但没有亲自操作过的概念,对数尺度缩放。

单纯对二维图像进行傅里叶变换,变换结果通常不容易观察,明亮的高频区域过小。为便于观察,可以进行对数缩放。

对数缩放在大二的课上没有讲过,但我在一个小工具里用过,比直接变换“美观”很多。

先回忆一下大二课堂上用matlab做过的傅里叶变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Image, map] = imread('C:/code/test.png', 'png');
imshow(Image);

Image = rgb2gray(Image); % 转换为灰度图像
fftI = fft2(Image); % 进行傅里叶变化
sfftI = fftshift(fftI); % 将频谱中心移至图像中心
RR = real(sfftI);
II = imag(sfftI);
A = sqrt(RR.^2 + II.^2); % 计算频谱幅值
A = (A - min(min(A))) / (max(max(A)) - min(min(A))) * 225; % 归一化

figure;
imshow(A);
axis square; % 将频谱图调整为正方形

再重温一下用OpenCV在C++中的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

using namespace cv;

int main()
{
Mat srcImage = imread("C:/code/test.png", 0); // 和matlab相似,但路径要用双引号
imshow("Original Image", srcImage);

int m = getOptimalDFTSize(srcImage.rows); // 扩充边界(不知道matlab是如何处理的)
int n = getOptimalDFTSize(srcImage.cols); // OpenCV的函数果然丰富,专门为DFT扩充边界的都有
Mat padded;
copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols,
BORDER_CONSTANT, Scalar::all(0)); // BORDER_CONSTANT = 0

// 为傅里叶变换结果分配储存空间——这是C++作为程序语言与matlab的不同之处
Mat planes[] = { Mat_<float>(padded),
Mat::zeros(padded.size(), CV_32F) }; // 另创建一个与padded同样大小的空矩阵
Mat complexI;
merge(planes, 2, complexI); // 增加一个通道,将两个planes合并,为了存储复数

dft(complexI, complexI); // 进行傅里叶变换

split(complexI, planes);
magnitude(planes[0], planes[1], planes[0]); // 计算复数幅值,并将结果储存到planes[0]
// CV_EXPORTS_W void magnitude(InputArray x, InputArray y, OutputArray magnitude);
Mat magnitudeImage = planes[0];

magnitudeImage += Scalar::all(1); // M = log(1 + M)
log(magnitudeImage, magnitudeImage); // 结果也储存在magnitudeImage中

// 若有奇数行、列,则进行裁剪(matlab里没做这步)
magnitudeImage = magnitudeImage(Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));

// 将频谱中心移至图像中心,显然比matlab要繁琐很多
int cx = magnitudeImage.cols / 2;
int cy = magnitudeImage.rows / 2;
Mat q0(magnitudeImage, Rect(0, 0, cx, cy)); // 左上
Mat q1(magnitudeImage, Rect(cx, 0, cx, cy)); // 右上
Mat q2(magnitudeImage, Rect(0, cy, cx, cy)); // 左下
Mat q3(magnitudeImage, Rect(cx, cy, cx, cy)); // 右下
/*
origin:
q0 | q1
q2 | q3
new:
q3 | q2
q1 | q0
*/
Mat temp;
q0.copyTo(temp); // q0 -> temp
q3.copyTo(q0); // q3 -> q0
temp.copyTo(q3); // temp -> q3 // 这种写法就很“面向对象”了,和之前写脚本语言的路数完全一样
q1.copyTo(temp);
q2.copyTo(q1);
temp.copyTo(q2);

// 归一化
normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX); // NORM_MINMAX = 32

imshow("DFT Image", magnitudeImage);
waitKey(0);

return 0;
}

在一开始,把交换坐标象限的代码写成了这个样子

1
2
3
4
5
6
7
Mat temp;
q0.copyTo(temp);
q3.copyTo(q0);
q0.copyTo(q3);
q1.copyTo(temp);
q2.copyTo(q1);
q1.copyTo(q2);

这样的结果为

1
2
3
4
5
6
7
8
/*
origin:
q0 | q1
q2 | q3
new:
q3 | q2
q2 | q3
*/

亮度对比度调整

亮度、对比度调整在PS中倒是经常用,但是在之前写代码的时候,也就是调一下亮度,为了让边缘检测适应亮场、暗场。

作为点操作(仅通过输入图像的像素值便可计算相应输出图像的像素值),亮度和对比度之所以成对出现,因为可以写在同一个变换公式中

$$g(i,j) = a*(i,j) + b$$

其中,a用来控制对比度,b用来控制亮度。

具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>

using namespace std;
using namespace cv;

static void on_ContrastAndBright(int, void*);

int g_nContrastValue, g_nBrightValue;
Mat g_srcImage, g_dstImage;

int main()
{
g_srcImage = imread("C:/code/test.png");
g_dstImage = Mat::zeros(g_srcImage.size(), g_srcImage.type());

g_nContrastValue = 80;
g_nBrightValue = 80;

// 创建结果窗口
namedWindow("效果图窗口", 1);

createTrackbar("对比度: ", "效果图窗口", &g_nContrastValue, 300, on_ContrastAndBright);
createTrackbar("亮度: ", "效果图窗口", &g_nBrightValue, 200, on_ContrastAndBright);

// 函数初始化
on_ContrastAndBright(g_nContrastValue, 0);
on_ContrastAndBright(g_nBrightValue, 0);

// 输入q时退出程序
while (char(waitKey(1) != 'q')) {}
return 0;
}

static void on_ContrastAndBright(int, void*)
{
namedWindow("原始图窗口", 1);

for (int y = 0; y < g_srcImage.rows; y++)
{
for (int x = 0; x < g_srcImage.cols; x++)
{
for (int planes = 0; planes < 3; planes++)
{
g_dstImage.at<Vec3b>(y, x)[planes] = saturate_cast<uchar>
((g_nContrastValue * 0.01) * g_srcImage.at<Vec3b>(y, x)[planes] + g_nBrightValue);
}
}
}

imshow("原始图窗口", g_srcImage);
imshow("效果图窗口", g_dstImage);
}

结语

OpenCV与VapourSynth比较,虽然VapourSynth为处理视频提供了(面向视频的)高级API,但毕竟没有像OpenCV那样提供丰富的“半成品”函数,像扩充图像边界这样的操作要自己去写,反而体验到一些偏“底层”的东西。

顺带一提,刚刚会用了VS的“转到定义”功能,确实好方便。