该篇围绕Core Functionality模块进行展开

该模块的主要作用是成为构建opencv更多高级功能的基础核心层。

Mat基础图像存储数据结构

  1. 将Mat对象赋值给其他Mat变量将会共享一个地址;
  2. 当使用赋值运算符和复制构造函数时仅复制标头,清除最后一个赋值的对象图像矩阵,将会清空所有存储该矩阵数据的Mat对象;
  3. 使用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;]

图像遍历

  1. 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 matricesCV_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 ;
}

图像的掩码操作(根据掩码矩阵对像素进行更改,也被称为核操作)

  1. 使用掩码对图像进行卷积操作的速度要远快于使用遍历图像像素进行处理的速度
// 使用掩码+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);elsesrc = 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 imagesconst 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 1if( 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 quadrantMat q1(magI, Rect(cx, 0, cx, cy));  // Top-RightMat q2(magI, Rect(0, cy, cx, cy));  // Bottom-LeftMat 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 resultimshow("spectrum magnitude", magI);waitKey();return EXIT_SUCCESS;
}

利用快速傅里叶变换用于实现卷积运算

注意:只有当卷积核较大时,使用傅里叶变换实现卷积运算才有优势;
基本步骤:
假设图像M有r行c列,对应的卷积核k有h行w列;

  1. 第一步进行图像填充,行列上下左右各填充(h-1)/ 2行与(w-1)/ 2列,填充后的图像M_Padded大小为行:r + h -1 列:c + w - 1
  2. 计算M_Padded和K的快速傅里叶变换最优尺寸,并使用0填充右侧和下侧,结果记为M_Padded_zeros和K_zeros
  3. 计算M_Padded_zeros和K_Zeros的傅里叶变换,fft_M和fft_K
  4. 计算两个复数矩阵的点乘 fft_MK = fft_M * fft_K
  5. 计算fft_MK的傅里叶逆变换,然后只取实部,得到的是全卷积的结果fullConv
  6. 裁剪。从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);// 补0Mat 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)

显著性检测的定义:图像显著性区域检测是近年来计算机视觉和图像处理领域的研究热点之一,其目标是在计算机上实现像人眼一样快速判断图像中显著性区域,可以广泛用于目标识别、图像编辑以及图像检索等领域。

步骤的详细描述如下:

  1. 计算图像的快速傅里叶变换矩阵 F
  2. 计算傅里叶变换的幅度谱的灰度级 graySpectrum
  3. 计算相位谱 phaseSpectrum,然后根据相位谱计算出对应的正弦谱和余弦谱
  4. 对第二步计算出的灰度级进行均值平滑
  5. 计算谱残差(spectralResidual)。谱残差的定义是第 2 步得到的幅度谱的灰度级减去第 4 步得到的平滑结果
  6. 对谱残差进行幂指数运算 exp(spectralResidual),即对谱残差矩阵中的每一个值进行指数运算
  7. 将第 6 步得到的幂指数作为新的 “幅度谱”,仍然使用原图的相位谱,根据新的 “幅度谱”和相位谱进行傅里叶逆变换,可得到一个复数矩阵
  8. 对于第 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);
}

使用快速傅里叶变换进行滤波处理

傅里叶滤波的基本流程:

常用的几种滤波器:(必须了解的:低频信息表示图像中灰度值缓慢变化的区域;而高频区域则正好相反,表示灰度值变化迅速的部分,如边缘

低通滤波器(相当于保留了低频信息,消弱或者移除了高频信息。)

  1. 理想低通滤波器
    其中radius表示截断频率。D(r, c)表示到中心位置的距离。
  2. 巴特沃斯低通滤波器
    n表示阶数:
  3. 高斯低通滤波器

高通滤波器(保留了频谱中的高频部分,通常是图像的边缘。)

  1. 理想高通滤波器
  2. 巴特沃斯高通滤波器
  3. 高斯高通滤波器

带通滤波器(带通滤波是指指保留某一范围区域的频率带)

更多频域滤波详细情况参考

带阻滤波器(与带通滤波相反,带阻滤波是指撤销或消弱指定范围区域的频率带。)

同态滤波器(使用同态滤波处理后可以看到原图中更多的信息)

同态滤波的基础流程:

案例代码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);//左侧和下侧补0Mat 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;elselpFilter.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]
}#endifstatic 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 outperformance.*//*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;
}

