该篇围绕Core Functionality模块进行展开
该模块的主要作用是成为构建opencv更多高级功能的基础核心层。
Mat基础图像存储数据结构
- 将Mat对象赋值给其他Mat变量将会共享一个地址;
- 当使用赋值运算符和复制构造函数时仅复制标头,清除最后一个赋值的对象图像矩阵,将会清空所有存储该矩阵数据的Mat对象;
- 使用clone()和copyTo()将会创建不共用的图像数据指针。
Mat A, C;
Mat B(A);
C = A;
// 以上都共用一个指针
Mat F = A.lone();
Mat G;
A.copyTo(G);
// 以上内存地址不共享
// 获取图像中的部分区域生成新的图像两种方法
Mat D (A, Rect(10,10,100,100)); // using a rectangle
Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries
// Mat类型的初始化操作
Mat M(2,2, CV_8UC3, Scalar(0, 0, 255)); // 创建一个长度位8bit的无符号类型的三通道图像,第一通道和第二通道初始化为0,三通道为255;
cv::Mat::zeros(3, 3, CV_64F);
cv::Mat::ones(3, 3, CV_32F);
cv::Mat::eye(3, 3, CV_8UC1);
Mat kernel = (Mat_<char>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); // [0, -1, 0; -1, 5, -1; 0, -1, 0;]
// 使用随机数填充MAT
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255)); // 需要指定最大值和最小值
// 其他常见的数据类型
// 1. 2D Point
Point2f P(5, 1); // Point <2D> = [5, 1]
// 2. 3D Point
Point3f P3f(2, 5, 6); // Point<3D> = [2, 6, 7]
// 将向量转换为Mat
vector<float> v;
v.push_back((float)CV_PI);
v.push_back((float)2);
v.push_back((float)4.23);
Mat(v); // [3.1415927, 2, 4.23;]
图像遍历
- RGB图像存储的形式:
该RGB图片的存储是按照BGR的顺序连续存储。可以使用 cv::Mat::isContinu()来判断是否是连续存储的。
// 使用指针访问图像像素,速度较快但不安全;
void ScanImageAndReduceptr(Mat& InputImage,Mat& outImage, int div)
{
// 判断是否为uchar类型数据
CV_Assert(InputImage.depth() == CV_8U);
const int channels = InputImage.channels();
outImage = InputImage.clone();
int rowNumber = outImage.rows; // 获取行列
int columnNumber = outImage.cols*channels ; // 列*通道数 = 每一行元素个数
for(int i = 0; i < rowNumber ; i++)
{
uchar* data = outImage.ptr<uchar>(i); // 获取i行的首地址
for(int j = 0; j < columnNumber ; j ++)
{
data[j] = data[j]/div * div + div/2; // 对每个像素进行处理
}
}
return;
}
// 使用迭代进行安全遍历图像像素(83ms),速度较慢但是安全;
void ScanImageAndReduceIterator(Mat& I,int dev)
{
// 判断是否为uchar类型数据
CV_Assert(I.depth() == CV_8U);
const int channels = I.channels();
switch(channels)
{
case 1:
{
MatIterator_<uchar> it, end;
for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
*it = *it / dev * dev + dev / 2;
break;
}
case 3:
{
MatIterator_<Vec3b> it, end;
for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
{
(*it)[0] = (*it)[0] / dev * dev + dev / 2;
(*it)[1] = (*it)[1] / dev * dev + dev / 2;
(*it)[2] = (*it)[2] / dev * dev + dev / 2;
}
}
}
return;
}
// 使用On-The-Fly RA遍历图像像素(93ms),该方法一般用于需改指定通道指定位置的值,与cv::Mat::at()函数的功能和定位相同。
void ScanImageAndReduceRandomAccess(Mat& InputImage, Mat& outImage, int div)
{
// accept only char type matrices
CV_Assert(InputImage.depth() == CV_8U);
const int channels = InputImage.channels();
outImage = InputImage.clone();
int rows= outImage.rows; // 获取行列
int cols= outImage.cols; // 列
switch(channels)
{
case 1:
{
for( int i = 0; i < rows; ++i)
for( int j = 0; j < cols; ++j )
outImage.at<uchar>(i,j) = InputImage.at<uchar>(i,j) / div * div + div/2 ;
break;
}
case 3:
{
for( int i = 0; i < rows; ++i)
{
for( int j = 0; j < cols; ++j )
{
outImage.at<Vec3b>(i,j)[0] = InputImage.at<Vec3b>(i,j)[0] / div * div + div/2 ;
outImage.at<Vec3b>(i,j)[1] = InputImage.at<Vec3b>(i,j)[1] / div * div + div/2 ;
outImage.at<Vec3b>(i,j)[2] = InputImage.at<Vec3b>(i,j)[2] / div * div + div/2 ;
}
}
}
}
return ;
}
图像的掩码操作(根据掩码矩阵对像素进行更改,也被称为核操作)
- 使用掩码对图像进行卷积操作的速度要远快于使用遍历图像像素进行处理的速度
// 使用掩码+filter2D 与使用遍历方式的像素加权的对比
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
using namespace std;
using namespace cv;
static void help(char* progName)
{
cout << endl
<< "This program shows how to filter images with mask: the write it yourself and the"
<< "filter2d way. " << endl
<< "Usage:" << endl
<< progName << " [image_path -- default lena.jpg] [G -- grayscale] " << endl << endl;
}
void Sharpen(const Mat& myImage,Mat& Result); // 遍历像素进行加权计算
int main( int argc, char* argv[])
{
help(argv[0]);
const char* filename = argc >=2 ? argv[1] : "lena.jpg";
Mat src, dst0, dst1;
if (argc >= 3 && !strcmp("G", argv[2]))
src = imread( samples::findFile( filename ), IMREAD_GRAYSCALE);
else
src = imread( samples::findFile( filename ), IMREAD_COLOR);
if (src.empty())
{
cerr << "Can't open image [" << filename << "]" << endl;
return EXIT_FAILURE;
}
namedWindow("Input", WINDOW_AUTOSIZE);
namedWindow("Output", WINDOW_AUTOSIZE);
imshow( "Input", src );
double t = (double)getTickCount();
Sharpen( src, dst0 );
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Hand written function time passed in seconds: " << t << endl;
imshow( "Output", dst0 );
waitKey();
Mat kernel = (Mat_<char>(3,3) << 0, -1, 0, // 声明并初始化掩码
-1, 5, -1,
0, -1, 0);
t = (double)getTickCount();
filter2D( src, dst1, src.depth(), kernel );
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Built-in filter2D time passed in seconds: " << t << endl;
imshow( "Output", dst1 );
waitKey();
return EXIT_SUCCESS;
}
void Sharpen(const Mat& myImage,Mat& Result)
{
CV_Assert(myImage.depth() == CV_8U); // accept only uchar images
const int nChannels = myImage.channels();
Result.create(myImage.size(),myImage.type());
for(int j = 1 ; j < myImage.rows-1; ++j)
{
const uchar* previous = myImage.ptr<uchar>(j - 1); // 获取上一行起始指针
const uchar* current = myImage.ptr<uchar>(j ); // 获取当前行起始指针
const uchar* next = myImage.ptr<uchar>(j + 1); // 获取下一行起始指针
uchar* output = Result.ptr<uchar>(j);
for(int i= nChannels;i < nChannels*(myImage.cols-1); ++i)
{
*output++ = saturate_cast<uchar>(5*current[i]
-current[i-nChannels] - current[i+nChannels] - previous[i] - next[i]);
}
}
Result.row(0).setTo(Scalar(0));
Result.row(Result.rows-1).setTo(Scalar(0));
Result.col(0).setTo(Scalar(0));
Result.col(Result.cols-1).setTo(Scalar(0));
}
获取图像指定坐标的像素强度
// 1. 获取图像像素值(单通道和多通道)
Scalar intensity = img.at<uchar>(y, x);
Scalar intensity = img.at<uchar>(Point(x, y));
// 获取uchar型数据
Vec3b intensity = img.at<Vec3b>(y, x);
uchar blue = intensity.val[0];
uchar green = intensity.val[1];
uchar red = intensity.val[2];
// 读取浮点型数据
Vec3f intensity = img.at<Vec3f>(y, x);
float blue = intensity.val[0];
float green = intensity.val[1];
float red = intensity.val[2];
// 更改指定位置的像素值
img.at<uchar>(y, x) = 128;
// 2. 转换图像类型
cvtColor(img, grey, COLOR_BGR2GRAY);
// 3. 更改图像数据的类型 8UC1 to 32FC1:
src.convertTo(dst, CV_32F);
// 4. 图像可视化
Mat img = imread("image.jpg");
namedWindow("image", WINDOW_AUTOSIZE);
imshow("image", img);
waitKey();
两个图象的加权和
Dst(I)=saturate(src1(I)∗alpha+src2(I)∗beta + gamma)
cv::addWeighted(inputArray src1, double alpha, inputArray src2, double beta, double gamma, outputArray dst, int dtype = -1);
该函数用于计算两个arrays的加权和,。一般有alpha = 1 - beta.
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
// we're NOT "using namespace std;" here, to avoid collisions between the beta variable and std::beta in c++17
using std::cin;
using std::cout;
using std::endl;
int main( void )
{
double alpha = 0.5; double beta; double input;
Mat src1, src2, dst;
cout << " Simple Linear Blender " << endl;
cout << "-----------------------" << endl;
cout << "* Enter alpha [0.0-1.0]: ";
cin >> input;
// We use the alpha provided by the user if it is between 0 and 1
if( input >= 0 && input <= 1 )
{ alpha = input; }
src1 = imread( samples::findFile("LinuxLogo.jpg") );
src2 = imread( samples::findFile("WindowsLogo.jpg") );
if( src1.empty() ) { cout << "Error loading src1" << endl; return EXIT_FAILURE; }
if( src2.empty() ) { cout << "Error loading src2" << endl; return EXIT_FAILURE; }
beta = ( 1.0 - alpha );
addWeighted( src1, alpha, src2, beta, 0.0, dst);
imshow( "Linear Blend", dst );
waitKey(0);
return 0;
}
更改图像的亮度和对比度g(i,j)=α⋅f(i,j)+β
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
// we're NOT "using namespace std;" here, to avoid collisions between the beta variable and std::beta in c++17
using std::cin;
using std::cout;
using std::endl;
using namespace cv;
int main( int argc, char** argv )
{
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
Mat image = imread( samples::findFile( parser.get<String>( "@input" ) ) );
if( image.empty() )
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " <Input image>" << endl;
return -1;
}
Mat new_image = Mat::zeros( image.size(), image.type() );
double alpha = 1.0; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
cout << "* Enter the beta value [0-100]: "; cin >> beta;
for( int y = 0; y < image.rows; y++ ) {
for( int x = 0; x < image.cols; x++ ) {
for( int c = 0; c < image.channels(); c++ ) {
// 下面就是核心函数
new_image.at<Vec3b>(y,x)[c] =
saturate_cast<uchar>( alpha*image.at<Vec3b>(y,x)[c] + beta ); // saturate_cast自动将数据转换到指定的uchar类型范围内。
}
}
}
imshow("Original Image", image);
imshow("New Image", new_image);
waitKey();
return 0;
官方推荐使用 LUT来更改图像像素,速度最快:
// 查表计算
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = saturate_cast<uchar>(pow(i / 255.0, gamma_) * 255.0);
// 上面相当于提前将0-255之间的数经过了计算,后面只是获取对应计算的结果就可以了,避免了重复计算,所以速度更快。
Mat res = img.clone();
LUT(img, lookUpTable, res);
离散傅里叶变换(DFT)与逆傅里叶变换(IDFT)
- 作用:对频率进行过滤,通过修改频率以达到图像增强、图像去噪、图像分割之边缘提取、图像特征提取、图像压缩等;
- 对于数字图像这种离散的信号,频率大小表示信号变换的剧烈程度或者说信号变化的快慢。频率越大,变换越剧烈,频率越小,信号越平缓,对应到的图像中,高频信号往往是图像中的边缘信号和噪声信号,而低频信号包含图像变化频繁的图像轮廓及背景等信号。
离散傅里叶变换
假设有M行N列的复数矩阵f, 其中f(x, y)代表f第x行第y列对应的值,x属于[0, M-1], y属于 [0, N-1], 其对应的傅里叶变换为:
可求的复数矩阵F为:
那么就有F为f的傅里叶变换,f为F的傅里叶逆变换。尽管图像矩阵为实数矩阵,但是计算得到的F一般包含复数元素。傅里叶变换的基本步骤为:
提示: 上面的第二公式可以分解为以为离散的傅里叶变换(先对行进行傅里叶变换再对列进行傅里叶变换):
下面是opencv提供的傅里叶变换以及逆变换的函数:
void cv::dft ( InputArray src, // 一维向量或图像数据
OutputArray dst, // 输出的尺寸和类型依赖于flags参数
int flags = 0, //
int nonzeroRows = 0 // 当非0时,函数会假设只有输入矩阵的第一个非零行包含非零元素,或只有输出矩阵的一个非零行包含非零元素。
)
参数flags:
DFT_INVERSE: 执行逆变换
DFT_SCALE: (是否除以M*N)输出的结果都会以1/N进行缩放,用于缩放操作,常和DFT_INVERSE结合使用,可以从上面的第一个公式看出。
DFT_ROWS: (输入举证的每行进行傅里叶变换或逆变换)对输入矩阵的每行进行正向或反向的变换,此标识符可以在处理多种矢量的时候用于减小资源开销,这些处理常常是三维或高维变换等复杂操作。
DFT_COMPLEX_OUTPUT:(输出复数形式)进行一维或二维实数数组正交换。这样的结果虽然是复数阵列,但拥有复数的共轭对称性,所以可以被写成一个拥有同样尺寸的实数阵列。
DFT_REAL_OUTPUT:(只输出实部)对一维二维复数数组进行逆向变换,这样的结果通常是一个尺寸相同的复数矩阵,但是如果输入矩阵有复数的共轭对称性(比如是一个带有DFT_COMPLEX_OUTPUT标识符的正变换结果),便会输出实数矩阵。
常相关的函数(计算向量的大小):
// 1. 用于计算2D向量的幅值。具体的公式如下图所示:
void cv::magnitude ( InputArray x, // floating-point array of x-coordinates of the vectors.
InputArray y, // floating-point array of y-coordinates of the vectors; it must have the same size as x.
OutputArray magnitude // output array of the same size and type as x.
)
快速离散傅里叶变换(在进行傅里叶变幻时,需要将图像的数据类型转换为浮点型)
opencv提供的函数为:
用于获取提升傅里叶计算速度适合的图像尺寸
int cv::getOptimalDFTSize ( int vecsize )
// vecsize 表示图像的高或宽
// 返回值为满足 2 ^p^ * 3 ^q^ * 5 ^r 的N,且N是比高或宽最接近的一个数,当然也可能直接使用((vecsize+1)/2)*2进行计算所得。
// 填充图像的边界,需要指定上下左右填充的行列数
copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0))
傅里叶幅度谱与相位谱:
注意:幅度矩阵的值可能会超过255,所以需要对计算结果进行数值压缩,再进行归一化处理,通常为log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2)),用于显示。
当然opencv中也提供了快速计算相位谱的函数:
void cv::phase (InputArray x,
InputArray y,
OutputArray angle,
bool angleInDegrees = false
)
// 如果angleInDegrees 为true,则该函数将以度为单位计算角度,否则,将以弧度为单位进行测量。
快速傅里叶变换的案例代码:
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char ** argv)
{
Mat I = imread("C:/Users/hp/Desktop/lena.jpg", IMREAD_GRAYSCALE);
if (I.empty()) {
cout << "Error opening image" << endl;
return EXIT_FAILURE;
}
//获取有利于傅里叶变换的图像尺寸
Mat padded;
int m = getOptimalDFTSize(I.rows);
int n = getOptimalDFTSize(I.cols);
// 使用0值填充扩展区域
copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0)); // 在图像的上下左右扩展指定像素宽度的像素区域
// 将扩展后的图像与另一个全为0的图像通道合并,前者用于存储实部,后者用于存储虚部。
Mat planes[] = { Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F) };
Mat complexI;
merge(planes, 2, complexI); // 第二个元素表示输入数据的长度
// 进行离散傅立叶变换
dft(complexI, complexI);
// 计算频谱图的幅值以及将幅值使用log对数缩放
// => log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
// planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
split(complexI, planes);
// 将复数转化为幅值,保存在planes[0]
magnitude(planes[0], planes[1], planes[0]); // planes[0]表示实数部分, planes[1]表示虚数部分
Mat magI = planes[0];
magI += Scalar::all(1);
log(magI, magI);
// 剪切和重分布幅度图像限,如果有奇数行或奇数列,进行频谱裁剪(方便进行重分布--重新排列傅里叶图像中的象限,使原点位于图像中心)
magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));
// -2实际转化为二进制的数为11111110。
// 当一个二进制数与11111110进行位的和运算时,该二进制数的最小数位就为0,转化为无符整型时,就是一个偶数。
int cx = magI.cols / 2;
int cy = magI.rows / 2;
Mat q0(magI, Rect(0, 0, cx, cy)); // Top-Left - Create a ROI per quadrant
Mat q1(magI, Rect(cx, 0, cx, cy)); // Top-Right
Mat q2(magI, Rect(0, cy, cx, cy)); // Bottom-Left
Mat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-Right
// swap quadrants (Top-Left with Bottom-Right)
Mat tmp;
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
// swap quadrant (Top-Right with Bottom-Left)
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
// 上面经过中心化后的图像,所得结果为中心区域表现为低频信号(边缘),周围为高频信号(背景纹理)
normalize(magI, magI, 0, 1, NORM_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();
return EXIT_SUCCESS;
}
利用快速傅里叶变换用于实现卷积运算
注意:只有当卷积核较大时,使用傅里叶变换实现卷积运算才有优势;
基本步骤:
假设图像M有r行c列,对应的卷积核k有h行w列;
- 第一步进行图像填充,行列上下左右各填充(h-1)/ 2行与(w-1)/ 2列,填充后的图像M_Padded大小为行:r + h -1 列:c + w - 1
- 计算M_Padded和K的快速傅里叶变换最优尺寸,并使用0填充右侧和下侧,结果记为M_Padded_zeros和K_zeros
- 计算M_Padded_zeros和K_Zeros的傅里叶变换,fft_M和fft_K
- 计算两个复数矩阵的点乘 fft_MK = fft_M * fft_K
- 计算fft_MK的傅里叶逆变换,然后只取实部,得到的是全卷积的结果fullConv
- 裁剪。从fullConv的左上角(h-1, w-1)开始裁剪到右下角(h-1+ r, w-1 +c)处,该区域就是原图像的卷积结果
// opencv提供了复数矩阵的点积运算函数
void cv::mulSpectrums(InputArray a, // 双通道复数矩阵
InputArray b, // 双通道复数矩阵
OutputArray c,
int flags,
bool conjB = false // 是否对b共轭
)
使用傅里叶变换替代卷积操作的案例代码
Mat fft2Conv(Mat I, Mat kernel, int borderType=BORDER_DEFAULT, Scalar value=Scalar())
{
// M 的宽高
int R = I.rows;
int C = I.cols;
// 卷积核K的宽高,一般卷积核的大小为奇数
int r = kernel.rows;
int c = kernel.cols;
// 卷积核的半径
int tb = (r - 1) / 2;
int lr = (c - 1) / 2;
/* 1.边界扩充 */
Mat I_padded;
copyMakeBorder(I, I_padded, tb, tb, lr, lr, borderType, value);
/* 2.补0以满足快速傅里叶变换的行数和列数 */
// 满足二维快速傅里叶变换的行数列数
int rows = getOptimalDFTSize(I_padded.rows + r - 1);
int cols = getOptimalDFTSize(I_padded.cols + c - 1);
// 补0
Mat I_padded_zeros, kernel_zeros;
copyMakeBorder(I_padded, I_padded_zeros, 0, rows-I_padded.rows, 0, cols - I_padded.cols, BORDER_CONSTANT, Scalar(0, 0, 0));
copyMakeBorder(kernel, kernel_zeros, 0, rows-kernel.rows, 0, cols-kernel.cols, BORDER_CONSTANT, Scalar(0,0,0));
/* 3.快速傅里叶变换 */
Mat fft2_Ipz, fft2_kz;
dft(I_padded_zeros, fft2_Ipz, DFT_COMPLEX_OUTPUT);
dft(kernel_zeros, fft2_kz, DFT_COMPLEX_OUTPUT);
/* 4. 两个傅里叶变换点乘 */
Mat Ipz_kz;
mulSpectrums(fft2_Ipz, fft2_kz, Ipz_kz, DFT_ROWS);
/* 5.傅里叶逆变换,并只取实部 */
Mat ifft2;
dft(Ipz_kz, ifft2, DFT_INVERSE+DFT_SCALE+DFT_REAL_OUTPUT);
/* 6.裁剪,与所输入的图像矩阵的尺寸相同 */
Mat sameConv = ifft2(Rect(c-1, r-1, C+c-1, R+r-1));
return sameConv;
}
傅里叶变换实现图像的显著性检测----普残差方法(Spectral Residual)
显著性检测的定义:图像显著性区域检测是近年来计算机视觉和图像处理领域的研究热点之一,其目标是在计算机上实现像人眼一样快速判断图像中显著性区域,可以广泛用于目标识别、图像编辑以及图像检索等领域。
步骤的详细描述如下:
- 计算图像的快速傅里叶变换矩阵 F
- 计算傅里叶变换的幅度谱的灰度级 graySpectrum
- 计算相位谱 phaseSpectrum,然后根据相位谱计算出对应的正弦谱和余弦谱
- 对第二步计算出的灰度级进行均值平滑
- 计算谱残差(spectralResidual)。谱残差的定义是第 2 步得到的幅度谱的灰度级减去第 4 步得到的平滑结果
- 对谱残差进行幂指数运算 exp(spectralResidual),即对谱残差矩阵中的每一个值进行指数运算
- 将第 6 步得到的幂指数作为新的 “幅度谱”,仍然使用原图的相位谱,根据新的 “幅度谱”和相位谱进行傅里叶逆变换,可得到一个复数矩阵
- 对于第 7 步得到的复数矩阵,计算该矩阵的实部和虚部的平方和的开方,然后进行高斯平滑,最后进行灰度级的转换,即得到显著性。
显著性检测案例代码----普残差方法介绍
// 计算幅度谱
void amplitudeSpectrum(Mat& _srcFFT, Mat& _dstSpectrum)
{
// 判断傅里叶变换有两个通道
CV_Assert(_srcFFT.channels() == 2);
// 分离通道
vector<Mat> FFT2Channel;
split(_srcFFT, FFT2Channel);
// 计算傅里叶变换的幅度谱
magnitude(FFT2Channel[0], FFT2Channel[1], _dstSpectrum);
}
Mat graySpectrum(Mat spectrum)
{
Mat dst;
log(spectrum+1, dst);
// 归一化
normalize(dst, dst, 0, 1, NORM_MINMAX);
// 为了进行灰度级显示,做类型转换
dst.convertTo(dst, CV_8UC1, 255, 0);
return dst;
}
// 计算相位谱
Mat phaseSpectrum(Mat _srcFFT)
{
// 相位谱
Mat phase;
phase.create(_srcFFT.size(), CV_64FC1);
// 分离通道
vector<Mat> FFT2Channel;
split(_srcFFT, FFT2Channel);
// 计算相位谱
for(int r = 0; r < phase.rows; r++)
{
for(int c = 0; c < phase.cols; c++)
{
// 实部 虚部
double real = FFT2Channel[0].at<double>(r, c);
double imaginary = FFT2Channel[1].at<double>(r,c);
// atan2 的返回值范围: [0, 180], [-180, 0]
phase.at<double>(r,c) = atan2(imaginary, real);
}
}
return phase;
}
int main()
{
string outdir = "./images/";
// 输入图像
Mat img = imread("img4.jpg");
Mat gray;
Mat fGray;
cvtColor(img, gray, COLOR_BGR2GRAY);
gray.convertTo(fGray, CV_64F, 1.0/255);
// 快速傅里叶变换
Mat F;
fft2Image(fGray, F);
// 幅度谱
Mat amplitude;
amplitudeSpectrum(F, amplitude);
Mat ampSpectrum = graySpectrum(amplitude);
// 对幅度谱进行对数运算
Mat logAmplitude;
log(amplitude + 1.0, logAmplitude);
// 均值平滑
Mat meanLogAmplitude;
blur(logAmplitude, meanLogAmplitude, Size(3,3), Point(-1, -1));
// 谱残差
Mat spectralResidual = logAmplitude - meanLogAmplitude;
// 相位谱
Mat phase = phaseSpectrum(F);
// 余弦谱 cos(phase)
Mat cosSpectrum(phase.size(), CV_64FC1);
// 正弦谱
Mat sinSpectrum(phase.size(), CV_64FC1);
for(int r = 0; r < phase.rows; r++)
{
for(int c = 0; c < phase.cols; c++)
{
cosSpectrum.at<double>(r, c) = cos(phase.at<double>(r,c));
sinSpectrum.at<double>(r, c) = sin(phase.at<double>(r,c));
}
}
// 指数运算
exp(spectralResidual, spectralResidual);
Mat real = spectralResidual.mul(cosSpectrum);
Mat imaginary = spectralResidual.mul(sinSpectrum);
vector<Mat> realAndImg;
realAndImg.push_back(real);
realAndImg.push_back(imaginary);
Mat complex;
merge(realAndImg, complex);
// 快速傅里叶变换
Mat ifft2;
dft(complex, ifft2, DFT_COMPLEX_OUTPUT+DFT_INVERSE);
// 傅里叶逆变换的幅度
Mat ifft2Amp;
amplitudeSpectrum(ifft2, ifft2Amp);
// 平方运算
pow(ifft2Amp, 2.0, ifft2Amp);
// 高斯平滑
GaussianBlur(ifft2Amp, ifft2Amp, Size(11, 11), 2.5);
// 显著性显示
normalize(ifft2Amp, ifft2Amp, 1.0, 0, NORM_MINMAX);
// 提升对比度,进行伽马变换
pow(ifft2Amp, 0.5, ifft2Amp);
// 数据类型转换
Mat saliencyMap;
ifft2Amp.convertTo(saliencyMap, CV_8UC1, 255);
imwrite(outdir+"img4_显著性.jpg", saliencyMap);
}
使用快速傅里叶变换进行滤波处理
傅里叶滤波的基本流程:
常用的几种滤波器:(必须了解的:低频信息表示图像中灰度值缓慢变化的区域;而高频区域则正好相反,表示灰度值变化迅速的部分,如边缘)
低通滤波器(相当于保留了低频信息,消弱或者移除了高频信息。)
- 理想低通滤波器
其中radius表示截断频率。D(r, c)表示到中心位置的距离。
- 巴特沃斯低通滤波器
n表示阶数:
- 高斯低通滤波器
高通滤波器(保留了频谱中的高频部分,通常是图像的边缘。)
- 理想高通滤波器
- 巴特沃斯高通滤波器
- 高斯高通滤波器
带通滤波器(带通滤波是指指保留某一范围区域的频率带)
带阻滤波器(与带通滤波相反,带阻滤波是指撤销或消弱指定范围区域的频率带。)
同态滤波器(使用同态滤波处理后可以看到原图中更多的信息)
同态滤波的基础流程:
案例代码2:周期噪声去除(周期噪声可以被明显的抑制通过使用频域滤波器)
#include<opencv2/opencv.hpp>
#include<vector>
#include<string>
#include<iostream>
using namespace cv;
using namespace std;
Mat I;//输入的图像矩阵
Mat F;//图像的快速傅里叶变换
Point maxLoc;//傅里叶谱的最大值的坐标
int radius = 20;//截断频率
const int Max_RADIUS = 100;//设置最大的截断频率
Mat lpFilter;//低通滤波器
int lpType = 0;//低通滤波器的类型
const int MAX_LPTYPE = 2;
Mat F_lpFilter;//低通傅里叶变换
Mat FlpSpectrum;//低通傅里叶变换的傅里叶谱的灰度级
Mat result;//低通滤波后的效果
string lpFilterspectrum = "低通傅里叶谱";//显示窗口的名称
//快速傅里叶变换
void fft2Image(Mat I, Mat &F);
//幅度谱
void amplitudeSpectrum(InputArray _srcFFT, OutputArray _dstSpectrum);
//幅度谱的灰度级显示
Mat graySpectrum(Mat spectrum);
void callback_lpFilter(int, void*);
//低通滤波器类型:理想低通滤波器、巴特沃斯低通滤波器、高斯低通滤波器
enum LPFILTER_TYPE { ILP_FILTER = 0, BLP_FILTER = 1, GLP_FILTER = 2 };
//构建低通滤波器
Mat createLPFilter(Size size, Point center, float radius, int type, int n);
//主函数
int main(int argc, char*argv[])
{
/* -- 第一步:读入图像矩阵 -- */
I = imread("D:/AFile_LIMAOHUA/OpencvProject/ConsoleApplication1/period_input.jpg", 0);
if (!I.data)
return -1;
// imwrite("I1.jpg", I);
//数据类型转换,转换为浮点型
Mat fI;
I.convertTo(fI, CV_32FC1, 1.0, 0.0);
/* -- 第二步:每一个数乘以(-1)^(r+c) -- ,保证幅度谱的中心点的幅度最大,距离中心点越近,频率越高,越远频率越小*/
for (int r = 0; r < fI.rows; r++)
{
for (int c = 0; c < fI.cols; c++)
{
if ((r + c) % 2)
{
fI.at<float>(r, c) *= -1;
}
}
}
/* -- 第三、四步:补0和快速傅里叶变换 -- */
fft2Image(fI, F); // 这里的F是双通道,包含实部和虚部
//傅里叶谱
Mat amplSpec;
amplitudeSpectrum(F, amplSpec); // 获取幅度谱
//傅里叶谱的灰度级显示
Mat spectrum = graySpectrum(amplSpec); // 用于显示
circle(spectrum,Point(320,240), 20, Scalar(255));
imshow("原傅里叶谱的灰度级显示", spectrum);
//imwrite("spectrum.jpg", spectrum);
//找到傅里叶谱的最大值的坐标
minMaxLoc(spectrum, NULL, NULL, NULL, &maxLoc);
/* -- 低通滤波 -- */
namedWindow(lpFilterspectrum, WINDOW_AUTOSIZE);
createTrackbar("低通类型:", lpFilterspectrum, &lpType, MAX_LPTYPE,
callback_lpFilter);
createTrackbar("半径:", lpFilterspectrum, &radius, Max_RADIUS,
callback_lpFilter);
callback_lpFilter(0, 0);
waitKey(0);
return 0;
}
void fft2Image(Mat I, Mat &F)
{
//得到I的行数和列数
int rows = I.rows;
int cols = I.cols;
//满足快速傅里叶变换的最优行数和列数
int rPadded = getOptimalDFTSize(rows);
int cPadded = getOptimalDFTSize(cols);
//左侧和下侧补0
Mat f;
copyMakeBorder(I, f, 0, rPadded - rows, 0, cPadded - cols, BORDER_CONSTANT, Scalar::all(0));
//单通道转为双通道
Mat planes[] = { f, Mat::zeros(f.size(), CV_32F) };
merge(planes, 2, f);
//快速傅里叶变换(双通道,用于存储实部和虚部)
dft(f, F, DFT_COMPLEX_OUTPUT);
}
void amplitudeSpectrum(InputArray _srcFFT, OutputArray _dstSpectrum)
{
//判断傅里叶变换有两个通道
CV_Assert(_srcFFT.channels() == 2);
//分离通道
vector<Mat> FFT2Channel;
split(_srcFFT, FFT2Channel);
//计算傅里叶变换的幅度谱 sqrt(pow(R,2)+pow(I,2))
magnitude(FFT2Channel[0], FFT2Channel[1], _dstSpectrum);
}
// 只是方便展示
Mat graySpectrum(Mat spectrum)
{
Mat dst;
log(spectrum + 1, dst);
//归一化
normalize(dst, dst, 0, 1, NORM_MINMAX);
//为了进行灰度级显示,做类型转换
dst.convertTo(dst, CV_8UC1, 255, 0);
return dst;
}
//回调函数:调整低通滤波器的类型及截断频率
void callback_lpFilter(int, void*)
{
/* -- 第五步:构建低通滤波器 -- */
lpFilter = createLPFilter(F.size(), maxLoc, radius, lpType, 2); // lpFilter的大小和
cout << "行:" << maxLoc.x << "列:" << maxLoc.y << endl;
/*-- 第六步:低通滤波器和图像的快速傅里叶变换点乘 --*/
F_lpFilter.create(F.size(), F.type());
for (int r = 0; r < F_lpFilter.rows; r++)
{
for (int c = 0; c < F_lpFilter.cols; c++)
{
//分别取出当前位置的快速傅里叶变换和理想低通滤波器的值
Vec2f F_rc = F.at<Vec2f>(r, c);
float lpFilter_rc = lpFilter.at<float>(r, c);
//低通滤波器和图像的快速傅里叶变换的对应位置相乘
F_lpFilter.at<Vec2f>(r, c) = F_rc * lpFilter_rc;
}
}
//低通傅里叶变换的傅里叶谱
amplitudeSpectrum(F_lpFilter, FlpSpectrum);
//低通傅里叶谱的灰度级显示
FlpSpectrum = graySpectrum(FlpSpectrum);
imshow(lpFilterspectrum, FlpSpectrum);
// imwrite("FlpSpectrum.jpg", FlpSpectrum);
/* -- 第七、八步:对低通傅里叶变换执行傅里叶逆变换,并只取实部 -- */
dft(F_lpFilter, result, DFT_SCALE + DFT_INVERSE + DFT_REAL_OUTPUT);
/* -- 第九步:同乘以(-1)^(x+y) -- */
for (int r = 0; r < result.rows; r++)
{
for (int c = 0; c < result.cols; c++)
{
if ((r + c) % 2)
result.at<float>(r, c) *= -1;
}
}
//注意将结果转换为 CV_8U 类型
result.convertTo(result, CV_8UC1, 1.0, 0);
/* -- 第十步:截取左上部分,其大小与输入图像的大小相同--*/
result = result(Rect(0, 0, I.cols, I.rows)).clone();
imshow("经过低通滤波后的图片", result);
imwrite("lF.jpg", result);
}
// 创建三种常用的低通滤波器, 高通滤波器可以直接根据上面的公式进行更改即可
Mat createLPFilter(Size size, Point center, float radius, int type, int n = 2)
{
Mat lpFilter = Mat::zeros(size, CV_32FC1);
int rows = size.height;
int cols = size.width;
if (radius <= 0)
return lpFilter;
//构建理想低通滤波器
if (type == ILP_FILTER)
{
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
float norm2 = pow(abs(float(r - center.y)), 2) + pow(abs(float(c - center.x)), 2);
if (sqrt(norm2) < radius)
lpFilter.at<float>(r, c) = 1;
else
lpFilter.at<float>(r, c) = 0;
}
}
}
//构建巴特沃斯低通滤波器
if (type == BLP_FILTER)
{
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
lpFilter.at<float>(r, c) = float(1.0 / (1.0 + pow(sqrt(pow(r - center.y, 2.0) + pow(c - center.x, 2.0)) / radius, 2.0*n)));
}
}
}
//构建高斯低通滤波器
if (type == GLP_FILTER)
{
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
lpFilter.at<float>(r, c) = float(exp(-(pow(c - center.x, 2.0) + pow(r - center.y, 2.0)) / (2 * pow(radius, 2.0))));
}
}
}
return lpFilter;
}
使用opencv进行并行编程
// 使用并行编程的两个场景:
1. 多个线程对同一个内存进行读写操作;
2. 多个线程对同意内存进行写操作;
// 官方的并行编程代码案例(并行卷积运算)
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
using namespace std;
using namespace cv;
namespace
{
//! [convolution-sequential]
void conv_seq(Mat src, Mat &dst, Mat kernel)
{
//![convolution-make-borders]
int rows = src.rows, cols = src.cols;
dst = Mat(rows, cols, src.type());
// 根据卷积核的大小去填充图像的边界
// Make border = kernel.rows / 2;
int sz = kernel.rows / 2;
copyMakeBorder(src, src, sz, sz, sz, sz, BORDER_REPLICATE);
//![convolution-make-borders]
//! [convolution-kernel-loop] 进行卷积运算
for (int i = 0; i < rows; i++)
{
uchar *dptr = dst.ptr(i);
for (int j = 0; j < cols; j++)
{
double value = 0;
for (int k = -sz; k <= sz; k++)
{
// slightly faster results when we create a ptr due to more efficient memory access.
uchar *sptr = src.ptr(i + sz + k);
for (int l = -sz; l <= sz; l++)
{
value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];
}
}
dptr[j] = saturate_cast<uchar>(value);
}
}
//! [convolution-kernel-loop]
}
//! [convolution-sequential]
// 对上面的函数进行重新的并行编程,第一种并行编程
#ifdef CV_CXX11
void conv_parallel(Mat src, Mat &dst, Mat kernel)
{
int rows = src.rows, cols = src.cols;
dst = Mat(rows, cols, CV_8UC1, Scalar(0)); // 创建默认值为零的单通道图像
int sz = kernel.rows / 2;
copyMakeBorder(src, src, sz, sz, sz, sz, BORDER_REPLICATE); // 填充图像边界
//! [convolution-parallel-cxx11]
parallel_for_(Range(0, rows * cols), [&](const Range &range) // 将for循环改为并行处理
{
for (int r = range.start; r < range.end; r++)
{
int i = r / cols, j = r % cols;
double value = 0;
for (int k = -sz; k <= sz; k++)
{
uchar *sptr = src.ptr(i + sz + k);
for (int l = -sz; l <= sz; l++)
{
value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];
}
}
dst.ptr(i)[j] = saturate_cast<uchar>(value);
}
});
//! [convolution-parallel-cxx11]
}
// 第二种并行编程方法
void conv_parallel_row_split(Mat src, Mat &dst, Mat kernel)
{
int rows = src.rows, cols = src.cols;
dst = Mat(rows, cols, CV_8UC1, Scalar(0));
// Taking care of edge values
// Make border = kernel.rows / 2;
int sz = kernel.rows / 2;
copyMakeBorder(src, src, sz, sz, sz, sz, BORDER_REPLICATE);
//! [convolution-parallel-cxx11-row-split]
parallel_for_(Range(0, rows), [&](const Range &range)
{
for (int i = range.start; i < range.end; i++)
{
uchar *dptr = dst.ptr(i);
for (int j = 0; j < cols; j++)
{
double value = 0;
for (int k = -sz; k <= sz; k++)
{
uchar *sptr = src.ptr(i + sz + k);
for (int l = -sz; l <= sz; l++)
{
value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];
}
}
dptr[j] = saturate_cast<uchar>(value);
}
}
});
//! [convolution-parallel-cxx11-row-split]
}
#else
//! [convolution-parallel]
class parallelConvolution : public ParallelLoopBody
{
private:
Mat m_src, &m_dst;
Mat m_kernel;
int sz;
public:
parallelConvolution(Mat src, Mat &dst, Mat kernel)
: m_src(src), m_dst(dst), m_kernel(kernel)
{
sz = kernel.rows / 2;
}
//! [overload-full]
virtual void operator()(const Range &range) const CV_OVERRIDE
{
for (int r = range.start; r < range.end; r++)
{
int i = r / m_src.cols, j = r % m_src.cols;
double value = 0;
for (int k = -sz; k <= sz; k++)
{
uchar *sptr = m_src.ptr(i + sz + k);
for (int l = -sz; l <= sz; l++)
{
value += m_kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];
}
}
m_dst.ptr(i)[j] = saturate_cast<uchar>(value);
}
}
//! [overload-full]
};
//! [convolution-parallel]
void conv_parallel(Mat src, Mat &dst, Mat kernel)
{
int rows = src.rows, cols = src.cols;
dst = Mat(rows, cols, CV_8UC1, Scalar(0));
// Taking care of edge values
// Make border = kernel.rows / 2;
int sz = kernel.rows / 2;
copyMakeBorder(src, src, sz, sz, sz, sz, BORDER_REPLICATE);
//! [convolution-parallel-function]
parallelConvolution obj(src, dst, kernel);
parallel_for_(Range(0, rows * cols), obj);
//! [convolution-parallel-function]
}
//! [conv-parallel-row-split]
class parallelConvolutionRowSplit : public ParallelLoopBody
{
private:
Mat m_src, &m_dst;
Mat m_kernel;
int sz;
public:
parallelConvolutionRowSplit(Mat src, Mat &dst, Mat kernel)
: m_src(src), m_dst(dst), m_kernel(kernel)
{
sz = kernel.rows / 2;
}
//! [overload-row-split]
virtual void operator()(const Range &range) const CV_OVERRIDE
{
for (int i = range.start; i < range.end; i++)
{
uchar *dptr = dst.ptr(i);
for (int j = 0; j < cols; j++)
{
double value = 0;
for (int k = -sz; k <= sz; k++)
{
uchar *sptr = src.ptr(i + sz + k);
for (int l = -sz; l <= sz; l++)
{
value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];
}
}
dptr[j] = saturate_cast<uchar>(value);
}
}
}
//! [overload-row-split]
};
//! [conv-parallel-row-split]
void conv_parallel_row_split(Mat src, Mat &dst, Mat kernel)
{
int rows = src.rows, cols = src.cols;
dst = Mat(rows, cols, CV_8UC1, Scalar(0));
// Taking care of edge values
// Make border = kernel.rows / 2;
int sz = kernel.rows / 2;
copyMakeBorder(src, src, sz, sz, sz, sz, BORDER_REPLICATE);
//! [convolution-parallel-function-row]
parallelConvolutionRowSplit obj(src, dst, kernel);
parallel_for_(Range(0, rows), obj);
//! [convolution-parallel-function-row]
}
#endif
static void help(char *progName)
{
cout << endl
<< " This program shows how to use the OpenCV parallel_for_ function and \n"
<< " compares the performance of the sequential and parallel implementations for a \n"
<< " convolution operation\n"
<< " Usage:\n "
<< progName << " [image_path -- default lena.jpg] " << endl
<< endl;
}
}
int main(int argc, char *argv[])
{
help(argv[0]);
const char *filepath = argc >= 2 ? argv[1] : "../../../../data/lena.jpg";
Mat src, dst, kernel;
src = imread(filepath, IMREAD_GRAYSCALE);
if (src.empty())
{
cerr << "Can't open [" << filepath << "]" << endl;
return EXIT_FAILURE;
}
namedWindow("Input", 1);
namedWindow("Output1", 1);
namedWindow("Output2", 1);
namedWindow("Output3", 1);
imshow("Input", src);
kernel = (Mat_<double>(3, 3) << 1, 0, -1,
1, 0, -1,
1, 0, -1);
/*
Uncomment the kernels you want to use or write your own kernels to test out
performance.
*/
/*
kernel = (Mat_<double>(5, 5) << 1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1);
kernel /= 100;
*/
/*
kernel = (Mat_<double>(3, 3) << 1, 1, 1,
0, 0, 0,
-1, -1, -1);
*/
double t = (double)getTickCount();
conv_seq(src, dst, kernel);
t = ((double)getTickCount() - t) / getTickFrequency();
cout << " Sequential implementation: " << t << "s" << endl;
imshow("Output1", dst);
waitKey(0);
t = (double)getTickCount();
conv_parallel(src, dst, kernel);
t = ((double)getTickCount() - t) / getTickFrequency();
cout << " Parallel Implementation: " << t << "s" << endl;
imshow("Output2", dst);
waitKey(0);
t = (double)getTickCount();
conv_parallel_row_split(src, dst, kernel);
t = ((double)getTickCount() - t) / getTickFrequency();
cout << " Parallel Implementation(Row Split): " << t << "s" << endl
<< endl;
imshow("Output3", dst);
waitKey(0);
// imwrite("src.png", src);
// imwrite("dst.png", dst);
return 0;
}