|
插件界面

void Transform_Plugin::setupUi(QWidget *parent)
{
ui = new Ui::PluginGui;
ui->setupUi(parent);
ui->borderTypeCombo->addItems(
QStringList()
<< "BORDER_CONSTANT"
<< "BORDER_REPLICATE"
<< "BORDER_REFLECT"
<< "BORDER_WRAP"
<< "BORDER_REFLECT_101");
ui->interpolationCombo->addItems(
QStringList()
<< "INTER_NEAREST"
<< "INTER_CUBIC"
<< "INTER_AREA"
<< "INTER_LANCZOS4");
connect(ui->resizeHalfRadio, SIGNAL(toggled(bool)), this, SLOT(on_resizeHalfRadio_toggled(bool)));
connect(ui->resizeDoubleRadio, SIGNAL(toggled(bool)), this, SLOT(on_resizeDoubleRadio_toggled(bool)));
connect(ui->remapRadio, SIGNAL(toggled(bool)), this, SLOT(on_remapRadio_toggled(bool)));
connect(ui->affineRadio, SIGNAL(toggled(bool)), this, SLOT(on_affineRadio_toggled(bool)));
connect(ui->perspectiveRadio, SIGNAL(toggled(bool)), this, SLOT(on_perspectiveRadio_toggled(bool)));
connect(ui->borderTypeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(on_borderTypeCombo_currentIndexChanged(int)));
connect(ui->interpolationCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(on_interpolationCombo_currentIndexChanged(int)));
}
void Transform_Plugin::processImage(const cv::Mat &inputImage, cv::Mat &outputImage)
{
using namespace cv;
if(ui->resizeHalfRadio->isChecked())
{
resize(inputImage,
outputImage,
Size(),
0.5,
0.5,
ui->interpolationCombo->currentIndex());
}
else if(ui->resizeDoubleRadio->isChecked())
{
resize(inputImage,
outputImage,
Size(),
2.0,
2.0,
ui->interpolationCombo->currentIndex());
}
else if(ui->affineRadio->isChecked())
{
Point2f triangleA[3];
Point2f triangleB[3];
triangleA[0] = Point2f(0, 0);
triangleA[1] = Point2f(inputImage.cols - 1, 0);
triangleA[2] = Point2f(0, inputImage.rows - 1);
triangleB[0] = Point2f(inputImage.cols*0.0, inputImage.rows*0.33);
triangleB[1] = Point2f(inputImage.cols*0.85, inputImage.rows*0.25);
triangleB[2] = Point2f(inputImage.cols*0.15, inputImage.rows*0.7);
Mat affineMat = getAffineTransform( triangleA, triangleB );
warpAffine( inputImage,
outputImage,
affineMat,
inputImage.size(),
ui->interpolationCombo->currentIndex(),
ui->borderTypeCombo->currentIndex());
}
else if(ui->perspectiveRadio->isChecked())
{
std::vector<Point2f> cornersA(4);
std::vector<Point2f> cornersB(4);
cornersA[0] = Point2f(0, 0);
cornersA[1] = Point2f(inputImage.cols, 0);
cornersA[2] = Point2f(inputImage.cols, inputImage.rows);
cornersA[3] = Point2f(0, inputImage.rows);
cornersB[0] = Point2f(inputImage.cols*0.25, 0);
cornersB[1] = Point2f(inputImage.cols * 0.90, 0);
cornersB[2] = Point2f(inputImage.cols, inputImage.rows);
cornersB[3] = Point2f(0, inputImage.rows * 0.80);
Mat homo = findHomography(cornersA, cornersB, RANSAC);
warpPerspective(inputImage,
outputImage,
homo,
inputImage.size(),
ui->interpolationCombo->currentIndex(),
ui->borderTypeCombo->currentIndex());
}
else if(ui->remapRadio->isChecked())
{
cvtColor(inputImage, outputImage, CV_32FC(1));;
Mat mapX, mapY;
mapX.create(inputImage.size(), CV_32FC(1));
mapY.create(inputImage.size(), CV_32FC(1));
Point2f center(inputImage.cols/2,
inputImage.rows/2);
for(int i=0; i<inputImage.rows; i++)
for(int j=0; j<inputImage.cols; j++)
{
double x = j - center.x;
double y = i - center.y;
x = x*x/500;
mapX.at<float>(i,j) = x + center.x;
mapY.at<float>(i,j) = y + center.y;
}
remap(inputImage,
outputImage,
mapX,
mapY,
INTER_LANCZOS4,
BORDER_CONSTANT);
}
}
在本节中,您将了解 OpenCV 中可用的图像转换函数。 通常,如果您查看 OpenCV 文档,则 OpenCV 中有两种图像转换类别,称为几何转换和其他(仅表示其他一切)转换。 在此解释其原因。
几何变换可以从其名称中猜出,主要处理图像的几何属性,例如图像的大小,方向,形状等。 注意,几何变换不会改变图像的内容,而只是根据几何变换类型通过在图像的像素周围移动来改变其形式和形状。 与我们在上一节开始时对图像进行过滤一样,几何变换函数还需要处理图像外部像素的外推,或者简单地说,在计算像素时对不存在的像素进行假设。 转换后的图像。 为此,当我们处理第一个示例copymakeborder_plugin时,可以使用本章前面学习的相同cv::BorderTypes枚举。
除此之外,除了所需的外推法之外,几何变换函数还需要处理像素的内插,因为变换后的图像中像素的计算位置将为float(或double)类型,而不是 integer,并且由于每个像素只能具有单一颜色,并且必须使用整数指定其位置,因此需要确定像素的值。 为了更好地理解这一点,让我们考虑一种最简单的几何变换,即调整图像大小,这是使用 OpenCV 中的resize函数完成的。 例如,您可以将图像调整为其大小的一半,完成后,计算出的图像中至少一半像素的新位置将包含非整数值。 位置(2,2)中的像素将位于调整大小后的图像中的位置(1,1),但是位置(3,2)中的像素将需要位于位置(1.5,1)中,依此类推。 OpenCV 提供了许多插值方法,这些方法在cv::InterpolationFlags枚举中定义,其中包括:
INTER_NEAREST:这是用于最近邻插值INTER_LINEAR:用于双线性插值INTER_CUBIC:这用于双三次插值INTER_AREA:这是用于像素区域关系重采样INTER_LANCZOS4:这是用于8x8附近的 Lanczos 插值
几乎所有的几何变换函数都需要提供cv::BorderType和cv::InterpolationFlags参数,以处理所需的外推和内插参数。
几何转换
现在,我们将从一些最重要的几何转换开始,然后学习色彩空间以及它们如何与一些广泛使用的非几何(或其他)转换相互转换。 因此,它们是:
resize:此函数可用于调整图像尺寸。 这是一个用法示例:
resize(inMat, outMat,
Size(),
0.5,
0.5,
INTER_LANCZOS4);
resize(inMat, outMat, Size(320,240));
-
warpAffine:此函数可用于执行仿射变换。 您需要为此函数提供适当的变换矩阵,可以使用getAffineTransform函数获得该矩阵。 getAffineTransform函数必须提供两个三角形(源三角形和变换三角形),或者换句话说,提供两组三个点。 这是一个例子: -
Point2f triangleA[3];
Point2f triangleB[3];
triangleA[0] = Point2f(0 , 0);
triangleA[1] = Point2f(1 , 0);
triangleA[2] = Point2f(0 , 1);
triangleB[0] = Point2f(0, 0.5);
triangleB[1] = Point2f(1, 0.5);
triangleB[2] = Point2f(0.5, 1);
Mat affineMat = getAffineTransform(triangleA, triangleB);
warpAffine(inputImage,
outputImage,
affineMat,
inputImage.size(),
INTER_CUBIC,
BORDER_WRAP);

