掌握imgproc组件:opencv-图像变换

1. 基于OpenCV的边缘检测

1.1 边缘检测的一般步骤

第一步:滤波
边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤波方法主要有高斯滤波,即采用离散化的高斯函数产生一组归一化的高斯核,然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和。
第二步:增强
增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算题都幅值来确定。
第三步:检测
经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际工程中,常用的方法是通过阈值化方法来检测。

1.2 canny算子

最优化边缘检测的三个主要评价标准。

  • 低错误率:表示出尽可能多的实际边缘,同时尽可能地减少噪声产生的误报。
  • 高定位率:标识出的边缘要与图像中的实际边缘尽可能接近。
  • 最小响应:图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。

1.2.1 Canny边缘检测步骤:

  1. 消除噪声
    一般情况下,使用高斯平滑滤波器卷积降噪。以下显示了一个size=5的高斯内核示例:
    在这里插入图片描述

  2. 计算梯度幅值和方向
    此处,按照Sobel滤波器的步骤来操作。

    1. 运用一对卷积阵列(分别作用于x和y方向)
      在这里插入图片描述
    2. 使用下列公式计算梯度幅值和方向
      在这里插入图片描述
      而梯度方向一般取这4个可能的角度之一——0、45、90、135度。
  3. 非极大值抑制
    这将删除那些不被认为是边缘的一部分的像素。因此,将只保留细线(候选边缘)。

  4. 滞后阈值
    这是最后一步,Canny使用了滞后阈值,滞后阈值需要两个阈值(高阈值和低阈值):

    • 若某一像素位置的幅值超过高阈值,该像素被保留为边缘像素。
    • 若某一像素位置的幅值小于地阈值,该像素被排除。
    • 若某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于高阈值的像素时被保留。
      Canny建议上下限的比例在2:1和3:1之间。

1.2.2 Canny边缘检测:Canny()函数

在这里插入图片描述

  • 第一个参数:输入图像
  • 第二个参数:输出的边缘图
  • 第三个参数:第一个滞后性的阈值
  • 第四个参数:第二个滞后性的阈值
  • 第五个参数:int类型的apertureSize,表示应用Sobel算子的孔径大小,其有默认值3
  • 第六个参数:一个标志,表示是否应该使用更精确的L2准则来计算图像梯度大小(L2gradient=true),或者默认的L1准则(L2gradient=false)。
    需要注意的是,这个函数1阈值和阈值2两者中较小的值用于边缘连接,而较大值用来控制强边缘的初始段,推荐的高低阔值比在 2:1 到3:1之间。

1.2.3 Canny边缘检测案例

#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat src = imread("../../image/1.tif");
	Mat src1 = src.clone();

	imshow("Canny边缘检测", src);

	/*
		1.转换成灰度图
		2.降噪
		3.canny
		4.将得到的边缘作为掩码拷贝原图到效果图上,得到彩色的边缘图
	*/

	Mat dst, edge, gray;

	//1.创建与src同类型和大小的矩阵dst
	dst.create(src1.size(), src1.type());
	
	//2.将原图转化为灰度图
	cvtColor(src1, gray, COLOR_RGB2GRAY);

	//3.使用3*3内核来降噪
	blur(gray, edge, Size(3, 3));

	//4.运算Canny算子
	Canny(edge, edge, 3, 9, 3);

	//5.将dst内的所有元素设置为0
	dst = Scalar::all(0);

	src1.copyTo(dst, edge);
	
	imshow("效果图", dst);
	waitKey();
	return 0;
}

在这里插入图片描述

1.3 sobel算子

Sobel 算子是一个主要用于边缘检测的离散微分算子。它结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子,都将会产生对应的梯度矢量或是其法矢量。
在这里插入图片描述
你可以很容易地注意到,在一个边缘,像素强度的变化是很明显的。一个表达变化的好方法是使用导数。梯度的高变化表示图像中的一个重大变化。