opencv回顾之Core module相关推荐

  1. Can't locate SVN/Core.pm in @INC (you may need to install the SVN::Core module)报错

    刚开始用mac,啥都不知道,原来它自带了git,svn;但是我用sourcetree,还有apicloud studio2都会报错, Can't locate SVN/Core.pm in @INC ...

  2. 2.Nginx学习-The HTTP Core module

    http core module是Ngnix提供WEB服务的最核心模块,默认被开启.本篇文章将讲述该模块的一些配置 配置文件结构: http {server {// virtual websitelo ...

  3. .NET Core开发日志——从ASP.NET Core Module到KestrelServer

    ASP.NET Core程序现在变得如同控制台(Console)程序一般,同样通过Main方法启动整个应用.而Main方法要做的事情很简单,创建一个WebHostBuilder类,调用其Build方法 ...

  4. 关于VSCode引用OpenCV模块出现提示Module ‘cv2‘ has no ‘imread‘ member

    关于VSCode引用OpenCV模块出现提示Module 'cv2' has no 'imread' member 具体提示如下: Module 'cv2' has no 'imread' membe ...

  5. opencv回顾之路Imgproc Module

    图像处理模块 一. 基础图像处理部分: 1.图形绘制操作 2. 图像文本显示 3. 图像平滑处理(降低图像噪声:g(i,j)=∑ (k,i) F(i+k,j+l)h(k,l)) 3.1 blur(): ...

  6. Angular项目中核心模块core Module只加载一次的实现

    核心模块CoreModule在整个系统中只加载一次,如何实现? 创建core Modele:ng g m core 既然CoreModule是类,就有构造函数,在构造函数中进行依赖注入. export ...

  7. Perforce研讨会回顾 | Helix Core在芯片行业的应用实例:芯片项目的版本控制、持续集成及自动化

    2023年2月28日,龙智联合全球领先的数字资产管理和DevSecOps工具厂商Perforce共同举办Perforce on Tour网络研讨会--"赋能'大'研发,助力'快'交付&quo ...

  8. CUDA: OpenCV requires enabled ‘cudev‘ module from ‘opencv_contrib

    wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/4.X.X.zip unzip opencv_c ...

  9. OpenCV之core 模块. 核心功能(1)Mat - 基本图像容器 OpenCV如何扫描图像、利用查找表和计时 矩阵的掩码操作 使用OpenCV对两幅图像求和(求混合(blending))

    Mat - 基本图像容器 目的 从真实世界中获取数字图像有很多方法,比如数码相机.扫描仪.CT或者磁共振成像.无论哪种方法,我们(人类)看到的是图像,而让数字设备来"看"的时候,则 ...

最新文章

  1. 在线项目管理软件leangoo 管理 技术支持
  2. ★如何证明自己不是精神病?
  3. iOS推送:要支持戴口罩解锁了!以为是黑科技,原来是骗我买iWatch...
  4. PHP_Memcache函数详解
  5. 程序员应该知道的二十三种设计模式
  6. 20155209 林虹宇 Exp9 Web安全基础
  7. 便捷式计算机无线功能按钮,TP-Link TL-MR13U便携式无线路由器Client模式设置
  8. 大数据教程(1.6):linux网络配置及CRT远程连接
  9. CSDN Androidclient开展(两):基于如何详细解释Java使用Jsoup爬行动物HTML数据
  10. 【转】hive简介安装 配置常见问题和例子
  11. VScode+Latex:代码和pdf互相跳转
  12. java工作流引擎(j-roadflow)快速入门教程
  13. emmagee 性能工具梳理
  14. 分解GIF图片、合成GIF图片
  15. 微信视频压缩画质怎么办?微信发视频怎么不压缩画质
  16. 爬虫关键信息获取与MongoDB数据库使用方法
  17. 百度网盘下载显示系统限制,无法下载解决
  18. linux重定向的 符号,linux基础学习-4.3-特殊符号与重定向符号详解
  19. 来来来!java程序员个人优势
  20. 10、netty结合websocket完成消息的单发和群发

热门文章

  1. 基于IAP实现的STM32F系列固件升级(远程升级 + SD卡升级)
  2. java截取split_Java使用split截取字符串过程解析
  3. 微信小程序设置打开pdf并进行保存到本地,下次打开就无需进行服务器请求
  4. php 数据导出 Excel 表 tp3
  5. 异常类练习——编写应用程序,从键盘输入两个数作为除数和被除数。捕获NumberFormatException 异常和ArithmeticException 异常,
  6. 6种难以启齿的离职原因,最好这么说
  7. html表格不随字数变化,设置table中的宽度不随文字改变让其固定
  8. Linux中安装的mysql查不到配置文件my.cnf
  9. ALSA音频架构之声卡设备信息
  10. java中剖析Unicode和UTF-8等其他编码格式