opencv学习周报 (上)
周报目录:
Opencv学习:
(1)基本的opencv数据类型的容器
(2)基本的函数学习(如输入输出,延时函数等)
(3)基础学习:
(1) 算数与几何操作
(2) LUT查找表
(3) 伪彩色与颜色表
(4) 图像通道分离与合并
(5) 色彩空间转换
(6) 像素统计
(7) 图像几何操作与图形绘制
(8) 规则ROI与不规则ROI提取
(9) 图像直方图
(4)空间滤波,边缘检测,图像复原与重建:
(1)图像卷积基本函数
(2)高斯,中值,均值卷积函数
(3) 图像噪声和去噪
(4)边缘保留滤波(高斯双边,非局部均值,均值迁移)
(5)图像梯度(几种算子)
(6)图像锐化:拉普拉斯和USM两种方法
(7)Canny边缘检测
(5)图像金字塔:
(1)高斯与拉普拉斯
(2)金字塔重建
(6) (...)
其中,绿色部分均整理在https://blog.csdn.net/weixin_61652397/article/details/123968821?spm=1001.2014.3001.5501
粉色部分有图像滤波以及常用的图像锐化原理
基础学习6:像素统计
void minMaxLoc(InputArray src, CV_OUT double* minVal, CV_OUT double* maxVal = 0, CV_OUT Point* minLoc = 0, CV_OUT Point* maxLoc = 0, InputArray mask = noArray()); src:输入图像。 minVal:返回像素最小值。可输入NULL表示不需要。 maxVal :返回像素最大值。可输入NULL表示不需要。 minLoc:返回最小值的位置。可输入NULL表示不需要。 maxLoc:返回最大值的位置。可输入NULL表示不需要。 mask:可选项。
void meanStdDev(InputArray src, OutputArray mean, OutputArray stddev, InputArray mask=noArray()); src:输入图像。 mean:返回图像像素均值。 stddev:返回图像像素均差值。(这里指的是standarddeviation是标准方差) mask:可选项。
主要是对于这两个库函数进行学习,他们的目的是获取图像的像素最大最小值以及其位置,像素均值以及标准方差,大致确定图像像素的取值区间。
这里用自定义参数传出数据。
基础学习7:图像几何操作与图形绘制
这一段便是基本的调用函数,没用复杂的数学推导或者理解:
https://www.cnblogs.com/MrMKG/p/16019612.html
基础学习8:规则ROI与不规则ROI提取
所谓ROI是图像目标区域裁剪,做好图片文字识别和有效信息提取,需要将图片有效信息区域裁剪出来,再做下面的文字识别及后续处理。
分为两种,规则roi与不规则roi提取:
(1)关于规则roi提取:
- 先划定一块区域,比如代码中的Rect类型,这个矩形区域的对象;(比较常用的提取ROI则是使用RECT类型)
- 再创建一个Mat类型 ,存储原图片截下来的的Rect区域,使用赋值表达式,赋值格式为:原图像(Rect类型的区域)
注意:这里提取的roi是一个指针,仍然指向原图像,因而Rect区域改变了,原图像也会相应改变。
- 因而,需要将上述提取的Mat类型的数据类型用深拷贝的方式赋值给另一个相同的Mat数据类型。
下列代码给出的是roi提取以及证明指针类型的Mat类型和原图片的roi区域是相关的,而深拷贝之后的roi区域不受影响。
注意:image.setTo(cv::Scalar(0, 0, 255));函数,功能是将提取的roi区域全部赋值为单一颜色的区域。
/* 以下是规则roi提取 int main() { cv::Mat Photo = cv::imread("0003.jpg"); cv::namedWindow("origin"); cv::imshow("origin", Photo); int h = Photo.rows; int w = Photo.cols; int cy = h / 2; int cx = w / 2; cv::Rect rect(cx + 130, cy -200, 200, 200); cv::Mat roi = Photo(rect); imshow("roi", roi); cv::Mat image = roi.clone(); // 直接更改ROI,因为是直接赋值的,指向同一块内存区域,所以原图也会被修改 roi.setTo(cv::Scalar(255, 0, 0)); cv::imshow("result",Photo); // 更改拷贝的ROI,指向不同的内存区域,所以原图不受影响 image.setTo(cv::Scalar(0, 0, 255)); cv::imshow("result2", Photo); imshow("copy roi", image); cv::waitKey(0); } */
(2)关于不规则roi提取:
- 需要hsv色彩空间转换:通过选取合适的彩色空间(HSV)将输入的彩色图像直接转换为二值图像。RGB是为了让机器更好的显示图像,对于人类来说并不直观,HSV更为贴近我们的认知,所以通常我们在针对某种颜色做提取时会转换到HSV颜色空间里面来处理。这里注意,imshow是按照rgb色彩空间给出的,imshow显示出来的hsv图像颜色显示是错误的。
下表给出BGR转HSV的数学上的转换,就是将rgb的坐标映射到锥体上:
- 使用inRange(hsv, cv::Scalar(0, 0, 0), cv::Scalar(128,128,255), mask);的函数将mask(以后提取要用的掩膜)通过比对搞出来,此时mask数组中ROI部分为白色,下面讲讲这种比对的原理:
void inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst);
大致的思路就是检测像素点是否在两个HSV色彩表示的参数之间,检测到了就标记为是ROI区域:
- 针对单通道图像
dst(I) = lowerb(I)0 ≤ src(I)0 < upperb(I)0
即,如果一幅灰度图像的某个像素的灰度值在指定的高、低阈值范围之内,则在dst图像中令该像素值为255,否则令其为0,这样就生成了一幅二值化的输出图像。
2. 针对三通道图像(其实就是将三个通道都规定限制的像素范围)
dst(I) = lowerb(I)0 ≤ src(I)0 < upperb(I)0 ∧ lowerb(I)1 ≤ src(I)1 < upperb(I)1 ∧ lowerb(I)2 ≤ src(I)2 < upperb(I)2
即,每个通道的像素值都必须在规定的阈值范围内!
- 接下来,获取掩膜后,使用图像逻辑操作的知识:
将roi非运算,将ROI部分赋为黑色。
将原图片与mask与运算,将ROI部分提取。与运算的话,1 && 1 为 1 也就是对白色部分提取,输出的是提取完roi后的残余图像。
- 其余代码主要是实现残余的图片与单色调的Mat进行融合的图片(设使用scarlar的数组为A;设B为输出的数组)
其中,再对于mask取非运算,使得ROI部分为白色,其他为黑色。
再取或运算,使得ROI部分与A数组或运算(不重要,只是为了加mask掩膜,因为图片对自身或运算是原图),将A数组中的对应ROI的区域挖出,存入B数组。(就像在A数组上盖了一层黑的膜,将非黑色部分赋值为A中的相应颜色)
最后通过算数运算,将两张图片B和扣过的图像相加,得到最终的图像。
int main(){ // 获取不规则形状的ROI,通过inRange函数 cv::Mat src2 = cv::imread("0008.jpg"); imshow("src2", src2); cv::Mat hsv, mask; cvtColor(src2, hsv, cv::COLOR_BGR2HSV); imshow("hsv", hsv); inRange(hsv, cv::Scalar(0, 0, 0), cv::Scalar(128,128,255), mask); imshow("mask", mask); // 通过mask提取人物部分,即我们的ROI。mask的白色区域才会执行与操作,黑色区域不执行 cv::Mat person; bitwise_not(mask, mask); imshow("mask2", mask); bitwise_and(src2, src2, person, mask); imshow("person", person); // 生成蓝色背景 cv::Mat result = cv::Mat::zeros(src2.size(), src2.type()); result.setTo(cv::Scalar(0, 0, 255)); // 将蓝色背景与ROI融合 cv::Mat dst; bitwise_not(mask, mask); bitwise_or(result, result, dst, mask); add(dst, person, dst); imshow("dst", dst); cv::waitKey(0); }
附:关于对图像自身逻辑或仍然是原图像的证明:
int main() { Mat Origin = imread("0003.jpg"); Mat OutPut = Mat(Origin.size(), Origin.type()); imshow("Origin", Origin); bitwise_or(Origin, Origin, OutPut); imshow("Output", OutPut); waitKey(0); }
基础学习9:图像直方图
图像直方图这块有点散乱,分为图像直方图均衡化(就是对图像进行非线性拉伸,重新分配图像像素值,使一定灰度范围内的像素数量大致相同)和统计以及直方图的计算与绘制
(一)直方图的均衡化:
C++ void equalizeHist(InputArray src, OutputArray dst) //第一个参数,源图像,需为8位单通道图像 //第二个参数,输出图像,尺寸、类型和源图像一致 注意:直方图均衡化就是通过拉伸像素强度分布范围来增强图像对比度的一种方法。
/* * 001 对于灰白图像的均衡化 * 这一段代码实现的是对于图像1维数读取以及 对于黑白图像的效果进行增强,即均衡化 注意:为了方便,我将本来存彩图的mat类型的东西变成了output */ #include <iostream> #include <math.h> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main() { int ChannelNumber; Mat OriginImage = imread("0003.jpg"); Mat OutPutImage = Mat(OriginImage.size(), OriginImage.type()); if (!OriginImage.data) { cout << "ERROR" << endl; return -1; } cvtColor(OriginImage, OutPutImage, COLOR_BGR2GRAY); imshow("origin", OutPutImage); ChannelNumber = OutPutImage.channels(); cout << "这个图像的通道数为" << ChannelNumber << endl; equalizeHist(OutPutImage, OriginImage); imshow("AFTER", OriginImage); waitKey(0); }
接下来就是三通道的彩色图像了,当时,代码里面出现了没用修改原数组,原数组却发生改变的状况,最后发现是指针传参的问题:
注意:这里的三个通道使用split分别对vector构建的mat类型处理,然后令BGR分别设置为mat类型,使用浅拷贝的方式,将分离出来的三个通道拷贝(因而对于RGBMat类型的操作其实也就是对于
三个通道的数组进行修改。
/* * 002彩色图像的均衡化 * 主要是运用split函数分离图像,存储入channels的数组里面 */ #include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main() { Mat Origin = imread("0003.jpg"); if (!Origin.data) { cout << "error" << endl; return -1; } Mat Output_1 = Mat(Origin.size(), Origin.type()); Mat Course_1 = Mat(Origin.size(), Origin.type()); imshow("00000",Origin); std::vector<cv::Mat> channels; split(Origin, channels); Mat G, B, R; B = channels.at(0); G = channels.at(1); R = channels.at(2); //因为opencv是按照BGR存储的 equalizeHist(B, B); equalizeHist(G, G); equalizeHist(R, R); merge(channels, Output_1); imshow("output", Output_1); waitKey(0); }
附上数学原理的推导:
这个就是证明图像直方图的转换函数,也证明了可实现,例子还没举
(二)直方图的计算与绘制:
C++ Void calcHist( const Mat* images,//输入图像指针 int images,// 图像数目 const int* channels,// 通道数 InputArray mask,// 输入mask,可选,不用 OutputArray hist,//输出的直方图数据 int dims,// 维数 const int* histsize,// 直方图级数 const float* ranges,// 值域范围 bool uniform,// true by default bool accumulate)// false by defaut
//寻找最值函数 C++ void minMaxLoc(InputArray src, double* minVal, double* maxVal=0, Point* minLoc=0,Point* maxLoc=0,InputArray mask=noArray()) //第一个参数:输入单通道阵列 //第二个参数:返回最小值的指针,若无需返回,此值置为NULL //第三个参数:返回最大值的指针,若无需返回,此值置为NULL //第四个参数:返回最小位置的指针(二维情况下),若无需返回,此值置为NULL //第五个参数:返回最大位置的指针(二维情况下),若无需返回,此值置为NULL //第六个参数:用于选择子阵列的可选掩膜
//#include<opencv2/opencv.hpp> //#include<iostream> //#include<math.h> // //using namespace cv; //using namespace std; // //const char* output = "image"; // //int main(int argc, char* argv) //{ // Mat src, dst, dst1; // src = imread("0003.jpg"); // if (!src.data) // { // printf("could not load image...\n"); // return -1; // } // char input[] = "input image"; // namedWindow(input, WINDOW_FREERATIO); // namedWindow(output,WINDOW_FREERATIO); // imshow(input, src); // // //步骤一:分通道显示 // vector<Mat>bgr_planes; // split(src, bgr_planes); // //split(// 把多通道图像分为多个单通道图像 const Mat &src, //输入图像 Mat* mvbegin)// 输出的通道图像数组 // // //步骤二:计算直方图 // int histsize = 256; // float range[] = { 0,256 }; // const float* histRanges = { range }; // Mat b_hist, g_hist, r_hist; // calcHist(&bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histsize, &histRanges, true, false); // calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histsize, &histRanges, true, false); // calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histsize, &histRanges, true, false); // // // //归一化 // int hist_h = 400;//直方图的图像的高 // int hist_w = 512; //直方图的图像的宽 // int bin_w = hist_w / histsize;//直方图的等级 // Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0));//绘制直方图显示的图像 // normalize(b_hist, b_hist, 0, hist_h, NORM_MINMAX, -1, Mat());//归一化 // normalize(g_hist, g_hist, 0, hist_h, NORM_MINMAX, -1, Mat()); // normalize(r_hist, r_hist, 0, hist_h, NORM_MINMAX, -1, Mat()); // // //步骤三:绘制直方图(render histogram chart) // for (int i = 1; i < histsize; i++) // { // //绘制蓝色分量直方图 // line(histImage, Point((i - 1) * bin_w, hist_h - cvRound(b_hist.at<float>(i - 1))), // Point((i)*bin_w, hist_h - cvRound(b_hist.at<float>(i))), Scalar(255, 0, 0), 2, LINE_AA); // //绘制绿色分量直方图 // line(histImage, Point((i - 1) * bin_w, hist_h - cvRound(g_hist.at<float>(i - 1))), // Point((i)*bin_w, hist_h - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2, LINE_AA); // //绘制红色分量直方图 // line(histImage, Point((i - 1) * bin_w, hist_h - cvRound(r_hist.at<float>(i - 1))), // Point((i)*bin_w, hist_h - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2, LINE_AA); // } // imshow(output, histImage); // waitKey(0); // return 0; //} // //
空间滤波,边缘检测,图像复原与重建(1):卷积的基本函数与格式
对于图像卷积的一些实用的卷积核(无推导)具体见:https://www.cnblogs.com/MrMKG/p/16062720.html
首先是对于边缘的填充(避免有些像素卷积不了)
C++ void copyMakeBorder(
Mat src, // 输入图像
Mat dst, // 添加边缘图像
int top, // 边缘长度,一般上下左右都取相同值,
int bottom,
int left,
int right,
int borderType // 边缘类型
Scalar value )
关于borderType的值: - BORDER_CONSTANT – 填充边缘用指定像素值 - BORDER_REPLICATE – 填充边缘像素用已知的边缘像素值。 - BORDER_WRAP – 用另外一边的像素来补偿填充
示例:
下面实现对于图片边缘地区进行填补像素:#include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main() { Mat Origin = imread("0003.jpg"); if (!Origin.data) { cout << "ERROR" << endl; return -1; } Mat OutPut1 = Mat(Origin.size(), Origin.type()); Mat OutPut2 = Mat(Origin.size(), Origin.type()); Mat OutPut3 = Mat(Origin.size(), Origin.type()); int added_cols = Origin.cols * 0.06; int added_rows = Origin.rows * 0.06; RNG rng(14); Scalar color = (rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); copyMakeBorder(Origin, OutPut1, added_cols, added_cols, added_rows, added_rows,BORDER_ISOLATED, color); copyMakeBorder(Origin, OutPut2, added_cols, added_cols, added_rows, added_rows,BORDER_DEFAULT, color); copyMakeBorder(Origin, OutPut3, added_cols, added_cols, added_rows, added_rows, BORDER_CONSTANT, color); imshow("OUTPUT BORDER_ISOLATED", OutPut1); imshow("OUTPUT BORDER_DEFAULT", OutPut2); imshow("OUTPUT BORDER_CONSTANT", OutPut3); waitKey(0); }
自定义卷积模糊格式:
Mat Model = (Mat_<char>(3,3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
C++ void filter2D( Mat src, //输入图像 Mat dst, // 模糊图像 int depth, // 图像深度32/8 Mat kernel, // 卷积核/模板 Point anchor, // 锚点位置 double delta // 计算出来的像素+delta)其中 kernel是可以自定义的卷积核
)
空间滤波,边缘检测,图像复原与重建(2):高斯,中值,均值卷积
函数的使用具体整理在:https://www.cnblogs.com/MrMKG/p/16079820.html
(1)关于高斯分布(其实就是正态分布概率密度函数这种东西)
参考了:https://www.cnblogs.com/wangguchangqing/p/6407717.html
以及https://zhuanlan.zhihu.com/p/398232839
高斯滤波器的本质我认为就是,以九宫格图像的中间为中心,将它看成是一种二维正态分布的概率密度函数进行卷积。
我暂时理解的高斯分布的二维概率密度函数就是它的体积为1,那这个模板就是对于这个图像的一种简化版,将九宫格的权重按照概率来算,最终的和也是1
void GaussianBlur( InputArray src, OutputArray dst, Size ksize, // 卷积窗口大小 double sigmaX, // X方向卷积系数 double sigmaY = 0, // Y方向卷积系数 int borderType = BORDER_DEFAULT //边缘插值方法 );
enum BorderTypes {
BORDER_CONSTANT = 0, // `iiiiii|abcdefgh|iiiiiii` with some specified `i`
BORDER_REPLICATE = 1, // `aaaaaa|abcdefgh|hhhhhhh`
BORDER_REFLECT = 2, // `fedcba|abcdefgh|hgfedcb`
BORDER_WRAP = 3, // `cdefgh|abcdefgh|abcdefg`
BORDER_REFLECT_101 = 4, // `gfedcb|abcdefgh|gfedcba`
BORDER_TRANSPARENT = 5, // `uvwxyz|abcdefgh|ijklmno`
BORDER_REFLECT101 = BORDER_REFLECT_101, // same as BORDER_REFLECT_101
BORDER_DEFAULT = BORDER_REFLECT_101, // same as BORDER_REFLECT_101
BORDER_ISOLATED = 16 // do not look outside of ROI
};
(补充:卷积系数(应该就是正态分布的)
(2)中值滤波
参考:https://blog.csdn.net/a8039974/article/details/80554520
在输入图像x(n1, n2)中,以任一像素为中心设置一个确定的邻域A,A的边长为2N+1,(N=0,1,2,…)。将邻域内各像素的强度值按大小顺序排列,取位于中间位置的那个值(中值)作为该像素点的输出值,遍历整幅图像就可完成整个滤波过程:A=x(i,j), y=Med{x1, x2, x3,…,x2N+1}
我认为,就是取区域内的中位数给中间值赋值。
函数 void medianBlur( InputArray src, OutputArray dst,int ksize ); 参数 src — 输入图像 dst — 输出图像, 必须与 src 相同类型 ksize — 内核大小 (只需一个值,因为使用正方形窗口),必须为奇数。
(3)均值滤波
字面意思,就是取卷积窗口内所有数字的均值,然后赋值给中间那个数字。
void blur( InputArray src, OutputArray dst, Size ksize, // 卷积窗口大小 Point anchor = Point(-1,-1), // 锚点(即处理的像素位于kernel的位置) int borderType = BORDER_DEFAULT //边缘插值方法 );
空间滤波,边缘检测,图像复原与重建 (3) :图像噪声和去噪(与(4)相关)
注:上文所讲述的高斯,中值,均值,均是噪声处理的方法
噪声的产生待补充,浅贴个代码,展示了两种:椒盐噪声以及高斯噪声
void add_salt_pepper_noise(Mat &image); void gaussian_noise(Mat &image);
void add_salt_pepper_noise(Mat &image) { RNG rng(12345); int h = image.rows; int w = image.cols; int nums = 10000; for (int i = 0; i < nums; i++) { int x = rng.uniform(0, w); int y = rng.uniform(0, h); if (i % 2 == 1) { image.at<Vec3b>(y, x) = Vec3b(255, 255, 255); } else { image.at<Vec3b>(y, x) = Vec3b(0, 0, 0); } } imshow("salt pepper", image); }
void gaussian_noise(Mat &image) { Mat noise = Mat::zeros(image.size(), image.type()); randn(noise, (15, 15, 15), (30, 30, 30)); Mat dst; add(image, noise, dst); imshow("gaussian noise", dst); dst.copyTo(image); }
空间滤波,边缘检测,图像复原与重建(4)边缘保留滤波(高斯双边,非局部均值,均值迁移)
【1】高斯双边
与高斯滤波的差异:对于高斯滤波,仅用空间距离的权值系数核与图像卷积后,确定中心点的灰度值。即认为离中心点越近的点,其权重系数越大。双边滤波中加入了对灰度信息的权重,即在邻域内,灰度值越接近中心点灰度值的点的权重更大,灰度值相差大的点权重越小。
总而言之,它是不仅仅考虑了空间上距离,还考虑了亮度的差异。
懂个原理,不会推导。
https://blog.csdn.net/qq_42261630/article/details/109538270
【2】非局部均值
与高斯滤波的差异:它使用了两个矩阵,一个用来扫描,得出整个图像与另一个矩阵的相似关系。而高斯滤波知识建立在一个n*n的矩阵内部的像素关系。
这一点据说可以避免一些纹理等重要信息的丢失,因为它是对于整个图像有所“了解”
void fastNlMeansDenoisingColored( InputArray src, OutputArray dst, float h = 3, float hColor = 3, int templateWindowSize = 7, int searchWindowSize = 21 ); src:输入一张8-bit,3-channel的图像 - dst:输出图像要求与输入通信有相同的尺寸和大小 - h:亮度元件参数调节滤波器强度。较大的h值可以很好地去除噪声,但也可能去除图像细节;较小的h值可以保留细节,但也可能保留一些噪声; - hColor:hColor 与 h 相同,但适用于颜色组件。对于大多数图像,值等于10就足以消除彩色噪声,而不会扭曲颜色; - templateWindowSize:用于计算权重的模板补丁的像素大小。应该是奇数。推荐值7像素; - searchWindowSize:用于计算给定像素加权平均值的窗口的像素大小。应该是奇数。线性影响性能:更大的搜索窗口大小-更大的去噪时间。推荐值21像素。
对于给定的一个像素,在其周围获取一个小窗口,在图像中搜索相似的窗口,求所有的窗口的平均值,最后用结果替换该像素
大致的思路是,对于一个图像,设立两个矩阵x,y 计算出xy的相似度(这里是w(x,y))作为权重,大致原理就是这样,其中,相似度的计算是根据距离(用欧式距离)以及自己定义的高斯核实现(用h表示)
其中,高斯核h越小,图像就越平缓,也就效果越不明显,虽然细节保留的很多。【h越小越平缓应该是对于高斯函数边缘来说的】
这一部分推导大致看懂意思,但是对于其中的函数求和具体操作不是很清楚。
【3】均值迁移
该算法着重点在两个窗口,即物理窗口和色彩空间窗口,他利用物理窗口求dx,dy也就是梯度,同时利用RGB色彩空间进行均值计算求新的RGB,之后利用dx和dy求得迁移的方向和距离,然后进行新的一轮计算,知道达到终止条件即收敛。
注:这种算法实现的东西是模糊化的处理,有点类似油画。
浅贴API,后续补充具体原理。
空间滤波,边缘检测,图像复原与重建(5):图像梯度(几种算子)+对于图像梯度(所谓“一阶微分锐化非线性图像”)的补充
空间滤波,边缘检测,图像复原与重建 (6)图像锐化方法:拉普拉斯和USM(在自定义卷积算子里面有)
【1】关于拉普拉斯算子:
具体数学原理:
(1)对于一阶微分的算子与二阶微分算子的区别:
首先拉普拉斯算子是二阶微分算子,它的功能是能比一阶微分更加精确展现出图像的边缘部分,比如下图:
其中可以看出,在“斜坡”处,一阶微分呈现出连续的一串数字,而二阶微分精准地描述出了边缘。
对于“台阶”处,二阶微分产生的是一个由0分开的双边缘,一阶微分则是一个不为0的常数。
PS:对于图像的二阶微分定义,有点问题。
(2)关于拉普拉斯算子:
简单推导见:https://zhuanlan.zhihu.com/p/79548457
注意:因为二阶微分的算子对于图像敏感,应该先进行滤波后再处理。
【2】Unsharp Masking (USM)
USM是先使用低通滤波器对图像进行滤波处理,得到图像中的低频分量,再用原图像减去滤波得到的低频分量图像得到高频分量,最后将高频分量和原图像叠加得到锐化后的图像。
比较详细的解答(未整理):
https://www.cnblogs.com/buyizhiyou/p/12979770.html
#include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main() { Mat Origin= imread("0003.jpg"); if (!Origin.data) { cout << "error" << endl; return -1; } Mat Output_G= Mat(Origin.size(), Origin.type()); Mat Output_L = Mat(Origin.size(), Origin.type()); Mat Output_USM = Mat(Origin.size(), Origin.type()); GaussianBlur(Origin, Output_G, Size(3, 3), 0); Laplacian(Origin, Output_L, -1, 3, 1.0, 0, BORDER_DEFAULT); addWeighted(Output_G, 1.0, Output_L, -1.0, 0, Output_USM); imshow("拉普拉斯", Output_L); imshow("高斯", Output_G); imshow("USM", Output_USM); waitKey(0); }
这个先是搞了高斯和拉普拉斯,前者滤波,后者提取图像中的边缘。
USM就是将两者相减,这里采用的是增加权重的函数进行这部操作
基本是这样。
空间滤波,边缘检测,图像复原与重建 (7)Canny边缘检测
主要分为以下几个步骤:
Canny边缘检测的步骤 1:应用高斯滤波去平滑图像为了去除噪音的影响。 2:计算图像的x轴和y轴的梯度,并计算梯度的合方向 3:使用非极大值抑制,对那些伪边界点抑制 4:把min_max应用到上面得到的图像 5:通过滞后跟踪边缘:通过抑制所有其他弱且未连接到强边缘的边缘来完成边缘的检测。
int main() { Mat Origin = imread("0003.jpg"); imshow("Origin", Origin); if (!Origin.data) { cout << "error" << endl; return -1; } Mat Output_G = Mat(Origin.size(), Origin.type()); Mat Output = Mat(Origin.size(), Origin.type()); GaussianBlur(Origin, Output_G, Size(3, 3), 0); imshow("高斯", Output_G); cvtColor(Output_G, Output_G, COLOR_BGR2GRAY); Canny(Output_G, Output, 100, 200, 3); imshow("canny", Output); waitKey(0); }
主要的推算过程:https://zhuanlan.zhihu.com/p/345758288
图像金字塔(1)高斯与拉普拉斯
图像金字塔就是放大缩小图片,其中从上到下不断变大,因此叫金字塔。最主要用于图像的分割,是一种以多分辨率来解释图像的有效但概念简单的结构。
底部是高分辨率的图片,向上的分辨率逐渐降低,是一种近似。
分为上采样和下采样
关于定义中的概念:
综述:拉普拉斯金字塔是重建,也就是放大,高斯金字塔是对图像降采样获得分辨率更小的图片。
●对图像向上采样:pyrUp函数
●对图像向下采样:pyrDown函数
(1)高斯金字塔:
降采样步骤:对于原图像高斯卷积----->删除偶数的行和列
上采样步骤:每个方向扩大为原来的两倍------>使用比之前图片的卷积核大4倍的卷积核进行卷积
但是高斯金字塔的效果很模糊,因为丢失的图像信息比较多。
(2)拉普拉斯金字塔:
式中的表示第i层的图像。而UP()操作是将源图像中位置为(x,y)的像素映射到目标图像的(2x+1,2y+1)位置,即在进行向上取样。符号
表示卷积,
为5x5的高斯内核。
而这个就是PyrUp()函数的原理,因此可以将这个简化为:
拉普拉斯金字塔是通过源图像(Gi)减去先缩小后再放大的图像(就是相应的高斯金子塔里面的同一层)的一系列(不是一个)图像构成的。
下图的解释很清楚:
最后总结:关于图像金字塔非常重要的一个应用就是实现图像分割。图像分割的话,先要建立一个图像金字塔,然后在G_i和G_i+1的像素直接依照对应的关系,建立起”父与子“关系。而快速初始分割可以先在金字塔高层的低分辨率图像上完成,然后逐层对分割加以优化。
#include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; #define WINDOW_NAME "lexus" Mat g_srcImage, g_dstIamge, g_tmpIamge; int main(int, char**) { string path = "0001.jpg"; g_srcImage = imread(path); if (!g_srcImage.data) { printf("GG simida!\n"); return 0; } namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE); imshow(WINDOW_NAME, g_srcImage); g_tmpIamge = g_srcImage; g_dstIamge = g_tmpIamge; int key = 0; while (1) { key = waitKey(9); // 读取键值到key中 switch (key) { // 退出操作 case 27: return 0; break; // 放大操作 case 'a': pyrUp(g_tmpIamge, g_dstIamge, Size(g_tmpIamge.cols * 2, g_tmpIamge.rows * 2)); printf("检测到按键'a',基于pyrUp,放大一倍\n"); break; // 缩小 case 'd': pyrDown(g_tmpIamge, g_dstIamge, Size(g_tmpIamge.cols / 2, g_tmpIamge.rows / 2)); printf("检测到按键'd',基于pyrDown,缩小一倍\n"); break; } imshow(WINDOW_NAME, g_dstIamge); g_tmpIamge = g_dstIamge; } return 0; }
图像金字塔与插值有点像,但不是明显一个东西,原理不一样。
图像金字塔(2)金字塔重建(就是对上文拉普拉斯金子塔那张图的演示)
一些参考的应用
https://blog.csdn.net/qq_27278957/article/details/97786866
https://zhuanlan.zhihu.com/p/395036081
https://zhuanlan.zhihu.com/p/454085730
拉普拉斯金字塔可以看作 base + detail 分解,base 就是低分辨率下的低频信号,detail 就是不同尺度下的高频细节。图像的 detail 中只有少部分是高频,大部分细节接近于 0,只要把接近于 0 的那部分数据置为 0,就可以减少数据存储所需空间,同时图像的基本信息不变。
对于它的基本应用,以后再来详细补充。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?