1.3.1 sobel算子的计算过程

我们假设被作用的图像为 I ,然后进行如下操作

  1. 分别在x和y两个方向求导
    • 水平变化:将 I 与一个奇数大小的内核Gx进行卷积。比如,当内核大小为3时,Gx的计算结果为
      在这里插入图片描述
    • 垂直变化:将 I 与一个奇数大小的内核Gy进行卷积。比如,当内核大小为3是,Gy的计算结果为
      -
  2. 在图像的每一点,结合以上两个结果求出近似梯度:
    在这里插入图片描述
    有时也可用下面更简单的公式代替
    在这里插入图片描述

1.3.2 使用Sobel算子:Sobel()函数

在这里插入图片描述

  • 第一个参数:输入图像

  • 第二个参数:目标图像

  • 第三个参数:int类型ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合
    在这里插入图片描述

  • 第四个参数:是否计算x方向上的导数标志

  • 第五个参数:是否计算y方向上的导数标志

  • 第六个参数:Sobel核的大小;它必须是1、3、5或7。

  • 第七个参数:double类型的scale,计算导数时可选的缩放因子,默认为1,表示默认情况下是没有应用缩放的。

  • 第八个参数:可选的delta值,在将结果存储到dst之前,将其添加到结果中。
    一般情况下,都是用ksize*ksize内核来计算导数的。然而有一个特殊的情况,当ksize为1时,往往会使用3*1或1*3的内核。且这种情况下,并没有进行高斯平滑操作。

  • 当内核大小为3时,Sobel内核可能产生表较明显的误差(毕竟,Sobel算子只是求取了导数的近似值而已)。为解决这一问题,OpenCV提供了Scharr函数,但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快,但结果更加精确,其内核是这样的在这里插入图片描述

  • 因为Sobel算子结合了高斯平滑和分化,因此结果会具有更多的抗噪性。大多数情况下,使用( xorder = 1, yorder = 0, ksize = 3) 计算图像X方向的导数, ( xorder = 0, yorder = 1, ksize = 3)计算Y方向的导数。

1.3.3 示例程序:Sobel算子的使用

#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {

	//创建grad_x和grad_y矩阵
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y, dst;

	Mat src = imread("../../image/1.tif");


	namedWindow("原图", 0);
	namedWindow("X方向的Sobel", 0);
	namedWindow("Y方向的Sobel", 0);
	namedWindow("整体方向Sobel", 0);
	imshow("原图", src);

	//求x方向的梯度
	Sobel(src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);
	/*
		convertScaleAbs (
				InputArray src, 
				OutputArray dst, 
				double alpha = 1.0, //alpha 为乘数因子
				double beta = 0.0 //beta为偏移量 
				);
				它的原理/用法为:
				(1)若像素值-255 < src * alpha + beta < 255,取绝对值;
				(2)若像素值 src * alpha + beta >= 255 || src * alpha + beta <= -255,取255,不可溢出。
	*/
	
	convertScaleAbs(grad_x, abs_grad_x);
	imshow("X方向的Sobel", abs_grad_x);

	//计算Y方向的梯度
	Sobel(src, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);
	convertScaleAbs(grad_y, abs_grad_y);
	imshow("Y方向的Sobel", abs_grad_y);

	//近似梯度
	addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5,0,dst);
	imshow("整体方向Sobel", dst);
	waitKey();
	return 0;
}

在这里插入图片描述

1.4 Laplacian算子

1.4.1 Laplacian算子简介

在这里插入图片描述

1.4.2 计算拉普拉斯变换:Laplacian()函数

在这里插入图片描述

  • 第一个参数:输入图像
  • 第二个图像:输出的边缘图
  • 第三个参数:int类型的ddept,目标图像的深度
  • 第四个参数:int类型的ksize,用于计算二阶导数的滤波器孔径尺寸,大小必须为正奇数
  • 第五个参数:double类型的scale,计算拉普拉斯值的时候可选的比例因子,有默认值1
  • 第六个参数:double类型的delta,表示在结果存入目标图之前可选地delta值