您也可以使用warpAffine函数来旋转源图像。 只需使用getRotationMatrix2D函数来获取我们在前面的代码中使用的变换矩阵,然后将其与warpAffine函数一起使用。 请注意,此方法可用于执行任意角度的旋转,而不仅仅是 90 度旋转及其乘数。 这是一个示例代码,它围绕图像的中心旋转源图像-45.0度。 您也可以选择缩放输出图像。 在此示例中,我们在旋转输出图像时将其缩放为源图像大小的一半:
Point2f center = Point(inputImage.cols/2,
inputImage.rows/2);
double angle = -45.0;
double scale = 0.5;
Mat rotMat = getRotationMatrix2D(center, angle, scale);
warpAffine(inputImage,
outputImage,
rotMat,
inputImage.size(),
INTER_LINEAR,
BORDER_CONSTANT);
warpPerspective:此函数对于执行透视变换很有用。 与warpAffine函数相似,此函数还需要可以使用findHomography函数获得的变换矩阵。 findHomography函数可用于计算两组点之间的单应性变化。 这是一个示例代码,其中我们使用两组角点来计算单应性更改矩阵(或warpPerspective的变换矩阵),然后使用它执行透视更改。 在此示例中,我们还将外推颜色值(可选)设置为深灰色阴影:
std::vector<Point2f> cornersA(4);
std::vector<Point2f> cornersB(4);
cornersA[0] = Point2f(0, 0);
cornersA[1] = Point2f(inputImage.cols, 0);
cornersA[2] = Point2f(inputImage.cols, inputImage.rows);
cornersA[3] = Point2f(0, inputImage.rows);
cornersB[0] = Point2f(inputImage.cols*0.25, 0);
cornersB[1] = Point2f(inputImage.cols * 0.90, 0);
cornersB[2] = Point2f(inputImage.cols, inputImage.rows);
cornersB[3] = Point2f(0, inputImage.rows * 0.80);
Mat homo = findHomography(cornersA, cornersB);
warpPerspective(inputImage,
outputImage,
homo,
inputImage.size(),
INTER_LANCZOS4,
BORDER_CONSTANT,
Scalar(50,50,50));

