文章目录
- 笔记_3
- 直方图匹配(直方图规定化) 主要针对单通道图像
- 模板匹配
- matchTemplate 模板匹配函数
- 图像卷积
- filter2D 卷积函数
- 过滤器
- 图像噪声的产生
- cvflann::rand_double 产生随机浮点数在(0~1)之间
- cvflann::rand_int 产生随机整数在(0~RAND_MAX)之间
- RAG::fill 生成高斯噪声
- 生成椒盐噪声图像
- 线性滤波 和 非线性滤波
- blur 均值滤波
- boxFilter 方框滤波
- 高斯滤波
- GaussianBlur 高斯滤波函数你 - 对高斯噪点处理效果好
- 中值滤波 非线性滤波
- medianBlur 中值滤波函数 - 对椒盐噪声处理效果好
- 可分离滤波 对线性滤波分离处理 -- 加快运行速度
- sepFilter2D 可分离滤波函数
- converScaleAbs 求矩阵绝对值
- 边缘检测
- Sobel 算子
- Scharr 算子 滤波的尺寸为3×3
- getDerivKernels
- convertScaleAbs 绝对值运算 -- 输出图像数据类型为 `CV_8U`
- Laplacian 拉普拉斯算子
- Canny 算法
- 连通域
- connectedComponents 只分割连通域
- connectedComponentsWithStats 分割连通域并统计信息
- 图像距离变换
- distanceTransform 距离变换函数 --- 计算源图像的每个像素到最接近的零像素的距离。
- 形态学操作
- 腐蚀
- getStructuringElement 结构元素生成函数
- erode 腐蚀操作函数
- 膨胀
- dilate 膨胀操作函数
- 应用
- 开运算 MORPH_OPEN
- 闭运算 MORPH_CLOSE
- 形态学梯度 MORPH_GRADIENT
- 顶帽运算 MORPH_TOPHAT
- 黑帽运算 MORPH_BLACKHAT
- 击中击不中变换 MORPH_HITMISS (-1表示必须不匹配的像素)
- morphologyEx 形态操作函数
笔记_3
直方图匹配(直方图规定化) 主要针对单通道图像
直方图匹配又称为直方图规定化,是指将一幅图像的直方图变成规定形状的直方图而进行的图像增强方法。
[1] 即将某幅影像或某一区域的直方图匹配到另一幅影像上。使两幅影像的色调保持一致。可以在单波段影像直方图之间进行匹配,
也可以对多波段影像进行同时匹配。两幅图像比对前,通常要使其直方图形式一致。
OpenCV没有实现完整的直方图匹配函数,下面是自己实现
代码演示:
//归一化并绘制直方图函数
void drawHist(Mat &hist, int type, string name)
{
//绘制直方图
int hist_w = 512;
int hist_h = 400;
int width = 2;
Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC1);
//直方图hist数据归一化 (这里归hist_h = 400化)
normalize(hist, hist, 1, 0, type);
for (int i = 0; i < hist.rows; i++)
{
//绘制直方图 这里*10000 显示直方图更明显些
rectangle(histImage, Point(width*(i), hist_h - 1), Point(width*(i + 1), cvRound(hist_h - 10000*hist.at<float>(i) - 1)), Scalar(255, 255, 255), -1);
}
namedWindow(name, WINDOW_NORMAL);
imshow(name, histImage);
}
int main()
{
Mat srcImg = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/Snipaste_2023-04-05_17-29-56shangu.png", IMREAD_GRAYSCALE);
Mat refImg = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg", IMREAD_GRAYSCALE);
if (srcImg.empty()|| refImg.empty())
{
cout << "图像打开失败。。。" << endl;
return -1;
}
Mat hist1, hist2;
//通道索引
const int channels[1] = { 0 };
//直方图的维度,其实就是像素灰度值的最大值
const int bins[1] = { 256 };
float inRanges[2] = { 0,255 };
//像素灰度值范围
const float*ranges[1] = { inRanges };
//计算原图图像和参考图像直方图
calcHist(&srcImg, 1, channels, Mat(), hist1, 1, bins, ranges);
calcHist(&refImg, 1, channels, Mat(), hist2, 1, bins, ranges);
//归一化两张图像的直方图
drawHist(hist1, NORM_L1, "hist");
drawHist(hist2, NORM_L1, "hist2");
//计算两张图像直方图的累积概率 (计算原始图像和目标图像的累积分布函数)
float hist1_cdf[256] = { hist1.at<float>(0) };
float hist2_cdf[256] = { hist2.at<float>(0) };
for (int i = 1; i < 256; i++)
{
hist1_cdf[i] = hist1_cdf[i - 1] + hist1.at<float>(i);
hist2_cdf[i] = hist2_cdf[i - 1] + hist2.at<float>(i);
//从归一化到这里 和这段代码相等
//hist1_cdf[i] = (double)hist1[i] / (image_width * image_height) + hist1_cdf[i - 1]
}
//构建累积概率误差矩阵
float diff_cdf[256][256];
for (int i = 0; i < 256; i++)
{
for (int j = 0; j < 256; j++)
{
//如果是负数,取绝对值
diff_cdf[i][j] = fabs(hist1_cdf[i] - hist2_cdf[j]);
}
}
//生成LUT映射表
Mat lut(1, 256, CV_8UC1);
for (int i = 0; i < 256; i++)
{
//查找灰度级为i的映射灰度和i的累积概率差值最小的规定化灰度
float min = diff_cdf[i][0];
int index = 0;
//寻找累积概率误差矩阵中的每一行的最小值
for (int j = 0; j < 256; j++)
{
if (min > diff_cdf[i][j])
{
min = diff_cdf[i][j];
index = j;
}
}
lut.at<uchar>(i) = (uchar)index;
}
Mat dstImg, hist3;
LUT(srcImg, lut, dstImg);
namedWindow("原图", WINDOW_NORMAL);
namedWindow("参考图模板", WINDOW_NORMAL);
namedWindow("目标图", WINDOW_NORMAL);
imshow("原图", srcImg);
imshow("参考图模板", refImg);
imshow("目标图", dstImg);
//获取 目标图 的直方图,并绘制
calcHist(&dstImg, 1, channels, Mat(), hist3, 1, bins, ranges);
drawHist(hist3, NORM_L1, "hist3");
waitKey(0);
waitKey(0);
return 0;
}
程序实现了基于直方图的图像匹配方法,使用直方图均衡化中的累积分布函数(CDF)进行图像规定化。具体步骤如下:
1:读取原始图像和参考图像。
2:计算原始图像和参考图像的直方图。
3:对两张图像的直方图进行归一化,以便进行比较。
4:计算原始图像和参考图像的累积概率分布函数(CDF)。
5:构建累积概率误差矩阵,计算每个灰度级的映射灰度和其累积概率之间的误差。
6:生成LUT映射表,查找灰度级为i的映射灰度和i的累积概率差值最小的规定化灰度。
7:使用LUT映射表将原始图像像素值映射到目标图像像素值。
8:计算目标图像的直方图,并绘制直方图。
在该程序中,使用OpenCV库中的calcHist()函数计算图像的直方图,并使用normalize()函数对直方图进行归一化。
LUT()函数用于将原始图像像素值映射到目标图像像素值。函数drawHist()用于绘制直方图。该程序使用了NORM_L1归一化类型,即直方图的值除以像素数量之和。
值得注意的是,在进行图像规定化之前,应该确保参考图像和原始图像的大小和分辨率相同。
如果参考图像和原始图像的大小和分辨率不同,需要使用图像缩放操作来将两张图像的大小和分辨率调整为相同。
直方图规定化应用领域
直方图规定化是一种图像处理技术,它在许多领域都有应用,以下是一些例子:
1.医学图像处理:直方图规定化技术可以应用于医学图像处理领域,例如,通过对CT扫描图像应用直方图规定化技术,可以使图像更加清晰,从而更容易检测出病灶。
2.计算机视觉:直方图规定化是计算机视觉领域中常用的技术之一,例如,在图像识别中,可以通过将测试图像的直方图与训练数据集中的直方图进行规定化来提高识别准确性。
3.图像增强:直方图规定化可以应用于图像增强领域,例如,可以将一张图像的直方图规定化到一张具有更高对比度的图像的直方图上,从而使图像更加鲜明。
4.数字信号处理:直方图规定化可以应用于数字信号处理领域,例如,通过对音频信号的直方图规定化来提高音频质量。
总之,直方图规定化是一种通用的图像处理技术,可以应用于许多领域,从而提高图像的质量和准确性。
另一个应用领域是计算机视觉中的图像处理。直方图规定化可以用于图像增强和图像匹配。在图像增强中,直方图规定化可以帮助增强图像的对比度和亮度,使其更加清晰明亮。在图像匹配中,直方图规定化可以将不同图像的颜色和亮度分布映射到一个共同的直方图上,从而使它们更容易进行比较和匹配。
另一个应用领域是数据分析和数据可视化。在数据分析中,直方图规定化可以帮助将不同数据集的分布映射到一个共同的直方图上.
从而使它们更容易进行比较和分析。在数据可视化中,直方图规定化可以帮助调整图表中的颜色和亮度分布,以使其更加易于理解和解释。
1:为什么直方图规定化中要使用累积概率误差矩阵?
在直方图规定化中,我们希望将原图像的灰度级分布映射到目标图像的灰度级分布,以达到使两幅图像的灰度分布相似的目的。
其中一个关键步骤是生成灰度级映射表,它定义了将原图像中每个灰度级映射到目标图像中的哪个灰度级。
为了生成最优的灰度级映射表,我们需要比较原图像和目标图像的灰度级分布,并找到一个最优的灰度级映射,
以最小化它们之间的差异。累积概率误差矩阵是一种常用的方法来比较两幅图像的灰度级分布,并生成最优的灰度级映射表。
累积概率误差矩阵的每个元素代表了在将原图像的某个灰度级映射到目标图像的某个灰度级时,产生的累积概率误差。
该矩阵的值越小,表示原图像和目标图像的灰度级分布越相似,从而得到的映射表也就越准确。
因此,使用累积概率误差矩阵可以帮助我们找到最优的灰度级映射,以最小化原图像和目标图像之间的差异。
2:为什么要寻找累积概率误差矩阵中每一行的最小值?
在直方图规定化中,我们使用累积概率误差矩阵来生成灰度级映射表,以便将原图像的灰度级分布映射到目标图像的灰度级分布。
累积概率误差矩阵的每一行表示将原图像的某个灰度级映射到目标图像的所有可能灰度级所产生的累积概率误差。
因此,寻找每一行的最小值可以帮助我们找到最优的灰度级映射,使累积概率误差最小化。
具体来说,对于累积概率误差矩阵中的每一行,我们希望找到一个最小值,表示将原图像的某个灰度级映射到目标图像的某个灰度级时,
产生的累积概率误差最小。然后,我们可以将该灰度级映射到对应的目标图像灰度级,并将其保存在灰度级映射表中。
这个过程可以重复进行,直到所有原图像的灰度级都被映射到目标图像的某个灰度级。
因此,寻找累积概率误差矩阵中每一行的最小值是直方图规定化的重要步骤之一,
它帮助我们找到最优的灰度级映射表,从而将原图像的灰度级分布映射到目标图像的灰度级分布,使得它们尽可能地相似。
模板匹配
matchTemplate 模板匹配函数
下面对 matchTemplate 函数中的每个算法的匹配结果进行介绍:
1:平方差匹配算法(TM_SQDIFF):
该算法使用模板图像与目标图像对应像素之间的平方差来计算匹配度。平方差匹配算法的匹配结果可以通过以下方式解释:
匹配结果中的每个像素表示模板图像与目标图像对应像素之间的平方差,即匹配误差。匹配误差越小,
说明两个像素越相似,匹配结果越接近于零。因此,平方差匹配算法的匹配结果越接近于零,说明匹配度越高。
2:归一化平方差匹配算法(TM_SQDIFF_NORMED):
该算法也使用平方差来计算匹配度,但它对匹配误差进行了归一化,使得匹配结果的值域范围在 0 到 1 之间。
归一化平方差匹配算法的匹配结果可以通过以下方式解释:匹配结果中的每个像素表示模板图像与目标图像对应像素之间的归一化平方差,
即归一化的匹配误差。匹配误差越小,说明两个像素越相似,匹配结果越接近于零。
因此,归一化平方差匹配算法的匹配结果越接近于零,说明匹配度越高。
3:相关性匹配算法(TM_CCORR):
该算法使用模板图像与目标图像对应像素之间的相关系数来计算匹配度。
相关性匹配算法的匹配结果可以通过以下方式解释:匹配结果中的每个像素表示模板图像与目标图像对应像素之间的相关系数,
即匹配相似度。相关系数越大,说明两个像素越相似,匹配结果越接近于零。
因此,相关性匹配算法的匹配结果越接近于零,说明匹配度越高。
4:归一化相关性匹配算法(TM_CCORR_NORMED):
该算法也使用相关系数来计算匹配度,并对匹配相似度进行了归一化,使得匹配结果的值域范围在 0 到 1 之间。
归一化相关性匹配算法的匹配结果可以通过以下方式解释:匹配结果中的每个像素表示模板图像与目标图像对应像素之间的归一化相关系数,
即归一化的匹配相似度。相关系数越大于零,说明两个像素越相似,匹配结果越接近于 1。
因此,归一化相关性匹配算法的匹配结果越接近于 1,说明匹配度越高。
5:相关性系数最大匹配算法(TM_CCOEFF):
该算法使用模板图像与目标图像对应像素之间的相关系数减去它们的均值来计算匹配度。
相关性系数最大匹配算法的匹配结果可以通过以下方式解释:匹配结果中的每个像素表示模板图像与目标图像对应像素之间的相关系数减去它们的均值,
即匹配相似度。相似度越大,说明两个像素越相似,匹配结果越接近于 1。
因此,相关性系数最大匹配算法的匹配结果越接近于 1,说明匹配度越高。
6:归一化相关性系数最大匹配算法(TM_CCOEFF_NORMED):
该算法也使用相关系数减去均值来计算匹配度,并对匹配相似度进行了归一化,使得匹配结果的值域范围在 0 到 1 之间。
归一化相关性系数最大匹配算法的匹配结果可以通过以下方式解释:
匹配结果中的每个像素表示模板图像与目标图像对应像素之间的归一化相关系数减去它们的均值,即归一化的匹配相似度。
相似度越大,说明两个像素越相似,匹配结果越接近于 1。
因此,归一化相关性系数最大匹配算法的匹配结果越接近于 1,说明匹配度越高。
简化回答:
1:平方差匹配算法(TM_SQDIFF):
匹配结果越小,说明匹配度越高,即越相似。
2:归一化平方差匹配算法(TM_SQDIFF_NORMED):
匹配结果越小,说明匹配度越高,即越相似。
3:相关性匹配算法(TM_CCORR):
匹配结果越大,说明匹配度越高,即越相似。
4:归一化相关性匹配算法(TM_CCORR_NORMED):
匹配结果越大,说明匹配度越高,即越相似。
5:相关性系数最大匹配算法(TM_CCOEFF):
匹配结果越大,说明匹配度越高,即越相似。
6:归一化相关性系数最大匹配算法(TM_CCOEFF_NORMED):
匹配结果越大,说明匹配度越高,即越相似。
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
Mat temp;
//抠出一部分图当做模板使用
img(Range(270, 300),Range(200, 250)).copyTo(temp);
if (img.empty()|| temp.empty())
{
cout << "图像打开失败。。。" << endl;
return -1;
}
Mat result;
matchTemplate(img, temp, result, TM_CCOEFF_NORMED);
double minVal, maxVal;
Point maxLoc, minLoc;
minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc);
rectangle(img, maxLoc, Point(maxLoc.x + temp.cols, maxLoc.y+temp.rows), Scalar(0, 0, 255), 2);
imshow("原图像", img);
imshow("模板", temp);
imshow("result", result);
图像卷积
filter2D 卷积函数
filter2D(img, dstImage, img.depth(), kernel,Point(2,2),0, BORDER_CONSTANT);//BORDER_CONSTANT 边缘填充为0
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
//Laplacian卷积核:用于图像锐化处理,通常为3x3或5x5大小的卷积核,
Mat kernel = (Mat_<double>(5, 5) <<
0 , 1 , 1 , 1 ,0,
1, - 2 ,- 2 ,- 2 ,1,
1, - 2 ,12 ,- 2 , 1,
1 ,- 2, - 2 ,- 2 , 1,
0 , 1 , 1, 1 , 0);
//Mat kernel = (Mat_<double>(3, 3) <<
// 1 , 2 ,1,
// 0 , 0, 0,
// - 1, - 2, - 1);
//数据归一化 (卷积核每个元素除以它本身所有元素的和)
double kernelSum = sum(kernel)[0];//单通道
if(kernelSum)
{
kernel /= sum(kernel);
}
Mat dstImage;
filter2D(img, dstImage, img.depth(), kernel);
imshow("原图", img);
imshow("卷积图", dstImage);
过滤器
1:卷积核就是由长和宽来指定的,是一个二维的概念。
2:而过滤器是是由长、宽和深度指定的,是一个三维的概念。
3:过滤器可以看做是卷积核的集合。
4:过滤器比卷积核高一个维度——深度。
下面结合一个多通道的例子马上就能理解了:
图像噪声的产生
cvflann::rand_double 产生随机浮点数在(0~1)之间
cvflann::rand_int 产生随机整数在(0~RAND_MAX)之间
RAG::fill 生成高斯噪声
mat:用于存放随机数的矩阵,目前只支持低于5通道的矩阵。
distType:随机数分布形式选择标志,目前生成的随机数支持均匀分布(RNG::UNIFORM,0)和高斯分布(RNG::NORMAL,1)
分布类型,RNG::UNIFORM 或 RNG::NORMAL。表示均匀分布和高斯分布。
a:确定分布规律的参数。当选择均匀分布时,该参数表示均匀分布的最小下限;当选择高斯分布时,该参数表示高斯分布的均值。
b:确定分布规律的参数。当选择均匀分布时,该参数表示均匀分布的最大上限;当选择高斯分布时,该参数表示高斯分布的标准差。
saturateRange:预饱和标志,仅用于均匀分布。
void fill( InputOutputArray mat, int distType, InputArray a, InputArray b, bool saturateRange=false );
InputOutputArray 输入输出矩阵,最多支持4通道,超过4通道先用reshape()改变结构
int distType UNIFORM 或 NORMAL,表示均匀分布和高斯分布
InputArray a disType是UNIFORM,a表示为下界(闭区间);disType是NORMAL,a均值
InputArray b disType是UNIFORM,b表示为上界(开区间);disType是NORMAL,b标准差
bool saturateRange=false 只针对均匀分布有效。当为真的时候,会先把产生随机数的范围变换到数据类型的范围,再产生随机数;
如果为假,会先产生随机数,再进行截断到数据类型的有效区间。
生成椒盐噪声图像
代码演示:
//生成椒盐噪声图像
void saltAndPepper(Mat img)
{
for (int k = 0; k < img.cols * img.rows / 2; k++)
{
//随机确定图像中位置
int i, j;
//取余运算,确保位置在图像中
i = std::rand() % img.cols; //cvflann::rand_int(img.cols,0);
j = std::rand() % img.rows;//cvflann::rand_int(img.rows,0);
//处理多通道图像
if (img.type() == CV_8UC3)
{
// 取余 如果是0 赋值0, 是1赋值255
img.at<cv::Vec3b>(j, i)[0] = std::rand() % 2 == 0 ? 0 : 255;
img.at<cv::Vec3b>(j, i)[1] = std::rand() % 2 == 0 ? 0 : 255;
img.at<cv::Vec3b>(j, i)[2] = std::rand() % 2 == 0 ? 0 : 255;
}
//处理单通道图像
else if (img.type() == CV_8UC1)
{
// 取余 如果是0 赋值0, 是1赋值255
img.at<uchar>(j, i) = std::rand() % 2 == 0 ? 0 : 255;
}
}
}
int main()
{
Mat colorLena = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
Mat grayLena = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg",IMREAD_GRAYSCALE);
if (colorLena.empty() || grayLena.empty())
{
return -1;
}
Mat colorLena_G, grayLena_G;
colorLena.copyTo(colorLena_G);
grayLena.copyTo(grayLena_G);
//创建两个和图像一样大小的0矩阵
Mat colorLena_noise_P = Mat::zeros(colorLena.rows, colorLena.cols, colorLena.type());
Mat grayLena_noise_P = Mat::zeros(grayLena.rows, grayLena.cols, grayLena.type());
imshow("彩色原图", colorLena);
imshow("灰色原图", grayLena);
//彩色图像添加椒盐噪声
saltAndPepper(colorLena_noise_P);
//灰色图像添加椒盐噪声
saltAndPepper(grayLena_noise_P);
imshow("彩色椒盐噪声", colorLena_noise_P);
imshow("灰色椒盐噪声", grayLena_noise_P);
imshow("添加彩色椒盐噪声图像", colorLena + colorLena_noise_P);
imshow("添加灰色椒盐噪声图像", grayLena + grayLena_noise_P);
//创建两个和图像一样大小的0矩阵
Mat colorLena_noise_G = Mat::zeros(colorLena_G.rows, colorLena_G.cols, colorLena_G.type());
Mat grayLena_noise_G = Mat::zeros(grayLena_G.rows, grayLena_G.cols, grayLena_G.type());
//创建高斯噪声矩阵
RNG rng;
rng.fill(colorLena_noise_G, RNG::NORMAL, 10, 2000);
rng.fill(grayLena_noise_G, RNG::NORMAL, 10, 2200);
imshow("彩色高斯噪声", colorLena_noise_G);
imshow("灰色高斯噪声", grayLena_noise_G);
imshow("添加彩色高斯噪声图像", colorLena_G+colorLena_noise_G);
imshow("添加灰色高斯噪声图像", grayLena_G+grayLena_noise_G);
waitKey(0);
return 0;
}
线性滤波 和 非线性滤波
blur 均值滤波
函数原型:
C++: void blur(InputArray src, OutputArray dst, Size ksize, Point anchor=Point(-1,-1), int borderType=BORDER_DEFAULT )
参数详解如下:
第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。该函数对通道是独立处理的,
且可以处理任意通道数的图片,但需要注意,待处理的图片深度应该为CV_8U, CV_16U, CV_16S, CV_32F 以及 CV_64F之一。
第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。比如可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。
第三个参数,Size类型(对Size类型稍后有讲解)的ksize,内核的大小。一般这样写Size( w,h )来表示内核的大小( 其中,w 为像素宽度, h为像素高度)。
Size(3,3)就表示3x3的核大小,Size(5,5)就表示5x5的核大小
第四个参数,Point类型的anchor,表示锚点(即被平滑的那个点),注意他有默认值Point(-1,-1)。如果这个点坐标是负值的话,就表示取核的中心为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。
第五个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它
用卷积实现:filter2D(…);
均值滤波卷积核:用于图像模糊处理和图像降噪,通常为奇数大小的卷积核,例如:
Copy code
1/9 1/9 1/9
1/9 1/9 1/9
1/9 1/9 1/9
boxFilter 方框滤波
boxFilter 和blur 类似
参数默认状态下需要对滤波器内所有的数值进行归一化。此时,在不考虑数据类型的情况下,框滤波函数boxFilter()和均值滤波函数blur()会具有相同的滤波结果。
● ddepth是处理结果图像的图像深度,一般使用-1表示与原始图像使用相同的图像深度。图像深度应该是CV_8U、CV_16U、CV_16S、CV_32F 或者 CV_64F中的一种。
● normalize 表示在滤波时是否进行归一化(这里指将计算结果规范化为当前像素值范围内的值)处理,该参数是一个逻辑值,可能为真(值为1)或假(值为0):
1.当参数normalize=1时,表示要进行归一化处理,要用邻域像素值的和除以面积。此时方框滤波与均值滤波效果相同。
2.当参数normalize=0时,表示不需要进行归一化处理,直接使用邻域像素值的和。当
normalize=0时,因为不进行归一化处理,因此滤波得到的值很可能超过当前像素值范围的最大值,从而被截断为最大值。这样,就会得到一幅纯白色的图像。
所以:如果原图像深度是CV_8U,ddepth可以选一个大于原图像深度的数据类型,这样就防止超出255数值了。
高斯滤波
GaussianBlur 高斯滤波函数你 - 对高斯噪点处理效果好
● src 是需要处理的图像,即原始图像。它能够有任意数量的通道,并能对各个通道 独立处理。图像深度应该是CV_8U、CV_16U、CV_16S、CV_32F 或者 CV_64F中的一 种。
● dst是返回值,表示进行高斯滤波后得到的处理结果。
● ksize 是滤波核的大小。滤波核大小是指在滤波处理过程中其邻域图像的高度和宽 度。需要注意,滤波核的值必须是奇数。
● sigmaX 是卷积核在水平方向上(X 轴方向)的标准差,其控制的是权重比例。
● sigmaY是卷积核在垂直方向上(Y轴方向)的标准差。如果将该值设置为0,则只采用sigmaX的值
如果sigmaX和sigmaY都是0,则通过ksize.width和ksize.height计算得到。其中:
sigmaX=0.3×[(ksize.width-1)×0.5-1] +0.8
sigmaY=0.3×[(ksize.height-1)×0.5-1]+0.8
● borderType是边界样式,该值决定了以何种方式处理边界。一般情况下,不需要考虑该值,直接采用默认值即可。 在该函数中,sigmaY和borderType是可选参数。sigmaX是必选参数,但是可以将该参数设置为0,让函数自己去计算sigmaX的具体值。
中值滤波 非线性滤波
medianBlur 中值滤波函数 - 对椒盐噪声处理效果好
代码演示:
//生成椒盐噪声图像
void saltAndPepper(Mat img)
{
for (int k = 0; k < img.cols * img.rows / 10; k++)
{
//随机确定图像中位置
int i, j;
//取余运算,确保位置在图像中
i = std::rand() % img.cols; //cvflann::rand_int(img.cols,0);
j = std::rand() % img.rows;//cvflann::rand_int(img.rows,0);
//处理多通道图像
if (img.type() == CV_8UC3)
{
// 取余 如果是0 赋值0, 是1赋值255
img.at<cv::Vec3b>(j, i)[0] = std::rand() % 2 == 0 ? 0 : 255;
img.at<cv::Vec3b>(j, i)[1] = std::rand() % 2 == 0 ? 0 : 255;
img.at<cv::Vec3b>(j, i)[2] = std::rand() % 2 == 0 ? 0 : 255;
}
//处理单通道图像
else if (img.type() == CV_8UC1)
{
// 取余 如果是0 赋值0, 是1赋值255
img.at<uchar>(j, i) = std::rand() % 2 == 0 ? 0 : 255;
}
}
}
int main()
{
Mat colorLena = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
Mat grayLena = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg", IMREAD_GRAYSCALE);
if (colorLena.empty() || grayLena.empty())
{
return -1;
}
Mat colorLena_G, grayLena_G;
Mat colorLena_S, grayLena_S;
colorLena.copyTo(colorLena_G);
grayLena.copyTo(grayLena_G);
//创建两个和图像一样大小的0矩阵
Mat colorLena_noise_P = Mat::zeros(colorLena.rows, colorLena.cols, colorLena.type());
Mat grayLena_noise_P = Mat::zeros(grayLena.rows, grayLena.cols, grayLena.type());
//imshow("彩色原图", colorLena);
imshow("灰色原图", grayLena);
//彩色图像添加椒盐噪声
saltAndPepper(colorLena_noise_P);
//灰色图像添加椒盐噪声
saltAndPepper(grayLena_noise_P);
//imshow("彩色椒盐噪声", colorLena_noise_P);
imshow("灰色椒盐噪声", grayLena_noise_P);
colorLena_S = colorLena + colorLena_noise_P;
grayLena_S = grayLena + grayLena_noise_P;
//imshow("添加彩色椒盐噪声图像", colorLena_S);
imshow("添加灰色椒盐噪声图像", grayLena_S);
//创建两个和图像一样大小的0矩阵
Mat colorLena_noise_G = Mat::zeros(colorLena_G.rows, colorLena_G.cols, colorLena_G.type());
Mat grayLena_noise_G = Mat::zeros(grayLena_G.rows, grayLena_G.cols, grayLena_G.type());
//创建高斯噪声矩阵
RNG rng;
rng.fill(colorLena_noise_G, RNG::NORMAL, 10, 20);
rng.fill(grayLena_noise_G, RNG::NORMAL, 10, 20);
//imshow("彩色高斯噪声", colorLena_noise_G);
imshow("灰色高斯噪声", grayLena_noise_G);
//imshow("添加彩色高斯噪声图像", colorLena_G += colorLena_noise_G);
imshow("添加灰色高斯噪声图像", grayLena_G += grayLena_noise_G);
//存放不含噪声滤波结果,后面数字代表滤波器尺寸
Mat result_3, result_9;
//存放高斯噪声滤波结果,后面数字代表滤波器尺寸
Mat result_3gauss, result_9gauss;
//存放椒盐噪声滤波结果,后面数字代表滤波器尺寸
Mat result_3salt, result_9salt;
//调用均值滤波函数blur()进行滤波
blur(grayLena, result_3, Size(3, 3));
blur(grayLena, result_9, Size(9, 9));
blur(grayLena_S, result_3salt, Size(3, 3));
blur(grayLena_S, result_9salt, Size(9, 9));
blur(grayLena_G, result_3gauss, Size(3, 3));
blur(grayLena_G, result_9gauss, Size(9, 9));
Mat result_3Norm, result_3_;
/*
方框滤波 boxFilter 和 sqrBoxFilter(平方方框滤波)
boxFilter 默认归一化,和blur结果一样
一般使用-1表示与原始图像使用相同的图像深度
*/
//归一化
boxFilter(grayLena_S, result_3Norm, -1, Size(3, 3), Point(-1, -1), true);
//没有归一化
boxFilter(grayLena_S, result_3_, -1, Size(3, 3), Point(-1, -1), false);
Mat result_3_G,result_5_G, result_9_G;
//高斯滤波 去高斯噪声
GaussianBlur(grayLena_G, result_3_G, Size(3, 3), 0, 0);
GaussianBlur(grayLena_G, result_5_G, Size(5, 5), 0, 0);
GaussianBlur(grayLena_G, result_9_G, Size(9, 9), 0, 0);
imshow("高斯滤波3", result_3_G);
imshow("高斯滤波5", result_5_G);
imshow("高斯滤波9", result_9_G);
//中值滤波 去椒盐噪声
Mat result_3_M, result_9_M;
medianBlur(grayLena_S, result_3_M, 3);
medianBlur(grayLena_S, result_9_M, 9);
imshow("中值滤波3", result_3_M);
imshow("中值滤波9", result_9_M);
waitKey(0);
return 0;
}
可分离滤波 对线性滤波分离处理 – 加快运行速度
sepFilter2D 可分离滤波函数
src:待滤波图像
dst:输出图像,与输入图像src具有相同的尺寸、通道数和数据类型。
ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。
kernelX:X方向的滤波器,
kernelY:Y方向的滤波器。
anchor:内核的基准点(锚点),其默认值为(-1,-1)代表内核基准点位于kernel的中心位置。基准点即卷积核中与进行处理的像素点重合的点,其位置必须在卷积核的内部。
delta:偏值,在计算结果中加上偏值。
borderType:像素外推法选择标志,取值范围在表3-5中给出。默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
注:kernelX,kernelY:函数使用的是可分离线性滤波器,也就是说行和列使用的核是不同的,它们都是一维矩阵。
分解卷积核的好处?
一个可分核可以理解成两个一维核,在卷积时先调用x内核,然后调用y内核。
两个矩阵进行卷积所产生的消耗可以用两个矩阵的面积的积来估算,
如此一来,用 n×n 的核对面积为A的图像进行卷积所需的时间是An²,但如果分解成 n×1 和 1×n 的两个核,
那么代价就是 An +An = 2An ,因此分解卷积核可以提高卷积计算的效率。
只要n不小于3,这种计算方式就能提高效率,并且随着n的增大,这种效益愈发明显。
int main()
{
system("color F0"); //更改输出界面颜色
float points[25] = { 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 };
Mat data(5, 5, CV_32FC1, points);
//X方向、Y方向和联合滤波器的构建
Mat a = (Mat_<float>(3, 1) << -1, 3, -1);
//1行3列
Mat b = a.reshape(1, 1);
Mat ab = a * b;
//验证高斯滤波的可分离性
Mat gaussX = getGaussianKernel(3, 0);
Mat gaussData, gaussDataXY;
GaussianBlur(data, gaussData, Size(3, 3), 0, 0, BORDER_CONSTANT);
sepFilter2D(data, gaussDataXY, -1, gaussX, gaussX, Point(-1, -1), 0, BORDER_CONSTANT);
//输入两种高斯滤波的计算结果
cout << "gaussData=" << endl
<< gaussData << endl;
cout << "gaussDataXY=" << endl
<< gaussDataXY << endl;
//线性滤波的可分离性
Mat dataYX, dataY, dataXY, dataXY_sep;
filter2D(data, dataY, -1, a, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(dataY, dataYX, -1, b, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(data, dataXY, -1, ab, Point(-1, -1), 0, BORDER_CONSTANT);
sepFilter2D(data, dataXY_sep, -1, b, b, Point(-1, -1), 0, BORDER_CONSTANT);
//输出分离滤波和联合滤波的计算结果
cout << "dataY=" << endl
<< dataY << endl;
cout << "dataYX=" << endl
<< dataYX << endl;
cout << "dataXY=" << endl
<< dataXY << endl;
cout << "dataXY_sep=" << endl
<< dataXY_sep << endl;
//对图像的分离操作
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
//Mat grayLena = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg", IMREAD_GRAYSCALE);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat imgYX, imgY, imgXY;
filter2D(img, imgY, -1, a, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(imgY, imgYX, -1, b, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(img, imgXY, -1, ab, Point(-1, -1), 0, BORDER_CONSTANT);
imshow("img", img);
imshow("imgY", imgY);
imshow("imgYX", imgYX);
imshow("imgXY", imgXY);
waitKey(0);
return 0;
}
图像处理中,常用的滤波算法有均值滤波、中值滤波以及高斯滤波等。
三种滤波器的对比 滤波器种类 基本原理 特点
均值滤波 使用模板内所有像素的平均值代替模板中心像素灰度值 易收到噪声的干扰,不能完全消除噪声,只能相对减弱噪声
中值滤波 计算模板内所有像素中的中值,并用所计算出来的中值体改模板中心像素的灰度值 对噪声不是那么敏感,能够较好的消除椒盐噪声,但是容易导致图像的不连续性
高斯滤波 对图像邻域内像素进行平滑时,邻域内不同位置的像素被赋予不同的权值 对图像进行平滑的同时,同时能够更多的保留图像的总体灰度分布特征
converScaleAbs 求矩阵绝对值
边缘检测
Sobel 算子
Sobel算子是通过离散微分方法求取图像边缘的边缘检测算子,其求取边缘的思想原理与我们前文介绍的思想一致,
除此之外Sobel算子还结合了高斯平滑滤波的思想,将边缘检测滤波器尺寸由ksize * 1改进为ksize * ksize,
提高了对平缓区域边缘的响应,相比前文的算法边缘检测效果更加明显。
使用Sobel边缘检测算子提取图像边缘的过程大致可以分为以下三个步骤:
src:待提取边缘的图像,
dst:输出图像,与输入图像src具有相同的尺寸和通道数,数据类型由第三个参数ddepth控制。
ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。
dx:X方向的差分阶数
dy:Y方向的差分阶数
ksize:Sobel边缘算子的尺寸,必须是1、3、5或者7。
scale:对导数计算结果进行缩放的缩放因子,默认系数为1,不进行缩放。
delta:偏值,在计算结果中加上偏值。
borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
该函数的使用方式与分离卷积函数sepFilter2D()相似,函数的前两个参数分别为输入图像和输出图像,第三个参数为输出图像的数据类型,这里需要注意由于提取边缘信息时有可能会出现负数,因此不要使用CV_8U数据类型的输出图像,与Sobel算子方向不一致的边缘梯度会在CV_8U数据类型中消失,使得图像边缘提取不准确。函数中第三个、第四个和第五个参数是控制图像边缘检测效果的关键参数,这三者存在的关系是任意一个方向的差分阶数都需要小于滤波器的尺寸,特殊情况是当ksize=1时,任意一个方向的阶数需要小于3。一般情况中,差分阶数的最大值为1时,滤波器尺寸选3;差分阶数的最大值为2时,滤波器尺寸选5;差分阶数最大值为3时,滤波器尺寸选7。当滤波器尺寸ksize=1时,程序中使用的滤波器尺寸不再是正方形,而是3×1或者1×3。最后三个参数为图像放缩因子、偏移量和图像外推填充方法的标志,多数情况下并不需要设置,只需要采用默认参数即可。
Scharr 算子 滤波的尺寸为3×3
虽然Sobel算子可以有效的提取图像边缘,但是对图像中较弱的边缘提取效果较差。因此为了能够有效的提取出较弱的边缘,需要将像素值间的差距增大,因此引入Scharr算子。Scharr算子是对Sobel算子差异性的增强,因此两者之间的在检测图像边缘的原理和使用方式上相同。Scharr算子的边缘检测滤波的尺寸为3×3,因此也有称其为Scharr滤波器。可以通过将滤波器中的权重系数放大来增大像素值间的差异,Scharr算子就是采用的这种思想,其在X方向和Y方向的边缘检测算子如(5.19)中所示。
src:待提取边缘的图像,
dst:输出图像,与输入图像src具有相同的尺寸和通道数,数据类型由第三个参数ddepth控制。
ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。
dx:X方向的差分阶数
dy:Y方向的差分阶数
scale:对导数计算结果进行缩放的缩放因子,默认系数为1,不进行缩放。
delta:偏值,在计算结果中加上偏值。
borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
该函数利用Scharr算子提取图像中的边缘信息,与Soble()函数相同,函数的前两个参数分别为输入图像和输出图像,第三个参数为输出图像的数据类型,这里需要注意由于提取边缘信息时有可能会出现负数,因此不要使用CV_8U数据类型的输出图像,与Scarr算子方向不一致的边缘梯度会在CV_8U数据类型中消失,使得图像边缘提取不准确。函数第四个和五个参数是提取X方向边缘还是Y方向边缘的标志,该函数要求这两个参数只能有一个参数为1,并且不能同时为0,否则函数将无法提取图像边缘,该函数默认的滤波器尺寸为3×3,并且无法修改。最后三个参数为图像放缩因子、偏移量和图像外推填充方法的标志,多数情况下并不需要设置,只需要采用默认参数即可。
getDerivKernels
过滤器通常传递给sepFilter2D函数进行图像过滤,导数核Sobel和Scharr核是可以分解的,所以这个函数返回的是两个核。
convertScaleAbs 绝对值运算 – 输出图像数据类型为 CV_8U
cv::convertScaleAbs
函数是 OpenCV 库中的一个图像处理函数,用于将输入图像进行缩放、绝对值运算和数据类型转换。
函数原型如下:
void cv::convertScaleAbs(
InputArray src,
OutputArray dst,
double alpha = 1,
double beta = 0
);
参数说明:
src
:输入图像,可以是单通道或多通道的图像,数据类型可以是CV_8U
、CV_16U
、CV_16S
、CV_32F
或CV_64F
。dst
:输出图像,与输入图像具有相同的尺寸和通道数,数据类型为CV_8U
。alpha
:缩放因子,默认为 1。将输入图像乘以alpha
。beta
:平移因子,默认为 0。将输入图像加上beta
。
函数功能:
convertScaleAbs
函数的作用是对输入图像进行线性变换,并将结果取绝对值后转换为 CV_8U
类型。具体操作为:将输入图像的每个像素值乘以 alpha
,然后加上 beta
,然后取结果的绝对值,并将结果转换为 CV_8U
类型。
注意事项:
- 输入图像的深度(数据类型)可以是
CV_8U
、CV_16U
、CV_16S
、CV_32F
或CV_64F
,但输出图像的数据类型固定为CV_8U
。 - 输出图像的每个像素值是输入图像经过线性变换并取绝对值后的结果。如果输入图像的像素值超出了
CV_8U
的范围(0-255),则会进行截断。
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg", IMREAD_GRAYSCALE);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
//Sobel 算子
Mat resultX_Sobel, resultY_Sobel, resultXY_Sobel;
//X方向一阶边缘
Sobel(img, resultX_Sobel, CV_16S, 1, 0, 3);
convertScaleAbs(resultX_Sobel, resultX_Sobel);
//Y方向一阶边缘
Sobel(img, resultY_Sobel, CV_16S, 0, 1, 3);
convertScaleAbs(resultY_Sobel, resultY_Sobel);
//整幅图像的一阶边缘
resultXY_Sobel = resultX_Sobel + resultY_Sobel;
//显示图像
imshow("resultX_Sobel", resultX_Sobel);
imshow("resultY_Sobel", resultY_Sobel);
imshow("resultXY_Sobel", resultXY_Sobel);
//Scharr 算子
Mat resultX_Scharr, resultY_Scharr, resultXY_Scharr;
//X方向一阶边缘
Scharr(img, resultX_Scharr, CV_16S, 1, 0);
convertScaleAbs(resultX_Scharr, resultX_Scharr);
//Y方向一阶边缘
Scharr(img, resultY_Scharr, CV_16S, 0, 1);
convertScaleAbs(resultY_Scharr, resultY_Scharr);
//整幅图像的一阶边缘
resultXY_Scharr = resultX_Scharr + resultY_Scharr;
//显示图像
imshow("resultX_Scharr", resultX_Scharr);
imshow("resultY_Scharr", resultY_Scharr);
imshow("resultXY_Scharr", resultXY_Scharr);
Mat sobel_x1, sobel_y1;
Mat scharr_x, scharr_y;
Mat sobelX1, scharrX;
//一阶X方向sobel算子
getDerivKernels(sobel_x1, sobel_y1, 1, 0, 3);
//3行1列 转 1行3列
sobel_x1 = sobel_x1.reshape(CV_8U, 1);
//生成3*3矩阵
sobelX1 = sobel_y1 * sobel_x1;
cout << "一阶X方向sobel算子:" << endl;
cout << sobel_x1 << endl;
cout << sobel_y1 << endl;
cout << sobelX1 << endl;
//一阶Y方向sobel算子
getDerivKernels(sobel_x1, sobel_y1, 0, 1, 3);
//3行1列 转 1行3列
sobel_x1 = sobel_x1.reshape(CV_8U, 1);
//生成3*3矩阵
sobelX1 = sobel_y1 * sobel_x1;
cout << "一阶Y方向sobel算子:" << endl;
cout << sobel_x1 << endl;
cout << sobel_y1 << endl;
cout << sobelX1 << endl;
//一阶X方向scharr算子
getDerivKernels(scharr_x, scharr_y, 1, 0, FILTER_SCHARR);
//3行1列 转 1行3列
scharr_x = scharr_x.reshape(CV_8U, 1);
//生成3*3矩阵
scharrX = scharr_y * scharr_x;
cout << "一阶X方向scharr算子:" << endl;
cout << scharr_x << endl;
cout << scharr_y << endl;
cout << scharrX << endl;
//一阶Y方向scharr算子
getDerivKernels(scharr_x, scharr_y, 0, 1, FILTER_SCHARR);
//3行1列 转 1行3列
scharr_x = scharr_x.reshape(CV_8U, 1);
//生成3*3矩阵
scharrX = scharr_y * scharr_x;
cout << "一阶Y方向scharr算子:" << endl;
cout << scharr_x << endl;
cout << scharr_y << endl;
cout << scharrX << endl;
waitKey(0);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZKfDYUQ-1686913024602)(_v_images/20230410213238194_25363.png =949x)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QKDVuPEL-1686913024602)(_v_images/20230410213103767_27126.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1BFJpDf-1686913024602)(_v_images/20230410213849937_5842.png =933x)]
Laplacian 拉普拉斯算子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nUNuFc0j-1686913024602)(_v_images/20230410214113002_30975.png =949x)]
上述的边缘检测算子都具有方向性,因此需要分别求取X方向的边缘和Y方向的边缘,之后将两个方向的边缘综合得到图像的整体边缘。Laplacian算子具有各方向同性的特点,能够对任意方向的边缘进行提取,具有无方向性的优点,因此使用Laplacian算子提取边缘不需要分别检测X方向的边缘和Y方向的边缘,只需要一次边缘检测即可。Laplacian算子是一种二阶导数算子,对噪声比较敏感,因此常需要配合高斯滤波一起使用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fqPAXgcz-1686913024602)(_v_images/20230410214306818_2599.png =916x)]
Canny 算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2fXKVA3w-1686913024602)(_v_images/20230410214730282_24531.png =910x)]
该函数利用Canny算法提取图像中的边缘信息。第一个参数是需要提取边缘的输入图像,目前只支持数据类型为CV_8U的图像,输入图像可以是灰度图像或者彩色图像。
第二个参数是边缘检测结果的输出图像,图像是数据类型为为CV_8U的单通道灰度图像。函数第三个和第四个参数是Canny算法中用于区分强边缘和弱边缘的两个阈值,
两个参数不区分较大阈值和较小阈值,函数会自动比较区分两个阈值的大小,不过一般情况下,较大阈值与较小阈值的比值在2:1到3:1之间。
函数最后一个参数是计算梯度幅值方法的选择标志,无特殊需求的情况下,使用默认值即可。
在Canny算子中,threshold1和threshold2是两个阈值,用于确定图像中的边缘。这两个阈值是根据梯度的大小来确定的。
Canny算子首先计算图像中每个像素点的梯度和方向,然后根据梯度的大小将每个像素点标记为强边缘、弱边缘或非边缘。强边缘是梯度值大于阈值2的像素点,
非边缘是梯度值小于阈值1的像素点,而弱边缘是梯度值介于阈值1和阈值2之间的像素点。
threshold1和threshold2的作用是确定哪些边缘是真正的边缘。阈值的选择需要根据具体情况进行调整。通常情况下,threshold1取值为较小的值,threshold2取值为较大的值,因为较大的阈值可以过滤掉更多的噪声,从而得到更准确的边缘。
例如,如果threshold1=50,threshold2=150,那么像素点的梯度值大于150的被标记为强边缘,小于50的被标记为非边缘,介于50和150之间的被标记为弱边缘。强边缘和弱边缘之间的连通性将被用于确定真正的边缘。
需要注意的是,阈值的选择需要根据具体情况进行调整。如果阈值选择过高,可能会错过一些边缘。如果阈值选择过低,可能会将一些噪声误判为边缘。因此,阈值的选择需要根据具体应用场景进行调整。
threshold1和threshold2是Canny算子中的两个阈值,用于确定边缘的强度和弱度。这两个阈值的选择是一个经验性的问题,取值通常由实际应用中的需要来决定。
通常情况下,建议按照以下步骤来选择这两个阈值:
使用cv::medianBlur或cv::GaussianBlur对输入图像进行平滑处理,以去除噪声。
计算图像的梯度幅值和方向,可以使用cv::Sobel或cv::Scharr函数。
根据梯度幅值的分布情况,选择合适的threshold1和threshold2阈值,以使边缘尽可能清晰地出现在输出的二值图像中。可以使用直方图分析、手动调整等方法来确定这两个阈值。
使用确定的threshold1和threshold2调用cv::Canny函数进行边缘检测。
值得注意的是,不同的应用场景可能需要不同的阈值,因此需要根据具体情况选择合适的阈值。一般而言,较低的阈值可以检测到较弱的边缘,较高的阈值可以过滤掉较弱的噪声,但过高的阈值会导致边缘丢失。因此,需要在保证边缘清晰可见的前提下,尽量选择较低的阈值。
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg", IMREAD_GRAYSCALE);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
//拉普拉斯 算子
Mat result, result_g, result_G;
//未滤波提取边缘
Laplacian(img, result, CV_16S, 3, 1, 0);
convertScaleAbs(result, result);
//滤波后提取Laplacian边缘
GaussianBlur(img, result_g, Size(3, 3), 5, 0); //高斯滤波
Laplacian(result_g, result_G, CV_16S, 3, 1, 0);
convertScaleAbs(result_G, result_G);
//Canny 算法
Mat resultHigh, resultLow, resultG;
//大阈值边缘检测图像边缘
Canny(img, resultHigh, 50, 150);
//小阈值边缘检测
Canny(img, resultLow, 50, 100);
//高斯模糊后检测
GaussianBlur(img, resultG, Size(3, 3),5);
Canny(resultG, resultG, 100, 200);
//显示图像
imshow("result_Laplacian", result);
imshow("result_G_Laplacian", result_G);
imshow("resultHigh_Canny", resultHigh);
imshow("resultLow_Canny", resultLow);
imshow("resultG_Canny", resultG);
连通域
图像连通域指的是在一个二值图像中,所有具有相同像素值(通常为黑色或白色)且相邻的像素构成的连通区域。其中,相邻的像素指的是在图像中上下、左右或对角线方向上相邻的像素。
一个图像可以包含多个不同的连通域,每个连通域可以表示为一个独立的对象或区域。在计算机视觉和图像处理中,通过对图像进行连通域分析,可以对图像进行物体检测、目标跟踪、图像分割等操作。
具体地说,连通域分析通常涉及以下步骤:
1:二值化:将图像转换为黑白二值图像。
2:连通域标记:遍历图像中的每个像素,将与其相邻的像素标记为同一连通域。
3:连通域处理:对于每个连通域,可以计算其位置、大小、形状等特征,以及进行进一步的处理和分析。
connectedComponents 只分割连通域
connectedComponentsWithStats 分割连通域并统计信息
stats矩阵中每列的属性
需要注意的是,输入的二值图像必须是经过阈值化处理的,即只包含两个像素值,一个表示前景(目标),一个表示背景。如果输入的图像不是二值图像,可以使用OpenCV中的`threshold`函数或其他阈值化函数进行处理。
函数的返回值为连通区域的总数(除了背景区域)。
`connectedComponentsWithStats`是OpenCV中的一个函数,用于实现图像的连通区域分析,可以将输入的二值图像分割成多个连通的区域,并计算每个区域的面积、中心点坐标、最小外接矩形等属性信息。下面是各个参数的详细解释:
1. `image`: 输入的二值图像,要求为8位单通道图像(即每个像素点的取值范围为0~255)。
2. `labels`: 输出的标记图像,与输入图像大小相同,每个像素点的取值为标记号(即1、2、3……),用于区分不同的连通区域。
3. `stats`: 输出的包含每个连通区域属性信息的矩阵,每一行代表一个连通区域,包含四个属性信息:面积、左上角点坐标(x、y)、宽度和高度。
4. `centroids`: 输出的包含每个连通区域中心点坐标的矩阵,每一行代表一个连通区域的中心点坐标(x、y)。
5. `connectivity`: 连通性,可以取4或8,表示分割时只考虑4连通或8连通。
6. `ltype`: `labels`图像的数据类型,可以取`CV_32S`或`CV_16U`,分别表示输出的标记图像数据类型为32位有符号整型或16位无符号整型。
在`connectedComponentsWithStats`函数中,`stats`和`centroids`这两个参数的属性顺序是固定的。
`stats`参数是一个大小为`(N, 5)`的矩阵,其中`N`表示连通区域的个数。矩阵的每一行包含了该连通区域的5个属性,分别为:
1. `left`: 连通区域最左边的坐标(即x轴方向上的坐标)
2. `top`: 连通区域最上边的坐标(即y轴方向上的坐标)
3. `width`: 连通区域的宽度
4. `height`: 连通区域的高度
5. `area`: 连通区域的面积
`centroids`参数是一个大小为`(N, 2)`的矩阵,其中`N`同样表示连通区域的个数。矩阵的每一行包含了该连通区域的中心点坐标,分别为:
1. `x`: 连通区域的中心点在x轴方向上的坐标
2. `y`: 连通区域的中心点在y轴方向上的坐标
因此,在使用这两个参数时,需要按照上述的属性顺序进行访问和处理,以确保正确地获取每个连通区域的属性信息。
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/rice.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat rice, riceBW;
//将图像转换为二值化
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
//生成随机颜色,用于区分不同连通域
RNG rng(10086);
Mat out;
int number = connectedComponents(riceBW, out, 8, CV_16U);
//生成三通道随机颜色 Vec3b(B,G,R)
vector<Vec3b> colors;
for (int i = 0; i < number; i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);
}
//以不同颜色标记出不同的连通域
Mat result = Mat::zeros(rice.size(), CV_8UC3);
int w = result.cols;
int h = result.rows;
for (int row = 0; row < h; row++)
{
for (int col = 0; col < w; col++)
{
int label = out.at<uint16_t>(row, col);
if (label == 0) //背景的黑色不改变
{
continue;
}
//标记每个像素点颜色存放result中
result.at<Vec3b>(row, col) = colors[label];
}
}
//显示结果
imshow("原图", img);
imshow("标记后的图像0", result);
{
Mat rice, riceBW;
//将图像转成二值图像,用于统计连通域
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
//生成随机颜色,用于区分不同连通域
RNG rng(10086);
Mat out, stats, centroids;
//统计图像中连通域的个数
int number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
vector<Vec3b> colors;
for (int i = 0; i < number; i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);
}
//以不同颜色标记出不同的连通域
Mat result = Mat::zeros(rice.size(), CV_8UC3);
int w = result.cols;
int h = result.rows;
for (int i = 1; i < number; i++)
{
// 中心位置
int center_x = centroids.at<double>(i, 0);
int center_y = centroids.at<double>(i, 1);
//矩形边框
int x = stats.at<int>(i, CC_STAT_LEFT);
int y = stats.at<int>(i, CC_STAT_TOP);
int w = stats.at<int>(i, CC_STAT_WIDTH);
int h = stats.at<int>(i, CC_STAT_HEIGHT);
int area = stats.at<int>(i, CC_STAT_AREA);
// 中心位置绘制
circle(img, Point(center_x, center_y), 2, Scalar(0, 255, 0), 2, LINE_8, 0);
// 外接矩形
Rect rect(x, y, w, h);
rectangle(img, rect, colors[i]);
putText(img, format("%d", i), Point(center_x, center_y),
FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 1);
cout << "number: " << i << ",area: " << area << endl;
}
//显示结果
imshow("标记后的图像1", img);
}
图像距离变换
计算源图像的每个像素到最接近的零像素的距离。
distanceTransform 距离变换函数 — 计算源图像的每个像素到最接近的零像素的距离。
在OpenCV中,常用的距离类型有以下几种:
- `CV_DIST_L1`:曼哈顿距离(也称为L1距离),即两点横向和纵向坐标差的绝对值之和。
- `CV_DIST_L2`:欧几里得距离(也称为L2距离),即两点的直线距离。
- `CV_DIST_C`:切比雪夫距离(也称为L∞距离),即两点横向和纵向坐标差的最大值。
- `CV_DIST_L12`:混合L1和L2距离。该距离是L1距离和L2距离的加权和,其中L1距离的权值为alpha,L2距离的权值为1-alpha。
- `CV_DIST_FAIR`:fair距离,一种抗噪声的距离度量。
- `CV_DIST_WELSCH`:welsch距离,一种抗噪声的距离度量。
- `CV_DIST_HUBER`:huber距离,一种抗噪声的距离度量。
以上距离类型在OpenCV的距离变换函数(`cv::distanceTransform`)和最近邻搜索函数(`cv::flann::Index`)中都会用到。在使用这些函数时,需要根据实际需求选择适合的距离类型。
代码演示:
//构建建议矩阵,用于求取像素之间的距离
Mat a = (Mat_<uchar>(5, 5) << 1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 0, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1);
Mat dist_L1, dist_L2, dist_C, dist_L12;
//计算街区距离
distanceTransform(a, dist_L1, DIST_L1, 3, CV_8U);
cout << "街区距离:" << endl << dist_L1 << endl;
//计算欧式距离
distanceTransform(a, dist_L2, DIST_L2, 5, CV_8U);
cout << "欧式距离:" << endl << dist_L2 << endl;
//计算棋盘距离
distanceTransform(a, dist_C, DIST_C, 5, CV_8U);
cout << "棋盘距离:" << endl << dist_C << endl;
//对图像进行距离变换
Mat rice = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/rice.png", IMREAD_GRAYSCALE);
if (rice.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat riceBW, riceBW_INV;
//将图像转成二值图像,同时把黑白区域颜色呼唤
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
threshold(rice, riceBW_INV, 50, 255, THRESH_BINARY_INV);
//距离变换
Mat dist, dist_INV;
distanceTransform(riceBW, dist, DIST_L1, 3, CV_32F); //为了显示清晰,将数据类型变成CV_32F
distanceTransform(riceBW_INV, dist_INV, DIST_L1, 3, CV_8U);
//显示变换结果
imshow("riceBW", riceBW);
imshow("dist", dist);
imshow("riceBW_INV", riceBW_INV);
imshow("dist_INV", dist_INV);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJ6E1E1R-1686913024605)(_v_images/20230413112219752_18204.png =1299x)]
形态学操作
腐蚀
getStructuringElement 结构元素生成函数
erode 腐蚀操作函数
膨胀
dilate 膨胀操作函数
代码演示:
//绘制包含区域函数
void drawState(Mat &img, int number, Mat centroids, Mat stats, String str)
{
//生成随机颜色,用于区分不同连通域
RNG rng(10086);
vector<Vec3b> colors;
for (int i = 0; i < number; i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);
}
//以不同颜色标记出不同的连通域
Mat result = Mat::zeros(img.size(), CV_8UC3);
int w = result.cols;
int h = result.rows;
for (int i = 1; i < number; i++)
{
// 中心位置
int center_x = centroids.at<double>(i, 0);
int center_y = centroids.at<double>(i, 1);
//矩形边框
int x = stats.at<int>(i, CC_STAT_LEFT);
int y = stats.at<int>(i, CC_STAT_TOP);
int w = stats.at<int>(i, CC_STAT_WIDTH);
int h = stats.at<int>(i, CC_STAT_HEIGHT);
int area = stats.at<int>(i, CC_STAT_AREA);
// 中心位置绘制
circle(img, Point(center_x, center_y), 2, Scalar(0, 255, 0), 2, LINE_8, 0);
// 外接矩形
Rect rect(x, y, w, h);
rectangle(img, rect, colors[i]);
putText(img, format("%d", i), Point(center_x, center_y),
FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 1);
cout << "number: " << i << ",area: " << area << endl;
}
//显示结果
imshow(str, img);
}
int main()
{
//生成用于腐蚀的原图像
Mat src = (Mat_<uchar>(6, 6) << 0, 0, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 0, 0);
Mat struct1, struct2, struct3;
//矩形结构元素
struct1 = getStructuringElement(MORPH_RECT, Size(3, 3));
//十字结构元素
struct2 = getStructuringElement(MORPH_CROSS, Size(3, 3));
//椭圆结构元素
struct3 = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
Mat erodeSrc;
//存放腐蚀后的图像 -- 使用十字结构
erode(src, erodeSrc, struct2);
namedWindow("腐蚀前", WINDOW_GUI_NORMAL);
namedWindow("腐蚀后", WINDOW_GUI_NORMAL);
imshow("腐蚀前", src);
imshow("腐蚀后", erodeSrc);
//验证腐蚀对小连通域的去除
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/rice.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat img2;
//克隆一个单独的图像,用于后期图像绘制
img.copyTo(img2);
Mat rice, riceBW;
//将图像转成二值图像,用于统计连通域
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
Mat out, stats, centroids;
//统计图像中连通域的个数
int number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
drawState(img, number, centroids, stats, "未腐蚀时统计连通域"); //绘制图像
//对图像进行腐蚀 -- 使用矩形结构
erode(riceBW, riceBW, struct1);
number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
drawState(img2, number, centroids, stats, "腐蚀后统计连通域"); //绘制图像
{
Mat dilateSrc;
//存放腐蚀后的图像 -- 使用十字结构
dilate(src, dilateSrc, struct2);
namedWindow("膨胀前", WINDOW_GUI_NORMAL);
namedWindow("膨胀后", WINDOW_GUI_NORMAL);
imshow("膨胀前", src);
imshow("膨胀后", dilateSrc);
//验证膨胀
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/learnCV.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat img2, learnCV, learnCVBW;
//将图像转成二值图像,用于统计连通域
cvtColor(img, learnCV, COLOR_BGR2GRAY);
threshold(learnCV, learnCVBW, 50, 255, THRESH_BINARY);
//对图像进行膨胀-- 使用矩形结构
dilate(learnCVBW, learnCVBW, struct1);
imshow("膨胀前learnCV", learnCV);
imshow("膨胀后learnCV", learnCVBW);
}
waitKey(0);
return 0;
}
应用
开运算 MORPH_OPEN
该操作先对图像进行腐蚀操作,然后再对腐蚀后的图像进行膨胀操作,能够消除小的物体和细小的噪点,同时保持物体的大小和形状。
实际应用:去除图像中的细小物体和噪点,平滑图像中的物体边缘等。
闭运算 MORPH_CLOSE
该操作先对图像进行膨胀操作,然后再对膨胀后的图像进行腐蚀操作,能够填充小的空洞和连接相邻的物体,同时保持物体的大小和形状。
实际应用:填充图像中的空洞,连接相邻的物体,平滑图像中的物体边缘等。
形态学梯度 MORPH_GRADIENT
该操作通过对输入图像进行膨胀和腐蚀操作,计算两个结果之间的差异,得到输入图像中物体的边缘信息。
实际应用:检测图像中物体的边缘,提取图像中物体的轮廓等。
顶帽运算 MORPH_TOPHAT
该操作通过将原始图像减去进行开运算后的图像,得到输入图像中的小细节信息。
实际应用:检测图像中小的物体和细节,提取图像中的纹理信息等。
黑帽运算 MORPH_BLACKHAT
该操作通过将进行闭运算后的图像减去原始图像,得到输入图像中的大细节信息。
实际应用:检测图像中大的物体和细节,提取图像中的纹理信息等。
击中击不中变换 MORPH_HITMISS (-1表示必须不匹配的像素)
该操作通过将原始图像与两个核进行卷积,分别得到命中和不命中的像素,并将结果写入输出图像中。该操作可以用于检测图像中的特定形状。
实际应用:检测图像中的特定形状,比如角点、T形交叉等。
说白了就是:在图像A中,找B(结构元structuring element)并输出其原点(origin)
MORPH_HITMISS操作用于检测图像中特定的形状,比如角点、T形交叉等,其核包含三个值:1表示必须匹配的像素,-1表示必须不匹配的像素,0表示忽略的像素。
在击中击不中算子中,我们可以使用0来表示忽略的像素,这意味着这些像素在结构元素匹配中不会被考虑。因此,结构元素中的0表示不影响像素匹配的像素。
morphologyEx 形态操作函数
op参数:
代码演示:
//用于验证形态学应用的二值化矩阵
Mat src = (Mat_<uchar>(9, 12) << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 0, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
//可以自由调节显示图像的尺寸
namedWindow("src", WINDOW_NORMAL);
imshow("src", src);
//3×3矩形结构元素
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
//Mat kernel = (Mat_<char>(3, 3) << 1, 1, 1,
// 1, 1, 1,
// 1, 1, 1);
//对二值化矩阵进行形态学操作
Mat open, close, gradient, tophat, blackhat, hitmiss;
//对二值化矩阵进行开运算
morphologyEx(src, open, MORPH_OPEN, kernel);
namedWindow("open", WINDOW_NORMAL); //可以自由调节显示图像的尺寸
imshow("open", open);
//对二值化矩阵进行闭运算
morphologyEx(src, close, MORPH_CLOSE, kernel);
namedWindow("close", WINDOW_NORMAL); //可以自由调节显示图像的尺寸
imshow("close", close);
//对二值化矩阵进行梯度运算
morphologyEx(src, gradient, MORPH_GRADIENT, kernel);
namedWindow("gradient", WINDOW_NORMAL); //可以自由调节显示图像的尺寸
imshow("gradient", gradient);
//对二值化矩阵进行顶帽运算
morphologyEx(src, tophat, MORPH_TOPHAT, kernel);
namedWindow("tophat", WINDOW_NORMAL); //可以自由调节显示图像的尺寸
imshow("tophat", tophat);
//对二值化矩阵进行黑帽运算
morphologyEx(src, blackhat, MORPH_BLACKHAT, kernel);
namedWindow("blackhat", WINDOW_NORMAL); //可以自由调节显示图像的尺寸
imshow("blackhat", blackhat);
//对二值化矩阵进行击中击不中变换
morphologyEx(src, hitmiss, MORPH_HITMISS, kernel, Point(-1, -1),1 );
namedWindow("hitmiss", WINDOW_NORMAL); //可以自由调节显示图像的尺寸
imshow("hitmiss", hitmiss);
//用图像验证形态学操作效果
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/learnCV.png", IMREAD_GRAYSCALE);
imshow("coin", img);
threshold(img, img, 130, 255, THRESH_BINARY);
imshow("二值化后的coin", img);
//5×5矩形结构元素
Mat kernel_coin = getStructuringElement(MORPH_RECT, Size(5, 5));
Mat open_coin, close_coin, gradient_coin;
Mat tophat_coin, blackhat_coin, hitmiss_coin;
//对图像进行开运算
morphologyEx(img, open_coin, MORPH_OPEN, kernel_coin);
imshow("open_coin", open_coin);
//对图像进行闭运算
morphologyEx(img, close_coin, MORPH_CLOSE, kernel_coin);
imshow("close_coin", close_coin);
//对图像进行梯度运算
morphologyEx(img, gradient_coin, MORPH_GRADIENT, kernel_coin);
imshow("gradient_coin", gradient_coin);
//对图像进行顶帽运算
morphologyEx(img, tophat_coin, MORPH_TOPHAT, kernel_coin);
imshow("tophat_coin", tophat_coin);
//对图像进行黑帽运算
morphologyEx(img, blackhat_coin, MORPH_BLACKHAT, kernel_coin);
imshow("blackhat_coin", blackhat_coin);
//对图像进行击中击不中变换
morphologyEx(img, hitmiss_coin, MORPH_HITMISS, kernel_coin);
imshow("hitmiss_coin", hitmiss_coin);
waitKey(0);