Laplacian() 函数其实主要是利用 Sobel算子的运算。它通过加上 Sobel算子运算出的图像x方向和y方向上的导数 来得到载入图像的拉普拉斯变换结果。
在这里插入图片描述
当ksize=1时,拉普拉斯的计算方法是用以下3×3的孔径对图像进行过滤:
在这里插入图片描述

1.4.3 示例程序: Laplacian 算子的使用

#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat src, src_gray, dst, abs_dst;

	src = imread("../../image/1.tif");
	imshow("原图", src);

	//使用高斯滤波消除噪声
	GaussianBlur(src, src, Size(3, 3), 0, 0);

	cvtColor(src, src_gray, COLOR_RGB2GRAY);

	Laplacian(src_gray, dst, CV_16S, 3, 1, 0);

	convertScaleAbs(dst, abs_dst);
	
	imshow("Laplace变换", abs_dst);
	waitKey();
	return 0;
}

在这里插入图片描述

1.5 scharr滤波器

我们一般直接称scharr为滤波器,而不是算子 。上文已经到,它在 OpenCV中主要是配合 Sobel 算子的运算而存在的。

在这里插入图片描述
它的参与与Sobel类似,只不过滤波器尺寸有固定尺寸和固定值。

#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat grad_x, grad_y;
	Mat src, abs_grad_x, abs_grad_y,dst;

	
	src = imread("../../image/lena_color_256.tif");
	imshow("原图", src);

	Scharr(src, grad_x, CV_16S, 1, 0, 1, 1);
	convertScaleAbs(grad_x, abs_grad_x);
	imshow("Scharr在x方向", abs_grad_x);

	Scharr(src, grad_x, CV_16S, 0, 1, 1, 1);
	convertScaleAbs(grad_x, abs_grad_y);
	imshow("Scharr在y方向", abs_grad_y);

	addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst);
	imshow("梯度近似", dst);

	waitKey();
	return 0;
}

在这里插入图片描述

2.霍夫变换

在图像处理和 计算机视觉领域中,如何从当前的图像种提取所需要的特征信息是图像识别的关键所在。在许多应用场什中需要快速准确地检测出直线或者圆。
其中一种非常有效的解决问题的方法是霍夫变换,其为图像处理中从图像中识别几何形状的基本方法之一 ,应川很广泛,也有很多改进算法。最基本的霍夫变换是从黑白图像中检测直线 (线段)。

2.1 霍夫变换概述

霍夫变化是图像处理中的一种特征提取技术,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的几何作为霍夫变换结果。最初的霍夫变换是设计用来检测直线和曲线的。起初的方法要求知道物体边界线的解析方程,但不需要有关区域位置的先验知识。这种方法的一个突出优点是分割结果的Robustness,即对数据的不完全或噪声不是非常敏感。然后,要获得描述边界的解析表达常常是不可能的。后于1972 年由 Rich rd Duda & Peter Hart 推广使用,经典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆。霍夫变换运用两个坐标空间之间的变换将一个空间中具有相同性状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意形状的问题转化成统计峰值的问题。

2.2 霍夫线变换

霍夫线变换是一种用来寻找直线的方法,在使用霍夫线性变换之前,首先要对图像进行边缘检测的处理,即霍夫线变化的直接输入只能是边缘二值图像。
OpenCV支持三种不同的霍夫线变换,他们分别是:标准霍夫变换、多尺度霍夫变化、累计概率霍夫变换。
其中多尺度霍夫变换为经典霍夫变换在多尺度下的一种变种。而累计概率霍夫变换算法是标准霍夫变换算法的一个改进,他在一定的范围内进行霍夫变换,计算单独线段的方法以及范围,从而减少计算量,缩短计算时间。之所以成为“概率”的,是因为并不讲累加器平面内的所有可能的点累加,而只是累加其中一部分,该想法是如果峰值足够高,只是一小部分时间去寻找它就够了。按照猜想,可以实质性的减少计算时间。
在OpenCV中,可以用HoughLines函数来调用标准霍夫变换和多尺度霍夫变换。
而HoughLinesP函数用于调用累计概率霍夫变换。累计概率霍夫变换执行效率很高,所以相比于HoughLines函数,我们更倾向于使用HoughLinesP函数。