Mat mapX, mapY;
mapX.create(inputImage.size(), CV_32FC(1));
mapY.create(inputImage.size(), CV_32FC(1));
for(int i=0; i<inputImage.rows; i++)
for(int j=0; j<inputImage.cols; j++)
{
mapX.at<float>(i,j) = j * 5;
mapY.at<float>(i,j) = i * 5;
}
remap(inputImage,
outputImage,
mapX,
mapY,
INTER_LANCZOS4,
BORDER_REPLICATE);
从前面的代码中可以看出,除了输入和输出图像以及内插和外推参数之外,我们还需要提供映射矩阵,一个用于X方向,另一个用于Y方向。 这是从前面的代码重新映射的结果。 它只是使图像缩小了五倍(请注意,图像尺寸在remap函数中保持不变,但内容基本上被压缩为原始尺寸的五倍)。 在下面的屏幕快照中显示了该内容:

您可以尝试通过简单地替换两个for循环中的代码,并用不同的值填充mapX和mapY矩阵来尝试多种不同的图像重映射。 以下是一些重新映射的示例:
mapX.at<float>(i,j) = j;
mapY.at<float>(i,j) = inputImage.rows-i;

mapX.at<float>(i,j) = inputImage.cols - j;
mapY.at<float>(i,j) = i;

通常最好将 OpenCV 图像坐标转换为标准坐标系(笛卡尔坐标系),并以标准坐标处理X和Y,然后再将其转换回 OpenCV 坐标系。 原因很简单,就是我们在学校或任何几何书籍或课程中学习的坐标系都使用笛卡尔坐标系。 另一个原因是它还提供负坐标,这在处理转换时具有更大的灵活性。 这是一个例子:
Mat mapX, mapY;
mapX.create(inputImage.size(), CV_32FC(1));
mapY.create(inputImage.size(), CV_32FC(1));
Point2f center(inputImage.cols/2,
inputImage.rows/2);
for(int i=0; i<inputImage.rows; i++)
for(int j=0; j<inputImage.cols; j++)
{
double x = j - center.x;
double y = i - center.y;
x = x*x/500;
y = y;
mapX.at<float>(i,j) = x + center.x;
mapY.at<float>(i,j) = y + center.y;
}
remap(inputImage,
outputImage,
mapX,
mapY,
INTER_LANCZOS4,
BORDER_CONSTANT);

remap函数的另一个(也是非常重要的)用途是校正图像中的镜头失真。 您可以使用initUndistortRectifyMap和initWideAngleProjMap函数在X和Y方向上获取所需的映射以进行失真校正,然后将它们传递给remap函数。
您可以使用以下链接获取transform_plugin的源代码副本,该代码与Computer_Vision项目兼容,并包括您在本节中学到的转换函数。 您可以使用同一插件来测试并生成本节中看到的大多数图像。 尝试扩展插件以控制更多参数,或者尝试不同的映射操作并自己尝试不同的图像
图像阈值
插件界面

