目录
由于最近学OpenCV,需要做个笔记,方便下次复习使用。本文分为以下几个部分:
- 对比度、亮度
- 离散傅里叶变换
对比度、亮度
对比度指的是一幅图像中明暗区域最亮的白和最暗的黑之间不同亮度层级的测量,差异范围越大代表对比越大,差异范围越小代表对比越小。
图像亮度通俗理解便是图像的明暗程度,数字图像 f(x,y) = i(x,y)* r(x, y) ,如果灰度值在[0,255]之间,则 f 值越接近0亮度越低,f 值越接近255亮度越高。而且我们也要把亮度和对比度区分开来,正如上述提的对比度指的是最高和最低灰度级之间的灰度差。
两种最常用的点操作(或者说点算子)是乘上一个常数(对应对比度调节)以及加上一个常数(对应亮度),公式如下:
其中,f(x)是源图像像素,g(x)是目标图像像素,参数a用来控制对比度,参数b用来控制亮度。
为了观察,我们将a取值为0-3.0,但是这样做可能会产生溢出(结果大于255),因此我们需要对结果进行一定的处理,即把数据限制在0-255之间,因此使用函数saturate_cast(也就是一个三目运算,小于0为0,大于255为255)
相关代码
int g_nContrastValue; //对比度
int g_nBrightValue; //亮度
Mat g_srcImage, g_dstImage;
static void on_ContrastAndBright(int, void*) {
//创建窗口
namedWindow("[原始窗口]", 1);
for (int i = 0; i < g_srcImage.rows; i++) {
for (int j = 0; j < g_srcImage.cols; j++) {
g_dstImage.at<Vec3b>(i, j)[0] = saturate_cast<uchar>((g_nContrastValue * 0.01) * g_srcImage.at<Vec3b>(i, j) [0]+ g_nBrightValue);
g_dstImage.at<Vec3b>(i, j)[1] = saturate_cast<uchar>((g_nContrastValue * 0.01) * g_srcImage.at<Vec3b>(i, j)[1] + g_nBrightValue);
g_dstImage.at<Vec3b>(i, j)[2] = saturate_cast<uchar>((g_nContrastValue * 0.01) * g_srcImage.at<Vec3b>(i, j)[2] + g_nBrightValue);
}
}
imshow("[原始窗口]", g_srcImage);
imshow("[效果窗口]", g_dstImage);
}
int main()
{
g_srcImage = imread("F:\\11\\xiaohuangren.jpeg");
g_dstImage = Mat::zeros(g_srcImage.size(),g_srcImage.type());
imshow("g_dstImage", g_dstImage);
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);
waitKey(0);
}
运行结果
离散傅里叶变换
离散傅里叶变换(DFT),是指傅里叶变换在时域和频域上都呈现离散的形式,将时序信号的采样变换为在离散时间傅里叶变换(DTFT)频域的采样。
离散傅里叶变换的原理
简单来说,对一张图像使用傅里叶变换就是将它分解成正弦跟余弦两部分。也就是将图像从空间域转换到频域。
这一转换的理论基础为:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅里叶变换就是一个用来将函数分解的工具。
傅里叶变换的作用
可以做到图像增强与图像去噪,图像分割之边缘检测,图像特征提取,图像压缩等。
dft()函数
dft函数的作用是对一维或二维浮点数数组进行正向或反向离散傅里叶变换
- void dft(InputArray src,OutputArray dst,int flags=0,int nonzeroRows = 0);
- 第三个参数。转换的标识符,可以取如下值:
- DFT_INVERSE 用一维或二维逆变换代替默认的正向变换
- DFT_SCALE 缩放比例标识符,输出的结果都会以1/N进行缩放
- DFT_ROWS 对输入矩阵的每行进行正向或反向变换。
- DFT_COMPLEX_OUTPUT 进行一维或二维实数数组正变换
- DFT_REAL_OUTPUT 进行一维或二维复数数组反变换
- 第四个参数,顾名思义,当此参数设为非零时,函数会假设只有输入矩阵的第一个非零行包括非零元素。
getOptimalDFTSize()函数
此函数会返回给定向量尺寸的傅里叶最优尺寸大小。
C++: int getOptimalDFTSize(int vecsize)
为了让转换更有效率,通常转变成2的n次幂(2,4,8,16…) ,这种是最快的。当然也能转变成2,3和5的倍数,如300。
例:508,转变成512。
copyMakeBorder()函数
得到了最优尺寸后,那么就应该对扩充的图像边界进行填充。
C++:void copyMakeBorder(InputArray src,OutputArray dst,int top,int bottom,int left,int right,int boderType,const Scalar&value = Scalar())
- top等4个参数,分别表示在源图像的四个方向上扩充多少像素
- 第七个参数,边界类型,常见取值为BORDER_CONSTANT
- 第八个参数,边界值
magnitude()函数
计算二维矢量的幅值。
C++:void magnitude(InputArray x,InputArray y,OutputArray magnitude)
- 第一个参数,表示矢量的浮点型X坐标值
- 第二个参数,表示矢量的浮点型Y坐标值
- 第三个参数,输出的幅值,它和第一个参数x有着同样的尺寸和类型。
log()函数
normalize()函数
进行矩阵的归一化
矩阵的归一化,减少某一数值太大的特征对其他较小特征的影响。
矩阵的列归一化,就是将矩阵每一列的值,除以每一列所有元素平方和的开方的绝对值,这样做的结果就是,矩阵每一列元素的平方和为1了。
X的l2-norm的定义为:
这里默认采用第二范式
相关代码
#include <opencv2/core.hpp>
using namespace std;
using namespace cv;
int main()
{
//[1]读取灰度图像,通道数是1
Mat srcImage = imread("F:\\11\\xiaohuangren.jpeg",0);
//[2]获取最优尺寸,边界用0填充,这里原本rows = 598,现在 m = 600
int m = getOptimalDFTSize(srcImage.rows);
int n = getOptimalDFTSize(srcImage.cols);
Mat padded;
copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols, BORDER_CONSTANT, Scalar::all(0));
//[3] 为傅里叶变换的结果(实部和虚部)分配空间
//傅里叶变换结果是复数,每个原图像值,有两个图像值
//频域值范围比时域值的范围大很多,因此需要存储在float中,也就是从原来的uchar转到float类型
Mat planes[] = { Mat_<float>(padded),Mat::zeros(padded.size(),CV_32F) };
Mat complexI;
merge(planes, 2, complexI); //两个输入矩阵.两个单通道合并成双通道,现在complexI是双通道类型的图片
//[4] 进行傅里叶变换
dft(complexI, complexI);
//[5] 将复数转换为幅值,complexI是双通道,对应两个值x和y
split(complexI, planes); //将多通道数组分离成几个单通道数组.
magnitude(planes[0], planes[1], planes[0]); //计算幅值,结果保存到planes[0]中
Mat magnitudeImage = planes[0];
//[6] 由于幅值过大,因此,需要进行自然对数的尺度缩放
//公式是M1 = ln(M+1),因此需要像素+1
magnitudeImage = magnitudeImage + Scalar::all(1); //像素+1操作
log(magnitudeImage, magnitudeImage); //求自然对数
//[7]剪切和重分布幅度图象限
//若有奇数行或奇数列,进行频谱裁剪
magnitudeImage = magnitudeImage(Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));
//-2实际转化为二进制的数为11111110。当一个二进制数与11111110进行位的和运算时,该二进制数的最小数位就为0,转化为无符整型时,就是一个偶数。个人觉得(uchar)~1,也可以。
//重新排列傅里叶图像中的象限,使原点位于图像中心.
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)); //右下
//交换象限(左上与右下进行交换)
Mat tmp;
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q0);
//交换象限(右上与左下进行交换)
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
//[8]归一化,为了显示,有了重分布后,幅度值仍然超过1,归一化到可显示范围
normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX);
//[9] 显示效果图
imshow("灰度图像", srcImage);
imshow("频谱幅值", magnitudeImage);
}
运行结果
图一是没有交换象限的操作得到的效果图:
图二是进行了象限的交换得到的效果图
红色的坐标轴是我自己加上去的,可见,原点在图像正中间。(这里是把4个角的角点汇聚在图片的中心)
图三是单通道原图
总结
由于刚学OpenCV,可能会些原理没掌握到位,但会努力去掌握。当然也有疑问,比如说为什么这样做能得到这样的结果呢?等等。