2.3 霍夫线变换的原理

直线在图像二维空间可由两个变量表示,有以下两种情况。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4 标准霍夫变换:HoughLines()函数

此函数可以找出采用标准霍夫变换的二值图像线条。
在这里插入图片描述

  • 第一个参数: 输入图像
  • 第二个参数:输出线条的向量。每条线由一个2或3元素的向量(ρ,θ)或(ρ,θ,votes)表示,其中ρ是与坐标原点(0,0)(图像的左上角)的距离,θ是线的旋转角度,单位是弧度(0∼垂直线,π/2∼水平线),votes是累积器的值。
  • 第三个参数:以像素为单位的距离精度。另一种表述方法是直接搜索时的进步尺寸的单位半径。
  • 第四个参数:double类型的theta,以弧度为单位的角度精度。另一种表述方式是直线搜索时的进步尺寸的单位角度。
  • 第五个参数:累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
  • 第六个参数:double类型的srn,有默认值0。对于对尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确地累加器进步尺寸为rho/srn。
  • 第七个参数:double类型的stn,有默认值0,对于对尺度霍夫变换,srn表示第四个参数进步尺寸的单位叫角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
  • 第八个参数:对于标准和多尺度Hough变换,检查线条的最小角度。必须落在0和max_theta之间。
  • 第九个参数:对于标准和多尺度Hough变换,是一个角度的上界。
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat dst, cdst, tmp;
	Mat src = imread("1.jpg");

	//边缘检测
	Canny(src, dst, 50, 200, 3);
	imshow("边缘检测后", dst);
	//将边缘复制到将在BGR中显示结果的图像上
	cvtColor(dst, cdst, COLOR_GRAY2BGR);


	//标准Hough线变换
	vector<Vec2f> lines; //将保存检测的结果
	HoughLines(dst, lines, 1, CV_PI / 180, 150, 0, 0); //运行实际的检测

	//画线
	for (size_t i = 0; i < lines.size(); i++) {
		float rho = lines[i][0], theta = lines[i][1];
		Point pt1, pt2;
		double a = cos(theta), b = sin(theta);
		double x0 = a * rho, y0 = b*rho;
		pt1.x = cvRound(x0 + 1000 * (-b));
		pt1.y = cvRound(y0 + 1000 * (a));
		pt2.x = cvRound(x0 - 1000 * (-b));
		pt2.y = cvRound(y0 - 1000 * (a));
		line(cdst, pt1, pt2, Scalar(0, 0, 255), 1, LINE_AA);
	}

	imshow("原图", src);
	imshow("效果图", cdst);

	waitKey();
	return 0;

}

在这里插入图片描述

2.5 累计概率霍夫变换:HoughLinesP()函数

在这里插入图片描述

  • 第一个参数:输入图像
  • 第二个参数:输出线的向量。每条线由一个4元素的向量(x1,y1,x2,y2)表示,其中(x1,y1)和(x2,y2)是每个检测到的线段的结束点。
  • 第三个参数:以像素为单位的距离精度
  • 第四个参数:以弧度为单位的角度精度
  • 第五个参数:累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
  • 第六个参数:double类型的minLineLength,有默认值0,表示最低段的长度,比这个设定参数端的线段就不能被显现出来。
  • 第七个参数:double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大距离。
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat src = imread("1.jpg");
	Mat dst, cdstP;

	Canny(src, dst, 50, 200, 3);
	imshow("轮廓图", dst);
	cvtColor(dst, cdstP, COLOR_GRAY2BGR);

	vector<Vec4i> linesP;
	HoughLinesP(dst, linesP, 1, CV_PI / 180, 50, 50, 10);
	for (size_t i = 0; i < linesP.size(); i++) {
		Vec4i l = linesP[i];
		line(cdstP, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 0, 255), 1, LINE_AA);
	}
	imshow("原图", src);
	imshow("效果图", cdstP);

	waitKey();
	return 0;
}