void Segmentation_Plugin::setupUi(QWidget *parent)
{
ui = new Ui::PluginGui;
ui->setupUi(parent);
ui->threshAdaptiveCombo->addItems(
QStringList()
<< "ADAPTIVE_THRESH_MEAN_C"
<< "ADAPTIVE_THRESH_GAUSSIAN_C");
ui->threshTypeCombo->addItems(
QStringList()
<< "THRESH_BINARY"
<< "THRESH_BINARY_INV"
<< "THRESH_TRUNC"
<< "THRESH_TOZERO"
<< "THRESH_TOZERO_INV");
connect(ui->threshAdaptiveCheck, SIGNAL(toggled(bool)), this, SLOT(on_threshAdaptiveCheck_toggled(bool)));
connect(ui->threshAdaptiveCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(on_threshAdaptiveCombo_currentIndexChanged(int)));
connect(ui->threshTypeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(on_threshTypeCombo_currentIndexChanged(int)));
connect(ui->threshSlider, SIGNAL(valueChanged(int)), this, SLOT(on_threshSlider_valueChanged(int)));
connect(ui->threshMaxSlider, SIGNAL(valueChanged(int)), this, SLOT(on_threshMaxSlider_valueChanged(int)));
}
void Segmentation_Plugin::processImage(const cv::Mat &inputImage, cv::Mat &outputImage)
{
using namespace cv;
Mat grayScale;
cvtColor(inputImage, grayScale, COLOR_BGR2GRAY);
if(ui->threshAdaptiveCheck->isChecked())
{
adaptiveThreshold(grayScale,
grayScale,
ui->threshMaxSlider->value(),
ui->threshAdaptiveCombo->currentIndex(),
ui->threshTypeCombo->currentIndex(),
7,
0);
}
else
{
threshold(grayScale,
grayScale,
ui->threshSlider->value(),
ui->threshMaxSlider->value(),
ui->threshTypeCombo->currentIndex());
}
cvtColor(grayScale, outputImage, COLOR_GRAY2BGR);
}
在计算机视觉科学中,阈值化是图像分割的一种方法,其本身就是在强度,颜色或任何其他图像属性方面区分相关像素组的过程。 OpenCV 框架通常提供许多功能来处理图像分割。 但是,在本节中,您将了解 OpenCV 框架(以及计算机视觉)中两种最基本的(尽管已广泛使用)图像分割方法:threshold和adaptiveThreshold。 因此,在不浪费更多单词的情况下,它们是:
threshold:此函数可用于向图像应用固定级别的阈值。 尽管可以对多通道图像使用此函数,但通常在单通道(或灰度)图像上使用它来创建二进制图像,该图像具有可接受的像素和超过阈值的像素。 让我们用一个示例场景来说明这一点,您可能会遇到很多情况。 假设我们需要检测图像的最暗部分,换句话说,检测图像中的黑色。 这是我们可以使用阈值函数来仅滤除图像中像素值几乎为黑色的像素的方法:
cvtColor(inputImage, grayScale, COLOR_BGR2GRAY); threshold(grayScaleIn, grayScaleOut, 45, 255, THRESH_BINARY_INV); cvtColor(grayScale, outputImage, COLOR_GRAY2BGR);
在前面的代码中,首先,我们将输入图像转换为灰度颜色空间,然后应用阈值函数,然后将结果转换回 BGR 颜色空间。 这是生成的输出图像:

在前面的示例代码中,我们使用THRESH_BINARY_INV作为阈值类型参数; 但是,如果我们使用THRESH_BINARY,我们将得到结果的倒排版本。 threshold函数只是为我们提供了所有大于阈值参数的像素,在前面的示例中为40。

下一个是adaptiveThreshold:
adaptiveThreshold:可用于将自适应阈值应用于灰度图像。 根据传递给它的自适应方法(cv::AdaptiveThresholdTypes),此函数可用于分别自动计算每个像素的阈值。 但是,您仍然需要传递最大阈值,块大小(可以为 3、5、7 等),以及将从计算出的块平均值中减去的常数,可以是零。 这是一个例子:
cvtColor(inputImage, grayScale, COLOR_BGR2GRAY); adaptiveThreshold(grayScale, grayScale, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 7, 0); cvtColor(grayScale, outputImage, COLOR_GRAY2BGR);

