九、视频分析
除了本书到目前为止所看到的所有内容之外,计算机视觉的故事还有另一面,它涉及视频,摄像机以及输入帧的实时处理。 它是最受欢迎的计算机视觉主题之一,并且有充分的理由,因为它可以为有生命的机器或设备供电,这些机器或设备可以监视周围环境中是否存在感兴趣的对象,运动,图案,颜色等。 我们已经了解的所有算法和类,尤其是在第 6 章,“OpenCV 中的图像处理”和第 7 章,“特征和描述符”只能用于单个图像,因此,由于相同的原因,它们可以以完全相同的方式轻松地应用于单个视频帧。 我们只需要确保将单个帧正确地读取(例如,使用cv::VideoCapture类)到cv::Mat类实例中,然后作为单个图像传递到这些函数中即可。 但是,在处理视频以及视频时,我们指的是来自网络的视频,摄像机,视频文件等,有时我们需要通过处理特定时间段内的连续视频帧获得的结果。 这意味着结果不仅取决于当前从视频中获取的图像,还取决于之前获取的帧。
在本章中,我们将学习 OpenCV 中一些最重要的算法和类,这些算法和类可用于连续帧。 因此,视频。 我们将从学习这些算法使用的一些概念开始,例如直方图和反投影图像,然后通过使用示例并获得动手经验来更深入地研究每种算法。 我们将学习如何使用臭名昭著的 MeanShift 和 CamShift 算法进行实时对象跟踪,并且将继续进行视频中的运动分析。 我们将在本章中学到的大多数内容都与 OpenCV 框架中的视频分析模块(简称为video)有关,但我们还将确保遍历该模块所需的其他模块中的任何相关主题。 为了有效地遵循本章中的主题,尤其是直方图和反投影图像,这对于理解本章中涉及的视频分析主题至关重要。 背景/前景检测也是我们将在本章中学习的最重要主题之一。 通过结合使用这些方法,您将能够有效地处理视频以检测和分析运动,基于视频的颜色隔离视频帧中的零件或片段,或者使用现有的 OpenCV 算法以一种或另一种方式处理它们以进行图像处理。
同样,基于我们从第 8 章,“多线程”中学到的知识,我们将使用线程来实现在本章中学习的算法。 这些线程将独立于任何项目类型。 无论它是独立的应用,库,插件等,您都可以简单地包含和使用它们。
本章将涵盖以下主题:
- 直方图以及如何提取,使用或可视化它们
- 图像反投影
- MeanShift 和 CamShift 算法
- 背景/前景检测和运动分析
了解直方图
如本章介绍部分所述,计算机视觉中的一些概念在处理视频处理以及我们将在本章稍后讨论的算法时特别重要。 这些概念之一是直方图。 由于了解直方图对于理解大多数视频分析主题至关重要,因此在继续下一个主题之前,我们将在本节中详细了解它们。 直方图通常被称为表示数据分布的一种方式。 这是一个非常简单和完整的描述,但让我们也描述它在计算机视觉方面的含义。 在计算机视觉中,直方图是图像中像素值分布的图形表示。 例如,在灰度图像中,直方图将是表示包含灰度中每个可能强度(0 到 255 之间的值)的像素数的图表。 在 RGB 彩色图像中,它将是三个图形,每个图形代表包含所有可能的红色,绿色或蓝色强度的像素数。 请注意,像素值不一定表示颜色或强度值。 例如,在转换为 HSV 色彩空间的彩色图像中,其直方图将包含色相,饱和度和值数据。
OpenCV 中的直方图是使用calcHist函数计算的,并存储在Mat类中,因为它们可以存储为数字数组,可能具有多个通道。 calcHist函数需要以下参数来计算直方图:
images或输入图像是我们要为其计算直方图的图像。 它应该是cv::Mat类的数组。nimages是第一个参数中的图像数量。 请注意,您还可以为第一个参数传递cv::Mat类的std::vector,在这种情况下,您可以省略此参数。channels是一个数组,其中包含将用于计算直方图的通道的索引号。mask可用于遮盖图像,以便仅使用部分输入图像来计算直方图。 如果不需要遮罩,则可以传递一个空的Mat类,否则,我们需要提供一个单通道Mat类,对于应遮罩的所有像素,该类包含零,对于计算直方图时应考虑的所有像素,包含非零值。hist是输出直方图。 这应该是Mat类,并且在函数返回时将用计算出的直方图填充。dims是直方图的维数。 它可以包含一个介于 1 到 32 之间的值(在当前的 OpenCV 3 实现中)。 我们需要根据用于计算直方图的通道数进行设置。histSize是一个数组,其中包含每个维度中直方图的大小,即所谓的箱子大小。 直方图中的合并是指在计算直方图时将相似值视为相同值。 我们将在后面的示例中看到它的确切含义,但现在,我们只需提及直方图的大小与其箱数相同的事实就足够了。ranges是一个数组数组,其中包含每个通道的值范围。 简而言之,它应该是一个数组,其中包含一对值,用于通道的最小和最大可能值。uniform是一个布尔值标志,它决定直方图是否应该统一。accumulate是布尔值标志,它决定在计算直方图之前是否应清除该直方图。 如果我们要更新先前计算的直方图,这可能非常有用。
现在,让我们来看几个示例如何使用此函数。 首先,为了方便使用,我们将计算灰度图像的直方图:
int bins = 256;
int channels[] = {0}; // the first and the only channel
int histSize[] = { bins }; // number of bins
float rangeGray[] = {0,255}; // range of grayscale
const float* ranges[] = { rangeGray };
Mat histogram;
calcHist(&grayImg,
1, // number of images
channels,
Mat(), // no masks, an empty Mat
histogram,
1, // dimensionality
histSize,
ranges,
true, // uniform
false // not accumulate
);
在前面的代码中,grayImg是Mat类中的灰度图像。 图像数量仅为一个,并且channels索引数组参数仅包含一个值(对于第一个通道为零),因为我们的输入图像是单通道和灰度。 dimensionality也是一个,其余参数与它们的默认值相同(如果省略)。
执行完前面的代码后,我们将在histogram变量内获取生成的灰度图像直方图。 它是具有256行的单通道单列Mat类,每行代表像素值与行号相同的像素数。 我们可以使用以下代码将Mat类中存储的每个值绘制为图形,并且输出将以条形图的形式显示我们的直方图:
double maxVal = 0;
minMaxLoc(histogram,
Q_NULLPTR, // don't need min
&maxVal,
Q_NULLPTR, // don't need index min
Q_NULLPTR // don't need index max
);
outputImage.create(640, // any image width
360, // any image height
CV_8UC(3));
outputImage = Scalar::all(128); // empty grayish image
Point p1(0,0), p2(0,outputImage.rows-1);
for(int i=0; i<bins; i++)
{
float value = histogram.at<float>(i,0);
value = maxVal - value; // invert
value = value / maxVal * outputImage.rows; // scale
p1.y = value;
p2.x = float(i+1) * float(outputImage.cols) / float(bins);
rectangle(outputImage,
p1,
p2,
Scalar::all(0),
CV_FILLED);
p1.x = p2.x;
}
这段代码起初可能看起来有些棘手,但实际上它很简单,它基于以下事实:直方图中的每个值都需要绘制为矩形。 对于每个矩形,使用value变量和图像宽度除以箱数(即histSize)来计算左上角的点。 在示例代码中,我们简单地将最大可能值分配给了箱子(即 256),这导致了直方图的高分辨率可视化,因为条形图图中的每个条形图都会代表灰度级的一个像素强度 。
请注意,从这个意义上说,分辨率不是指图像的分辨率或质量,而是指构成条形图的最小块数的分辨率。
我们还假定输出可视化高度将与直方图的峰值(最高点)相同。 如果我们在下图左侧所示的灰度图像上运行这些代码,则所得的直方图将是右侧所示的直方图:
让我们解释输出直方图的可视化,并进一步说明我们在代码中使用的参数通常具有什么作用。 首先,每个条形从左到右是指具有特定灰度强度值的像素数。 最左边的条(非常低)指的是绝对黑色(强度值为零),最右边的条指的是绝对白色(255),中间的所有条指的是不同的灰色阴影。 注意最右边的小跳。 这实际上是由于输入图像的最亮部分(左上角)而形成的。 每个条形的高度除以最大条形值,然后缩放以适合图像高度。
我们还要看看bins变量的作用。 降低bins将导致强度分组在一起,从而导致较低分辨率的直方图被计算和可视化。 如果运行bins值为20的相同代码,则将得到以下直方图:
如果我们需要一个简单的图形而不是条形图视图,则可以在上一个代码末尾的绘图循环中使用以下代码:
Point p1(0,0), p2(0,0);
for(int i=0; i<bins; i++)
{
float value = histogram.at<float>(i,0);
value = maxVal - value; // invert
value = value / maxVal * outputImage.rows; // scale
line(outputImage,
p1,
Point(p1.x,value),
Scalar(0,0,0));
p1.y = p2.y = value;
p2.x = float(i+1) * float(outputImage.cols) / float(bins);
line(outputImage,
p1, p2,
Scalar(0,0,0));
p1.x = p2.x;
}
如果再次使用256的bins值,将导致以下输出:
同样,我们可以计算和可视化彩色(RGB)图像的直方图。 我们只需要为三个单独的通道修改相同的代码即可。 为了做到这一点,首先我们需要将输入图像划分为其基础通道,然后为每个图像计算直方图,就好像它是单通道图像一样。 这是如何拆分图像以获取三个Mat类,每个类代表一个通道:
vector<Mat> planes;
split(inputImage, planes);
现在,您可以在循环中使用planes[i]或类似的东西,并将每个通道视为图像,然后使用前面的代码示例来计算和可视化其直方图。 如果我们使用其自己的颜色可视化每个直方图,结果将是类似的结果(生成此直方图的图像是我们在整本书中使用的上一个示例的彩色图像):
同样,结果的内容几乎可以像以前一样解释。 前面的直方图图像显示了颜色如何分布在 RGB 图像的不同通道中。 但是,除了获取像素值分布的信息以外,我们如何真正使用直方图? 下一节将介绍直方图可用于修改图像的方式。
了解图像反投影
除了直方图中的视觉信息外,它还有更重要的用途。 这称为直方图的反投影,可用于使用其直方图来修改图像,或者正如我们将在本章稍后看到的那样,在图像中定位感兴趣的对象。 让我们进一步分解。 正如我们在上一节中了解到的,直方图是图像上像素数据的分布,因此如果我们以某种方式修改所得的直方图,然后将其重新应用于源图像(就好像它是像素值的查找表) ,则生成的图像将被视为反投影图像。 重要的是要注意,反投影图像始终是单通道图像,其中每个像素的值都是从直方图中的相应像素中提取的。
让我们将其视为另一个示例。 首先,这是在 OpenCV 中如何计算反投影:
calcBackProject(&image,
1,
channels,
histogram,
backprojection,
ranges);
calcBackProject函数的使用方式与calcHist函数非常相似。 您只需要确保传递一个附加的Mat类实例即可获得图像的反投影。 由于在背投图像中,像素值是从直方图中获取的,因此它们很容易超出标准灰度范围,该范围在0和255(含)之间。 这就是为什么我们需要在计算反投影之前相应地标准化直方图的结果。 方法如下:
normalize(histogram,
histogram,
0,
255,
NORM_MINMAX);
normalize函数将缩放直方图中的所有值以适合提供的最小值和最大值,分别为0和255。 只是重复一次,必须在calcBackProject之前调用此函数,否则,您将在反投影图像中产生溢出的数据,如果您尝试使用[[ imshow函数。
如果我们在查看反投影图像时未对生成它的直方图进行任何修改,那么在我们的示例情况下,我们将获得以下输出图像:
先前图像中每个像素的强度与包含该特定值的图像中像素的数量有关。 例如,请注意反投影图像的右上最暗部分。 与较亮的区域相比,该区域包含的像素值很少。 换句话说,明亮的区域包含的像素值在图像中以及图像的各个区域中都存在得多。 再说一遍,在处理图像和视频帧时如何使用呢?
本质上,反投影图像可用于为计算机视觉操作获取有用的遮罩图像。 到目前为止,我们还没有在 OpenCV 函数中真正使用掩码参数(并且它们存在于大多数函数中)。 让我们从使用前面的反投影图像的示例开始。 我们可以使用简单的阈值修改直方图,以获得用于过滤掉不需要的图像部分的遮罩。 假设我们想要一个可用于获取包含最暗值(例如,从0到39像素值)的像素的遮罩。 为此,首先我们可以通过将第一个40元素(只是最暗值的阈值,可以将其设置为任何其他值或范围)设置为灰度范围内的最大可能值来修改直方图(255),然后将其余的取到最小可能值(零),然后计算反投影图像。 这是一个例子:
calcHist(&grayImg,
1, // number of images
channels,
Mat(), // no masks, an empty Mat
histogram,
1, // dimensionality
histSize,
ranges);
for(int i=0; i<histogram.rows; i++)
{
if(i < 40) // threshold
histogram.at<float>(i,0) = 255;
else
histogram.at<float>(i,0) = 0;
}
Mat backprojection;
calcBackProject(&grayImg,
1,
channels,
histogram,
backprojection,
ranges);
通过运行前面的示例代码,我们将在backprojection变量内获得以下输出图像。 实际上,这是一种阈值技术,可为使用 OpenCV 的任何计算机视觉处理获得合适的遮罩,以隔离图像中最暗的区域。 我们使用此示例代码获得的遮罩可以传递到任何接受遮罩的 OpenCV 函数中,这些遮罩用于对与遮罩中白色位置对应的像素执行操作,而忽略与黑位置对应的像素:
类似于我们刚刚学习的阈值化方法的另一种技术可以用于遮盖图像中包含特定颜色的区域,因此可以将其仅用于处理(例如修改颜色)图像的某些部分,甚至跟踪图像的某些部分。 具有特定颜色的对象,我们将在本章稍后学习。 但是在此之前,让我们首先了解 HSV 颜色空间的直方图(使用色相通道)以及如何隔离具有特定颜色的图像部分。 让我们通过一个例子来进行研究。 假设您需要查找图像中包含特定颜色的部分,例如下图中的红玫瑰:
您不能根据阈值简单地滤除红色通道(在 RGB 图像中),因为它可能太亮或太暗,但仍然可以是红色的其他阴影。 另外,您可能需要考虑与红色过于相似的颜色,以确保您尽可能准确地获得玫瑰。 使用色调,饱和度,值(HSV)颜色空间,其中颜色保留在单个通道(色相或 H 通道)中,可以最好地处理这种情况以及需要处理颜色的类似情况。 这可以通过使用 OpenCV 进行示例实验来证明。 只需尝试在新应用中运行以下代码段即可。 它可以是控制台应用或小部件,没关系:
Mat image(25, 180, CV_8UC3);
for(int i=0; i<image.rows; i++)
{
for(int j=0; j<image.cols; j++)
{
image.at<Vec3b>(i,j)[0] = j;
image.at<Vec3b>(i,j)[1] = 255;
image.at<Vec3b>(i,j)[2] = 255;
}
}
cvtColor(image,image,CV_HSV2BGR);
imshow("Hue", image);
请注意,我们仅更改了三通道图像中的第一个通道,其值从0更改为179。 这将导致以下输出:
如前所述,其原因是这样的事实,即色调是造成每个像素颜色的原因。 另一方面,饱和度和值通道可用于获得相同颜色的较亮(使用饱和度通道)和较暗(使用值通道)变化。 请注意,在 HSV 颜色空间中,与 RGB 不同,色相是介于 0 到 360 之间的值。这是因为色相被建模为圆形,因此,每当其值溢出时,颜色就会回到起点。 如果查看上一张图像的开始和结尾,这两个都是红色,则很明显,因此 0 或 360 附近的色相值必须是带红色的颜色。
但是,在 OpenCV 中,色相通常会除以 2 以容纳 8 位(除非我们为像素数据使用 16 位或更多位),因此色相的值可以在0和180之间变化。 如果返回上一个代码示例,您会注意到在Mat类的列上,色相值从0设置为180,这将导致我们的色谱输出图像。
现在,让我们使用我们刚刚学到的东西创建一个颜色直方图,并使用它来获取背投图像以隔离我们的红玫瑰。 为了达到这个目的,我们甚至可以使用一段简单的代码将其变成蓝玫瑰,但是正如我们将在本章稍后学习的那样,该方法与 MeanShift 和 CamShift 算法结合使用来跟踪对象, 有特定的颜色。 我们的直方图将基于图像的 HSV 版本中的颜色分布或色相通道。 因此,我们需要首先使用以下代码将其转换为 HSV 颜色空间:
Mat hsvImg;
cvtColor(inputImage, hsvImg, CV_BGR2HSV);
然后,使用与上一个示例完全相同的方法来计算直方图。 这次(在可视化方面)的主要区别在于,由于直方图是颜色分布,因此直方图还需要显示每个垃圾箱的颜色,否则输出将难以解释。 为了获得正确的输出,这次我们将使用 HSV 到 BGR 的转换来创建一个包含所有箱子的颜色值的缓冲区,然后相应地填充输出条形图中的每个条形。 这是用于在计算出色相通道直方图(或换句话说就是颜色分布图)之后将其正确可视化的源代码:
Mat colors(1, bins, CV_8UC3);
for(int i=0; i<bins; i++)
{
colors.at<Vec3b>(i) =
Vec3b(saturate_cast<uchar>(
(i+1)*180.0/bins), 255, 255);
}
cvtColor(colors, colors, COLOR_HSV2BGR);
Point p1(0,0), p2(0,outputImage.rows-1);
for(int i=0; i<ui->binsSpin->value(); i++)
{
float value = histogram.at<float>(i,0);
value = maxVal - value; // invert
value = value / maxVal * outputImage.rows; // scale
p1.y = value;
p2.x = float(i+1) * float(outputImage.cols) / float(bins);
rectangle(outputImage,
p1,
p2,
Scalar(colors.at<Vec3b>(i)),
CV_FILLED);
p1.x = p2.x;
}
正如我们在前面的代码示例中看到的,maxVal是使用minMaxLoc函数从直方图数据中计算出来的。 bins只是箱子的数量(或直方图大小),在这种情况下不能高于180; 众所周知,色相只能在0和179之间变化。 其余部分几乎相同,除了设置图形中每个条形的填充颜色值。 如果我们在示例玫瑰图像中使用最大箱子大小(即180)执行上述代码,则将获得以下输出:
在此直方图中,基本上所有具有色相精度(八位)的可能颜色都在直方图中考虑,但是我们可以通过减小箱子大小来进一步简化此操作。 24的箱子大小足够小,可以简化并将相似的颜色分组在一起,同时提供足够的精度。 如果将箱子大小更改为24,则会得到以下输出:
通过查看直方图,可以明显看出直方图中24条的前两个(左起)和后两个条是最带红色的颜色。 就像以前一样,我们将简单地限制其他所有内容。 这是如何做:
for(int i=0; i<histogram.rows; i++)
{
if((i==0) || (i==22) || (i==23)) // filter
histogram.at<float>(i,0) = 255;
else
histogram.at<float>(i,0) = 0;
}
一个好的实践案例是创建一个用户界面,该界面允许选择直方图中的箱子并将其过滤掉。 您可以根据自己到目前为止所学的知识,通过使用QGraphicsScene和QGraphicsRectItem绘制条形图和直方图来进行此操作。 然后,您可以启用项目选择,并确保在按下Delete按钮时,条被删除并因此被滤除。
在简单阈值之后,我们可以使用以下代码来计算反投影。 请注意,由于我们的直方图是一维直方图,因此仅当输入图像也是单通道时,才可以使用反向投影重新应用它。 这就是为什么我们首先需要从图像中提取色相通道的原因。 mixChannels函数可用于将通道从一个Mat类复制到另一个。 因此,我们可以使用相同的函数将色相通道从 HSV 图像复制到单通道Mat类中。 仅需要为mixChannels函数提供源和目标Mat类(仅具有相同的深度,不一定是通道),源和目标图像的数量以及一对整数(在以下代码的fromto数组中),用于确定源通道索引和目标通道索引:
Mat hue;
int fromto[] = {0, 0};
hue.create(hsvImg.size(), hsvImg.depth());
mixChannels(&hsvImg, 1, &hue, 1, fromto, 1);
Mat backprojection;
calcBackProject(&hue,
1,
channels,
histogram,
backprojection,
ranges);
在将其转换为 RGB 颜色空间后,使用imshow或 Qt Widget 在输出中直接显示背投图像,您将在玫瑰图像示例中看到我们的红色微调完美遮罩:
现在,如果我们将色相通道中的值偏移正确的数量,则可以从红色玫瑰中得到蓝色玫瑰; 不仅是相同的静态蓝色,而且在所有相应像素中具有正确的阴影和亮度值。 如果返回本章前面创建的色谱图像输出,您会注意到红色,绿色,蓝色和红色再次与色相值0,120,240和360完全一致。 当然,再次,如果我们考虑除以二(因为360不能适合一个字节,但是180可以适合),它们实际上是0,60,120和180。 这意味着,如果我们要在色调通道中移动红色以获得蓝色,则必须将其偏移120,并且类似地要转换以获得其他颜色。 因此,我们可以使用类似的方法正确地改变颜色,并且只能在之前的背投图像突出显示的像素中进行。 请注意,我们还需要注意溢出问题,因为最高的色相值应为179,且不能大于:
for(int i=0; i<hsvImg.rows; i++)
{
for(int j=0; j<hsvImg.cols; j++)
{
if(backprojection.at<uchar>(i, j))
{
if(hsvImg.at<Vec3b>(i,j)[0] < 60)
hsvImg.at<Vec3b>(i,j)[0] += 120;
else if(hsvImg.at<Vec3b>(i,j)[0] > 120)
hsvImg.at<Vec3b>(i,j)[0] -= 60;
}
}
}
Mat imgHueShift;
cvtColor(hsvImg, imgHueShift, CV_HSV2BGR);
通过执行前面的代码,我们将获得下面的结果图像,它是从红色像素变为蓝色的图像转换回的 RGB 图像:
对于不同的柱状图大小,请尝试相同的操作。 另外,作为练习,您可以尝试构建适当的 GUI 以进行色移。 您甚至可以尝试编写一个程序,该程序可以将图像中具有特定颜色(精确的颜色直方图)的对象更改为其他颜色。 电影和照片编辑程序中广泛使用了一种非常相似的技术来改变图像或连续视频帧中特定区域的颜色(色相)。
直方图比较
使用calcHist函数计算出的两个直方图,或者从磁盘加载并填充到Mat类中的直方图,或者使用任何方法按字面意义创建的两个直方图,都可以相互比较以找出它们之间的距离或差异(或差异), 通过使用compareHist方法。 请注意,只要直方图的Mat结构与我们之前看到的一致(即列数,深度和通道),就可以实现。
compareHist函数采用存储在Mat类中的两个直方图和comparison方法,它们可以是以下常量之一:
HISTCMP_CORRELHISTCMP_CHISQRHISTCMP_INTERSECTHISTCMP_BHATTACHARYYAHISTCMP_HELLINGERHISTCMP_CHISQR_ALTHISTCMP_KL_DIV
请注意,compareHist函数的返回值以及应如何解释完全取决于comparison方法,它们的变化很大,因此请务必查看 OpenCV 文档页面以获取详细的列表。 每种方法中使用的基础比较方程。 这是示例代码,可使用所有现有方法来计算两个图像(或两个视频帧)之间的差异:
Mat img1 = imread("d:/dev/Packt/testbw1.jpg", IMREAD_GRAYSCALE);
Mat img2 = imread("d:/dev/Packt/testbw2.jpg", IMREAD_GRAYSCALE);
float range[] = {0, 255};
const float* ranges[] = {range};
int bins[] = {100};
Mat hist1, hist2;
calcHist(&img1, 1, 0, Mat(), hist1, 1, bins, ranges);
calcHist(&img2, 1, 0, Mat(), hist2, 1, bins, ranges);
qDebug() << compareHist(hist1, hist2, HISTCMP_CORREL);
qDebug() << compareHist(hist1, hist2, HISTCMP_CHISQR);
qDebug() << compareHist(hist1, hist2, HISTCMP_INTERSECT);
// Same as HISTCMP_HELLINGER
qDebug() << compareHist(hist1, hist2, HISTCMP_BHATTACHARYYA);
qDebug() << compareHist(hist1, hist2, HISTCMP_CHISQR_ALT);
qDebug() << compareHist(hist1, hist2, HISTCMP_KL_DIV);
我们可以在以下两个图像上尝试前面的代码:
比较的结果可以在 Qt Creator 输出中查看,如下所示:
-0.296291
1.07533e+08
19811
0.846377
878302
834340
通常,通常使用直方图差异来比较图像。 还可以在视频帧中使用类似的技术来检测与场景或场景中存在的对象的差异。 因此,应该存在一个预先准备好的直方图,然后将其与每个传入视频帧的直方图进行比较。
直方图均衡
图像的直方图可用于调整图像的亮度和对比度。 OpenCV 提供了一个称为equalizeHist的函数,该函数在内部计算给定图像的直方图,对直方图进行归一化,计算直方图的积分(所有仓位的总和),然后使用更新后的直方图作为查找表来更新输入图像的像素,导致输入图像中的亮度和对比度标准化。 使用此函数的方法如下:
equalizeHist(image, equalizedImg);
如果您在亮度不适当或收缩的图像上尝试使用此函数,则将在亮度和对比度方面将它们自动调整到视觉上更好的水平。 此过程称为直方图均衡。 以下示例显示两个亮度级别太低或太高的图像及其直方图,它们显示相应的像素值分布。 左侧的图像是使用equalizeHist函数生成的,对于左侧的两个图像,它看起来或多或少都是相同的。 注意输出图像的直方图中的变化,这反过来会导致图像更具视觉吸引力:
大多数数码相机使用类似的技术来根据像素在整个图像中的分布量来调整像素的暗度和亮度。 您也可以在任何常见的智能手机上尝试此操作。 只需将相机对准明亮的区域,智能手机上的软件就会开始降低亮度,反之亦然。
MeanShift 和 CamShift
到目前为止,我们在本章中学到的知识除了已经看到的用例之外,还旨在为我们正确使用 MeanShift 和 CamShift 算法做准备,因为它们从直方图和反投影图像中受益匪浅。 但是,MeanShift 和 CAMShift 算法是什么?
让我们从 MeanShift 开始,然后继续进行 CamShift,它基本上是同一算法的增强版本。 因此,MeanShift 的一个非常实用的定义(如当前 OpenCV 文档中所述)如下:
在反投影图像上找到对象
这是对 MeanShift 算法的一个非常简单但实用的定义,并且在使用它时我们将或多或少地坚持下去。 但是,值得注意的是底层算法,因为它有助于轻松,高效地使用它。 为了开始描述 MeanShift 的工作原理,首先,我们需要将反投影图像(或通常为二进制图像)中的白色像素视为二维平面上的分散点。 那应该很容易。 以此为借口,我们可以说,MeanShift 实际上是一种迭代方法,用于查找点在分布点的平面上最密集的位置。 该算法具有一个初始窗口(指定整个图像一部分的矩形),该窗口用于搜索质心,然后将窗口中心移动到新找到的质心。 重复查找质量中心并使窗口中心偏移的过程,直到所需的偏移小于提供的阈值(ε)或达到最大迭代次数为止。 下图显示了在 MeanShift 算法中每次迭代之后窗口移动到最密集的位置(或者甚至在达到迭代计数之前,甚至之前)移动窗口的方式:
基于此,通过确保在每个帧的反投影中区分对象,MeanShift 算法可用于跟踪视频中的对象。 当然,为此,我们需要使用与之前相似的阈值方法。 最常见的方法是应用已经准备好的直方图,并使用它来计算反投影(在我们之前的示例中,我们只是修改了输入直方图)。 让我们通过一个示例逐步进行此操作。 因此,我们将创建一个QThread子类,可以在任何独立的 Qt 应用中创建该子类,也可以在 DLL 或插件中使用该子类,这将用于computer_vision项目。 无论如何,对于所有项目类型,此线程将保持完全相同。
如第 8 章,“多线程处理”中所讨论的,处理视频应在单独的线程中完成(如果我们不希望找到任何丑陋的解决方法),以使它不会阻塞 GUI 线程,并且可以自由地响应用户的操作。 请注意,该相同线程也可以用作创建任何其他(相似)视频处理线程的模板。 因此,让我们开始:
- 我们将创建一个 Qt 窗口小部件应用,该应用可以跟踪一个对象(具有任何颜色,但在这种情况下不是完全白色或黑色),该对象最初将使用鼠标,相机的实时供稿并使用 MeanShift 算法进行选择。 在初始选择之后的任何时候,我们都可以再次从摄像机的实时供稿中更改到场景中的另一个对象。 第一次选择对象时,然后每次更改选择时,将提取视频帧的色相通道,并使用直方图和反投影图像计算并提供给 MeanShift 算法,并且该对象将被跟踪。 因此,我们需要首先创建一个 Qt Widgets 应用并为其命名,例如
MeanShiftTracker,然后继续实际的跟踪器实现。 - 正如我们在第 8 章,“多线程”中了解的那样,创建一个
QThread子类。 将其命名为QCvMeanShiftThread,并确保相应地在私有和公共成员区域中包括以下内容。 我们将使用setTrackRect函数通过此函数设置初始MeanShift跟踪窗口,但还将使用此函数提供将跟踪更改为另一个对象的方法。newFrame非常明显,它将在处理完每帧后发出,以便 GUI 可以显示它。 使用私有区域和 GUI 的成员将在后面的步骤中进行描述,但是它们包含了到目前为止我们已经了解的一些最重要的主题:
public slots:
void setTrackRect(QRect rect);
signals:
void newFrame(QPixmap pix);
private:
void run() override;
cv::Rect trackRect;
QMutex rectMutex;
bool updateHistogram;
setTrackRect函数只是用于更新我们希望 MeanShift 算法跟踪的矩形(初始窗口)的setter函数。 这是应如何实现:
void QCvMeanShiftThread::setTrackRect(QRect rect)
{
QMutexLocker locker(&rectMutex);
if((rect.width()>2) && (rect.height()>2))
{
trackRect.x = rect.left();
trackRect.y = rect.top();
trackRect.width = rect.width();
trackRect.height = rect.height();
updateHistogram = true;
}
}
QMutexLocker与rectMutex一起用于为我们的trackRect提供访问序列化。 由于我们还将以一种实时工作的方式实现跟踪方法,因此我们需要确保在处理trackRect时不会对其进行更新。 我们还确保其大小合理,否则将被忽略。
- 至于我们的跟踪器线程的
run函数,我们需要使用VideoCapture打开计算机上的默认相机并向我们发送帧。 请注意,如果框架为空(损坏),相机关闭或从线程外部请求线程中断,则循环将退出:
VideoCapture video;
video.open(0);
while(video.isOpened() && !this->isInterruptionRequested())
{
Mat frame;
video >> frame;
if(frame.empty())
break;
// rest of the process ...
....
}
在循环内,将其标记为rest of the process ...,首先,我们将使用cv::Rect类的area函数来查看trackRect是否已设置。 如果是,那么我们将锁定访问权限并继续进行跟踪操作:
if(trackRect.size().area() > 0)
{
QMutexLocker locker(&rectMutex);
// tracking code
}
至于 MeanShift 算法和真实跟踪,我们可以使用以下源代码:
Mat hsv, hue, hist;
cvtColor(frame, hsv, CV_BGR2HSV);
hue.create(hsv.size(), hsv.depth());
float hrange[] = {0, 179};
const float* ranges[] = {hrange};
int bins[] = {24};
int fromto[] = {0, 0};
mixChannels(&hsv, 1, &hue, 1, fromto, 1);
if(updateHistogram)
{
Mat roi(hue, trackRect);
calcHist(&roi, 1, 0, Mat(), hist, 1, bins, ranges);
normalize(hist,
hist,
0,
255,
NORM_MINMAX);
updateHistogram = false;
}
Mat backProj;
calcBackProject(&hue,
1,
0,
hist,
backProj,
ranges);
TermCriteria criteria;
criteria.maxCount = 5;
criteria.epsilon = 3;
criteria.type = TermCriteria::EPS;
meanShift(backProj, trackRect, criteria);
rectangle(frame, trackRect, Scalar(0,0,255), 2);
上面的代码按照以下顺序执行以下操作:
- 使用
cvtColor函数将输入帧从 BGR 转换为 HSV 色彩空间。 - 使用
mixChannels函数仅提取色调通道。 - 如果需要,可以使用
calcHist和normalize函数计算并归一化直方图。 - 使用
calcBackproject函数计算反投影图像。 - 通过提供迭代标准,在背投图像上运行 MeanShift 算法。 这是通过
TermCriteria类和meanShift函数完成的。meanShift会简单地更新提供的矩形(trackRect每帧有一个新矩形)。 - 在原始图像上绘制检索到的矩形。
除了TermCriteria类和meanShift函数本身之外,您刚刚看到的任何代码中都没有新内容。 如前所述,MeanShift 算法是一种迭代方法,需要根据移位量(ε)和迭代次数来确定一些停止条件。 简而言之,增加迭代次数可以减慢算法的速度,但也可以使其更加准确。 另一方面,提供较小的ε值将意味着更加敏感的行为。
在处理完每个帧之后,线程仍需要使用专用信号将其发送到另一个类。 方法如下:
emit newFrame(
QPixmap::fromImage(
QImage(
frame.data,
frame.cols,
frame.rows,
frame.step,
QImage::Format_RGB888)
.rgbSwapped()));
请注意,除了发送QPixmap或QImage等,我们还可以发送不是QObject子类的类。 为了能够通过 Qt 信号发送非 Qt 类,它必须具有公共默认构造器,公共副本构造器和公共析构器。 还需要先注册。 例如,Mat类包含必需的方法,但不是已注册的类型,因此可以按如下所示进行注册:qRegisterMetaType<Mat>("Mat");。 之后,您可以在 Qt 信号和插槽中使用Mat类。
- 除非我们完成此线程所需的用户界面,否则仍然看不到任何结果。 让我们用
QGraphicsView来做。 只需使用设计器将一个拖放到mainwindow.ui上,然后将以下内容添加到mainwindow.h中。 我们将使用QGraphicsView类的橡皮筋功能轻松实现对象选择:
private:
QCvMeanShiftThread *meanshift;
QGraphicsPixmapItem pixmap;
private slots:
void onRubberBandChanged(QRect rect,
QPointF frScn, QPointF toScn);
void onNewFrame(QPixmap newFrm);
- 在
mainwindow.cpp文件和MainWindow类的构造器中,确保添加以下内容:
ui->graphicsView->setScene(new QGraphicsScene(this));
ui->graphicsView->setDragMode(QGraphicsView::RubberBandDrag);
connect(ui->graphicsView,
SIGNAL(rubberBandChanged(QRect,QPointF,QPointF)),
this,
SLOT(onRubberBandChanged(QRect,QPointF,QPointF)));
meanshift = new QCvMeanShiftThread();
connect(meanshift,
SIGNAL(newFrame(QPixmap)),
this,
SLOT(onNewFrame(QPixmap)));
meanshift->start();
ui->graphicsView->scene()->addItem(&pixmap);
在第 5 章,“图形视图框架”中详细讨论了如何使用 Qt 图形视图框架。
- 还应确保在关闭应用时注意线程,如下所示:
meanshift->requestInterruption();
meanshift->wait();
delete meanshift;
- 剩下的唯一事情就是在 GUI 本身上设置传入的
QPixmap,并且还传递更新被跟踪对象所需的矩形:
void MainWindow::onRubberBandChanged(QRect rect,
QPointF frScn,
QPointF toScn)
{
meanshift->setTrackRect(rect);
}
void MainWindow::onNewFrame(QPixmap newFrm)
{
pixmap.setPixmap(newFrm);
}
尝试运行该应用并选择一个在相机上可见的对象。 使用鼠标在图形视图上绘制的矩形将跟随您选择的对象,无论它在屏幕上的任何位置。 这是从视图中选择 Qt 徽标后对其进行跟踪的一些屏幕截图:
可视化反投影图像并查看幕后发生的魔术也是一个好主意。 请记住,如前所述,MeanShift 算法正在搜索质心,当在反投影图像中观察时,这很容易感知。 只需用以下代码替换我们用于可视化线程内图像的最后几行:
cvtColor(backProj, backProj, CV_GRAY2BGR);
frame = backProj;
rectangle(frame, trackRect, Scalar(0,0,255), 2);
现在再试一次。 您应该在图形视图中具有反投影图像:
从结果可以看出,MeanShift 算法或精确的meanShift函数非常易于使用,只要为其提供灰度图像即可,该图像可以使用任何阈值方法隔离感兴趣的对象。 是的,反投影也类似于阈值设置,在该阈值设置中,您可以基于颜色,强度或其他条件让某些像素通过或某些其他像素不通过。 现在,如果我们回到 MeanShift 算法的初始描述,完全可以说它可以基于反投影图像找到并跟踪对象。
尽管meanShift函数易于使用,但它仍然缺少几个非常重要的功能。 这些是对被跟踪对象的比例和方向更改的容限。 无论对象的大小或其方向如何,camShift函数都将提供一个大小和旋转度完全相同的窗口,而该窗口只是试图以目标对象为中心。 这些问题在 MeanShift 算法的增强版本中得以解决,该增强版本称为连续自适应 MeanShift 算法,或简称为 CamShift。
CamShift函数是 OpenCV 中 CamShift 算法的实现,与 MeanShift 算法有很多共同之处,并且出于同样的原因,它的使用方式几乎相同。 为了证明这一点,只需将前面代码中对meanShift算法的调用替换为CamShift即可,如下所示:
CamShift(backProj, trackRect, criteria);
如果再次运行该程序,您会发现什么都没有真正改变。 但是,此函数还提供RotatedRect类型的返回值,该返回值基本上是矩形,但具有中心,大小和角度属性。 您可以保存返回的RotatedRect并将其绘制在原始图像上,如下所示:
RotatedRect rotRec = CamShift(backProj, trackRect, criteria);
rectangle(frame, trackRect, Scalar(0,0,255), 2);
ellipse(frame, rotRec, Scalar(0,255,0), 2);
请注意,我们实际上在这段代码中绘制了一个适合RotatedRect类属性的椭圆。 我们还绘制了先前存在的矩形,以便与旋转的矩形进行比较。 如果您尝试再次运行该程序,则结果如下:
请注意,绿色椭圆相对于红色矩形的旋转是CamShift函数的结果。 尝试将要跟踪的彩色物体移离相机或靠近相机,然后查看CamShift如何尝试适应这些变化。 另外,尝试使用非正方形物体观察CamShift提供的旋转不变跟踪。
CamShift函数还可以用于根据物体的颜色检测物体。 当然,如果可以与周围环境区分开。 因此,您需要设置一个预先准备好的直方图,而不是像我们的示例那样在运行时设置它。 您还需要将初始窗口大小设置为很大的窗口大小,例如整个图像的大小,或图像中预期将出现对象的最大区域。 通过运行相同的代码,您会注意到,在每一帧之后,窗口将变得越来越小,直到仅覆盖我们为其提供直方图的目标对象为止。
背景/前景检测
背景/前景检测或分割(由于很好的原因通常也称为背景减法)是一种区分图像(前景)中移动区域或变化区域的方法,而不是或多或少的恒定或静态区域(背景)。 该方法在检测图像中的运动时也非常有效。 OpenCV 包括许多不同的背景扣除方法,默认情况下,当前的 OpenCV 安装中提供了两种方法,即BackgroundSubtractorKNN和BackgroundSubtractorMOG2。 与我们在第 7 章,“特征和描述符”中了解到的特征检测器类相似,这些类也源自cv::Algorithm类,并且它们都非常容易且相似地使用,因为它们的用法或结果不同,而在类的实现方面不同。
BackgroundSubtractorMOG2可以通过使用高斯混合模型来检测背景/前景。 另一方面,通过使用 KNN 或 K 最近邻方法,BackgroundSubtractorKNN也可以用于实现相同的目标。
如果您对这些算法的内部细节或如何实现感兴趣,可以参考以下文章以获取更多信息:
Zoran Zivkovic and Ferdinand van der Heijden. Efficient adaptive density estimation per image pixel for the task of background subtraction. Pattern recognition letters, 27(7):773-780, 2006.
Zoran Zivkovic. Improved adaptive gaussian mixture model for background subtraction. In Pattern Recognition, 2004. ICPR 2004. Proceedings of the 17th International Conference on, volume 2, pages 28-31. IEEE, 2004.
首先让我们看看它们是如何使用的,然后再介绍它们的一些重要功能。 与上一节中创建的QCvMeanShiftThread类相似,我们可以通过将QThread子类化来创建新线程。 将其命名为QCvBackSubThread或您认为合适的任何名称。 唯一有区别的部分是覆盖的run函数,它看起来如下所示:
void QCvBackgroundDetect::run()
{
using namespace cv;
Mat foreground;
VideoCapture video;
video.open(0);
Ptr<BackgroundSubtractorMOG2> subtractor =
createBackgroundSubtractorMOG2();
while(video.isOpened() && !this->isInterruptionRequested())
{
Mat frame;
video >> frame;
if(frame.empty())
break; // or continue if this should be tolerated
subtractor->apply(frame, foreground);
Mat foregroundBgr;
cvtColor(foreground, foregroundBgr, CV_GRAY2BGR);
emit newFrame(
QPixmap::fromImage(
QImage(
foregroundBgr.data,
foregroundBgr.cols,
foregroundBgr.rows,
foregroundBgr.step,
QImage::Format_RGB888)
.rgbSwapped()));
}
}
请注意,背景减法所需的唯一调用是BackgroundSubtractorMOG2类的构造并调用apply函数。 就使用它们而言,仅此而已,这使它们非常简单易用。 在每帧,根据图像所有区域的变化历史更新前景,即Mat类。 由于我们只是通过调用createBackgroundSubtractorMOG2函数使用了默认参数,因此我们没有更改任何参数,而是继续使用默认值,但是如果要更改算法的行为,我们需要为此提供以下参数:
history(默认设置为 500)是影响背景减法算法的最后一帧的数量。 在我们的示例中,我们还在 30 FPS 摄像机或视频上使用了大约 15 秒的默认值。 这意味着,如果一个区域在过去 15 秒钟内完全未变,则它将完全变黑。varThreshold(默认设置为 16)是算法的差异阈值。detectShadows(默认设置为true)可用于忽略或计数检测阴影变化。
尝试运行前面的示例程序,该程序使用默认参数并观察结果。 如果镜头前没有任何动作,您应该会看到一个全黑的屏幕,但是即使很小的移动也可以被视为输出上的白色区域。 您应该会看到以下内容:
切换到BackgroundSubtractorKNN类非常容易,您只需要用以下内容替换构造线:
Ptr<BackgroundSubtractorKNN> subtractor =
createBackgroundSubtractorKNN();
没什么需要改变的。 但是,要修改此算法的行为,可以使用以下参数,其中一些参数也与BackgroundSubtractorMOG2类共享:
history与之前的算法完全相同。detectShadows,也与先前的算法相同。dist2Threshold默认情况下设置为400.0,并且是像素与样本之间平方距离的阈值。 为了更好地理解这一点,最好在线查看 K 最近邻算法。 当然,您可以简单地使用默认值并使用算法,而无需提供任何参数。
试用各种参数并观察结果,没有什么可以帮助您提高使用这些算法的效率。 例如,您会注意到增加历史值将有助于检测甚至更小的运动。 尝试更改其余参数,以自己观察和比较结果。
在前面的示例中,我们尝试输出通过使用背景减法类提取的前景遮罩图像。 您还可以在copyTo函数中使用相同的前景遮罩,以输出前景的实际像素。 这是如何做:
frame.copyTo(outputImage, foreground);
其中frame是相机输入的帧,foreground是通过背景减法算法获得的,与前面的示例相同。 如果尝试显示输出图像,则将具有以下类似内容:
请注意,此处看到的输出是移动摄像机的结果,与移动视频中的对象基本相同。 但是,如果您在视频中尝试在静态背景上四处移动其他任何彩色对象的视频中使用同一示例,则可以使用 CamShift 算法在移动对象周围获取一个边界框以提取该对象,或由于任何原因对其进行进一步处理。
使用 OpenCV 中的现有视频分析类编写应用的机会是巨大的,这仅取决于您对使用它们的熟悉程度。 例如,通过使用背景减除算法,您可以尝试编写运行警报的应用,或者在检测到运动时执行另一个过程。 可以通过测量在前面的示例中看到的提取的前景图像中像素的总和或平均值,然后检测超过某些阈值的突然增加,来轻松地完成类似的操作。 我们甚至无法开始列举所有可能性,但可以肯定的是,您是混合使用这些算法来解决特定任务的大师,并且包括该书在内的任何指南都只是您如何操作的路标集合。 使用现有算法。
总结
编写执行实时图像处理的计算机视觉应用是当今的热门话题,并且 OpenCV 包含许多类和函数来帮助简化此类应用的开发。 在本章中,我们试图介绍 OpenCV 提供的一些最重要的类和函数,这些类和函数用于实时处理视频和图像。 我们了解了 OpenCV 中的 MeanShift,CamShift 和背景减法算法,这些算法打包在快速高效的类中,同时,它们非常易于使用,前提是您熟悉大多数语言中使用的基本概念 ,例如直方图和反投影图像。 这就是为什么我们首先要学习所有有关直方图的知识,以及如何进行计算,可视化和相互比较。 我们还学习了如何计算反投影图像并将其用作查找表以更新图像。 我们在 MeanShift/CamShift 算法中也使用了相同的算法来跟踪特定颜色的对象。 到现在为止,我们应该能够高效地编写基于其中的零件和零件运动来处理视频和图像的应用。
本章是最后一章,我们将介绍 OpenCV 和 Qt 框架的详细信息。 一本书,甚至一本书,永远都不足以覆盖 OpenCV 和 Qt 框架中的所有现有材料,但是我们试图以一种可以跟进其余部分的方式来呈现整个情况的概述。 现有的类和函数可以自己开发有趣的计算机视觉应用。 确保与 OpenCV 和 Qt 框架的新开发保持同步,因为它们正在开展并吸引着正在进行中的项目,并且进展似乎不会很快停止。
本书的下一章将专门介绍如何调试,测试和部署 Qt 和 OpenCV 应用并将其部署给用户。 我们将首先了解 Qt Creator 的调试功能,然后继续使用 Qt Test 命名空间及其基础功能,这些功能可用于轻松进行 Qt 应用的单元测试。 在下一章中,我们还将介绍 Qt 安装程序框架,甚至为应用创建一个简单的安装程序。
十、调试与测试
自从使用 OpenCV 3 和 Qt5 框架进行计算机视觉之旅以来,我们已经走了很长一段路。 现在,我们可以非常轻松地安装这些强大的框架,并配置运行 Windows,MacOS 或 Linux 操作系统的计算机,以便我们可以设计和构建计算机视觉应用。 在前几章中,我们学习了如何使用 Qt 插件系统来构建模块化和基于插件的应用。 我们学习了如何使用 Qt 样式表对应用进行样式设置,以及如何使用 Qt 中的国际化技术使其支持多种语言。 我们使用 Qt 图形视图框架构建了功能强大的图形查看器应用。 该框架中的类帮助我们更加有效地显示图形项目,并具有更大的灵活性。 我们能够构建可以放大和缩小图像的图形查看器,而不必处理源图像本身(这要归功于场景-视图-项目架构)。 后来,我们开始更深入地研究 OpenCV 框架,并且了解了许多类和函数,这些类和函数使我们能够以多种方式转换图像并对其进行处理,以实现特定的计算机视觉目标。 我们学习了用于检测场景中对象的特征检测和描述符提取。 我们浏览了 OpenCV 中的许多现有算法,这些算法旨在以更加智能的方式处理图像内容,而不仅仅是原始像素值。 在最近的章节中,我们了解了 Qt 提供的多线程和线程同步工具。 我们了解了 Qt 框架提供的用于处理应用中多线程的低级(QThread)和高级(QtConcurrent)技术,而与平台无关。 最后,在上一章中,我们学习了视频的实时图像处理以及可以跟踪具有特定颜色的对象的 OpenCV 算法。 到现在为止,我们应该以这样一种方式熟悉 Qt 和 OpenCV 框架的许多方面:我们自己可以跟进更高级的主题,并且仅依赖于文档。
除了前面提到的所有内容以及在前几章中我们取得的成就的一长串清单之外,我们仍然没有谈论软件开发的一个非常重要的方面以及在与 Qt 和 OpenCV 一起工作时如何处理软件,即测试过程。 在将计算机程序部署到该应用的用户之前,无论该程序是简单的小型二进制文件,大型计算机视觉应用还是任何其他应用,都必须经过测试。 测试是开发过程中一个永无止境的阶段,它是在开发应用后立即进行的,并且时不时地解决问题或添加新功能。 在本章中,我们将学习现有技术来测试使用 Qt 和 OpenCV 构建的应用。 我们将学习开发时间测试和调试。 我们还将学习如何使用 Qt 测试框架对应用进行单元测试。 在将应用交付给最终用户之前,这是最重要的过程。
我们将在本章中介绍的主题如下:
- Qt Creator 的调试功能
- 如何使用 Qt 测试命名空间进行单元测试
- 数据驱动的测试
- GUI 测试和重放 GUI 事件
- 创建测试用例项目
将 Qt Creator 用于调试
调试器是一种程序,在程序执行过程中突然崩溃或程序逻辑中发生意外行为时,可用于测试和调试其他程序。 在大多数情况下(如果不是总是),调试器用于开发环境中,并与 IDE 结合使用。 在我们的案例中,我们将学习如何在 Qt Creator 中使用调试器。 重要的是要注意,调试器不是 Qt 框架的一部分,并且像编译器一样,它们通常由操作系统 SDK 提供。 如果系统中存在调试器,则 Qt Creator 会自动检测并使用调试器。 可以通过依次通过主菜单“工具”和“选项”进入“Qt Creator 选项”页面进行检查。 确保从左侧列表中选择Build&Run,然后从顶部切换到Debuggers选项卡。 您应该能够在列表上看到一个或多个自动检测到的调试器。
Windows 用户:此信息框后,您应该会看到类似于屏幕截图的内容。 如果没有,则意味着您尚未安装任何调试器。 您可以按照此处提供的说明轻松下载并安装它。
或者,您可以独立地在线搜索以下主题:
Windows 调试工具(WinDbg,KD,CDB,NTSD)。
但是,在安装调试器之后(假定是 Microsoft Visual C++ 编译器的 CDB 或 Microsoft 控制台调试器,以及 GCC 编译器的 GDB),您可以重新启动 Qt Creator 并返回此页面。 您应该可以具有一个或多个类似于以下内容的条目。 由于我们已经安装了 32 位版本的 Qt 和 OpenCV 框架,因此选择名称中带有 x86 的条目以查看其路径,类型和其他属性。
MacOS 和 Linux 用户:
不需要执行任何操作,根据操作系统,您会看到 GDB,LLDB 或其他调试器中的条目。
这是“选项”页面上“构建和运行”选项卡的屏幕截图:
根据操作系统和已安装的调试器的不同,前面的屏幕快照可能会略有不同。 但是,您将需要一个调试器,以确保已正确设置为所用 Qt Kit 的调试器。 因此,记下调试器的路径和名称,并切换到 Kits 选项卡,然后在选择了所用的 Qt Kit 后,请确保正确设置了调试器,如以下屏幕快照所示:
不必担心选择错误的调试器或任何其他选项,因为在顶部选择的 Qt Kit 图标旁边会警告您相关的图标。 当工具包一切正常时,通常会显示下图所示的图标,左侧的第二个图标表示有问题的不正确,右侧的图标表示严重错误。 将鼠标移到该图标上时,可以查看有关解决该问题所需的详细操作的更多信息:
Qt 套件的关键问题可能是由许多不同的因素引起的,例如缺少编译器,这将使套件在解决问题之前完全无用。 Qt 套件中的警告消息示例可能是缺少调试器,这不会使套件无用,但您将无法将其与调试器一起使用,因此,与完全配置的 Qt 套件相比,它意味着功能更少。
正确设置调试器后,您可以采用以下几种方法之一开始调试应用,这些方法基本上具有相同的结果:最终进入 Qt Creator 的调试器视图:
- 在调试模式下启动应用
- 附加到正在运行的应用(或进程)
请注意,可以通过多种方式来启动调试过程,例如通过将其附加到在另一台计算机上运行的过程中来远程启动。 但是,上述方法在大多数情况下就足够了,尤其是与 Qt + OpenCV 应用开发以及我们在本书中学到的内容有关的情况。
调试模式入门
要在调试模式下启动应用,请在打开 Qt 项目后使用以下方法之一:
- 按下
F5按钮 - 使用“开始调试”按钮,在通常的“运行”按钮下,带有类似图标,但上面有一个小错误
- 按以下顺序使用主菜单项:调试/开始调试/开始调试
要将调试器附加到正在运行的应用,可以按以下顺序使用主菜单项:调试/启动调试/附加到正在运行的应用。 这将打开“进程列表”窗口,从中可以使用其进程 ID 或可执行文件名选择应用或要调试的任何其他进程。 您还可以使用“过滤器”字段(如下图所示)来找到您的应用,因为很有可能进程列表很长。 选择正确的过程后,请确保按下“附加到过程”按钮。
无论使用哪种方法,都将最终进入 Qt Creator 调试模式,该模式与“编辑”模式非常相似,但是它还可以执行以下操作:
- 在代码中添加,启用,禁用和查看断点(断点只是我们希望调试器在过程中暂停的代码中的点或线,并允许我们对程序状态进行更详细的分析 )
- 中断正在运行的程序和进程以查看和检查代码
- 查看和检查函数调用栈(调用栈是一个包含导致断点或中断状态的函数的层次结构列表的栈)
- 查看和检查变量
- 反汇编源代码(从这种意义上来说,反汇编意味着提取与我们程序中的函数调用和其他 C++ 代码相对应的确切指令)
在调试模式下启动应用时,您会注意到性能下降,这显然是因为调试器正在监视和跟踪代码。 这是 Qt Creator 调试模式的屏幕截图,其中前面提到的所有功能都可以在单个窗口中以及在 Qt Creator 的调试模式下看到:
您在本书中已经使用并且非常熟悉的代码编辑器中的上一个屏幕快照中用数字1指定的区域。 每行代码都有一个行号; 您可以单击其左侧以在代码中所需的任何位置切换断点。 您还可以右键单击行号以设置,删除,禁用或启用断点,方法是选择“在行 X 处设置断点”,“删除断点 X”,“禁用断点 X”或“启用断点 X”,其中所有提到的命令中的 X 这里需要用行号代替。 除了代码编辑器,您还可以使用前面的屏幕快照中编号为4的区域来添加,删除,编辑和进一步修改代码中的断点。
在代码中设置断点后,只要程序到达代码中的该行,它将被中断,并且您将被允许使用代码编辑器正下方的控件来执行以下任务:
- 继续:这意味着继续执行程序的其余流程(或再次按
F5)。 - 步过:用于执行下一步(代码行),而无需进入函数调用或类似的代码,这些代码可能会更改调试光标的当前位置。 请注意,调试游标只是正在执行的当前代码行的指示器。 (这也可以通过按
F10来完成。) - 单步执行:与单步执行相反,它可以用于进一步深入函数调用,以更详细地分析代码和调试。 (与按
F11相同。) - 退出:可用于退出函数调用并在调试时返回到调用点。 (与按
Shift + F11相同。)
您也可以右键单击包含调试器控件的代码编辑器下方的同一工具栏,以打开以下菜单,并添加或删除更多窗格以显示其他调试和分析信息。 我们将介绍默认的调试器视图,但请确保自行检查以下每个选项,以进一步熟悉调试器:
在前面的代码中用数字2指定的区域可用于查看调用栈。 无论您是通过按“中断”按钮还是在运行时从菜单中选择“调试/中断”来中断程序,设置断点并在特定代码行中停止程序,还是发生故障的代码都会导致程序陷入陷阱,并暂停该过程(因为调试器将捕获崩溃和异常),您始终可以查看导致中断状态的函数调用的层次结构,或者通过检查前面 Qt Creator 屏幕截图中的区域 2 来进一步分析它们。
最后,您可以使用上一个屏幕快照中的第三个区域在代码中被中断的位置查看程序的局部变量和全局变量。 您可以查看变量的内容,无论它们是标准数据类型(例如整数和浮点数还是结构和类),还可以进一步扩展和分析其内容以测试和分析代码中的任何可能问题。
有效地使用调试器可能意味着数小时的测试和解决您的代码中的问题。 就调试器的实际使用而言,实际上没有别的方法,只有尽可能多地使用它,并养成使用调试器的习惯,而且还要记下您在使用过程中发现的良好做法和技巧, 我们刚刚经历的那些。 如果您有兴趣,还可以在线阅读有关其他可能的调试方法的信息,例如远程调试,使用故障转储文件的调试(在 Windows 上)等。
Qt 测试框架
在开发应用时进行调试和测试是完全不可避免的,但是许多开发人员往往会错过的一件事就是进行单元测试,这一点尤为重要,尤其是在大型项目和难以手动进行全面测试的应用中。 在构建它们的时间或在其代码中的某个位置修复了错误。 单元测试是一种测试应用中的零件(单元)以确保它们按预期工作的方法。 还值得注意的是,测试自动化是当今软件开发的热门话题之一,它是使用第三方软件或编程来自动化单元测试的过程。
在本节中,我们将学习精确使用 Qt 测试框架(即 Qt 测试命名空间)(以及一些其他与测试相关的类)的知识,这些知识可用于为使用 Qt 构建的应用开发单元测试。 与第三方测试框架相反,Qt 测试框架是内部(基于 Qt 框架本身)和轻量级测试框架,并且在其众多功能中,它提供基准测试,数据驱动的测试和 GUI。 测试:基准测试可用于衡量函数或特定代码段的性能,而数据驱动的测试可帮助运行使用不同数据集作为输入的单元测试。 另一方面,可以通过模拟鼠标和键盘的交互来进行 GUI 测试,这又是 Qt 测试框架涵盖的另一个方面。
创建单元测试
可以通过子类化QObject类并添加 Qt 测试框架所需的插槽以及一个或多个用于执行各种测试的插槽(测试函数)来创建单元测试。 下列插槽(专用插槽)可以存在于每个测试类中,并且除了测试函数外,还可以由 Qt Test 调用:
initTestCase:在调用第一个测试函数之前调用它。 如果此函数失败,则整个测试将失败,并且不会调用任何测试函数。cleanupTestCase:在调用最后一个测试函数后调用。init:在调用每个测试函数之前调用它。 如果此函数失败,将不会执行前面的测试函数。cleanup:在调用每个测试函数后调用。
让我们用一个真实的例子创建我们的第一个单元测试,看看如何将刚才提到的函数添加到测试类中,以及如何编写测试函数。 为了确保我们的示例是现实的并且易于同时进行,我们将避免过多地担心要测试的类的实现细节,而主要关注于如何测试它们。 基本上,相同的方法可用于测试具有任何级别复杂性的任何类。
因此,作为第一个示例,我们假设我们有一个返回图像像素数量(宽度乘以图像高度)的类,并且我们想使用单元测试进行测试:
- 可以使用 Qt Creator 创建单元测试,类似于创建 Qt 应用或库,也可以在“欢迎”模式下使用“新建项目”按钮,或者从“文件”菜单中选择“新建文件”或“项目”来创建单元测试。 确保选择以下内容作为项目模板:
-
单击选择,然后输入
HelloTest作为单元测试项目的名称,然后单击下一步。 -
选择与 Qt 项目完全相同的工具包,然后再次单击“下一步”。
-
在下一个屏幕截图中看到的“模块”页面中,您会注意到 QtCore 和 QtTest 模块是默认选择的,不能取消选择它们。 该页面只是一个帮助程序,或者是一个帮助您以交互方式选择所需模块的所谓向导。 如果忘记了添加类正常工作所需的模块,则以后也可以使用项目
*.pro文件添加或删除模块。 这使得有必要再次重复一个重要的观点。 单元测试就像使用您的类和函数的应用一样。 唯一的区别是,您仅将其用于测试目的,并且仅用于确保事情按预期运行,并且没有回归:
- 选择模块并单击下一步后,将显示“详细信息”页面或“测试类别信息”页面。 在以下屏幕截图中看到的“测试插槽”字段中输入
testPixelCount,然后单击“下一步”。 其余选项(如前一个窗口)只是简单的帮助程序,可轻松地以交互方式添加所需的函数,并包括对测试单元的指令,如果缺少任何内容,也可以稍后在源文件中添加这些指令。 不过,本章稍后将了解它们的含义以及如何使用它们。
- 确认所有对话框后,我们将进入 Qt Creator 编辑模式下的代码编辑器。 检查
HelloTest.pro文件,您会注意到它与标准 Qt 项目(小部件或控制台应用)的*.pro文件非常相似,具有以下模块定义,可将 Qt 测试模块导入该项目。 这就是您可以在任何单元测试项目中使用 Qt Test 的方式。 但是,如果您不使用“新建文件”或“项目”向导,则该向导会自动添加:
QT += testlib
在继续下一步之前,请确保像在 Qt Widgets 应用中一样将 OpenCV 库添加到 pro 文件。 (有关这方面的更多信息,请参阅本书的初始章节。)
- 现在,添加您创建的类以将图像的像素计数到该项目中。 请注意,在这种情况下,添加和复制不是同一回事。 您可以在单独的文件夹中将属于另一个项目的类头文件和源文件添加到项目中,而无需将其复制到项目文件夹中。 您只需要确保它们包含在
*.pro文件的HEADERS和SOURCES列表中,并且可以选择将类所在的文件夹添加到INCLUDEPATH变量中。
实际上,永远不要将要测试的类的源文件复制到测试项目中,正如我们将在本节中进一步讨论的那样,即使包含subdirs模板,也应始终使用subdirs模板制作单个项目,以便至少将一个单元测试添加到项目中,并在每次构建主项目时自动执行测试。 但是,严格来讲,无论您将类文件复制到其中还是将其简单地添加到它们中,单元测试都将以相同的方式工作。
- 现在该编写我们的测试类了,因此在 Qt Creator 代码编辑器中打开
tst_hellotesttest.cpp。 除了明显的#include指令外,这里还需要注意几件事:一个是HelloTestTest类,这是在“新建文件”或“项目”向导期间提供的类名称。 它不过是QObject子类,因此不要在此处查找任何隐藏内容。 它有一个称为testPixelCount的专用插槽,该插槽也是在向导期间设置的。 它的实现包括带有QVERIFY2宏的一行,我们将在后面的步骤中进行介绍。 但是,最后两行是新的:
QTEST_APPLESS_MAIN(HelloTestTest)
#include "tst_hellotesttest.moc"
QTEST_APPLESS_MAIN是由 C++ 编译器和moc扩展的宏(有关moc的更多信息,请参见第 3 章,“创建一个全面的 Qt + OpenCV 项目”),以创建适当的 C++ main函数来执行我们在HelloTestTest类中编写的测试函数。 它仅创建测试类的实例并调用QTest::qExec函数以启动测试过程。 测试过程将自动调用测试类中的所有专用插槽,并输出测试结果。 最后,如果我们在单个cpp源文件中创建测试类,而不是在单独的标头和源文件中创建 Qt 框架,则最后一行是必需的。 确保使用include指令将要测试的类添加到tst_hellotesttest.cpp文件中。 (为便于参考,我们假设其名为PixelCounter。)
- 现在,您可以使用适当的测试宏之一来测试此类中负责计算图像像素的函数。 假设该函数采用文件名和路径(
QString类型)并返回整数。 让我们使用testPixelCount插槽内已经存在的VERIFY2宏,如下所示:
void HelloTestTest::testPixelCount()
{
int width = 640, height = 427;
QString fname = "c:/dev/test.jpg";
PixelCounter c;
QVERIFY2(c.countPixels(fname) == width*height, "Failure");
}
在此测试中,我们仅提供了一个图像文件,该图像文件的像素数已知(宽度乘以高度),以测试我们的函数是否正常工作。 然后,我们将创建PixelCounter类的实例,并最终执行QVERIFY2宏,该宏将执行countPixels函数(假设这是我们要测试的公共函数的名称),并根据比较失败或通过来进行测试。 如果测试失败,它也会输出Failure字符串。
我们刚刚建立了第一个单元测试项目。 单击运行按钮以运行此测试,并在 Qt Creator 输出窗格中查看结果。 如果测试通过,那么您将看到类似以下内容:
********* Start testing of HelloTestTest *********
Config: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by MSVC 2015)
PASS : HelloTestTest::initTestCase()
PASS : HelloTestTest::testPixelCount()
PASS : HelloTestTest::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 26ms
********* Finished testing of HelloTestTest *********
如果发生故障,您将在输出中看到以下内容:
********* Start testing of HelloTestTest *********
Config: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by MSVC 2015)
PASS : HelloTestTest::initTestCase()
FAIL! : HelloTestTest::testPixelCount() 'c.countPixels(fname) == width*height' returned FALSE. (Failure)
..HelloTesttst_hellotesttest.cpp(26) : failure location
PASS : HelloTestTest::cleanupTestCase()
Totals: 2 passed, 1 failed, 0 skipped, 0 blacklisted, 26ms
********* Finished testing of HelloTestTest *********
结果几乎是不言而喻的,但是我们可能需要注意一件事,那就是在所有测试函数之前调用initTestCase,在所有测试函数之后调用cleanupTestCase的事实, 正如我们前面提到的。 但是,由于这些函数实际上并不存在,因此它们被标记为PASS。 如果您实现这些函数并执行实际的初始化和完成任务,则可能会有所改变。
在前面的示例中,我们看到了单元测试的最简单形式,但现实情况是,编写一个高效且可靠的单元测试来解决所有可能的问题,这是一项艰巨的任务,并且与我们面对的情况相比要复杂得多。 为了能够编写适当的单元测试,您可以在每个测试函数中使用以下宏。 这些宏在QTest中定义如下:
QVERIFY:可用于检查是否满足条件。 条件只是一个布尔值或任何计算结果为布尔值的表达式。 如果不满足条件,则测试将停止,失败并记录在输出中;否则,测试将失败。 否则,它将继续。QTRY_VERIFY_WITH_TIMEOUT:类似于QVERIFY,但是此功能尝试检查提供的条件,直到达到给定的超时时间(以毫秒为单位)或满足条件。QTRY_VERIFY:类似于QTRY_VERIFY_WITH_TIMEOUT,但是超时设置为默认值 5 秒。QVERIFY2,QTRY_VERIFY2_WITH_TIMEOUT和QTRY_VERIFY2:这些宏与名称非常相似的以前的宏非常相似,除了在测试失败的情况下函数还会输出给定消息之外,这些宏也是如此。QCOMPARE:可用于将实际值与预期的值进行比较。 它非常类似于QVERIFY,不同之处在于此宏还输出实际值和期望值以供以后参考。QTRY_COMPARE_WITH_TIMEOUT:类似于QCOMPARE,但是此函数尝试比较实际值和期望值,直到达到给定的超时时间(以毫秒为单位)或相等为止。QTRY_COMPARE:类似于QTRY_COMPARE_WITH_TIMEOUT,但是超时设置为默认值 5 秒。
数据驱动的测试
除了与每个测试函数内部提供的输入数据进行简单比较外,QTest还提供了使用一组更有条理和结构化的输入数据执行单元测试的方法,以执行数据驱动的测试,或者换句话说,通过不同的输入数据集。 这是通过QFETCH宏以及QTest::addColumn和QTest::newRow函数来完成的。 QFETCH函数可在测试函数内使用,以获取所需的测试数据。 这需要为我们的测试函数创建一个数据函数。 数据函数还是另一个专用插槽,其名称与测试函数的名称完全相同,但名称后面附加了_data。 因此,如果我们回到前面的示例,要进行数据驱动的测试,我们需要在测试类中添加一个新的专用插槽,类似于以下内容:
void HelloTestTest::testPixelCount_data()
{
QTest::addColumn<QString>("filename");
QTest::addColumn<int>("pixelcount");
QTest::newRow("huge image") <<
"c:/dev/imagehd.jpg" << 2280000;
QTest::newRow("small image") <<
"c:/dev/tiny.jpg" << 51200;
}
请注意,数据函数名称在其名称末尾附加了_data。 QTest中的测试数据被视为表格; 这就是为什么在数据函数中,addColumn函数用于创建新的列(或字段),而addRow函数用于向其中添加新的行(或记录)的原因。 前面的代码将产生类似于以下内容的测试数据表:
| 索引 | 名称(或标签) | 文件名 | 像素计数 |
|---|---|---|---|
| 0 | 大图像 | c:/dev/imagehd.jpg | 2280000 |
| 1 | 小图像 | c:/dev/tiny.jpg | 51200 |
现在,我们可以修改测试函数testPixelCount以使用此测试数据,而不是在同一函数中使用提供的单个文件名。 我们新的testPixelCount看起来与此类似(同时,为了更好的测试日志输出,我们也将QVERIFY替换为QCOMPARE):
void HelloTestTest::testPixelCount()
{
PixelCounter c;
QFETCH(QString, filename);
QFETCH(int, pixelcount);
QCOMPARE(c.countPixels(filename), pixelcount);
}
重要的是要注意,必须为QFETCH提供在数据函数内部创建的测试数据中每一列的确切数据类型和元素名称。 如果我们再次执行测试,则测试框架将调用testPixelCount,与测试数据中的行一样多,每次它将通过获取并使用新行并记录输出来运行测试函数。 使用数据驱动的测试函数有助于保持实际的测试函数完整,并且不是从测试函数内部创建测试数据,而是从简单且结构化的数据函数中获取它们。 不用说,您可以扩展它以从磁盘上的文件或其他输入方法(例如网络位置)中获取测试数据。 无论数据来自何处,当数据函数存在时,数据都应完整存在并正确构造。
基准管理
QTest提供QBENCHMARK和QBENCHMARK_ONCE宏来测量函数调用或任何其他代码的性能(基准)。 这两个宏的区别仅在于它们重复一段代码以衡量其性能的次数,而后者显然只运行一次代码。 您可以通过以下方式使用这些宏:
QBENCHMARK
{
// Piece of code to be benchmarked
}
同样,我们可以在前面的示例中使用它来衡量PixelCounter类的性能。 您可以简单地将以下行添加到testPixelCount函数的末尾:
QBENCHMARK
{
c.countPixels(filename);
}
如果再次运行测试,您将在测试日志输出中看到类似于以下内容的输出。 请注意,这些数字仅是在随机测试 PC 上运行的示例,在各种系统上它们可能会有很大不同:
23 msecs per iteration (total: 95, iterations: 4)
前面的测试输出意味着每次使用特定的测试图像对函数进行测试都花费了 23 毫秒。 另一方面,迭代次数为4,用于基准测试的总时间约为 95 毫秒。
GUI 测试
与执行特定任务的测试类相似,也可以创建用于测试 GUI 功能或小部件行为的单元测试。 在这种情况下,唯一的区别是需要为 GUI 提供鼠标单击,按键和类似的用户交互。 QTest支持通过模拟鼠标单击和其他用户交互来测试使用 Qt 创建的 GUI。 QTest命名空间中提供以下函数,以编写能够执行 GUI 测试的单元测试。 注意,几乎所有它们都依赖于以下事实:Qt 中的所有小部件和 GUI 组件都是QWidget的子类:
keyClick:可以用来模拟单击键盘上的按键。 为了方便起见,此函数有许多重载版本。 您可以选择提供修改键(ALT,CTRL等)和/或单击该键之前的延迟时间。keyClick不应与mouseClick混淆,稍后我们将对其进行介绍,它指的是一次按键并释放,从而导致单击。keyClicks:这与keyClick十分相似,但是它可以用于模拟序列中的按键单击,同样具有可选的修饰符或两者之间的延迟。keyPress:这再次类似于keyClick,但是它仅模拟按键的按下,而不释放它们。 如果我们需要模拟按下一个键,这将非常有用。keyRelease:这与keyPress相反,这意味着它仅模拟键的释放而没有按下键。 如果我们想使用keyPress模拟释放先前按下的键,这将很有用。keyEvent:这是键盘模拟函数的更高级版本,带有一个附加的动作参数,该参数定义是否按下,释放,单击(按下并释放)键,或者它是快捷键。mouseClick:类似于keyClick,但是它可以通过单击鼠标进行操作。 这就是为此函数提供的键是鼠标按钮(例如,左,右,中等)的原因。 键的值应该是Qt::MouseButton枚举的条目。 它还支持键盘修饰符和模拟点击之前的延迟时间。 此外,此函数和所有其他鼠标模拟函数还带有一个可选点(QPoint),其中包含要单击的小部件(或窗口)内的位置。 如果提供了一个空白点,或者如果省略了此参数,则模拟的点击将发生在小部件的中间。mouseDClick:这是mouseClick函数的双击版本。mousePress:这与mouseClick十分相似,但是仅模拟鼠标的按下,而不释放它。 如果要模拟按住鼠标按钮,这将很有用。mouseRelease:与mousePress相反,这意味着它仅模拟鼠标按钮的释放而没有按下。 这可以用来模拟一段时间后释放鼠标按钮。mouseMove:可以用来模拟在小部件上移动鼠标光标。 此函数必须提供点和延迟。 与其他鼠标交互函数类似,如果未设置任何点,则将鼠标移动到小部件的中间点。 与mousePress和mouseRelease结合使用时,此函数可用于模拟和测试拖放。
让我们创建一个简单的 GUI 测试以熟悉在实践中如何使用上述函数。 假设要测试已经创建的窗口或窗口小部件,则必须首先将其包含在 Qt 单元测试项目中。 因此,从创建单元测试项目开始,与在上一个示例以及我们的第一个测试项目中类似。 在项目创建期间,请确保还选择QtWidgets作为必需的模块之一。 然后,将窗口小部件类文件(可能是标头,源文件和 UI 文件)添加到测试项目。 在我们的示例中,我们假设我们有一个带有按钮和标签的简单 GUI。 每次按下该按钮,标签上的数字将乘以 2。 为了能够测试此功能或任何其他 GUI 功能,我们必须首先通过将其公开,确保表单,容器小部件或窗口上的小部件对测试类公开。 在实现此目的的许多方法中,最快,最简单的方法是在类声明中也以公共成员的身份定义相同的小部件。 然后,只需将ui变量(在使用“新建文件”或“项目”向导创建的所有 Qt 窗口小部件中找到的)变量中的类分配给整个类的成员。 假设我们窗口上的按钮和标签分别命名为nextBtn和infoLabel(使用设计器设计时),然后我们必须在类声明public成员中定义以下内容:
QPushButton *nextBtn;
QLabel *infoLabel;
并且,我们必须在构造器中分配它们,如下所示:
ui->setupUi(this);
this->nextBtn = ui->nextBtn;
this->infoLabel = ui->infoLabel;
确保在调用setupUi之后始终分配使用设计器和 UI 文件创建的窗口小部件; 否则,您的应用肯定会崩溃,因为直到调用setupUi才真正创建任何小部件。 现在,假设我们的小部件类称为TestableForm,我们可以在测试类中拥有一个专用的testGui插槽。 请记住,每次按下nextBtn时,infoLabel上的数字都将乘以 2,因此testGui函数中可以有类似以下内容:
void GuiTestTest::testGui()
{
TestableForm t;
QTest::mouseClick(t.nextBtn, Qt::LeftButton);
QCOMPARE(t.infoLabel->text(), QString::number(1));
QTest::mouseClick(t.nextBtn, Qt::LeftButton);
QCOMPARE(t.infoLabel->text(), QString::number(2));
QTest::mouseClick(t.nextBtn, Qt::LeftButton);
QCOMPARE(t.infoLabel->text(), QString::number(4));
// repeated until necessary
}
替换以下行也非常重要:
QTEST_APPLESS_MAIN(GuiTestTest)
添加以下行:
QTEST_MAIN(GuiTestTest)
否则,不会在幕后创建QApplication,并且测试将完全失败。 使用 Qt 测试框架测试 GUI 时要记住这一点很重要。 现在,如果您尝试运行单元测试,则将单击nextBtn小部件 3 次,然后每次检查infoLabel显示的值是否正确。 如果发生故障,它将记录在输出中。 这很容易,但是问题是,如果所需交互的数量增加了怎么办? 如果必须执行大量的 GUI 交互该怎么办? 为了克服这个问题,您可以结合使用数据驱动的测试和 GUI 测试来轻松重放 GUI 交互(或事件,在 Qt 框架中称为事件)。 请记住,要在测试类中具有测试函数的数据函数,必须创建一个新函数,该函数的名称应与_data完全相同。 因此,我们可以创建一个名为testGui_data的新函数,该函数准备交互集和结果集,并使用QFETCH将其传递给测试函数,就像我们在前面的示例中使用的那样:
void GuiTestTest::testGui_data()
{
QTest::addColumn<QTestEventList>("events");
QTest::addColumn<QString>("result");
QTestEventList mouseEvents; // three times
mouseEvents.addMouseClick(Qt::LeftButton);
mouseEvents.addMouseClick(Qt::LeftButton);
mouseEvents.addMouseClick(Qt::LeftButton);
QTest::newRow("mouse") << mouseEvents << "4";
QTestEventList keybEvents; // four times
keybEvents.addKeyClick(Qt::Key_Space);
keybEvents.addDelay(250);
keybEvents.addKeyClick(Qt::Key_Space);
keybEvents.addDelay(250);
keybEvents.addKeyClick(Qt::Key_Space);
keybEvents.addDelay(250);
keybEvents.addKeyClick(Qt::Key_Space);
QTest::newRow("keyboard") << keybEvents << "8";
}
QTestEventList类是 Qt 测试框架中的便捷类,可用于轻松创建 GUI 交互列表并对其进行仿真。 它包含添加所有我们之前提到的所有可能交互的功能,这些交互是可以使用 Qt Test 执行的可能事件的一部分。
要使用此数据函数,我们需要覆盖testGui函数,如下所示:
void GuiTestTest::testGui()
{
TestableForm t;
QFETCH(QTestEventList, events);
QFETCH(QString, result);
events.simulate(t.nextBtn);
QCOMPARE(t.infoLabel->text(), result);
}
类似于任何数据驱动的测试,QFETCH获取由数据函数提供的数据。 但是,在这种情况下,存储的数据为QEventList,并填充了一系列必需的交互操作。 此测试方法在重放错误报告中的一系列事件以重现,修复和进一步测试特定问题方面非常有效。
测试用例项目
在前面的部分及其相应的示例中,我们看到了一些简单的测试用例,并使用 Qt Test 函数对其进行了解决。 我们了解了数据驱动和 GUI 测试,以及如何结合两者以重放 GUI 事件并执行更复杂的 GUI 测试。 我们在每种情况下学到的相同方法都可以进一步扩展,以应用于更复杂的测试用例。 在本节中我们将学习确保在构建项目时自动执行测试。 当然,根据测试所需的时间和我们的喜好,我们可能希望轻松地暂时跳过自动测试,但是最终,在构建项目时,我们将需要轻松执行测试。 为了能够自动运行您的 Qt 项目的测试单元(我们将其称为主项目),首先,我们需要确保始终使用Subdirs模板创建它们,然后将单元测试项目配置为测试案例项目。 这也可以通过已经存在但不在Subdirs模板中的项目来完成。 只需按照本节提供的步骤将现有项目添加到Subdirs模板,并为其创建一个单元测试(配置为测试用例),该单元测试在您构建主项目时将自动运行:
-
首先使用 Qt Creator 中“欢迎”模式下的“新建项目”按钮创建一个新项目,或者从“文件”菜单中选择“新建文件”或“项目”项。
-
确保选择
Subdirs项目,如以下屏幕截图所示,然后单击“选择”:
- 为您的项目选择一个名称。 该名称可以与您的主项目名称相同。 假设它称为
computer_vision。 继续前进,然后在最后一个对话框中,单击“完成 & 添加子项目”按钮。 如果您是从头开始创建项目,则可以像整本书一样简单地创建项目。 否则,这意味着如果您想添加现有项目(假设在名为src的文件夹内),只需单击“取消”,然后将要为其构建测试的现有项目复制到此新创建的项目文件夹subdirs中。 然后,打开computer_vision.pro文件,并将其修改为类似于以下代码行:
TEMPLATE = subdirs
SUBDIRS += src
- 现在,您可以创建一个单元测试项目,该项目也是
computer_vision子目录项目的子项目,并对它进行编程以测试src文件夹中存在的类(您的主项目,它是实际的应用本身) )。 因此,再次从项目窗格中右键单击computer_vision,然后通过选择“新建子项目”,开始使用在上一节中学到的所有内容来创建单元测试。 - 创建测试后,无论使用哪个主项目查看测试结果,都应该能够单独运行它。 但是,要确保将其标记为测试用例项目,需要将以下代码行添加到单元测试项目的
*.pro文件中:
CONFIG += testcase
- 最后,您需要在 Qt Creator 中切换到项目模式,并将检查添加到
Make arguments字段中,如以下屏幕截图所示。 确保首先使用“详细信息”扩展器按钮扩展“制作”部分; 否则,它将不可见:
现在,无论您是否专门运行单元测试项目都没有关系,并且每次运行主项目或尝试构建它时,测试都会自动执行。 这是一种非常有用的技术,可确保对一个库的更改不会对另一个库造成负面影响。 关于此技术要注意的重要一点是,测试结果实际上会影响构建结果。 意思是,您会在构建测试时注意到测试是否自动失败,并且测试结果将在 Qt Creator 的编译器输出窗格中可见,可以使用底部的栏或按ALT + 4键。
总结
在本章中,您学习了如何使用 Qt Creator 进行调试以及它提供的功能,以便进一步分析代码,发现问题并尝试使用断点,调用栈查看器等对其进行修复。 这只是使用调试器可以完成的工作的一点点尝试,它的目的是让您准备自己继续使用调试器,并养成自己的编码和调试习惯,从而可以帮助您克服更多编程问题。 缓解。 除了调试和开发人员级别的测试外,我们还了解了 Qt 中的单元测试,这对于使用 Qt 框架编写的越来越多的应用和项目尤其重要。 测试自动化是当今应用开发行业中的热门话题之一,对 Qt 测试框架有清晰的想法将有助于您开发更好和可靠的测试。 习惯于为项目编写单元测试非常重要,是的,即使是非常小的项目也是如此。 对于初学者或业余爱好者而言,测试应用和避免回归的成本并不容易理解,因此,为在开发生涯的后期肯定会遇到的事情做好准备是一个好主意。
在接近本书最后几章的同时,我们也越来越关注使用 Qt 和 OpenCV 进行应用开发的最后阶段。 因此,在下一章中,您将学习有关向最终用户部署应用的知识。 您还将了解应用的动态和静态链接,以及创建可以轻松安装在具有不同操作系统的计算机上的应用包。 下一章将是我们在台式机平台上使用 OpenCV 和 Qt 进行计算机视觉之旅的最后一章。
十一、链接与部署
在前几章中了解了使用 Qt Creator 和 Qt Test 框架调试和测试应用之后,我们进入了应用开发的最后阶段之一,即将应用部署到最终用户。 该过程本身具有多种变体,并且可以根据目标平台采取很多不同的形式,但是它们都有一个共同点,就是以一种可以在目标平台中简单地执行它的方式打包应用。 困扰应用的依赖项。 请记住,并非所有目标平台(无论是 Windows,MacOS 还是 Linux)都具有 Qt 和 OpenCV 库。 因此,如果继续进行操作,仅向应用的用户提供应用的可执行文件,它很可能甚至不会开始执行,更不用说正常工作了。
在本章中,我们将通过学习创建应用包(通常是包含所有必需文件的文件夹)的正确方法来解决这些问题,该应用包可以在我们自己的计算机以及开发环境以外的其他计算机上简单执行,而无需用户照顾任何必需的库。 为了能够理解本章中描述的一些概念,我们首先需要了解创建应用可执行文件时幕后发生情况的一些基础知识。 我们将讨论构建过程的三个主要阶段,即预处理,编译和链接应用可执行文件(或库)。 然后,我们将学习可以用两种不同的方式完成链接,即动态链接和静态链接。 我们将讨论它们之间的差异以及它们如何影响部署,以及如何在 Windows,MacOS 和 Linux 操作系统上动态或静态地构建 Qt 和 OpenCV 库。 之后,我们将为所有提到的平台创建并部署一个简单的应用。 我们将借此机会还了解 Qt Installer 框架,以及如何创建网站下载链接,闪存驱动器或任何其他媒体上交付给最终用户的安装程序。 到本章结束时,我们将仅向最终用户提供他们执行我们的应用所需的内容,仅此而已。
本章将讨论的主题包括:
- Qt 和 OpenCV 框架的动态和静态链接
- 配置 Qt 项目来使用静态库
- 部署使用 Qt 和 OpenCV 编写的应用
- 使用 Qt Installer 框架创建跨平台安装程序
幕后制作过程
当我们通过编辑一些 C++ 头文件或源文件,在项目文件中添加一些模块并最后按下运行按钮来编写应用时,这似乎很自然。 但是,在幕后还有一些流程,这些流程通过按正确的顺序由 IDE(在我们的情况下为 Qt Creator)执行,从而使开发过程具有顺畅自然的感觉。 通常,当我们按 Qt Creator 或任何其他 IDE 的运行或构建按钮时,有三个主要过程可导致创建可执行文件(例如*.exe)。 这是这三个过程:
- 预处理
- 编译
- 链接
这是从源文件创建应用时所经过的过程和阶段的非常高级的分类。 这种分类允许对过程进行更简单的概述,并以更简单的方式大致了解其目的。 但是,这些过程包括许多子过程和阶段,不在本书的讨论范围之内,因为我们对以一种或另一种方式影响部署过程的过程最为感兴趣。 但是,您可以在线阅读它们,也可以阅读有关编译器和链接器的任何书籍。
预处理
此阶段是在将源代码传递到实际编译器之前将其转换为最终状态的过程。 为了进一步解释这一点,请考虑所有包含的文件,各种编译器指令,或更重要的是,对于 Qt 框架,请考虑不属于标准 C++ 语言的 Qt 特定的宏和代码。 在第 3 章,“创建全面的 Qt + OpenCV 项目”中,我们了解了uic和moc,它们可以转换使用 Qt 特定宏和准则编写的 UI 文件和 C++ 代码。 转换为标准 C++ 代码(确切地说,是在最新版本的 Qt 中,转换为 C++ 11 或更高版本)。 即使这些不是对 C++ 源代码执行的标准预处理的一部分,但是当我们使用 Qt 框架或基于自己的规则集生成代码的框架时,它们仍处于大致相同的阶段。
下图描述了预处理阶段,该阶段与使用uic,moc等进行 Qt 特定的代码生成相结合:
该过程的输出在上一个图像中被标记为用于编译器的单个输入文件,显然是一个单个文件,其中包含用于编译源代码的所有必需标记和信息。 然后将该文件传递给编译器和编译阶段。
编译
在构建过程的第二个主要阶段,编译器获取预处理器的输出,或者在我们的示例中为预处理阶段,该输出还包括uic和moc生成的代码,并将其编译为机器代码。 。 可以在构建过程中保存并重复使用该机器代码,因为只要不更改源文件,生成的机器代码也将保持不变。 通过确保重复使用各个单独编译的对象(例如*.obj或*.lib文件),而不是在每次构建项目时都生成该对象,此过程有助于节省大量时间。 所有这一切的好处是,IDE 会照顾它,我们通常不需要理会它。 然后,由编译器生成的输出文件将传递到链接器,然后我们进入链接阶段。
链接
链接器是在构建过程链中被调用的最后一个程序,其目标是链接由编译器生成的对象以生成可执行文件或库。 这个过程对我们而言至关重要,因为它会对部署应用的方式,可执行文件的大小等产生巨大影响。 为了更好地理解这一点,首先我们需要讨论两种可能的链接类型之间的区别:
- 动态链接
- 静态链接
动态链接是链接编译器生成的对象的过程,方法是将函数的名称放在生成的可执行文件或库中,以使该特定函数的实际代码位于共享库(例如*.dll文件)中 ),并且库的实际链接和加载是在运行时完成的。 动态链接的最明显的优缺点是:
- 您的应用将在运行时需要共享库,因此您必须将它们与应用的可执行文件一起部署,并确保可以访问它们。 例如,在 Windows 上,可以通过将其复制到与应用可执行文件相同的文件夹中来完成,或者在 Linux 上,可以将它们放在默认库路径(例如
/lib/)中来完成。 - 动态链接通过将应用的各个部分保留在单独的共享库文件中,提供了极大的灵活性。 这样,共享库可以单独更新,而无需重新编译应用的每个部分。
与动态链接相反,可以使用静态链接将所有必需的代码链接到生成的可执行文件中,从而创建静态库或可执行文件。 您可以猜测,使用静态库与使用共享库具有完全相反的优点和缺点,它们是:
- 您不需要部署用于构建应用的静态库,因为它们的所有代码实际上都已复制到生成的可执行文件中
- 应用可执行文件的大小将变大,这意味着更长的初始加载时间和更大的文件要部署
- 对库或应用任何部分的任何更改都需要对其所有组成部分进行完整的重建过程
在整本书中,特别是在为我们全面的计算机视觉应用开发插件时,我们使用了共享库和动态链接。 这是因为当我们使用所有默认的 CMake 设置构建 OpenCV,并使用第 1 章,“OpenCV 和 Qt 简介”中的官方安装程序安装 Qt 框架时, 动态链接和共享的库(Windows 上为*.dll,MacOS 上为*.dylib等)。 不过,在下一节中,我们将学习如何使用它们的源代码静态地构建 Qt 和 OpenCV 库。 通过使用静态链接库,我们可以创建不需要在目标系统上存在任何共享库的应用。 这可以极大地减少部署应用所需的工作量。 在 MacOS 和 Linux 操作系统中的 OpenCV 尤其如此,您的用户除了复制和运行您的应用外完全不需要执行任何操作,而他们将需要采取一些措施或必须执行一些脚本操作以确保执行您的应用时,所有必需的依赖项均已就绪。
构建 OpenCV 静态库
让我们从 OpenCV 开始,它遵循与构建动态库几乎相同的指令集来构建静态库。 您可以参考第 1 章,“OpenCV 和 Qt 简介”以获得更多信息。 只需下载源代码,解压缩并使用 CMake 来配置您的构建,如本章所述。 但是,这次,除了选中BUILD_opencv_world选项旁边的复选框外,还要取消选中每个选项旁边的复选框,以确保关闭了以下所有选项:
BUILD_DOCSBUILD_EXAMPLESBUILD_PERF_TESTSBUILD_TESTSBUILD_SHARED_LIBSBUILD_WITH_STATIC_CRT(仅在 Windows 上可用)
关闭前四个参数仅是为了加快构建过程,并且是完全可选的。 禁用BUILD_SHARED_LIBS仅启用 OpenCV 库的静态(非共享)构建模式,而最后一个参数(在 Windows 上)有助于避免库文件不兼容。 现在,如果您使用第 1 章,“OpenCV 和 Qt 简介”中提供的相同说明开始构建过程,这次,而不是共享库(例如,在 Windows 上, *.lib和*.dll文件),您将在安装文件夹中得到静态链接的 OpenCV 库(同样,在 Windows 中,仅*.lib文件,没有任何*.dll文件)。 接下来需要做的是将项目配置为使用 OpenCV 静态库。 通过使用*.pri文件,或直接将它们添加到 Qt 项目*.pro文件中,您需要以下几行,以便您的项目可以使用 OpenCV 静态库:
win32: {
INCLUDEPATH += "C:/path_to_opencv_install/include"
Debug: {
LIBS += -L"C:/path_to_opencv_install/x86/vc14/staticlib"
-lopencv_world330d
-llibjpegd
-llibjasperd
-littnotifyd
-lIlmImfd
-llibwebpd
-llibtiffd
-llibprotobufd
-llibpngd
-lzlibd
-lipp_iw
-lippicvmt
}
Release: {
LIBS += -L"C:/path_to_opencv_install/x86/vc14/staticlib"
-lopencv_world330
-llibjpeg
-llibjasper
-littnotify
-lIlmImf
-llibwebp
-llibtiff
-llibprotobuf
-llibpng
-lzlib
-lipp_iw
-lippicvmt
}
}
前面代码中库的顺序不是随机的。 这些库需要以其依赖关系的正确顺序包括在内。 您可以在 Visual Studio 2015 中自己检查一下,方法是从主菜单中选择Project,然后选择Project Build Order…。 对于 MacOS 用户,必须在前面的代码中将win32替换为unix: macx,并且库的路径必须与您的构建文件夹中的路径匹配。 对于 Linux,您可以使用与动态库相同的pkgconfig行,如下所示:
unix: !macx{
CONFIG += link_pkgconfig
PKGCONFIG += opencv
}
请注意,即使在 Windows OS 上以静态方式构建 OpenCV 时,输出文件夹中仍将有一个库作为动态库,即opencv_ffmpeg330.dll。 您无需将其包含在*.pro文件中; 但是,您仍然需要将其与应用可执行文件一起部署,因为 OpenCV 本身依赖于它才能支持某些众所周知的视频格式和编码。
构建 Qt 静态库
默认情况下,官方 Qt 安装程序仅提供动态 Qt 库。 在第 1 章,“OpenCV 和 Qt 简介”中也是如此,当我们使用以下链接提供的安装程序在开发环境中安装 Qt 时。
因此,简单来说,如果要使用静态 Qt 库,则必须使用其源代码自行构建它们。 您可以按照此处提供的步骤来配置,构建和使用静态 Qt 库:
- 为了能够构建一组静态 Qt 库,您需要首先从 Qt 下载网站下载源代码。 通常将它们作为包含所有必需源代码的单个压缩文件(
*.zip,*.tar.xz等)提供。 在我们的情况下(Qt 版本 5.9.1),您可以使用以下链接下载 Qt 源代码。
下载qt-everywhere-opensource-src-5.9.1.zip(或*.tar.xz),然后继续下一步。
- 将源代码提取到您选择的文件夹中。 我们假定提取的文件夹名为
Qt_Src,并且位于c:/dev文件夹中(在 Windows 操作系统上)。 因此,假设我们提取的 Qt 源代码的完整路径为c:/dev/Qt_Src。
对于 MacOS 和 Linux 用户,该路径可能类似于Users/amin/dev/Qt_Src,因此,如果您使用的是上述操作系统之一而不是 Windows,则需要在提供的所有引用它的说明中将其替换。 现在应该已经很明显了。
- 现在,您需要先处理一些依赖关系,然后再继续下一步。 MacOS 和 Linux 用户通常不需要执行任何操作,因为默认情况下,所有必需的依赖项都存在于这些操作系统上。 但是,这不适用于 Windows 用户。 通常,在从源代码构建 Qt 之前,计算机上必须存在以下依赖项:
- ActivePerl。
- Python,您需要版本 2.7.X,而 X 已被最新的现有版本替换,在撰写本书时为 14。
- 为了方便 Windows 用户,在 Qt 源代码 ZIP 文件的
gnuwin32子文件夹内提供了 Bison。 只需确保将c:/dev/Qt_Src/gnuwin32/bin添加到PATH环境变量即可。 - Flex 与 Bison 相同,位于
gnuwin32子文件夹内,需要添加到PATH中。 - 在
gnuwin32子文件夹内提供了与 Bison 和 Flex 相同的 GNUgperf,需要将其添加到PATH中。
为确保一切正常,请尝试运行相关命令以执行我们刚刚提到的每个依赖项。 可能是您忘记将其中一个依赖项添加到PATH的情况,或者对于 MacOS 和 Linux 用户,由于任何可能的原因,它们已被删除并且不存在。 仅在命令提示符(或终端)中执行以下每个命令并确保您不会遇到无法识别或找不到的错误类型就足够了:
perl
python bison
flex gperf
- 现在,在 Windows 上运行 VS2015 的开发人员命令提示符。 在 MacOS 或 Linux 上,运行终端。 您需要运行一组连续的命令,以根据源代码配置和构建 Qt。 该配置是此步骤中最关键的部分,是通过使用
configure命令完成的。configure命令位于 Qt 源文件夹的根目录中,接受以下参数(请注意,实际的参数集很长,因此我们可以满足使用最广泛的参数的要求):
此处提供的参数列表应足以构建具有更多或更少默认设置的静态版本的 Qt 框架:
- 现在是时候配置我们的 Qt 构建了。 首先,我们需要使用以下命令切换到 Qt 源代码文件夹:
cd c:/dev/Qt_Src"
- 然后通过键入以下命令开始配置:
configure -opensource -confirm-license -static -skip webengine
-prefix "c:devQtStatic" -platform win32-msvc
我们提供-skip webengine的原因是因为(编写本书时)目前尚不支持静态构建 Qt WebEngine 模块。 另请注意,我们提供了-prefix参数,这是我们要获取静态库的文件夹。您需要谨慎使用此参数,因为您不能稍后再复制它,并且由于您的构建配置, 静态库仅在它们保留在磁盘上的该位置时才起作用。 我们已经在参数列表中描述了其余参数。
您还可以将以下内容添加到configure命令中,以跳过可能不需要的部分并加快构建过程,因为这将花费很长时间:
-nomake tests -nomake examples
在 MacOS 和 Linux 上,必须从configure命令中省略以下部分。 这样做的原因仅仅是该平台将被自动检测的事实。 在 Windows 上当然也是如此,但是由于我们要强制 Qt 库的 32 位版本(以支持更大范围的 Windows 版本),因此我们将坚持使用此参数:
-platform win32-msvc
根据您的计算机规格,配置过程不会花费太长时间。 配置完成后,您应该会看到类似以下的输出,否则,您需要再次仔细地执行上述步骤:
Qt is now configured for building. Just run 'nmake'.
Once everything is built, you must run 'nmake install'.
Qt will be installed into 'c:devQtStatic'.
Prior to reconfiguration, make sure you remove any leftovers from
the previous build.
请注意,在 MacOS 和 Linux 上,上述输出中的nmake将替换为make。
- 正如配置输出中提到的那样,您需要输入
build和install命令。
在 Windows 上,使用以下命令:
nmake
nmake install
在 MacOS 和 Linux 上,使用以下命令:
make
make install
请注意,由于 Qt 框架包含许多需要构建的模块和库,因此第一个命令通常需要很长时间才能完成(取决于您的计算机规格),因此在此步骤中需要耐心等待。 无论如何,如果您到目前为止已经完全按照提供的所有步骤进行操作,则应该没有任何问题。
重要的是要注意,如果您使用计算机受限区域中的安装文件夹(-prefix参数),则必须确保使用管理员级别(如果使用 Windows)运行命令提示符实例(如果使用 Windows) 带有sudo前缀的build和install命令(如果您使用的是 MacOS 或 Linux)。
- 运行
install命令后,应该将静态 Qt 库放入配置过程中作为前缀参数提供的文件夹(即安装文件夹)中。 因此,在此步骤中,您需要在 Qt Creator 中将这组新建的 Qt 静态库添加为工具包。 为此,请打开 Qt Creator,然后从主菜单中选择“工具”,然后选择“选项”。 从左侧的列表中,选择Build & Run,然后选择Qt Versions选项卡。 现在,按“添加”按钮,然后浏览至Qt build安装文件夹,选择qmake.exe,在本例中,该文件应位于C:devQtStaticbin文件夹内。 以下屏幕截图显示了正确添加新的 Qt 构建后 Qt 版本标签中的状态:
- 现在,切换到“套件”选项卡。 您应该能够看到整本书中用来构建 Qt 应用的工具包。 例如,在 Windows 上,它应该是 Desktop Qt 5.9.1 MSVC2015 32bit。 选择它并按“克隆”按钮,然后选择在上一步的“Qt 版本”选项卡中设置的 Qt 版本(如果您在那里看不到自己的版本,则可能需要按一次“应用”按钮,然后按“将显示在组合框中)。 另外,请确保从其名称中删除
Clone of,并在其后附加Static一词,以便于区分。 以下屏幕快照表示“工具”选项卡的状态及其配置方式:
关于构建和配置静态 Qt 套件的问题。 现在,您可以使用与默认 Qt 套件(动态套件)完全相同的方式开始使用它创建 Qt 项目。 您唯一需要注意的就是在创建和配置 Qt 项目时将其选择为目标套件。 让我们用一个简单的例子来做到这一点。 首先创建一个 Qt Widgets 应用,并将其命名为StaticApp。 在“工具包选择”页面上,确保选择了新建的静态 Qt 工具包,然后继续按“下一步”,直到进入 Qt 代码编辑器。 以下屏幕快照描述了“工具包选择”页面及其外观(在 Window OS 上):
无需进行太多更改或添加任何代码,只需按“运行”按钮即可构建并执行该项目。 现在,如果浏览到该项目的build文件夹,您会注意到可执行文件的大小比我们使用默认动态工具包进行构建时的大小要大得多。 为了进行比较,在 Windows 操作系统和调试模式下,动态构建的版本应小于 1 兆字节,而静态构建的版本应约为 30 兆字节,甚至更多。 如前所述,这样做的原因是所有必需的 Qt 代码现在都链接到可执行文件中。 尽管严格说来,从技术上讲它并不正确,但是您可以将其视为将库(*.dll文件等)嵌入可执行文件本身中。
现在,让我们尝试在示例项目中也使用静态 OpenCV 库。 只需将所需的附加内容添加到StaticApp.pro文件中,然后尝试使用几个简单的 OpenCV 函数(例如imread,dilate和imshow)来测试一组静态 OpenCV 库。 如果现在检查静态链接的可执行文件的大小,您会发现文件大小现在更大。 这样做的明显原因是所有必需的 OpenCV 代码都链接到可执行文件本身。
部署 Qt + OpenCV 应用
向最终用户提供应用包是非常重要的,该包包含它能够在目标平台上运行所需的一切,并且在照顾所需的依赖方面几乎不需要用户付出任何努力。 为应用实现这种开箱即用的条件主要取决于用于创建应用的链接的类型(动态或静态),以及目标操作系统。
使用静态链接的部署
静态部署应用意味着您的应用将独立运行,并且消除了几乎所有需要的依赖项,因为它们已经在可执行文件内部。 只需确保在构建应用时选择发布模式即可,如以下屏幕截图所示:
在发布模式下构建应用时,您只需选择生成的可执行文件并将其发送给用户。
如果尝试将应用部署到 Windows 用户,则在执行应用时可能会遇到类似于以下错误:
发生此错误的原因是,在 Windows 上,即使以静态方式构建 Qt 应用,您仍然需要确保目标系统上存在 Visual C++ 可再发行组件。 这是使用 Microsoft Visual C++ 生成的 C++ 应用所必需的,并且所需的可再发行版本与计算机上安装的 Microsoft Visual Studio 相对应。 在我们的案例中,这些库的安装程序的正式名称是 Visual Studio 2015 的 Visual C++ 可再发行组件,可以从以下链接下载。
通常的做法是在我们的应用的安装程序中包含可再发行文件的安装程序,如果尚未安装,请对其进行静默安装。 大多数情况下,您在 Windows PC 上使用的大多数应用都会执行此过程,而您甚至没有注意到它。
我们已经简要地讨论了静态链接的优点(要部署的文件较少)和缺点(可执行文件的大小较大)。 但是,当在部署环境中使用它时,还需要考虑更多的复杂性。 因此,当使用静态链接部署应用时,这是另一个(更完整的)缺点列表:
- 构建花费更多的时间,并且可执行文件的大小越来越大。
- 您不能混合使用静态和共享(动态)Qt 库,这意味着您不能使用插件的功能和扩展应用而无需从头开始构建所有内容。
- 从某种意义上说,静态链接意味着隐藏用于构建应用的库。 不幸的是,并非所有库都提供此选项,并且不遵守该选项可能导致应用出现许可问题。 之所以会出现这种复杂性,部分原因是 Qt 框架使用了一些第三方库,这些库没有提供与 Qt 本身相同的许可选项。 谈论许可问题不是适合本书的讨论,因此,当您计划使用 Qt 库的静态链接创建商业应用时,您必须一定要小心。 有关 Qt 内第三方库使用的许可证的详细列表,您可以始终通过以下链接引用 Qt 网页中使用的许可证。
有关 Qt 模块中使用的各种 LGPL 许可证及其版本(以及可在网上找到的许多其他开源软件)的完整参考,请参考以下链接。
您还可以使用以下链接完整讨论有关选择 Qt 开源许可证之前需要了解的知识。
静态链接,即使有我们刚刚提到的所有缺点,仍然是一种选择,在某些情况下,如果您可以遵守 Qt 框架的许可选项,那么它还是一个很好的选择。 例如,在 Linux 操作系统中,为我们的应用创建安装程序需要额外的工作和精力,静态链接可以极大地减少部署应用所需的工作量(仅复制和粘贴)。 因此,是否使用静态链接的最终决定主要取决于您以及您打算如何部署应用。 当您对可能的链接和部署方法进行了概述时,到本章末尾,制定此重要决定将变得更加容易。
使用动态链接的部署
使用共享库(或动态链接)部署使用 Qt 和 OpenCV 构建的应用时,需要确保应用的可执行文件能够访问 Qt 和 OpenCV 的运行时库,以便加载和使用它们。 运行时库的这种可到达性或可见性取决于操作系统,可能具有不同的含义。 例如,在 Windows 上,您需要将运行时库复制到应用可执行文件所在的文件夹中,或将它们放在附加到PATH环境值的文件夹中。
Qt 框架提供了命令行工具,以简化 Windows 和 MacOS 上 Qt 应用的部署。 如前所述,您需要做的第一件事是确保您的应用是在“发布”模式而不是“调试”模式下构建的。 然后,如果您使用的是 Windows,请首先将可执行文件(假设我们将其称为app.exe)从构建文件夹复制到一个单独的文件夹(我们将其称为deploy_path),然后使用命令执行以下命令行实例:
cd deploy_path
QT_PATHbinwindeployqt app.exe
windeployqt工具是一个部署帮助工具,可简化将所需的 Qt 运行时库复制到与应用可执行文件相同的文件夹中的过程。 它只是将可执行文件作为参数,并在确定用于创建可执行文件的模块之后,复制所有必需的运行时库以及所有其他必需的依赖项,例如 Qt 插件,翻译等。 这将处理所有必需的 Qt 运行时库,但是我们仍然需要处理 OpenCV 运行时库。 如果您遵循第 1 章,“OpenCV 和 Qt 简介”中的所有步骤来动态构建 OpenCV 库,则只需手动复制opencv_world330.dll和opencv_ffmpeg330.dll 将文件从 OpenCV 安装文件夹(在x86vc14bin文件夹内)复制到应用可执行文件所在的文件夹中。
在本书的早期章节中构建 OpenCV 时,我们并没有真正受益于打开BUILD_opencv_world选项的好处。 但是,现在应该清楚的是,这通过以下方式简化了 OpenCV 库的部署和使用:在*.pro文件中只要求 LIBS 的单个条目,并且在以下情况下仅手动复制单个文件(不计算ffmpeg库): 部署 OpenCV 应用。 还应注意的是,即使您在项目中不需要或不使用所有 OpenCV 代码的所有模块,此方法也存在沿应用复制所有 OpenCV 代码的缺点。
还请注意,在 Windows 上,如在“使用静态链接进行部署”一节中所述,您仍然需要类似地向应用的最终用户提供 Microsoft Visual C++ 重分发版。
在 MacOS 操作系统上,还可以轻松部署使用 Qt 框架编写的应用。 因此,可以使用 Qt 提供的macdeployqt命令行工具。 与windeployqt相似,该文件接受 Windows 可执行文件并用所需的库填充同一文件夹,macdeployqt接受 MacOS 应用捆绑包,并通过将所有必需的 Qt 运行时复制为捆绑包内部的私有框架,使其可部署。 这是一个例子:
cd deploy_path
QT_PATH/bin/macdeployqt my_app_bundle
(可选)您还可以提供一个附加的-dmg参数,该参数导致创建 macOS *.dmg(磁盘图像)文件。 至于使用动态链接时 OpenCV 库的部署,您可以使用 Qt Installer 框架(我们将在下一节中学习),第三方供应商或确保所需运行时库的脚本来创建安装程序。 复制到其所需的文件夹。 这是因为以下事实:仅将运行时库(无论是 OpenCV 还是其他文件)复制到与应用可执行文件相同的文件夹中,并不能使它们对 MacOS 上的应用可见。 这同样适用于 Linux 操作系统,不幸的是,该操作系统甚至还没有用于部署 Qt 运行时库的工具(至少目前是这样),因此除了 OpenCV 库,我们还需要照顾 Qt 库,方法是受信任的第三方供应商(您可以在线搜索)或通过使用 Qt 本身提供的跨平台安装程序,再结合一些脚本来确保执行我们的应用时所有内容都就位。
Qt 安装程序框架
Qt 安装程序框架允许您为 Windows,MacOS 和 Linux 操作系统创建 Qt 应用的跨平台安装程序。 它允许创建标准的安装程序向导,在该向导中,用户会通过提供所有必要信息的连续对话框进入,并最终显示安装应用时的进度等,这与您可能遇到的大多数安装类似,尤其是安装 Qt 框架本身。 Qt 安装程序框架基于 Qt 框架本身,但以不同的包提供,并且不需要计算机上存在 Qt SDK(Qt 框架,Qt Creator 等)。 也可以使用 Qt Installer 框架来为任何应用(不仅仅是 Qt 应用)创建安装包。
在本节中,我们将学习如何使用 Qt Installer 框架创建基本的安装程序,该程序将在目标计算机上安装应用并复制所有必要的依赖项。 结果将是一个可执行的安装程序文件,您可以将其放在 Web 服务器上进行下载,或以 USB 记忆棒或 CD 或任何其他媒体类型提供。 该示例项目将帮助您自己着手解决 Qt Installer 框架的许多强大功能。
您可以使用以下链接下载并安装 Qt 安装程序框架。 使用此链接或其他任何下载源时,请确保仅下载最新版本。 目前,最新版本是 3.0.2。
下载并安装 Qt Installer 框架之后,可以开始创建 Qt Installer 框架创建安装程序所需的必需文件。 您可以通过简单地浏览到 Qt Installer 框架并从examples文件夹复制tutorial文件夹来完成此操作,如果要快速重命名和重新编辑所有文件并创建自己的文件夹,这也是一个快速安装模板。 我们将采用另一种方式手动创建它们; 首先,因为我们想了解 Qt Installer 框架所需文件和文件夹的结构,其次,因为它仍然非常容易和简单。 以下是创建安装程序的必需步骤:
- 假设您已经完成了 Qt 和 OpenCV 应用的开发,则可以从创建一个包含安装程序文件的新文件夹开始。 假设此文件夹名为
deploy。 - 在
deploy文件夹中创建一个 XML 文件,并将其命名为config.xml。 此 XML 文件必须包含以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<Installer>
<Name>Your application</Name>
<Version>1.0.0</Version>
<Title>Your application Installer</Title>
<Publisher>Your vendor</Publisher>
<StartMenuDir>Super App</StartMenuDir>
<TargetDir>@HomeDir@/InstallationDirectory</TargetDir>
</Installer>
确保用与您的应用相关的信息替换前面代码中的必需 XML 字段,然后保存并关闭此文件:
-
现在,在
deploy文件夹内创建一个名为packages的文件夹。 该文件夹将包含您希望用户能够安装的单个包,或者使它们成为必需或可选的包,以便用户可以查看并决定要安装的包。 -
对于使用 Qt 和 OpenCV 编写的更简单的 Windows 应用,通常仅包含一个包就可以运行您的应用,甚至可以静默安装 Microsoft Visual C++ 重分发版。 但是对于更复杂的情况,尤其是当您想更好地控制应用的各个可安装元素时,您还可以使用两个或多个包,甚至子包。 通过为每个包使用类似域的文件夹名称来完成此操作。 每个包文件夹都可以具有类似
com.vendor.product的名称,其中,供应商和产品将被开发人员名称或公司及应用所代替。 可以通过在父包的名称后添加.subproduct来标识包的子包(或子组件)。 例如,您可以在packages文件夹中包含以下文件夹:
com.vendor.product
com.vendor.product.subproduct1
com.vendor.product.subproduct2
com.vendor.product.subproduct1.subsubproduct1
...
我们可以根据需要选择任意数量的产品(包装)和子产品(子包装)。 对于我们的示例案例,让我们创建一个包含可执行文件的文件夹,因为它描述了所有可执行文件,您可以通过将其他包简单地添加到packages文件夹中来创建其他包。 让我们将其命名为com.amin.qtcvapp。 现在,请执行以下必需步骤:
- 现在,在我们创建的新包文件夹
com.amin.qtcvapp文件夹中创建两个文件夹。 将它们重命名为data和meta。 这两个文件夹必须存在于所有包中。 - 将您的应用文件复制到
data文件夹中。 该文件夹将完全按原样提取到目标文件夹中(我们将在后面的步骤中讨论如何设置包的目标文件夹)。 如果您打算创建多个包,请确保以合理的方式正确分离其数据。 当然,如果不这样做,您将不会遇到任何错误,但是您的应用的用户可能会感到困惑,例如,通过跳过应始终安装的包并最终安装已安装的应用,这行不通。 - 现在,切换到
meta文件夹并在该文件夹中创建以下两个文件,并为每个文件提供的代码填充它们。
package.xml文件应包含以下内容。 无需提及,您必须使用与包相关的值填充 XML 内的字段:
<?xml version="1.0" encoding="UTF-8"?>
<Package>
<DisplayName>The component</DisplayName>
<Description>Install this component.</Description>
<Version>1.0.0</Version>
<ReleaseDate>1984-09-16</ReleaseDate>
<Default>script</Default>
<Script>installscript.qs</Script>
</Package>
前一个 XML 文件中的脚本(可能是安装程序创建中最重要的部分)是指 Qt 安装程序脚本(*.qs文件),其名称为installerscript.qs,可用于进一步自定义包 ,其目标文件夹等。 因此,让我们在meta文件夹中创建一个具有相同名称(installscript.qs)的文件,并在其中使用以下代码:
function Component()
{
// initializations go here
}
Component.prototype.isDefault = function()
{
// select (true) or unselect (false) the component by default
return true;
}
Component.prototype.createOperations = function()
{
try {
// call the base create operations function
component.createOperations();
} catch (e) {
console.log(e);
}
}
这是最基本的组件脚本,可自定义我们的包(很好,它仅执行默认操作),并且可以选择扩展它以更改目标文件夹,在“开始”菜单或桌面(在 Windows 上)中创建快捷方式,等等。 。 密切注意 Qt Installer 框架文档并了解其脚本,以便能够创建功能更强大的安装程序,这些程序可以自动将应用的所有必需依赖项放置到位,是一个好主意。 您还可以浏览 Qt Installer 框架examples文件夹内的所有示例,并了解如何处理不同的部署案例。 例如,您可以尝试为 Qt 和 OpenCV 依赖关系创建单独的包,并允许用户取消选择它们,前提是他们的计算机上已经具有 Qt 运行时库。
- 最后一步是使用
binarycreator工具来创建我们的单个和独立安装程序。 只需使用命令提示符(或终端)实例运行以下命令:
binarycreator -p packages -c config.xml myinstaller
binarycreator位于 Qt Installer 框架bin文件夹内。 它需要我们已经准备好的两个参数。 -p之后必须是我们的packages文件夹,-c之后必须是配置文件(或config.xml)文件。 执行此命令后,您将获得myinstaller(在 Windows 上,可以在其后附加*.exe),可以执行该命令来安装应用。 该单个文件应包含运行您的应用所需的所有必需文件,其余部分将得到处理。 您只需要提供此文件的下载链接,或通过 CD 将其提供给您的用户。
以下是此默认和最基本的安装程序中将面对的对话框,其中包含安装应用时可能会遇到的大多数常见对话框:
如果转到安装文件夹,您会注意到其中包含的文件比放入包数据文件夹中的文件多。 安装程序需要这些文件来处理修改和卸载应用。 例如,您的应用用户可以通过执行maintenancetool可执行文件轻松卸载您的应用,这将产生另一个简单且用户友好的对话框来处理卸载过程:
总结
无论您是否可以在目标计算机上轻松安装并在目标计算机上轻松使用,这都意味着赢得或失去大量用户。 特别是对于非专业用户而言,必须确保创建和部署包含所有必需依赖项的安装程序,并且可以在目标平台上直接使用。 在本章中,我们对此进行了相当多的讨论。 我们了解了构建过程以及所选择的链接方法如何完全改变部署体验。 我们了解了现有的 Qt 工具,以简化 Windows 和 MacOS 上的部署过程。 请注意,这些工具包含的参数比我们在本章中看到的要多得多,因此值得您自己深入研究,并尝试各种参数以了解它们对自己的影响。 在本章的最后一部分,我们了解了 Qt Installer 框架,并通过使用它创建了一个简单的安装程序。 我们学习了如何创建使用安装程序在目标系统上提取的包。 可以使用此相同技能将所有依赖项放入其所需的文件夹中。 例如,可以将 OpenCV 库添加到包中,并在安装时将它们放在 Linux 操作系统的/usr/lib/或/usr/local/lib/中,以便您的应用可以毫无问题地访问它们。 有了这最后一组技能,我们现在已经熟悉了开发人员(尤其是计算机视觉开发人员)必须知道的开发周期的大多数现有阶段。
在本书的最后一章中,我们将向您介绍 Qt Quick 和 QML。 我们将学习如何使用 Qt 的功能和 QML 的简单性来创建漂亮的 UI。 我们还将学习如何组合 C++ 和 QML 代码,以编写使用第三方框架(例如 OpenCV)的类,这些类可从我们的 QML 代码中轻松使用。 本书的最后一章旨在帮助您结合使用 OpenCV 和极其易于使用且美观的 Qt Quick Controls,开始开发用于移动设备(Android 和 iOS)的计算机视觉应用。
十二、Qt Quick 应用
使用 Qt 窗口小部件应用项目允许通过使用 Qt Creator 设计模式创建灵活而强大的 GUI,或者在文本编辑器中手动修改 GUI 文件(*.ui)。 到目前为止,在本书的所有章节中,我们都基于 Qt Widgets 应用作为创建的 GUI 的基础,并且我们在第 3 章,“创建一个全面的 Qt + OpenCV 项目”中了解到,我们可以使用样式表来有效地更改 Qt 应用的外观。 但是,除了 Qt Widgets 应用并使用QtWidgets和QtGui模块之外,Qt 框架还提供了另一种创建 GUI 的方法。 这种方法基于QtQuick模块和 QML 语言,并且允许创建更加灵活的 GUI(在外观,感觉,动画,效果等方面),并且更加轻松。 使用这种方法创建的应用称为 Qt Quick 应用。 请注意,在较新的 Qt 版本(5.7 和更高版本)中,您还可以创建 Qt Quick Controls 2 应用,它为创建 Qt Quick 应用提供了更多改进的类型,我们还将重点关注这一点。
QtQuick模块和QtQml模块是包含所有必需类的模块,以便在 C++ 应用中使用 Qt Quick 和 QML 编程。 另一方面,QML 本身是一种高度可读的声明性语言,它使用类似于 JSON 的语法(与脚本结合)来描述用户界面的各种组件以及它们之间的交互方式。 在本章中,我们将向您介绍 QML 语言以及如何使用它简化创建 GUI 应用的过程。 通过创建示例基于 QML 的 GUI 应用(或更确切地说是 Qt Quick Controls 2 应用),我们将了解其简单易读的语法以及如何在实践中使用它。 尽管使用 QML 语言不一定需要对 C++ 语言有深入的了解,但了解 Qt Quick 项目的结构仍然非常有用,因此我们将简要介绍最基本的 Qt Quick 应用的结构。 通过研究一些最重要的 QML 库,我们将了解现有的可视和非可视 QML 类型,这些类型可用于创建用户界面,向其中添加动画,访问硬件等。 我们将学习如何使用集成到 Qt Creator 中的 Qt Quick Designer 通过图形设计器修改 QML 文件。 稍后,通过学习 C++ 和 QML 的集成,我们将填补它们之间的空白,并学习如何在 Qt Quick 应用中使用 OpenCV 框架。 在最后一章中,我们还将学习如何使用与 Qt 和 OpenCV 相同的桌面项目来创建移动计算机视觉应用,并将我们的跨平台范围扩展到桌面平台之外,并扩展到移动世界。
本章涵盖的主题包括:
- QML 简介
- Qt Quick 应用项目的结构
- 创建 Qt Quick Controls 2 应用
- 使用 Qt Quick Designer
- 集成 C++ 和 QML
- 在 Android 和 iOS 上运行 Qt 和 OpenCV 应用
QML 简介
如引言中所述,QML 具有类似于 JSON 的结构,可用于描述用户界面上的元素。 QML 代码导入一个或多个库,并且具有一个包含所有其他可视和非可视元素的根元素。 以下是 QML 代码的示例,该代码导致创建具有指定宽度,高度和标题的空窗口(ApplicationWindow类型):
import QtQuick 2.7
import QtQuick.Controls 2.2
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
}
每个import语句后都必须带有 QML 库名称和版本。 在前面的代码中,导入了包含大多数默认类型的两个主要 QML 库。 例如,在QtQuick.Controls 2.2库中定义了ApplicationWindow。 现有 QML 库及其正确版本的唯一真实来源是 Qt 文档,因此请确保始终引用它,以防需要使用其他任何类。 如果使用 Qt Creator 帮助模式搜索ApplicationWindow,您将发现所需的import语句就是我们刚刚使用的。 值得一提的另一件事是,先前代码中的ApplicationWindow是单个根元素,并且所有其他 UI 元素都必须在其中创建。 让我们通过添加显示一些文本的Label元素来进一步扩展代码:
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
Label
{
x: 25
y: 25
text: "This is a label<br>that contains<br>multiple lines!"
}
}
由于它们与以前的代码相同,因此我们跳过了前面的代码中的import语句。 请注意,新添加的Label具有text属性,该属性是标签上显示的文本。 x和y只是指Label在ApplicationWindow内部的位置。 可以使用非常类似的方式添加诸如组框之类的容器项。 让我们添加一个,看看它是如何完成的:
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
GroupBox
{
x: 50
y: 50
width: 150
height: 150
Label
{
x: 25
y: 25
text: "This is a label<br>that contains<br>multiple lines!"
}
}
}
此 QML 代码将导致一个类似于以下所示的窗口:
请注意,每个元素的位置都是与其父元素的偏移量。 例如,将GroupBox内提供给Label的x和y值添加到GroupBox本身的x和y属性中,这就是在根元素(在本例中为ApplicationWindow)中确定 UI 元素的最终位置。
与 Qt 窗口小部件类似,您也可以在 QML 代码中使用布局来控制和组织 UI 元素。 为此,可以使用GridLayout,ColumnLayout和RowLayout QML 类型,但首先,需要使用以下语句导入它们:
import QtQuick.Layouts 1.3
现在,您可以将 QML 用户界面元素作为子项添加到布局中,并由其自动管理。 让我们在ColumnLayout中添加一些按钮,看看如何完成此操作:
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
ColumnLayout
{
anchors.fill: parent
Button
{
text: "First Button"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
Button
{
text: "Second Button"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
Button
{
text: "Third Button"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
这将导致类似于以下的窗口:
在前面的代码中,ColumnLayout的行为类似于我们在 Qt Widgets 应用中使用的垂直布局。 从上到下,作为子元素添加到ColumnLayout的每个元素都会显示在前一个元素之后,无论ColumnLayout的大小如何,始终调整其大小和位置以保持垂直布局视图。 关于上述内容,还有两点需要注意。 首先,使用以下代码将ColumnLayout本身的大小设置为父大小:
anchors.fill: parent
anchors是 QML 视觉元素的最重要属性之一,它照顾元素的大小和位置。 在这种情况下,通过将anchors的fill值设置为另一个对象(parent对象),我们将ColumnLayout的大小和位置描述为与ApplicationWindow相同。 通过正确使用锚点,我们可以以更大的功能和灵活性处理对象的大小和位置。 作为另一个示例,将代码中的anchors.fill行替换为以下内容,然后看看会发生什么:
width: 100
height: 100
anchors.centerIn: parent
显然,我们的ColumnLayout现在具有恒定的大小,并且当ApplicationWindow调整大小时它不会改变; 但是,布局始终保持在ApplicationWindow的中心。 关于上述代码的最后一点是:
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
添加到ColumnLayout的每个项目内的该行使该项目将自身垂直和水平定位在其单元格的中心。 请注意,这种意义上的单元格不包含任何可视边界,并且与布局本身一样,布局内的单元格也是在其中组织项目的非可视方式。
QML 代码的扩展遵循相同的模式,无论添加或需要多少项。 但是,随着 UI 元素的数量越来越大,最好将用户界面分成单独的文件。 可以将同一文件夹中的 QML 文件用作预定义的重要项目。 假设我们有一个名为MyRadios.qml的 QML 文件,其中包含以下代码:
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
Item
{
ColumnLayout
{
anchors.centerIn: parent
RadioButton
{
text: "Video"
}
RadioButton
{
text: "Image"
}
}
}
您可以在同一文件夹的另一个 QML 文件中使用此 QML 文件及其Item。 假设我们在MyRadios.qml所在的文件夹中有一个main.qml文件。 然后,您可以像这样使用它:
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
ColumnLayout
{
anchors.fill: parent
MyRadios
{
width: 100
height: 200
}
}
}
请注意,只要 QML 文件都在同一文件夹中,就不需要导入语句。 如果要在代码中使用的 QML 文件位于单独的文件夹(同一文件夹中的子文件夹)中,则必须使用以下语句将其导入:
import "other_qml_path"
显然,在前面的代码中,other_qml_path是我们的 QML 文件的相对路径。
QML 中的用户交互和脚本编写
对 QML 代码中的用户操作和事件的响应是通过将脚本添加到项目的插槽中来完成的,这与 Qt 窗口小部件非常相似。 此处的主要区别在于,在 QML 类型内部定义的每个信号还具有为其自动生成的对应插槽,并且可以填充脚本以在发出相关信号时执行操作。 好吧,让我们看另一个例子。 QML Button类型具有按下信号。 这自动意味着有一个onPressed插槽,可用于编码特定按钮的所需操作。 这是一个示例代码:
Button
{
onPressed:
{
// code goes here
}
}
有关 QML 类型的可用插槽的列表,请参阅 Qt 文档。 如前所述,您可以通过大写信号名称的第一个字母并在其前面加上on来轻松猜测每个信号的插槽名称。 因此,对于pressed信号,您将有一个onPressed插槽,对于released信号,您将有一个onReleased插槽,依此类推。
为了能够从脚本或插槽中访问其他 QML 项目,首先,您必须为其分配唯一的标识符。 请注意,这仅是您要访问和修改或与之交互的项目所必需的。 在本章的所有先前示例中,我们仅创建了项目,而没有为其分配任何标识符。 通过为项目的id属性分配唯一标识符,可以轻松完成此操作。 id属性的值遵循变量命名约定,这意味着它区分大小写,不能以数字开头,依此类推。 这是一个示例代码,演示如何在 QML 代码中分配和使用id:
ApplicationWindow
{
id: mainWindow
visible: true
width: 300
height: 500
title: "Hello QML"
ColumnLayout
{
anchors.fill: parent
Button
{
text: "Close"
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
onPressed:
{
mainWindow.close()
}
}
}
}
在前面的代码中,ApplicationWindow分配有一个 ID; 也就是mainWindow,它在Button的onPressed插槽内用于访问它。 您可以猜测,按前面代码中的“关闭”按钮将导致mainWindow被关闭。 无论在 QML 文件中的哪个位置定义 ID,都可以在该特定 QML 文件中的任何位置访问它。 这意味着 ID 的范围不限于相同的项目组或项目的子级,依此类推。 简而言之,任何 ID 对 QML 文件中的所有项目都是可见的。 但是,单独的 QML 文件中某项的id呢? 为了能够访问单独的 QML 文件中的项目,我们需要通过将其分配给property alias来导出它,如以下示例所示:
Item
{
property alias videoRadio: videoRadio
property alias imageRadio: imageRadio
ColumnLayout
{
anchors.centerIn: parent
RadioButton
{
id: videoRadio
text: "Video"
}
RadioButton
{
id: imageRadio
text: "Image"
}
}
}
前面的代码是相同的MyRadios.qml文件,但是这次,我们使用根项的别名属性导出了其中的两个RadioButton项。 这样,我们可以在使用MyRadios的单独 QML 文件中访问这些项目。 除了导出项目中的项目外,属性还可用于包含特定项目所需的任何其他值。 因此,这是在 QML 项中定义附加属性的一般语法:
property TYPE NAME: VALUE
在TYPE可以包含任何 QML 类型的情况下,NAME是属性的给定名称,VALUE是属性的值,必须与提供的类型兼容。
使用 Qt Quick Designer
由于 QML 文件的语法简单易读,因此可以使用任何代码编辑器轻松对其进行修改和扩展。 但是,您也可以使用 Qt Creator 中集成的快速设计器来简化 QML 文件的设计和修改。 如果您尝试在 Qt Creator 中打开 QML 文件并切换到“设计”模式,则会看到以下“设计”模式,它与标准 Qt Widgets 设计器(用于*.ui文件)有很大不同, 包含使用 QML 文件快速设计用户界面所需的大部分内容:
在“Qt Quick 设计器”屏幕的左侧,您可以在“库”窗格中看到可以添加到用户界面的 QML 类型的库。 它与 Qt Widgets 工具箱类似,但肯定有更多组件可用于设计应用的用户界面。 您只需在用户界面上拖放它们中的每一个,它们就会自动添加到您的 QML 文件中:
“库”窗格的正下方是“导航器”窗格,它在用户界面上显示组件的层次结构视图。 您可以使用“导航器”窗格,只需双击它们即可快速设置 QML 文件中的项目 ID。 此外,您可以将项目导出为别名,以便可以在其他 QML 文件中使用它,也可以在设计时将其隐藏(以便查看重叠的 QML 项目)。 在“导航器”窗格上的以下屏幕快照中,请注意在将button2导出为别名并将button3在设计期间隐藏之后,组件旁边的小图标是如何变化的:
在 Qt Quick 设计器的右侧,您可以找到“属性”窗格。 与标准 Qt 设计模式下的“属性”窗格相似,此窗格可用于详细操作和修改 QML 项的属性。 该窗格的内容根据用户界面上的选定项目而变化。 除了 QML 项目的标准属性外,此窗格还允许修改与单个项目的布局有关的属性。 以下屏幕快照描绘了在用户界面上选择“按钮”项时“属性”窗格的不同视图:
除了用于设计 QML 用户界面的辅助工具外,Qt Quick Designer 可以帮助您了解 QML 语言本身,因为在设计器中完成的所有修改都将转换为 QML 代码并存储在同一 QML 文件中。 通过使用它来设计用户界面,以确保熟悉它的用法。 例如,您可以尝试设计一些与创建 Qt Widgets 应用时相同的用户界面,但是这次使用 Qt Quick Designer 和 QML 文件。
Qt Quick 应用的结构
在本节中,我们将学习 Qt Quick 应用项目的结构。 与 Qt Widgets 应用项目类似,使用 Qt Creator 创建新项目时,会自动创建 Qt Quick 应用项目所需的大多数文件,因此您实际上并不需要记住所有的最低要求,但是仍然重要的是要理解如何处理 Qt Quick 应用的一些基本概念,以便能够进一步扩展它,或者,如我们将在本章后面的部分中了解的那样,在 QML 文件中集成和使用 C++ 代码。
让我们通过创建一个示例应用来解决这个问题。 首先打开 Qt Creator,然后在欢迎屏幕上按“新建项目”按钮,或者从“文件”菜单中选择“新建文件”或“项目”。 选择“Qt Quick Controls 2 应用”作为模板类型,然后按“选择”,如以下屏幕截图所示:
将项目名称设置为CvQml,然后按Next。 在“定义构建系统”页面中,将“构建系统”保留为qmake,默认情况下应将其选中。 在“定义项目详细信息”页面中,可以为 Qt Quick Controls 2 样式选择以下选项之一:
- 默认
- 材料
- 通用
您在此屏幕中选择的选项会影响应用的整体样式。 “默认”选项会导致使用默认样式,从而使 Qt Quick Controls 2 以及我们的 Qt Quick 应用具有最高性能。 Material 样式可用于根据 Google Material Design 准则创建应用。 它提供了更具吸引力的组件,但也需要更多资源。 最后,通用样式可用于基于 Microsoft 通用设计准则创建应用。 与 Material 风格相似,这也需要更多资源,但提供了另一套引人注目的用户界面组件。
您可以参考以下链接,以获得有关用于创建“材质”和“通用”样式的准则的更多信息:
https://dev.windows.com/design
下面的截图描述了一些常见的组件之间的差异,在选择的三种可能的风格每一个选项如何看您的应用:
无论您选择什么,以后都可以在名为qtquickcontrols2.conf的专用设置文件中轻松更改此设置,该文件会自动包含在新项目中。 甚至可以在以后更改颜色以匹配深色或浅色主题或任何其他颜色。 无论如何,请选择所需的一个(或将其保留为默认),然后继续按Next,直到最终进入 Qt 代码编辑器。 现在,您的项目几乎包含 Qt Quick 应用所需的最少文件。
请注意,每当我们在本章中提到 Qt Quick 应用时,我们实际上是指 Qt Quick Controls 2 应用,它是我们刚刚创建并将扩展到的新的增强型 Qt Quick 应用(在 Qt 5.7 和更高版本中可用)。 完整,美观的跨平台计算机视觉应用。
首先,让我们看一下项目(*.pro)文件中的区别。 在与 Qt Widgets 应用相对的 Qt Quick 应用中,默认情况下使用QtQml和QtQuick模块代替QtCore,QtGui和QtWidgets模块。 您可以通过打开CvQml.pro文件来进行检查,该文件的顶部具有以下行:
QT += qml quick
您可以在 Qt 项目中期望的两个文件,无论是 Qt Widgets 应用还是 Qt Quick 应用,都是一个项目和一个包含main函数的 C++ 源文件。 因此,除了CvQml.pro文件之外,还有一个main.cpp文件,其中包含以下内容:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
该main.cpp与创建 Qt Widgets 应用时所看到的完全不同。 记住,在 Qt Widgets 应用的main.cpp内部和主函数中,创建了QApplication,然后显示主窗口,程序进入事件循环,以便该窗口保持活动状态,并且所有事件已处理,如下所示:
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
类似地,在 Qt Quick 应用中,创建了QGuiApplication,但是这次没有加载任何窗口,而是使用QQmlApplicationEngine加载了 QML 文件,如下所示:
QQmlApplicationEngine engine;
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
这清楚地表明 QML 文件实际上是在运行时加载的,因此您可以从磁盘加载它们,或者在我们的示例中,可以从作为资源存储在qml.qrc文件中并嵌入到可执行文件中的main.qml文件加载它们。 实际上,这是开发 Qt Quick 应用的常用方法,如果您检查新创建的CvQml项目,则会注意到它包含一个名为qml.qrc的 Qt 资源文件,其中包含该项目的所有 QML 文件 。 qml.qrc文件包含以下文件:
main.qml,它是main.cpp文件中加载的 QML 文件,它是我们 QML 代码的入口点。Page1.qml包含Page1FormQML 类型的交互和脚本。Page1Form.ui.qml包含Page1Form类型内的用户界面和 QML 项目。 请注意,成对的Page1.qml和Page1Form.ui.qml是分离用户界面及其底层代码的常用方法,类似于在开发 Qt Widgets 应用时使用mainwindow.ui,mainwindow.h和mainwindow.cpp文件的方法。 。qtquickcontrols2.conf文件是可用于更改 Qt Quick 应用样式的配置文件。 它包含以下内容:
; This file can be edited to change the style of the application
; See Styling Qt Quick Controls 2 in the documentation ...
; http://doc.qt.io/qt-5/qtquickcontrols2-styles.html
[Controls]
Style=Default
[Universal]
Theme=Light
;Accent=Steel
[Material]
Theme=Light
;Accent=BlueGrey
;Primary=BlueGray
行首的分号;表示仅是注释。 您可以将前面代码中的Style变量的值更改为Material和Universal,以更改应用的整体样式。 根据所设置的样式,可以在前面的代码中使用Theme,Accent或Primary值来更改应用中使用的主题。
有关主题和颜色的完整列表,以及有关如何在每个主题中使用各种可用的自定义设置的其他信息,您可以参考以下链接:
https://goo.gl/jDZGPm(用于默认样式)
https://goo.gl/Um9qJ4(用于材料样式)
https://goo.gl/U6uxrh(用于通用样式)
关于 Qt Quick 应用的一般结构。 这种结构可立即用于任何平台的任何类型的应用。 请注意,您没有义务使用自动创建的文件,并且可以简单地从一个空项目开始或删除不必要的默认文件并从头开始。 例如,在我们的示例 Qt Quick 应用(标题为CvQml)中,我们不需要Page1.qml和Page1Form.ui.qml文件,因此只需从qml.qrc文件中选择它们并通过右键单击将其删除。 然后选择删除文件。 当然,这将导致main.qml文件中缺少代码。 因此,在继续下一部分之前,请确保将其更新为以下内容:
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
ApplicationWindow
{
visible: true
width: 300
height: 500
title: qsTr("CvQml")
}
集成 C++ 和 QML 代码
即使 QML 库已经成长为可以处理视觉,网络,摄像机等的完整类型集合,但仍然可以使用 C++ 类的功能对其进行扩展仍然很重要。 幸运的是,QML 和 Qt 框架提供了足够的规定以能够轻松地处理此问题。 在本节中,我们将学习如何创建一个非可视的 C++ 类,该类可以在 QML 代码内使用 OpenCV 处理图像。 然后,我们将创建一个 C++ 类,该类可用作 QML 代码中的可视项以显示图像。
请注意,默认情况下,QML 中有一个图像类型,可通过将其 URL 提供给“图像”项来显示保存在磁盘上的图像。 但是,我们将创建一个可用于显示QImage对象的图像查看器 QML 类型,并利用此机会来学习 CML 类(可视化)在 QML 代码中的集成。
首先将 OpenCV 框架添加到上一节中创建的项目中。 这与创建 Qt Widgets 应用时完全相同,并且在*.pro文件中包含必需的行。 然后,通过在项目窗格中右键单击新的 C++ 类并将其添加到项目中,然后选择“添加新的”。 确保类名称为QImageProcessor且其基类为QObject,如以下屏幕截图所示:
将以下#include指令添加到qimageprocessor.h文件中:
#include <QImage>
#include "opencv2/opencv.hpp"
然后将以下函数添加到QImageProcessor类的公共成员区域:
Q_INVOKABLE void processImage(const QString &path);
Q_INVOKABLE是 Qt 宏,它允许使用 Qt 元对象系统调用(调用)函数。 由于 QML 使用相同的 Qt 元对象作为对象之间的基础通信机制,因此用Q_INVOKABLE宏标记函数就足够了,以便可以从 QML 代码中调用它。 另外,将以下信号添加到QImageProcessor类:
signals:
void imageProcessed(const QImage &image);
我们将使用此信号将经过处理的图像传递给稍后将创建的图像查看器类。 最后,为了实现processImage函数,请将以下内容添加到qimageprocessor.cpp文件中:
void QImageProcessor::processImage(const QString &path)
{
using namespace cv;
Mat imageM = imread(path.toStdString());
if(!imageM.empty())
{
bitwise_not(imageM, imageM); // or any OpenCV code
QImage imageQ(imageM.data,
imageM.cols,
imageM.rows,
imageM.step,
QImage::Format_RGB888);
emit imageProcessed(imageQ.rgbSwapped());
}
else
{
qDebug() << path << "does not exist!";
}
}
这里没有我们没有看到或使用过的新东西。 此函数仅获取图像的路径,从磁盘读取图像,执行图像处理,但为了简单起见,我们可以使用bitwise_not函数将所有通道中的像素值取反,最后使用我们定义的信号的图像产生结果。
我们的图像处理器现已完成。 现在,我们需要创建一个 Visual C++ 类型,该类型可在 QML 中用于显示QImage对象。 因此,创建另一个类并将其命名为QImageViewer,但这一次请确保它是QQuickItem子类,如以下新类向导屏幕截图所示:
修改qimageviewer.h文件,如下所示:
#include <QQuickItem>
#include <QQuickPaintedItem>
#include <QImage>
#include <QPainter>
class QImageViewer : public QQuickPaintedItem
{
Q_OBJECT
public:
QImageViewer(QQuickItem *parent = Q_NULLPTR);
Q_INVOKABLE void setImage(const QImage &img);
private:
QImage currentImage;
void paint(QPainter *painter);
};
我们已经将QImageViewer类设为QQuickPaintedItem的子类。 同样,构造器也会进行更新以匹配此修改。 我们在此类中使用Q_INVOKABLE宏定义了另一个函数,该函数将用于设置要在此类实例上显示的QImage,或者确切地说,将设置使用该类型创建的 QML 项。 QQuickPaintedItem提供了一种创建新的可视 QML 类型的简单方法; 也就是说,通过对其进行子类化并重新实现paint函数,如前面的代码所示。 传递给此类中的paint函数的painter指针可用于绘制我们需要的任何内容。 在这种情况下,我们只想在其上绘制图像; 也就是说,我们已经定义了currentImage,它是QImage,它将保存要在QImageViewer类上绘制的图像。
现在,我们需要添加setImage的实现并绘制函数,并根据在头文件中所做的更改来更新构造器。 因此,请确保qimageviewer.cpp文件如下所示:
#include "qimageviewer.h"
QImageViewer::QImageViewer(QQuickItem *parent)
: QQuickPaintedItem(parent)
{
}
void QImageViewer::setImage(const QImage &img)
{
currentImage = img.copy(); // perform a copy
update();
}
void QImageViewer::paint(QPainter *painter)
{
QSizeF scaled = QSizeF(currentImage.width(),
currentImage.height())
.scaled(boundingRect().size(), Qt::KeepAspectRatio);
QRect centerRect(qAbs(scaled.width() - width()) / 2.0f,
qAbs(scaled.height() - height()) / 2.0f,
scaled.width(),
scaled.height());
painter->drawImage(centerRect, currentImage);
}
在前面的代码中,setImage函数非常简单; 它会复制图像并将其保存,然后调用QImageViwer类的更新函数。 在QQuickPaintedItem(类似于QWidget)内部调用update时,将导致重新绘制,因此将调用我们的绘制函数。 如果我们想在QImageViewer的整个可显示区域上拉伸图像,则此函数仅需要最后一行(centerRect替换为boundingRect); 但是,我们希望结果图像适合屏幕并保留宽高比。 因此,我们进行了比例转换,然后确保图像始终位于可显示区域的中心。
我们快到了,我们的两个新 C++ 类(QImageProcessor和QImageViewer)都可以在 QML 代码中使用。 剩下要做的唯一事情就是确保它们对我们的 QML 代码可见。 因此,我们需要确保使用qmlRegisterType函数注册了它们。 必须在我们的main.cpp文件中调用此函数,如下所示:
qmlRegisterType<QImageProcessor>("com.amin.classes",
1, 0, "ImageProcessor");
qmlRegisterType<QImageViewer>("com.amin.classes",
1, 0, "ImageViewer");
然后将其放在main.cpp文件中定义QQmlApplicationEngine的位置之前。 不用说,您必须通过使用以下#include指令在main.cpp文件中包括我们两个新类:
#include "qimageprocessor.h"
#include "qimageviewer.h"
请注意,qmlRegisterType函数调用中的com.amin.classes可以用您自己的类似域的标识符替换,这是我们为包含QImageProcessor和QImageViewer类的库提供的名称。 以下1和0引用该库的版本 1.0,最后一个文字字符串是可在我们的 QML 类型内部使用的类型标识符,以访问和使用这些新类。
最后,我们可以开始使用main.qml文件中的 C++ 类。 首先,确保您的import语句符合以下条件:
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import QtMultimedia 5.8
import com.amin.classes 1.0
最后一行包括我们刚刚创建的ImageProcessor和ImageViewer QML 类型。 我们将使用 QML 摄像机类型访问摄像机并使用它捕获图像。 因此,将以下内容添加为main.qml文件中ApplicationWindow项目的直接子代:
Camera
{
id: camera
imageCapture
{
onImageSaved:
{
imgProcessor.processImage(path)
}
}
}
在前面的代码中,imgProcessor是我们的ImageProcessor类型的id,还需要将其定义为ApplicationWindow的子项,如下所示:
ImageProcessor
{
id: imgProcessor
onImageProcessed:
{
imgViewer.setImage(image);
imageDrawer.open()
}
}
请注意,由于我们在QImageProcessor类内部创建了imageProcessed信号,因此自动生成了前面代码中的onImageProcessed插槽。 您可以猜测imgViewer是我们之前创建的QImageViewer类,并且将其图像设置在onImageProcessed插槽内。 在此示例中,我们还使用了 QML Drawer,该 QML Drawer在调用其打开函数时在另一个窗口上滑动,并且我们已嵌入imgViewer作为此Drawer的子项。 Drawer和ImageViewer的定义如下:
Drawer
{
id: imageDrawer
width: parent.width
height: parent.height
ImageViewer
{
id: imgViewer
anchors.fill: parent
Label
{
text: "Swipe from right to left<br>to return to capture mode!"
color: "red"
}
}
}
就是这样,剩下要做的唯一一件事情就是添加一个 QML VideoOutput,它可以预览摄像机。 我们将使用此VideoOutput捕获图像,从而调用 QML Camera类型的imageCapture.onImageSaved插槽,如下所示:
VideoOutput
{
source: camera
anchors.fill: parent
MouseArea
{
anchors.fill: parent
onClicked:
{
camera.imageCapture.capture()
}
}
Label
{
text: "Touch the screen to take a photo<br>and process it using OpenCV!"
color: "red"
}
}
如果立即启动该应用,您将立即面对计算机上默认照相机的输出。 如果单击视频输出内部,将捕获并处理图像,然后将其显示在Drawer上,该Drawer在当前页面上从左到右滑动。 以下是该应用执行时的屏幕截图:
Android 和 iOS 上的 Qt 和 OpenCV 应用
理想情况下,您可以在台式机和移动平台上构建并运行通过使用 Qt 和 OpenCV 框架创建的应用,而无需编写任何特定于平台的代码。 但是,实际上,这并不像看起来那样容易,因为 Qt 和 OpenCV 之类的框架充当操作系统本身功能的包装器(在某些情况下),并且由于它们仍在进行广泛的开发,因此可能会有一些尚未在特定操作系统(例如 Android 或 iOS)中完全实现的案例。 好消息是,随着新版本的 Qt 和 OpenCV 框架的发布,这些情况变得越来越罕见,即使现在(Qt 5.9 和 OpenCV 3.3),这两个框架中的大多数类和函数都可以在 Windows 中轻松使用。 ,Linux,macOS,Android 和 iOS 操作系统。
因此,首先,请牢记我们刚才提到的内容,可以说(实际上是相对于理想情况而言),要能够在 Android 和 iOS 上构建和运行使用 Qt 和 OpenCV 编写的应用,我们需要确保以下东西:
- 必须安装适用于 Android 和 iOS 的相应 Qt 套件。 这可以在 Qt 框架的初始安装过程中完成(有关此信息,请参阅第 1 章,“OpenCV 和 Qt 简介”)。
请注意,Android 套件可在 Windows,Linux 和 MacOS 上使用,而 iOS 套件仅适用于 macOS,因为使用 Qt 的 iOS 应用开发仅限于 macOS(目前)。
- 必须从 OpenCV 网站上下载适用于 Android 和 iOS 的预构建 OpenCV 库(目前,它们是从 opencv.org 提供)并提取到您的计算机中。 必须按照在 Windows 或任何其他桌面平台中添加的方式将它们添加到 Qt 项目文件中。
- 对于 iOS,在您的 MacOS 操作系统上拥有最新版本的 Xcode 就足够了。
- 对于 Android,您必须确保在计算机上安装 JDK,Android SDK,Android NDK 和 Apache Ant。 如果使用 Qt Creator 选项内“设备”页面中的 Android 选项卡,将所需的程序下载并安装到计算机上,则 Qt Creator 可以简化 Android 开发环境的配置(请参见以下屏幕截图):
请注意上图中“浏览”按钮旁边的按钮。 它们提供了下载页面的链接以及在线链接,您可以从中获得所有必需依赖项的副本。
如果要为 Android 和 iOS 操作系统构建应用,这就是您需要照顾的所有事情。 使用 Qt 和 OpenCV 构建的应用也可以在 Windows,macOS,Android 和 iOS 的应用商店中发布。 此过程通常涉及与这些操作系统的提供者注册为开发人员。 您可以在上述应用商店中找到在线和在全球范围内发布应用的准则和要求。
总结
在本章中,我们了解了 Qt Quick 应用开发和 QML 语言。 我们从这种高度可读且易于使用的语言的裸露语法开始,然后转向开发包含可以相互交互以实现一个共同目标的组件的应用。 我们学习了如何填补 QML 和 C++ 代码之间的空白,然后建立了可视类和非可视类来处理和显示使用 OpenCV 处理的图像。 我们还简要介绍了在 Android 和 iOS 平台上构建和运行相同应用所需的工具。 本书的最后一章旨在通过开始使用新的 Qt Quick Controls 2 模块开发快速,美观的应用,并将 C++ 代码和 OpenCV 等第三方框架的功能结合起来,来帮助您站起来。 在开发移动和桌面应用时获得最大的功能和灵活性。
构建跨平台和吸引人的应用从未如此简单。 通过使用 Qt 和 OpenCV 框架,尤其是 QML 的功能,可以快速轻松地构建应用,您可以立即开始实现所有计算机视觉创意。 我们在本章中学到的只是对 Qt Quick 和 QML 语言必须提供的所有可能性的介绍。 但是,您是需要将这些部分放在一起以构建可解决该领域中现有问题的应用的人。
963

被折叠的 条评论
为什么被折叠?