在这里插入图片描述

2.6 霍夫圆变换

霍夫圆变换的基本原理和上面讲的霍夫线性变化大体上是很类似,只是点对应的二维极径极角空间被三维的点心x、y和半径r空间取代。说“大体上类似”的原因是,如果完全相同的方法的话,累加平面会被三维的累加容器所代替——在这三维中,一维是x,一维是y,另外一维是圆的半径r。这就意味着需要大量的内存而且执行效率会很低,速度会很慢。
对直线来说,一条直线能由参数极径极角(r,θ)表示,而对圆来说需要三个参数表示:(xcenter,ycenter,r)
其中(xcenter,ycenter)定义了中心位置(绿点),r是半径,这使得我们可以完全定义一个圆,如下图所示:
在这里插入图片描述
在OpenCV中,我们常常通过一个叫做“霍夫梯度法”的方法来解决圆变换的问题。

2.6.1 霍夫梯度法的原理

  1. 首先对图像应用边缘检测,比如用canny边缘检测
  2. 然后,对边缘图像中的每一个非零点,考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一阶导数得到梯度
  3. 利用得到的梯度,由斜率指定的直线上的每一个点都在累加器中被累加,这里的斜率是从一个指定的最小值到指定的最大值的距离。
  4. 同时,标记边缘图像中每一个非0像素的位置
  5. 然后从二维累加器中这些点中选择候选的中心,这些中心都大于给定阈值并且大于其所有近邻。这些候选的中心按照类价值将序排序,以便于最支持像素的中心首先出现。
  6. 接下来地每一个中心,考虑所有的非零像素。
  7. 这些像素按照其与中心距离排序。从到最大半径的最小距离算起,选择非0像素最支持的一条半径
  8. 如果一个中心收到边缘图像非0像素最充分的支持,并且到前期被选择的中心有足够的的距离,那么它就会被保留下来。

这个实现可以使算法执行起来更高效 ,或许更加重要的是,能够帮助解决三维累加器中会产生许多噪声并且使得结果不稳定的稀疏分布问题。

2.6.2 霍夫梯度法的缺点

  1. 在霍夫梯度法中,我们使用Sobel 导数来计算局部梯度,那么随之而来的假设是,它可以视作等同于一条局部切线,这并不是一个数值稳定的做法。在大多数情况下,这样做会得到正确的结果,但或许会在输出 产生一些噪声。
  2. 在边缘图像中的整个非0像素集被看做每个中心的候选部分。因此,如果把累加器的阈值设置偏低,算法将要消耗比较长的时间。此外,因为每一个中心只选择一个圆,如果有同心圆,就只能选择其中的一个。
  3. 因为中心是按照其关联的累加器值的升序排列的,并且如果新的中心过于接近之前已经接受的中心的话,霍夫梯度法的倾向是保留最大的一个圆。可以说这是一种比较极端的做法,因为在这里默认Sobel导数会产生噪声,入市对于无穷分别率的平滑图像而言的话,这才是必须的。

2.6.3 霍夫圆变换:HoughCircles()函数