离散傅立叶变换
傅立叶变换可用于从时间函数中获取基本频率。 另一方面,离散傅里叶变换或 DFT 是一种计算采样时间函数(因此是离散的)的基础频率的方法。 那是一个纯粹的数学定义,从这个意义上来说是一个很短的定义,因此,就计算机视觉和图像处理而言,您首先需要尝试将图像(灰度图像)视为点上离散点的分布。 三维空间,其中每个离散元素的X和Y是图像中的像素位置,Z是像素的强度值。 如果您能够做到这一点,那么您还可以想象存在一个可以在空间中产生这些点的函数。 考虑到这种情况,傅立叶变换是将函数转换为其基础频率的方法。 如果您仍然感到迷路,请不要担心。 如果您不熟悉该概念,则绝对应该考虑在线阅读有关傅立叶变换的数学知识,或者甚至可以咨询您的数学教授。
在数学中,傅立叶分析是一种基于输入数据的傅立叶变换来获取信息的方法。 同样,为了使这种含义更具有计算机视觉意义,可以使用图像的 DFT 来导出最初在原始图像本身中不可见的信息。 视计算机视觉应用的目标领域而定,差异很大,但是我们将看到一个示例案例,以更好地理解 DFT 的使用方式。 因此,首先,您可以在 OpenCV 中使用dft函数来获取图像的 DFT。 请注意,由于图像(灰度)是 2D 矩阵,因此dft实际上将执行 2D 离散傅立叶变换,从而产生具有复数值的频率函数。 这是在 OpenCV 中对灰度(单通道)图像执行 DFT 的方法:
- 我们需要首先获得最佳大小来计算图像的 DFT。 在大小为 2 的幂(2、4、8、16 等)的数组上执行 DFT 变换是一个更快,更有效的过程。 对大小为
2乘积的数组执行的 DFT 转换也非常有效。 因此,使用我们刚刚提到的原理的getOptimalDFTSize用于获得大于我们图像尺寸的最小尺寸,这对于执行 DFT 是最佳的。 这是完成的过程:
int optH = getOptimalDFTSize( grayImg.rows );
int optW = getOptimalDFTSize( grayImg.cols );
- 接下来,我们需要创建具有此最佳尺寸的图像,并使用零填充添加的宽度和高度中的像素。 因此,我们可以使用本章前面了解的
copyMakeBorder函数:
Mat padded;
copyMakeBorder(grayImg,
padded,
0,
optH - grayImg.rows,
0,
optW - grayImg.cols,
BORDER_CONSTANT,
Scalar::all(0));
- 现在,我们在
padded中拥有了最佳尺寸的图像。 我们现在需要做的是形成一个适合于馈入dft函数的两通道Mat类。 这可以使用合并函数来完成。 请注意,由于dft需要浮点Mat类,因此我们还需要将最佳尺寸的图像转换为带有浮点元素的Mat类,如下所示:
Mat channels[] = {Mat_<float>(padded),
Mat::zeros(padded.size(),
CV_32F)};
Mat complex;
merge(channels, 2, complex);
-
一切准备就绪即可执行离散傅立叶变换,因此我们将其简称为此处所示。 结果也存储在complex中,这将是一个复杂值Mat类: dft(complex, complex);
- 现在,我们需要将复杂的结果分为真实和复杂的部分。 为此,我们可以再次使用
channels数组,如下所示:
split(complex, channels);
- 现在,我们需要使用
magnitude函数将复杂结果转换为其大小; 经过更多的转换之后,这将是适合于显示目的的结果。 由于channels现在包含复杂结果的两个通道,因此我们可以在magnitude函数中使用它,如下所示:
Mat mag;
magnitude(channels[0], channels[1], mag);
-
magnitude函数的结果(如果尝试查看元素)将非常大,以至于无法使用灰度图像的可能比例进行可视化。 因此,我们将使用以下代码行将其转换为更小的对数刻度: mag += Scalar::all(1);
log(mag, mag);
- 由于我们使用最佳大小计算了 DFT,因此如果行或列的数量为奇数,我们现在需要裁剪结果。 使用以下代码片段可以轻松完成此操作。 请注意,使用
-2的按位and操作用于删除正整数中的最后一位,并使其成为偶数,或者基本上是创建带有额外像素的padded图像时所做的操作的反面:
mag = mag(Rect(
0,
0,
mag.cols & -2,
mag.rows & -2));
- 由于结果是一个频谱,显示了由 DFT 获得的频率函数所产生的波,因此我们应将结果的原点移至其中心,该中心当前位于左上角。 我们可以使用以下代码为结果的四分之四创建四个 ROI,然后将结果左上角的四分之一与右下角的四分之一交换,也将结果右上角的四分之一与左下角的四分之一交换:
int cx = mag.cols/2;
int cy = mag.rows/2;
Mat q0(mag, Rect(0, 0, cx, cy));
Mat q1(mag, Rect(cx, 0, cx, cy));
Mat q2(mag, Rect(0, cy, cx, cy));
Mat q3(mag, Rect(cx, cy, cx, cy));
Mat tmp;
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
- 除非我们使用
normalize函数将结果缩放到正确的灰度范围(0至255),否则尝试将结果可视化仍然是不可能的,如下所示:
normalize(mag, mag, 0, 255, NORMAL_MINMAX);
- 使用 OpenCV 中的
imshow函数,我们已经可以查看结果了,但是为了能够在 Qt 小部件中查看结果,我们需要将其转换为正确的深度(8 位)和多个通道,因此我们需要以下内容作为最后一步:
Mat_<uchar> mag8bit(mag);
cvtColor(mag8bit, outputImage, COLOR_GRAY2BGR);
现在,您可以尝试在我们的测试图像上运行它。 结果将如下所示:

您在结果中看到的结果应解释为从上方直接观看的波,其中每个像素的亮度实际上是其高度的表示。 尝试在不同种类的每个图像上运行相同的过程,以查看结果如何变化。 除了通过外观检查 DFT 结果(取决于用例)之外,DFT 的一个非常特殊的用例(我们将留给您自己尝试)是在掩盖 DFT 结果的一部分后执行反向 DFT, 以获取原始图像。 此过程可以通过多种方式更改原始图像,具体取决于已过滤 DFT 结果的一部分。 这个主题在很大程度上取决于原始图像的内容,并且与 DFT 的数学特性有着深厚的联系,但是绝对值得研究和试验。 总之,您可以通过调用相同的dft函数并将附加的DCT_INVERSE参数传递给它来执行逆 DFT。 显然,这次,输入应该是图像的计算出的 DFT,输出将是图像本身。
OpenCV 中的绘图
通常,当主题是 OpenCV 和计算机视觉时,就不能忽略在图像上绘制文本和形状。 由于无数原因,您将需要在输出图像上绘制(输出)一些文本或形状。 例如,您可能想编写一个在其上打印图像日期的程序。 或者,您可能需要在执行面部检测后在图像中的面部周围绘制一个正方形。 即使 Qt 框架也提供了处理这些任务的强大功能,也可以使用 OpenCV 本身来绘制图像。 在本节中,您将学习到使用 OpenCV 绘图函数的方法,它们令人惊讶的非常容易使用,以及示例代码和输出结果。
可以理解,OpenCV 中的绘图函数接受输入和输出图像,以及一些大多数参数共有的参数。 以下是 OpenCV 中提到的图形函数的常用参数,以及它们的含义和可能的值:
color:此参数只是在图像上绘制的对象的颜色。 它可以使用标量创建,并且必须采用 BGR 格式(用于彩色图像),因为它是大多数 OpenCV 函数的默认颜色格式。thickness:此参数默认设置为1,是在图像上绘制的对象轮廓的粗细。 此参数以像素为单位指定。lineType:这可以是cv::LineTypes枚举中的条目之一,它决定在图像上绘制的对象轮廓的细节。 如下图所示,LINE_AA(抗锯齿)较平滑,但绘制速度也比LINE_4和LINE_8(默认为lineType)慢。 下图描述了cv::LineTypes之间的区别:shift:仅在提供给绘图函数的点和位置包括小数位的情况下使用此参数。 在这种情况下,首先使用以下转换函数根据移位参数对每个点的值进行移位。 对于标准整数点值,移位值将为零,这也使以下转换对结果没有影响:
Point(X , Y) = Point( X * pow(2,-shift), Y * pow(2,-shift) )
现在,让我们从实际的绘图函数开始:
-
line:可以通过获取线条的起点和终点来在图像上画一条线条。 以下示例代码在图像上绘制了一个X标记(两条线连接该图像的角),其厚度为3像素,并带有红色: -
cv::line(img,
Point(0,0),
Point(img.cols-1,img.rows-1),
Scalar(0,0,255),
3,
LINE_AA);
cv::line(img,
Point(img.cols-1,0),
Point(0, img.rows-1),
Scalar(0,0,255),
3,
LINE_AA);
-
arrowedLine:用于绘制箭头线。 箭头的方向由终点(或第二个点)决定,否则,此函数的用法与line相同。 这是一个示例代码,用于从顶部到图像中心绘制一条箭头线: -
cv::arrowedLine(img,
Point(img.cols/2, 0),
Point(img.cols/2, img.rows/3),
Scalar(255,255,255),
5,
LINE_AA);
-
rectangle:可用于在图像上绘制矩形。 您可以向其传递一个矩形(Rect类)或两个点(Point类),第一个点对应于矩形的左上角,第二个点对应于矩形的右下角。 以下是在图像中心绘制的矩形示例:
cv::rectangle(img,
Point(img.cols/4, img.rows/4),
Point(img.cols/4*3, img.rows/4*3),
Scalar(255,0,0),
10,
LINE_AA);
putText:此函数可用于在图像上绘制(或书写或放置)文本。 除了 OpenCV 绘图函数中的常规绘图参数外,您还需要为该函数提供需要在图像上绘制的文本以及字体和比例尺参数。 字体可以是cv::HersheyFonts枚举中的条目之一,而尺度是与字体有关的字体缩放。 以下代码块的示例可用于在图像中写入Computer Vision:
cv::putText(img,
"建波的女朋友",
Point(0, img.rows/2),
FONT_HERSHEY_PLAIN,
2,
Scalar(255,255,255),
2,
LINE_AA);
模板匹配
OpenCV 框架提供了许多不同的方法来进行对象检测,跟踪和计数。 模板匹配是 OpenCV 中对象检测的最基本方法之一,但是,如果正确使用它并与良好的阈值结合使用,它可以用于有效检测和计数图像中的对象。 通过在 OpenCV 中使用一个称为matchTemplate函数的函数来完成此操作。
matchTemplate函数将图像作为输入参数。 考虑将要搜索的图像作为我们感兴趣的对象(或者更好的是可能包含模板的场景)。它也将模板作为第二个参数。 该模板也是一幅图像,但是它是将在第一个图像参数中搜索的模板。 此函数所需的另一个参数(也是最重要的参数和决定模板匹配方法的一个参数)是method参数,它可以是cv::TemplateMatchModes枚举中的条目之一:
TM_SQDIFFTM_SQDIFF_NORMEDTM_CCORRTM_CCORR_NORMEDTM_CCOEFFTM_CCOEFF_NORMED
如果您有兴趣,可以访问matchTemplate文档页面,以了解上述每种方法的数学计算,但是,实际上,您可以通过了解matchTemplate的一般工作原理,来了解每种方法的执行方式。
matchTemplate函数使用method参数中指定的方法,将大小为WxH的模板滑动到大小为QxS的图像上,并将模板与图像的所有重叠部分进行比较,然后存储 result Mat中的比较。 显然,图像大小(QxS)必须大于模板大小(WxH)。 重要的是要注意,所得的Mat大小实际上是Q-WxS-H,即图像高度和宽度减去模板高度和宽度。 这是由于以下事实:模板的滑动仅发生在源图像上,甚至不发生在其外部的单个像素上。
如果使用名称中带有_NORMED的方法之一进行模板匹配,则在模板匹配函数之后无需进行标准化,因为结果将在0和1之间; 否则,我们将需要使用normalize函数对结果进行归一化。 将结果归一化后,可以使用minMaxLoc函数在结果图像中定位全局最小值(图像中的最暗点)和全局最大值(图像中的最亮点)。 请记住,result Mat类包含模板和图像重叠部分之间的比较结果。 这意味着,根据所使用的模板匹配方法,result Mat类中的全局最小值或全局最大值位置实际上是最佳模板匹配。 因此,是我们检测结果的最佳候选者。 假设我们想将左侧屏幕上的图像与以下屏幕截图右侧屏幕上的图像匹配:
|