HoughCircle函数可以利用霍夫变换算法检测出灰度图中的圆。它相比之前的eHoughLines和HoughLinesP,比较明显的一个区别是不需要源图是二值的,而HoughLines和HoughLinesP都需要源图为二值图像。
在这里插入图片描述

  • 第一个参数:源图像,需为8位的灰度单通道图像
  • 第二个参数:OutputArray 类型 circles 经过调用 HoughCircles 函数后此数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x,y,radius)表示。
  • 第三个参数:检测方法,可用的方法是HOUGH_GRADIENT和HOUGH_GRADIENT_ALT
  • 第四个参数:累加器分辨率与图像分辨率的反比。例如,如果 dp=1 ,累积器的分辨率与输入图像相同。如果 dp=2 ,累加器的宽度和高度都是源图的一半。对于HOUGH_GRADIENT_ALT,推荐的值是dp=1.5,除非需要检测一些非常小的圆圈。
  • 第五个参数:double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能会被错误地检测成了一个重合的圆。反之,这个参数设置太大,某些圆就不能被检测出来。
  • 第六个参数:double类型的param1,有默认值100。它是第三个参数metho参数设置的检测方法的对应的参数。对霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
  • 第七个参数: 第二个方法特有的参数。在HOUGH_GRADIENT的情况下,它是检测阶段的圆心累积阈值。它越小,可能会检测到更多的错误圆圈。对应于较大的累加器值的圆将首先被返回。在HOUGH_GRADIENT_ALT算法的情况下,这是圆的 "完美性 "测量。它越接近1,算法选择的圆的形状就越好。在大多数情况下,0.9应该是不错的。如果你想获得更好的小圆圈检测,你可以把它降低到0.85、0.8甚至更低。但也要尽量限制搜索范围[minRadius, maxRadius],以避免许多假圆。
  • 第八个参数:圆的最小半径
  • 第九个参数:圆的最大半径
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat srcImage = imread("1.jpg");
	Mat tmpImage, dstImage;

	imshow("原图", srcImage);
	cvtColor(srcImage, tmpImage, COLOR_BGR2GRAY);
	GaussianBlur(tmpImage, tmpImage, Size(9, 9), 2, 2);

	vector<Vec3f> circles;
	HoughCircles(tmpImage, circles, HOUGH_GRADIENT, 1.5, 10, 200, 100, 0, 0);
	
	//在图中绘制出圆
	for (size_t i = 0; i < circles.size(); i++) {
		Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
		int radius = cvRound(circles[i][2]);

		//绘制圆心
		circle(srcImage, center, 3, Scalar(0, 255, 0), -1, 8, 0);
		//绘制圆轮廓
		circle(srcImage, center, radius, Scalar(155, 50, 255), 3, 8, 0);

	}
	imshow("效果图", srcImage);
	waitKey();
	return 0;

}

在这里插入图片描述

3. 重映射

3.1 重映射的概念

重映射,就是把一幅图像中某位置的像素放置到另一图片指定位置的过程。为了完成映射过程,需要获得一些插值为非整数像素的坐标,因为源图像与目标图像的像素坐标不是一 一对应的。一般情况下,我们通过重映射来表达每个像素的位置(x,y)。

g ( x , y ) = f ( h ( x , y ) ) g(x,y)=f(h(x,y)) g(x,y)=f(h(x,y))
其中g()是被目标图像,f()是源图像,h(x,y)是在(x,y)上操作的映射函数。

若有一幅图 I,对其按照下面的条件作重映射:

h ( x , y ) = ( I . c o l s − x , y ) h(x,y)=(I.cols-x,y) h(x,y)=(I.colsx,y)
图像会按照x轴方向发生翻转。

3.2 实现重映射:remap()函数

在这里插入图片描述

  • 第一个参数:源图像,需要为单通道8位或者浮点型图像。
  • 第二个参数:输出图像
  • 第三个参数:InputArray类型的map1,它有两种可能的表示对象
    • 表示点(x,y)的第一个映射
    • 表示CV_16SC2、CV_32FC1或CV_32FC2类型的x值
  • 第四个参数:InputArray类型的map2,也有两种可能的表示对象
    • 若map1表示点(x,y)时。这个参数不代表任何值。
    • 表示CV_16UC1, CV_32FC2类型的Y值
  • 第五个参数:int类型的interpolation,表示插值方式。本函数不支持INTER_AREA和INTER_LINEAR_EXACT方法。
  • 第六个参数:像素外推法(见BorderTypes)。当borderMode=BORDER_TRANSPARENT时,意味着目标图像中对应于源图像中的 "离群点 "的像素不会被该函数修改。
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat srcImage, dstImage;
	Mat map_x, map_y;

	srcImage = imread("../../image/lena_gray_256.tif");
	imshow("原图", srcImage);
	//创建和原始图一样的效果图
	dstImage.create(srcImage.size(), srcImage.type());
	map_x.create(srcImage.size(), CV_32FC1);
	map_y.create(srcImage.size(), CV_32FC1);

	//双层循环,遍历每一个像素点,改变map_x和map_y的值
	for (int i = 0; i < srcImage.rows; i++) {
		for (int j = 0; j < srcImage.cols; j++) {
			/*
			* at函数
				对于单通道图像"picture1",picture1.at<uchar>(i,j)就表示在第i行第j列的像素值。
				对于多通道图像如RGB图像"picture2",可以用picture2.at<Vec3b>(i,j)[c]来表示某个通道中在(i,j)位置的像素值。

			*/
			map_x.at<float>(i, j) = static_cast<float>(j);
			map_y.at<float>(i, j) = static_cast<float>(srcImage.rows - i);
		}
	
	}

	remap(srcImage, dstImage, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT,Scalar(0,0,0));
	imshow("程序窗口", dstImage);
	waitKey();

}

在这里插入图片描述

4.仿射变换

4.1 认识仿射变换

仿射变换(Affine Transformation或Affine Map),又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变化为另一个向量空间的过程
它保持了二维图像的平直性(直线经过变换之后依然是直线)和平行性(二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线向上点的位置顺序不变)。

一个任意的仿射变换都能表示为乘以一个矩阵(线性变换)接着再加上一个向量(平移)的形式。
常见的变换形式:

  • 旋转:rotation(线性变换)
  • 平移:translation(向量加)
  • 缩放:scale(线性变换)
    仿射变换代表的是两幅图之间的一种仿射关系
    我们通常使用2*3的矩阵来表示仿射变换。
    在这里插入图片描述

4.2 仿射变换的求法

在这里插入图片描述
OpenCV仿射变换相关的函数一般涉及到warpAffine和getRotationMatrix2D这两个函数。

  • 使用OpenCV函数warpAffine来实现一些简单的重映射
  • 使用OpenCV函数getRotationMatrix2D来获得旋转矩阵

4.3 进行仿射变换:warpAffine()函数

对一个图像进行仿生变换。
函数warpAffine使用指定的矩阵对源图像进行变换:

d s t ( x , y ) = s r c ( M 11 x + M 12 y + M 13 , M 21 x + M 22 y + M 23 ) dst(x,y)=src(M11x+M12y+M13,M21x+M22y+M23) dst(x,y)=src(M11x+M12y+M13,M21x+M22y+M23)

在这里插入图片描述

  • 第一个参数:输入图像
  • 第二个参数:输出图像
  • 第三个参数:InputArray类型的M,2*3的变换矩阵
  • 第四个参数:表示输出图像的尺寸
  • 第五个参数:插值 方法的标识符

4.4 计算二维旋转变换矩阵:getRotationMatrix2D()函数

在这里插入图片描述

  • 第一个参数:源图像的旋转中心
  • 第二个参数:旋转角度。角度为正值表示向逆时针旋转
  • 第三个参数:缩放系数

该函数计算出以下矩阵:
在这里插入图片描述

4.3 案例

#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

#define WINDOW_NAME1 "原始窗口"
#define WINDOW_NAME2 "Warp后的图像"
#define WINDOW_NAME3 "Warp后和Rotate后的图像"
int main() {

	//定义两组点,代表两个三角形
	Point2f srcTriangle[3];
	Point2f dstTriangle[3];


	Mat rotMat(2, 3, CV_32FC1);
	Mat warpMat(2, 3, CV_32FC1);

	Mat srcImage, dstImage_warp, dstImage_warp_rotate;
	
	srcImage = imread("../../image/1.tif");
	if (!srcImage.data) {
		cout << "读取图片错误" << endl;
		return false;
	}

	//设置目标图像的大小和类型与源图像一样
	dstImage_warp = Mat::zeros(srcImage.size(), srcImage.type());

	//设置源图像和目标图像上的三组点以计算仿射变换
	srcTriangle[0] = Point2f(0, 0);
	srcTriangle[1] = Point2f(static_cast<float>(srcImage.cols - 1), 0);
	srcTriangle[2] = Point2f(0, static_cast<float>(srcImage.rows - 1));

	dstTriangle[0] = Point2f(static_cast<float>(srcImage.cols * 0.0),
		static_cast<float>(srcImage.rows * 0.33));
	dstTriangle[1] = Point2f(static_cast<float>(srcImage.cols * 0.65),
		static_cast<float>(srcImage.rows * 0.35));
	dstTriangle[2] = Point2f(static_cast<float>(srcImage.cols * 0.15),
		static_cast<float>(srcImage.rows * 0.6));


	//求得仿射变换
	warpMat = getAffineTransform(srcTriangle, dstTriangle);

	//对源图像应用刚刚求得的仿射变换
	warpAffine(srcImage, dstImage_warp, warpMat, dstImage_warp.size());

	//对图像进行缩放后再旋转
	Point center = Point(dstImage_warp.cols / 2, dstImage_warp.rows / 2);
	double angle = -30.0;
	double scale = 0.8;

	//通过上面旋转细节信息求得旋转矩阵
	rotMat = getRotationMatrix2D(center, angle, scale);
	warpAffine(dstImage_warp, dstImage_warp_rotate, rotMat, dstImage_warp.size());

	//显示结果
	imshow(WINDOW_NAME1, srcImage);
	imshow(WINDOW_NAME2, dstImage_warp);
	imshow(WINDOW_NAME3, dstImage_warp_rotate);
	
	waitKey();
	return 0;
}

在这里插入图片描述

5. 直方图均衡化

5.1 直方图均衡化的概念和特点

什么是图像直方图?
它是一个图像的强度分布的图形表示。
它量化了每个强度值的像素数量。
在这里插入图片描述
什么是直方图均衡化?
这是一种改善图像对比度的方法,目的是拉长强度范围(也可参见维基百科的相应条目)。
为了更清楚地说明问题,从上面的图像中,你可以看到像素似乎集中在可用强度范围的中间。直方图均衡化所做的就是把这个范围拉长。请看下面的图:绿色的圆圈表示密度不足的地方。应用均衡化后,我们得到的直方图就像中间的图。得到的图像如右图所示。
在这里插入图片描述
它是如何工作的?
均衡意味着将一个分布(给定的直方图)映射到另一个分布(一个更广泛、更均匀的强度值分布),这样强度值就会分布在整个范围内。
为了达到均衡的效果,重映射应该是累积分布函数。对于直方图H(i),其累积分布H′(i)为:
在这里插入图片描述
为了将其作为重映射函数,我们必须将H′(i)归一化,使其最大值为255(或图像强度的最大值)。从上面的例子来看,累积函数是:
在这里插入图片描述
最后,我们使用一个简单的重映射程序来获得均衡化图像的强度值:
在这里插入图片描述

5.2 实现直方图均衡化:equalizeHist()函数

在这里插入图片描述

#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main() {
	Mat srcImage, dstImage;
	srcImage = imread("../../image/1.tif");

	cvtColor(srcImage, srcImage, COLOR_BGR2GRAY);
	imshow("原始图", srcImage);

	equalizeHist(srcImage, dstImage);
	imshow("效果图", dstImage);
	waitKey();
	return 0;
}

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值