军人运动会官方网站建设目标网络营销策划方案案例

当前位置: 首页 > news >正文

MainWindow::MainWindow(QWidget *parent)
QMainWindow(parent), ui(new Ui::MainWindow) {ui-setupUi(this); } MainWindow::~MainWindow() {delete ui; } void MainWindow::on_pushButton_clicked() { } 为了能够显示然后处理图像我们需要定义一个cv::Mat类成员变量。 这是在MainWindow类类的头文件中完成的。 现在此标头的内容如下 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include QtGui/QMainWindow #include QFileDialog #include opencv2/core/core.hpp #include opencv2/highgui/highgui.hppnamespace Ui {class MainWindow; } class MainWindow : public QMainWindow {Q_OBJECT public:MainWindow(QWidget *parent 0);~MainWindow(); private:Ui::MainWindow ui;cv::Mat image; // the image variable private slots:void on_pushButton_clicked(); };#endif // MAINWINDOW_H请注意我们还包括了core.hpp和highgui.hpp头文件。 正如我们在前面的秘籍中所了解的那样我们一定不要忘记编辑项目文件以附加 OpenCV 库信息。 然后可以添加 OpenCV 代码。 第一个按钮打开源图像。 这是通过将以下代码添加到相应的插槽方法来完成的 void MainWindow::on_pushButton_clicked() {QString fileName QFileDialog::getOpenFileName(this,tr(Open Image), ., tr(Image Files (.png *.jpg *.jpeg .bmp)));image cv::imread(fileName.toAscii().data());cv::namedWindow(Original Image);cv::imshow(Original Image, image); }然后通过右键单击第二个按钮来创建新的插槽。 第二个插槽将对所选输入图像执行一些处理。 以下代码将简单地翻转图像 void MainWindow::on_pushButton_2_clicked() {cv::flip(image,image,1);cv::namedWindow(Output Image);cv::imshow(Output Image, image); }现在您可以编译并运行该程序您的 2 键 GUI 将允许您选择图像并进行处理。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AGBAQrbx-1681873909550)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_23.jpg)] 输入和输出图像显示在我们定义的两个highgui窗口上。 工作原理 在 Qt 的 GUI 编程框架下对象使用信号和插槽进行通信。 每当窗口小部件更改状态或发生事件时都会发出信号。 该信号具有预定义的签名如果另一个对象想要接收该信号则它必须定义一个具有相同签名的插槽。 因此插槽是一种特殊的类方法当它所连接的信号发出时会自动调用。 信号和插槽被定义为类方法但必须在指定插槽和信号的 Qt 访问下声明。 当您在按钮上添加插槽时这就是 Qt Creator 所做的即 private slots:void on_pushButton_clicked();信号和插槽是松散耦合的也就是说信号不知道与连接插槽的对象有关的任何信息而插槽也不知道是否连接了信号。 同样许多插槽可以连接到一个信号并且一个插槽可以接收来自许多物体的信号。 唯一的要求是信号的签名和时隙方法必须匹配。 从QObject类继承的所有类都可以包含信号和插槽。 这些通常是小部件类的子类QWidget的子类但是任何其他类都可以定义插槽和信号。 实际上信号和时隙概念是一种非常强大的类通信机制。 但是它特定于 Qt 框架。 在 Qt 中主窗口是类MainWindow的实例。 您可以通过在MainWindow类定义中声明的成员变量ui来访问它。 另外GUI 的每个小部件也是一个对象。 创建 GUI 时指向您已添加到主窗口的每个小部件实例的指针与ui变量相关联。 因此您可以访问程序中每个窗口小部件的属性和方法。 例如如果要在选择输入图像之前禁用处理按钮则您需要做的就是在 GUI 初始化时在MainWindow构造器中调用以下方法。
ui-pushButton_2-setEnabled(false);指针变量pushbutton_2在此对应于处理按钮。 然后当成功加载图像时您可以启用按钮在打开图像按钮中 if (image.data) {ui-pushButton_2-setEnabled(true); }还值得注意的是在 Qt 下GUI 的布局在 XML 文件中已完全描述。 这是带有.ui扩展名的文件。 如果进入项目目录并使用文本编辑器打开.ui文件则将能够读取该文件的 XML 内容。 定义了几个 XML 标签。 在此秘籍中介绍的示例应用的情况下您将找到两个定义为QPushButton的窗口小部件类标记。 名称与这些窗口小部件类的标记关联该名称与附加到ui对象的指针变量的名称相对应。 其中的每一个都定义了描述其位置和大小的几何属性。 还定义了许多其他属性标签。 Qt Creator 有一个属性选项卡显示每个小部件的属性值。 因此即使 Qt Creator 是创建 GUI 的最佳工具您也可以编辑.ui XML 文件来创建和修改 GUI。 更多 使用 Qt在 GUI 上直接显示图像相对容易。 您需要做的就是在窗口中添加一个标签对象。 然后将图像分配给该标签以显示该图像。 请记住您可以通过ui指针在我们的示例中为ui-label的相应指针属性访问标签实例。 但是此图像必须是QImage类型即处理图像的 Qt 数据结构。 转换相对简单只是需要反转三个颜色通道的顺序从cv::Mat中的 BGR 到QImage中的 RGB。 我们可以使用cv::cvtColor函数。 然后我们简单的 GUI 应用的处理按钮可以更改为 void MainWindow::on_pushButton_2_clicked() {cv::flip(image,image,1); // process the image// change color channel orderingcv::cvtColor(image,image,CV_BGR2RGB); // Qt imageQImage img QImage((const unsigned char
)(image.data), image.cols,image.rows,QImage::Format_RGB888);// display on labelui-label-setPixmap(QPixmap::fromImage(img)); // resize the label to fit the imageui-label-resize(ui-label-pixmap()-size()); }结果现在将输出图像直接显示在 GUI 上如下所示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kp4EmE6p-1681873909550)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_24.jpg)] 另见 有关 Qt GUI 模块以及信号和插槽机制的更多信息请查阅位于这个页面的在线 Qt 文档。 二、操纵像素 在本章中我们将介绍 访问像素值用指针扫描图像使用迭代器扫描图像编写有效的图像扫描循环使用邻居访问扫描图像执行简单的图像算术定义兴趣区域 简介 为了构建计算机视觉应用您必须能够访问图像内容并最终修改或创建图像。 本章将教您如何操作图像元素又称像素。 您将学习如何扫描图像并处理其每个像素。 您还将学习如何有效地执行此操作因为即使尺寸适中的图像也可能包含数万个像素。 从根本上讲图像是数值矩阵。 这就是为什么 OpenCV 2 使用cv::Mat数据结构来操作它们的原因。 矩阵的每个元素代表一个像素。 对于灰度图像“黑白”图像像素为无符号的 8 位值其中 0 对应于黑色而 255 对应于白色。 对于彩色图像每个像素需要三个这样的值才能代表通常的三个原色通道红绿蓝。 因此在这种情况下矩阵元素由值的三元组组成。 如上一章所述OpenCV 还允许您创建具有不同类型例如整数CV_8U和浮点数[CV_32F的像素值的矩阵或图像。 这些对于在某些图像处理任务中存储例如中间值非常有用。 大多数操作可以应用于任何类型的矩阵其他操作则需要特定类型的矩阵或者仅适用于给定数量的通道。 因此对函数或方法的先决条件有充分的了解对于避免常见的编程错误至关重要。 在本章中我们使用以下彩色图像作为输入请参见本书的网站以彩色方式查看该图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QG7GNoT-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_01.jpg)] 访问像素值 为了访问矩阵的每个单独元素您只需要指定其行号和列号即可。 将返回对应的元素在多通道图像的情况下该元素可以是单个数值或值的向量。 准备 为了说明对像素值的直接访问我们将创建一个简单的函数在图像中添加椒盐噪声。 顾名思义椒盐噪声是一种特殊类型的噪声其中某些像素被白色或黑色像素代替。 当某些像素的值在传输过程中丢失时这种类型的噪声可能会出现在错误的通信中。 在我们的例子中我们将简单地随机选择一些像素并将其分配为白色。 操作步骤 我们创建一个接收输入图像的函数。 这是将由我们的函数修改的图像。 为此我们使用了传递引用机制。 第二个参数是我们要覆盖白色值的像素数 void salt(cv::Mat image, int n) {for (int k0; kn; k) {// rand() is the MFC random number generator// try qrand() with Qtint i rand()%image.cols;int j rand()%image.rows;if (image.channels() 1) { // gray-level imageimage.atuchar(j,i) 255; } else if (image.channels() 3) { // color imageimage.atcv::Vec3b(j,i)[0] 255; image.atcv::Vec3b(j,i)[1] 255; image.atcv::Vec3b(j,i)[2] 255; }} }该函数由单个循环组成该循环将n乘以255值乘以随机选择的像素。 在此使用随机数生成器选择像素列i和行j。 请注意我们通过检查与每个像素关联的通道数来区分灰度图像和彩色图像的两种情况。 在灰度图像的情况下将的数字255分配给单个 8 位值。 对于彩色图像需要为三个原色通道分配255以获得白色像素。 您可以通过向其传递先前打开的图像来调用此函数 // open the imagecv::Mat image cv::imread(boldt.jpg);// call function to add noisesalt(image,3000);// display imagecv::namedWindow(Image);cv::imshow(Image,image);生成的图像如下所示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6SrciH3a-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_02.jpg)] 工作原理 类cv::Mat包括几种访问图像不同属性的方法。 公共成员变量cols和rows为您提供图像中的列数和行数。 对于元素访问cv::Mat具有方法at(int y, int x)。 但是必须在编译时知道方法返回的类型并且由于cv::Mat可以保存任何类型的元素因此程序员需要指定期望的返回类型。 这就是at方法已被实现为模板方法的原因。 因此在调用它时必须指定图像元素类型如下所示 image.atuchar(j,i) 255; 重要的是要注意确保指定的类型与矩阵中包含的类型匹配是程序员的责任。 at方法不执行任何类型转换。 在彩色图像中每个像素与三个分量相关联红色绿色和蓝色通道。 因此包含彩色图像的cv::Mat将返回三个 8 位值的向量。 OpenCV 具有针对此类短向量的定义类型称为cv::Vec3b。 它是 3 个unsigned char的向量。 这就解释了为什么元素访问彩色像素的像素写为 image.atcv::Vec3b(j,i)[channel] value; 索引channel指定三个颜色通道之一。 2 元素和 4 元素向量cv::Vec2b和cv::Vec4b以及其他元素类型也存在类似的向量类型。 在此后一种情况下最后一个字母由short的sint的ifloat的f和double的d替换。 所有这些类型都是使用模板类cv::VecT,N定义的其中T是类型N是向量元素的数量。 更多 使用cv::Mat类的at方法有时会很麻烦因为必须为每个调用将返回的类型指定为模板参数。 在已知矩阵类型的情况下可以使用cv::Mat_类它是cv::Mat的模板子类。 此类定义了一些其他方法但没有新的数据属性因此可以将指向一个类的指针或引用直接转换为另一个类。 在其他方法中operator()允许直接访问矩阵元素。 因此如果image是对uchar矩阵的引用则可以编写 cv::Mat_uchar im2 image; // im2 refers to imageim2(50,100) 0; // access to row 50 and column 100由于cv::Mat_元素的类型是在创建变量时声明的因此operator()方法在编译时就知道要返回哪种类型。 除了编写时间短之外使用operator()方法可提供与at方法完全相同的结果。 另见 “编写高效的图像扫描循环”秘籍可讨论此方法的效率。 用指针扫描图像 在大多数图像处理任务中需要扫描图像的所有像素才能执行计算。 考虑到将需要访问的大量像素以有效的方式执行此任务至关重要。 本秘籍以及下一篇秘籍将向您展示实现图像扫描循环的不同方法。 该秘籍使用指针算法。 准备 我们将通过完成一个简单的任务来说明图像扫描过程减少图像中的颜色数量。 彩色图像由 3 通道像素组成。 这些通道中的每一个对应于三种原色红色绿色蓝色之一的强度值。 由于这些值均为 8 位unsigned char因此颜色总数为256x256x256超过 1600 万种颜色。 因此为减少分析的复杂性有时减少图像中的颜色数量很有用。 一种简单的方法可以将 RGB 空间细分为相等大小的多维数据集。 例如如果将每个尺寸的颜色数量减少 8那么您将获得总共32x32x32的颜色。 然后原始图像中的每种颜色在色彩缩减图像中被分配一个新的颜色值该值对应于其所属的多维数据集中心的值。 因此基本的色彩缩减算法很简单。 如果N是缩小因子则对于图像中的每个像素以及该像素的每个通道将值除以N整数除法因此会丢失提示。 然后将结果乘以N这将为您提供N在输入像素值以下的倍数。 只需加N / 2即可获得N的两个相邻倍数之间的间隔的中心位置。如果对每个 8 位通道值重复此过程则总共将获得256 / N x 256 / N x 256 / N可能的颜色值。 操作步骤 我们的色彩缩减函数的签名如下 void colorReduce(cv::Mat image, int div64);用户提供图像和每个通道的缩小系数。 此处在原位中完成**处理即通过该函数修改了输入图像的像素值。 请参见“本秘籍的更多内容”部分提供了具有输入和输出参数的更通用的函数签名。 通过创建遍历所有像素值的双循环即可简单地完成处理 void colorReduce(cv::Mat image, int div64) {int nl image.rows; // number of lines// total number of elements per lineint nc image.cols * image.channels(); for (int j0; jnl; j) {// get the address of row juchar* data image.ptruchar(j);for (int i0; inc; i) {// process each pixel ———————data[i] data[i]/div*div div/2;// end of pixel processing —————-} // end of line } }可以使用以下代码片段测试此函数 // read the imageimage cv::imread(boldt.jpg);// process the imagecolorReduce(image);// display the imagecv::namedWindow(Image);cv::imshow(Image,image);例如这将为您提供以下图像请参见本书的网站以彩色查看此图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8BdOaPy-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_03.jpg)] 工作原理 在彩色图像中图像数据缓冲区的前 3 个字节给出左上像素的 3 个颜色通道值接下来的 3 个字节是第一行第二个像素的值依此类推请注意OpenCV 使用 默认情况下BGR 通道顺序因此蓝色通常是第一个通道。 宽度为W且高度为H的图像将需要WxHx3个uchar的存储块。 但是出于效率原因可以用很少的额外像素来填充行的长度。 这是因为某些多媒体处理器芯片例如 Intel MMX 架构在行数为 4 或 8 的倍数时可以更有效地处理图像。 值将被忽略。 OpenCV 将填充行的长度指定为关键字。 显然如果未用多余像素填充图像则有效宽度将等于实际图像宽度。 数据属性cols为您提供图像宽度即列数属性rows为您提供图像高度而step数据属性为您提供有效宽度。 字节数。 即使您的图像不是uchar的类型step仍会为您提供连续的字节数。 像素元素的大小由方法elemSize给出例如对于 3 通道短整数矩阵CV_16SC3elemSize将返回 6。 图像中的通道数由nchannels方法给出对于灰度图像为 1对于彩色图像为 3。 最后方法total返回矩阵中像素的总数即矩阵项。 然后每行的像素值数量由下式给出 int nc image.cols * image.channels(); 为了简化指针算术的计算cv::Mat类提供了一种直接为您提供图像行地址的方法。 这是ptr方法。 这是一个模板方法返回行号j的地址 uchar* data image.ptruchar(j);注意在处理语句中我们可以等效地使用指针算法在列之间移动。 所以我们可以这样写 *data *data/div*div div2;更多 本秘籍中介绍的色彩缩减函数仅提供完成此任务的一种方法。 人们还可以使用其他色彩缩减公式。 该函数的更通用版本也将允许指定不同的输入和输出图像。 通过考虑图像数据的连续性还可以使图像扫描更有效。 最后也可以使用常规的低级指针算法来扫描图像缓冲区。 以下各小节将讨论所有这些元素。 其他颜色缩减秘籍 在我们的示例中通过利用整数除法来实现色彩缩减该整数除法将除法结果取整为最接近的较低整数 data[i] data[i]/div*div div/2;还可以使用模运算符计算出减少的颜色该运算符将我们带到div的最接近倍数1D 减少因子 data[i] data[i] – data[i]%div div/2;但是此计算要慢一些因为它需要两次读取每个像素值。 另一种选择是使用按位运算符。 确实如果我们将缩减因子限制为 2 的幂即divpow(2,n)则屏蔽像素值的前n位将为我们提供div的最接近的较低倍数。 该掩码可以通过简单的移位来计算 // mask used to round the pixel valueuchar mask 0xFFn; // e.g. for div16, mask 0xF0颜色减少将通过以下方式给出 datai div/2;通常按位运算会导致非常高效的代码因此当需要效率时它们可以构成强大的替代方案。 具有输入和输出参数 在我们的色彩缩减示例中该变换直接应用于输入图像这称为原地变换。 这样不需要额外的图像来保存输出结果这在需要时可以节省内存使用。 但是在某些应用中用户希望保持原始图像不变。 然后在调用该函数之前将迫使用户创建图像的副本。 请注意创建图像的相同深层副本的最简单方法是调用clone方法例如 // read the imageimage cv::imread(boldt.jpg);// clone the imagecv::Mat imageClone image.clone();// process the clone// orginal image remains untouchedcolorReduce(imageClone);// display the image resultcv::namedWindow(Image Result);cv::imshow(Image Result,imageClone);通过定义一个向用户提供使用或不使用原地处理选项的函数可以避免这种额外的过载。 该方法的签名将是 void colorReduce(const cv::Mat image, // input image cv::Mat result, // output imageint div64);请注意现在将输入图像作为const引用传递这意味着该图像不会被该函数修改。 如果首选原地处理则将同一图像指定为输入和输出 colorReduce(image,image);如果没有则可以提供另一个cv::Mat实例例如 cv::Mat result;
colorReduce(image,result);此处的关键是首先验证输出图像是否具有分配的数据缓冲区该缓冲区的大小和像素类型与输入图像的大小和像素类型匹配。 非常方便的是此检查封装在[H​​TG1]的create方法中。 这是必须使用新的大小和类型重新分配矩阵时使用的方法。 如果偶然地矩阵已经具有指定的大小和类型则不执行任何操作并且该方法仅返回而无需接触实例。 因此我们的函数应该仅从对create的调用开始该调用将构建与输入图像大小和类型相同的矩阵如有必要 result.create(image.rows,image.cols,image.type());请注意create始终创建连续图像即没有填充的图像。 分配的内存块的大小为total()elemSize() 。然后使用两个指针完成循环 for (int j0; jnl; j) {// get the addresses of input and output row jconst uchar data_in image.ptruchar(j);uchar* data_out result.ptruchar(j);for (int i0; inc; i) {// process each pixel ———————data_out[i] data_in[i]/div*div div/2;// end of pixel processing —————-} // end of line 在提供相同图像作为输入和输出的情况下此函数变得完全等同于本秘籍中介绍的第一个版本。 如果提供另一个图像作为输出则该函数将正常运行而不管函数调用之前是否分配了该图像。 高效扫描连续图像 前面我们曾解释过出于效率的考虑可以在每行的末尾用额外的像素填充图像。 但是有趣的是当未填充图像时可以将图像视为WxH像素的长一维数组。 方便的cv::Mat方法可以告诉我们是否已填充图像。 如果图像不包含填充像素则isContinuous方法返回true。 在某些特定的处理算法中可以通过在一个较长循环中处理图像来利用图像的连续性。 然后我们的处理函数将编写如下 void colorReduce(cv::Mat image, int div64) {int nl image.rows; // number of linesint nc image.cols * image.channels(); if (image.isContinuous()) {// then no padded pixelsnc ncnl; nl 1; // it is now a 1D array}// this loop is executed only once// in case of continuous imagesfor (int j0; jnl; j) { uchar data image.ptruchar(j);for (int i0; inc; i) {// process each pixel ———————data[i] data[i]/div*div div/2;// end of pixel processing —————-} // end of line } }现在当连续性测试告诉我们图像不包含填充像素时我们通过将宽度设置为 1 并将高度设置为WxH来消除外部循环。 注意这里还可以使用reshape方法。 在这种情况下您将编写以下内容 if (image.isContinuous()) {// no padded pixelsimage.reshape(1, // new number of channelsimage.cols*image.rows) ; // new number of rows}int nl image.rows; // number of linesint nc image.cols * image.channels(); 方法reshape无需任何内存复制或重新分配即可更改矩阵尺寸。 第一个参数是新的通道数第二个参数是新的行数。 列数会相应调整。 在这些实现中内部循环按顺序处理所有图像像素。 当将几个小图像同时扫描到同一循环中时此方法特别有利。 低级指针算法 在cv::Mat类中图像数据包含在unsigned char的存储块中。 该存储块第一个元素的地址由data属性给定该属性返回一个无符号的char指针。 因此要在图像的开头开始循环您可以编写 uchar *data image.data;通过使用有效宽度移动行指针可以完成从一行到另一行的移动 data image.step; // next linestep方法为您提供一行中的字节总数包括填充的像素。 通常您可以按以下方式获取行j和列i的像素地址 // address of pixel at (j,i) that is image.at(j,i)
data image.dataj*image.stepi*image.elemSize(); 但是即使这在我们的示例中可行也不建议以这种方式进行。 除了容易出错外这种方法也不适用于兴趣区域。 本章末尾讨论了兴趣区域。 另见 “编写高效的图像扫描循环”秘籍用于讨论此处介绍的扫描方法的效率。 使用迭代器扫描图像 在面向对象的编程中通常使用迭代器完成对数据集合的循环。 迭代器是专门构建的类用于遍历集合的每个元素隐藏了如何针对给定的集合专门对每个元素进行迭代。 信息隐藏原理的这种应用使扫描集合变得更加容易。 此外无论使用哪种类型的集合它的形式都相似。 标准模板库STL具有与其每个集合类关联的迭代器类。 然后OpenCV 提供一个cv::Mat迭代器类该类与 C STL 中的标准迭代器兼容。 准备 在此秘籍中我们再次使用先前秘籍中描述的色彩缩减示例。 操作步骤 可以通过首先创建cv::MatIterator_对象来获得cv::Mat实例的迭代器对象。 与cv::Mat_子类的情况一样下划线表示这是模板方法。 实际上由于使用了图像迭代器来访问图像元素因此必须在编译时就知道返回类型。 然后将迭代器声明如下 cv::MatIterator_cv::Vec3b it;另外您还可以使用Mat_模板类中定义的iterator类型 cv::Mat_cv::Vec3b::iterator it;然后您可以使用常规的begin和end迭代器方法遍历像素但这些方法又是模板方法。 因此我们的色彩缩减函数现在编写如下 void colorReduce(cv::Mat image, int div64) {// obtain iterator at initial positioncv::Mat_cv::Vec3b::iterator it image.begincv::Vec3b();// obtain end positioncv::Mat_cv::Vec3b::iterator itend image.endcv::Vec3b();// loop over all pixelsfor ( ; it! itend; it) {// process each pixel ———————(*it)0[0]/div*div div/2;(*it)1[1]/div*div div/2;(*it)2[2]/div*div div/2;// end of pixel processing —————-} }请记住这里的迭代器返回cv::Vec3b因为我们正在处理彩色图像。 使用解引用operator[]访问每个颜色通道元素。 工作原理 使用迭代器无论扫描哪种集合都始终遵循相同的模式。 首先使用适当的专用类在我们的示例中为cv::Mat_cv::Vec3b::iterator或cv::MatIterator_cv::Vec3b创建迭代器对象。 然后您将获得一个在起始位置在我们的示例中为图像的左上角初始化的迭代器。 这是使用begin方法完成的。 对于cv::Mat实例您将其获取为image.begincv::Vec3b()。 您还可以在迭代器上使用算术。 例如如果您希望从图像的第二行开始则可以在image.begincv::Vec3b()image.rows处初始化cv::Mat迭代器。 可以使用end方法类似地获得收藏的结束位置。 但是如此获得的迭代器就在您的集合之外。 这就是为什么您的迭代过程到达最终位置时必须停止的原因。 您还可以在此迭代器上使用算术例如如果希望在最后一行之前停止则最终迭代将在迭代器达到image.endcv::Vec3b()-image.rows时停止。 初始化迭代器后您将创建一个遍历所有元素的循环直到到达末尾为止。 典型的while循环如下所示 while (it! itend) {// process each pixel ———————…// end of pixel processing —————-it;}operator是用于移至下一个元素的那个。 您还可以指定更大的步长。 例如it10将每 10 个像素处理一次。 最后在处理循环内部使用解引用operator*来访问当前元素您可以使用该元素读取例如element *it;或写入例如*it element;。 请注意如果收到对const cv::Mat的引用或者希望表示当前循环不修改cv::Mat实例则也可以创建使用的常量迭代器。 这些声明如下 cv::MatConstIterator_cv::Vec3b it;或者 cv::Mat_cv::Vec3b::const_iterator it;更多 在此秘籍中使用模板方法begin和end获得迭代器的开始和结束位置。 就像我们在本章第一章中所做的那样我们也可以使用对cv::Mat_实例的引用来获得它们。 这样可以避免在begin和end方法中指定迭代器类型的需要因为在创建cv::Mat_引用时就指定了该迭代器类型。 cv::Mat_cv::Vec3b cimage image;cv::Mat_cv::Vec3b::iterator it cimage.begin();cv::Mat_cv::Vec3b::iterator itend cimage.end();另见 “编写高效的图像扫描循环”秘籍讨论了扫描图像时迭代器的效率。 另外如果您不熟悉面向对象编程中迭代器的概念以及如何在 ANSI C 中实现迭代器则应阅读有关 STL 迭代器的教程。 您只需用关键字“STL 迭代器”在网络上搜索就可以找到许多关于该主题的参考。 编写有效的图像扫描循环 在本章的先前秘籍中我们介绍了扫描图像以处理其像素的不同方法。 在本秘籍中我们将比较这些不同方法的效率。 当您编写图像处理函数时效率通常是一个问题。 在设计函数时经常需要检查代码的计算效率以发现可能会减慢程序速度的任何瓶颈。 但是必须注意的是除非有必要否则不应以降低程序清晰度为代价进行优化。 简单的代码的确总是更容易调试和维护。 只有对程序效率至关重要的代码部分才应进行严重优化。 操作步骤 为了测量一个函数或部分代码的执行时间存在一个非常方便的称为cv::getTickCount()的 OpenCV 函数。 此函数为您提供自上次启动计算机以来发生的时钟周期数。 由于我们希望以毫秒为单位给出代码部分的执行时间因此我们使用了另一种方法cv::getTickFrequency() 。 这给了我们每秒的循环数。 为了获得给定函数或部分代码的计算时间而使用的常用模式将是 double duration; duration static_castdouble(cv::getTickCount());colorReduce(image); // the function to be testedduration static_castdouble(cv::getTickCount())-duration; duration / cv::getTickFrequency(); // the elapsed time in ms持续时间结果应在函数的多次调用中取平均值。 在colorReduce函数的测试中我们还实现了使用at方法进行像素访问的函数版本。 然后此实现的主循环将读为 for (int j0; jnl; j) {for (int i0; inc; i) {// process each pixel ———————image.atcv::Vec3b(j,i)[0]image.atcv::Vec3b(j,i)[0]/div*div div/2;image.atcv::Vec3b(j,i)[1] image.atcv::Vec3b(j,i)[1]/div*div div/2;image.atcv::Vec3b(j,i)[2] image.atcv::Vec3b(j,i)[2]/div*div div/2;// end of pixel processing —————-} // end of line }工作原理 在此报告本章中colorReduce函数的不同实现的执行时间。 一台机器的绝对运行时数会有所不同这里我们使用的是奔腾双核 2.2GHz。 看看它们的相对差异是很有趣的。 我们的测试报告减少分辨率为4288x2848像素的图像的颜色所需的平均时间。 下表中汇总了结果并在下面进行了讨论 方法平均时间data[i] data[i]/div*div div/2 ;37ms*data *data/div*div div/2;37ms*data v - v%div div/2;52ms*data *datamask div/2;35mscolorReduce(input, output);44msiimage.cols*image.channels();65msMatIterator67ms.at(j,i)80ms3-channel loop29ms 首先我们比较通过指针扫描图像的“更多内容”部分中介绍的三种计算色彩缩减的方法第 1-4 行。不出所料使用按位运算符的版本最快执行时间为35ms。 使用整数除法的版本取37ms而取模的版本取52ms。 最快与最慢之间相差近 50% 因此重要的是要花一些时间来确定在图像循环中计算结果的最有效方法因为净影响可能非常显着。 注意当指定需要重新分配的输出图像而不是原地处理第 5 行时执行时间变为44ms。 额外的持续时间代表内存分配的开销。 在循环中应避免重复计算可能会预先计算的值。 这显然会浪费时间。 例如如果您替换颜色减少函数的以下内部循环 int nc image.cols * image.channels(); …for (int i0; inc; i) {与此 for (int i0; iimage.cols * image.channels(); i) {那是一个循环您需要一次又一次地计算一行中的元素总数。 您将获得65ms的运行时比35ms的原始版本第 6 行慢 80%。 使用迭代器第 7 行的色彩缩减函数版本如秘籍“使用迭代器扫描图像”所示在67ms处的结果较慢。 迭代器的主要目的是简化图像扫描过程并减少出错的可能性。 不一定要优化此过程。 使用上一节末尾介绍的at方法的实现要慢得多第 8 行。 获得80ms的运行时。 然后应将这种方法用于图像像素的随机访问但在扫描图像时绝对不要使用。 即使处理的元素总数相同使用较少语句的较短循环通常比使用单个语句的较长循环更有效地执行。 同样如果您要对一个像素应用N个不同的计算请全部执行一个循环而不要编写N个连续的循环每次计算一次。 然后您应该偏爱循环在较长的循环中进行更多的工作而较长的循环会减少计算量。 举例来说我们可以处理内部循环中的所有三个通道并在列数上进行迭代而不是使用原始版本其中循环遍历元素总数即像素数的 3 倍 。 然后将颜色减少函数编写如下这是最快的版本 void colorReduce(cv::Mat image, int div64) {int nl image.rows; // number of linesint nc image.cols ; // number of columns// is it a continous image?if (image.isContinuous()) {// then no padded pixelsnc ncnl; nl 1; // it is now a 1D array}int n static_castint(log(static_castdouble(div))/log(2.0));// mask used to round the pixel valueuchar mask 0xFFn; // e.g. for div16, mask 0xF0// for all pixels for (int j0; jnl; j) {// pointer to first column of line juchar data image.ptruchar(j);for (int i0; inc; i) {// process each pixel ———————*data *datamask div/2;*data *datamask div/2;*data *datamask div/2;// end of pixel processing —————-} // end of line } }通过此修改执行时间现在为29ms第 9 行。 我们还添加了连续性测试该连续性测试在连续图像的情况下会产生一个循环而不是对行和列进行常规的双循环。 对于非常大的图像如我们在测试中使用的图像这种优化并不重要但总的来说使用此策略始终是一种很好的做法因为它可以大大提高速度。 更多 多线程是提高算法效率的另一种方法尤其是自多核处理器问世以来。 OpenMP 和英特尔线程构建模块TBB是在并发编程中用于创建和管理线程的两种流行的 API。 另见 看看“执行简单图像算术”秘籍了解使用 OpenCV 2 算术图像运算符的色彩缩减方法的实现。 使用邻居访问扫描图像 在图像处理中通常具有基于相邻像素的值来计算每个像素位置处的值的处理函数。 当该邻域包含上一行和下一行的像素时则需要同时扫描图像的几行。 此秘籍向您展示如何做。 准备 为了说明这一秘籍我们将应用处理函数以使图像清晰。 它基于拉普拉斯算子将在第 6 章中进行讨论。 在图像处理中确实是众所周知的结果如果从图像中减去其拉普拉斯算子则会放大图像边缘从而获得更清晰的图像。 该锐化运算符的计算如下 sharpened_pixel 5current-left-right-up-down;其中left是当前像素左侧的像素up是前一行对应的像素依此类推。 操作步骤 这次处理无法原地完成。 用户需要提供输出图像。 图像扫描是通过使用三个指针完成的一个指针用于当前行一个指针用于上一行另一个指针用于下一行。 另外由于每个像素计算都需要访问相邻像素因此无法为图像的第一行和最后一行的像素以及第一列和最后一列的像素计算值。 然后可以将循环编写如下 void sharpen(const cv::Mat image, cv::Mat result) {// allocate if necessaryresult.create(image.size(), image.type()); for (int j 1; jimage.rows-1; j) { // for all rows // (except first and last)const uchar previous image.ptrconst uchar(j-1); // previous rowconst uchar* current image.ptrconst uchar(j); // current rowconst uchar* next image.ptrconst uchar(j1); // next rowuchar* output result.ptruchar(j); // output rowfor (int i1; iimage.cols-1; i) {*output cv::saturate_castuchar(5*current[i]-current[i-1]-current[i1]-previous[i]-next[i]); }}// Set the unprocess pixels to 0result.row(0).setTo(cv::Scalar(0));result.row(result.rows-1).setTo(cv::Scalar(0));result.col(0).setTo(cv::Scalar(0));result.col(result.cols-1).setTo(cv::Scalar(0)); }如果我们将此函数应用于测试图像的灰度版本则会获得以下示例 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BKmM6Y8I-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_04.jpg)] 工作原理 为了访问上一行和下一行的相邻像素必须简单定义共同增加的其他指针。 然后您可以在扫描循环中访问这些行的像素。 在输出像素值的计算中对运算结果调用模板函数cv::saturate_cast。 这是因为经常发生这样的情况对像素应用数学表达式会导致结果超出允许的像素值范围小于 0 或大于 255。 然后的解决方案是恢复该 8 位范围内的值。 这是通过将负值更改为 0 并将值更改为 255 至 255 来完成的。这正是cv::saturate_castuchar函数所做的。 此外如果输入参数是浮点数则结果将四舍五入到最接近的整数。 您显然可以将此函数与其他类型一起使用以确保结果将保持在此类型定义的范围内。 由于邻域未完全定义而无法处理的边界像素需要单独处理。 在这里我们将它们简单地设置为 0。在其他情况下可以对这些像素执行一些特殊的计算但是在大多数情况下花费时间来处理这些很少的像素是没有意义的。 在我们的函数中使用两种特殊方法将这些边界像素设置为 0。 第一个是row及其对偶的col。 它们返回一个特殊的cv::Mat实例该实例由参数中指定的单行或单列组成。 这里没有进行复制因为如果修改此一维矩阵的元素它们也将在原始图像中被修改。 这就是调用方法setTo时所做的事情。 此方法为矩阵的所有元素分配一个值。 因此声明 result.row(0).setTo(cv::Scalar(0));将值 0 分配给结果图像第一行的所有像素。 对于 3 通道彩色图像可以使用cv::Scalar(a,b,c)指定三个值以分配给像素的每个通道。 更多 当在像素邻域上完成计算时通常用核矩阵表示它。 该核描述了如何将计算中涉及的像素进行组合以获得所需的结果。 对于此秘籍中使用的锐化过滤器核为 0-10-15-10-10 除非另有说明否则当前像素对应于核的中心。 核每个单元中的值代表一个乘以相应像素的因子。 然后将所有这些乘法的总和给出核应用于像素的结果。 核的大小对应于邻域的大小此处为3x3。 使用这种表示法可以看出按照锐化过滤器的要求当前像素的四个水平和垂直邻居都乘以 -1而当前像素的水平和垂直邻居都乘以 5。 除了方便的表示之外它是信号处理中卷积概念的基础。 核定义了应用于图像的过滤器。 由于过滤是图像处理中的常见操作因此 OpenCV 定义了执行此任务的特殊函数 cv::filter2D函数。 要使用它只需定义一个核以矩阵的形式。 然后使用图像和核调用该函数并返回过滤后的图像。 因此使用此函数可以很容易地重新定义锐化函数如下所示 void sharpen2D(const cv::Mat image, cv::Mat result) {// Construct kernel (all entries initialized to 0)cv::Mat kernel(3,3,CV_32F,cv::Scalar(0));// assigns kernel valueskernel.atfloat(1,1) 5.0;kernel.atfloat(0,1) -1.0;kernel.atfloat(2,1) -1.0;kernel.atfloat(1,0) -1.0;kernel.atfloat(1,2) -1.0;//filter the imagecv::filter2D(image,result,image.depth(),kernel); }此实现产生与上一个完全相同的结果并且具有相同的效率。 但是对于较大的核使用filter2D方法是有利的因为在这种情况下它使用更有效的算法。 另见 第 6 章“过滤图像”对图像过滤的概念进行了更多说明。 执行简单的图像运算 图像可以以不同的方式组合。 由于它们是规则矩阵因此可以相加相减相乘或相除。 OpenCV 提供了各种图像算术运算符本秘籍中将讨论它们的用法。 准备 让我们处理第二个图像使用算术运算符将其合并到输入图像中。 以下是第二张图片 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J6sygKVt-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_06.jpg)] 操作步骤 在这里我们添加两个图像。 当需要创建一些特殊效果或将信息覆盖在图像上时此函数很有用。 我们通过调用cv::add函数或更精确地说是cv::addWeighted函数来实现此目的因为我们需要加权和即 cv::addWeighted(image1,0.7,image2,0.9,0.,result);该操作将产生一个新图像如以下屏幕截图所示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXmFQC1h-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_07.jpg)] 工作原理 所有二进制算术函数的工作方式均相同。 提供了两个输入第三个参数指定了输出。 在某些情况下可以指定在操作中用作标量乘数的权重。 这些函数中的每一个都有几种风格。 cv::add是多种形式的可用函数的典范 // c[i] a[i]b[i];cv::add(imageA,imageB,resultC); // c[i] a[i]k;cv::add(imageA,cv::Scalar(k),resultC); // c[i] k1*a[1]k2*b[i]k3; cv::addWeighted(imageA,k1,imageB,k2,k3,resultC);// c[i] k*a[1]b[i]; cv::scaleAdd(imageA,k,imageB,resultC);对于某些函数您还可以指定一个掩码 // if (mask[i]) c[i] a[i]b[i]; cv::add(imageA,imageB,resultC,mask); 如果应用遮罩则仅对遮罩值不为null的像素遮罩必须为 1 通道执行该操作。 看看cv::subtractcv::absdiff cv::multiply和cv::divide函数的不同形式。 还可以使用按位运算符cv::bitwise_andcv::bitwise_orcv::bitwise_xor和cv::bitwise_not。 查找每个元素的最大或最小像素值的运算符cv::min和cv::max也非常有用。 在所有情况下始终使用函数cv::saturate_cast请参见前面的秘籍以确保结果保持在定义的像素值域内即避免上溢或下溢。 图像必须具有相同的尺寸和类型如果输出图像与输入尺寸匹配则将重新分配输出图像。 而且由于操作是按元素执行的因此输入图像之一可以用作输出。 也可以使用将单个图像作为输入的几种运算符cv::sqrtcv::powcv::abscv::cuberootcv::exp和cv::log。 实际上几乎所有需要对图像执行的操作都具有 OpenCV 函数。 更多 也可以在cv::Mat实例或cv::Mat实例的各个通道上使用常规的 C 算术运算符。 以下两个小节说明了如何执行此操作。 重载的图像运算符 非常方便的是大多数算术函数在 OpenCV 2 中都有相应的运算符重载。因此对cv::addWeighted的调用可以写为 result 0.7*image10.9*image2;这是一种更紧凑的形式也更易于阅读。 这两种写加权总和的方法是等效的。 特别是在两种情况下函数cv::saturate_cast仍将被调用。 大多数 C 运算符已被重载。 其中按位运算符 |, ^~minmax和abs函数比较运算符和! 这些后来返回一个 8 位二进制图像。 您还会发现矩阵乘法m1*m2其中m1和m2都是cv::Mat实例矩阵求逆m1.inv()转置m1.t()行列式m1.determinant()向量范数v1.norm() 叉积v1.cross(v2)点积v1.dot(v2)等。 在这种情况下您还可以定义op运算符例如。 在“编写高效的图像扫描循环”秘籍中我们提出了一种色彩缩减函数该函数是通过使用循环扫描图像像素以对其执行一些算术运算而编写的。 根据我们在这里学到的知识可以使用输入图像上的算术运算符简单地重写此函数即 image(imagecv::Scalar(mask,mask,mask))cv::Scalar(div/2,div/2,div/2);cv::Scalar的使用是由于我们正在处理彩色图像。 执行与在“编写高效的图像扫描循环”秘籍中所做的相同测试我们获得89ms的执行时间。 这主要是因为如所写该表达式需要调用两个函数按位与和标量和而不是在一个图像循环内执行完整的操作。 即使生成的代码并非始终是最佳的使用图像运算符也使代码如此简单并且程序员如此高效以至于在大多数情况下都应考虑使用它们。 分割图像通道 有时您可能需要独立处理图像的不同通道。 例如您可能只想在图像的一个通道上执行操作。 当然您可以在图像扫描循环中实现此目的。 但是您也可以使用cv::split函数它将彩色图像的三个通道复制到三个不同的cv::Mat实例中。 假设我们只想将雨图像添加到蓝色通道。 以下是我们将如何进行 // create vector of 3 imagesstd::vectorcv::Mat planes;// split 1 3-channel image into 3 1-channel imagescv::split(image1,planes);// add to blue channelplanes[0] image2;// merge the 3 1-channel images into 1 3-channel imagecv::merge(planes,result);cv::merge函数执行双重操作即从三个 1 通道图像创建彩色图像。 定义兴趣区域 有时仅需要在图像的一部分上应用处理函数。 该秘籍将教您如何在图像内定义兴趣区域。 准备 假设我们要组合两个大小不同的图像。 例如假设我们要在测试图像中添加以下小徽标 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w5u0FbJU-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_08.jpg)] 但是函数cv::add需要两张相同大小的图像。 在这种情况下可以定义兴趣区域ROI可以在其上应用cv::add。 只要 ROI 与我们徽标图像的大小相同这将起作用。 ROI 的位置将确定徽标将在图像中插入的位置。 操作步骤 第一步包括定义 ROI。 定义后可以将 ROI 作为常规cv::Mat实例进行操作。 关键是 ROI 指向与其父映像相同的数据缓冲区。 然后将徽标插入如下 // define image ROIcv::Mat imageROI;imageROI image(cv::Rect(385,270,logo.cols,logo.rows));// add logo to image cv::addWeighted(imageROI,1.0,logo,0.3,0.,imageROI);然后获得以下图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-how2Bpky-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_09.jpg)] 由于徽标的颜色已添加到图像的颜色中还可能应用了饱和度因此视觉效果将不总是令人满意的。 因此最好将图像的像素值简单地设置为该图像出现的徽标值。 为此您可以使用遮罩将徽标复制到 ROI // define ROIimageROI image(cv::Rect(385,270,logo.cols,logo.rows));// load the mask (must be gray-level)cv::Mat mask cv::imread(logo.bmp,0);// copy to ROI with masklogo.copyTo(imageROI,mask);然后结果图像为 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-inL8eUmC-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_10.jpg)] 工作原理 定义 ROI 的一种方法是使用cv::Rect实例。 顾名思义它通过指定左上角的位置构造器的前两个参数和矩形的大小后两个参数给出的宽度和高度来描述矩形区域。 还可以使用行和列范围来描述 ROI。 范围是从开始索引到结束索引的连续序列不包括在内。 cv::Range结构用于表示此概念。 因此可以从两个范围定义 ROI例如在我们的示例中ROI 可以等效地定义如下 cv::Mat imageROI image(cv::Range(270,270logo.rows), cv::Range(385,385logo.cols))cv::Mat的operator()返回另一个cv::Mat实例该实例随后可用于子序列调用中。 ROI 的任何变换都会影响相应区域中的原始图像因为图像和 ROI 共享相同的图像数据。 由于 ROI 的定义不会复制数据因此无论 ROI 的大小如何它都将在固定时间内执行。 如果要定义由图像的某些行组成的 ROI可以使用以下调用 cv::Mat imageROI image.rowRange(start,end) ;同样对于由某些图像列组成的 ROI cv::Mat imageROI image.colRange(start,end) ;秘籍“使用访问邻居扫描图像”中使用的方法row和col是这些后来方法的特殊情况其中开始索引和结束索引相等以便定义一个在线或单列 ROI。 三、使用类处理图像 在本章中我们将介绍 在算法设计中使用策略模式使用控制器与处理模块通信使用单例设计模式使用模型-视图-控制器架构设计应用转换色彩空间 简介 好的计算机视觉程序始于好的编程习惯。 构建无错误的应用仅仅是开始。 您真正想要的是一个应用您和与您一起工作的程序员将能够轻松适应新需求的发展。本章将向您展示如何充分利用一些面向对象的编程原理以便建立高质量的软件程序。 特别是我们将介绍一些重要的设计模式这些模式将帮助您构建由易于测试维护和重用的组件组成的应用。 设计模式是软件工程中众所周知的概念。 基本上设计模式是对软件设计中经常出现的一般性问题的一种可重复使用的合理解决方案。 已经引入了许多软件模式并有据可查。 好的程序员应该对这些现有模式有一定的了解。 本章还有第二个目标。 它将教您如何使用图像颜色。 本章中使用的示例将向您展示如何检测给定颜色的像素最后的秘籍将说明如何使用不同的颜色空间。 在策略设计中使用策略模式 策略设计模式的目标是将算法封装到一个类中。 这样将给定算法替换为另一个算法或将多个算法链接在一起以构建更复杂的过程变得更加容易。 另外该模式通过将尽可能多的复杂性隐藏在直观的编程接口后面从而促进了算法的部署。 准备 假设我们要构建一种简单的算法该算法将识别图像中具有给定颜色的所有像素。 然后算法必须接受图像和颜色作为输入并返回显示具有指定颜色的像素的二进制图像。 我们希望接受颜色的容差将是运行算法之前要指定的另一个参数。 操作步骤 该算法的核心过程很容易构建。 这是一个遍历每个像素的简单扫描循环将其颜色与目标颜色进行比较。 使用我们在上一章中学到的知识该循环可以写为 // get the iteratorscv::Mat_cv::Vec3b::const_iterator itimage.begincv::Vec3b();cv::Mat_cv::Vec3b::const_iterator itendimage.endcv::Vec3b();cv::Mat_uchar::iterator itout result.beginuchar();// for each pixelfor ( ; it! itend; it, itout) {// process each pixel ———————// compute distance from target colorif (getDistance(*it)minDist) {*itout 255;} else {*itout 0;}// end of pixel processing —————-}cv::Mat变量image表示输入图像而result表示二进制输出图像。 因此第一步包括设置所需的迭代器。 这样即可轻松实现扫描for循环。 每次迭代都会检查当前像素颜色和目标颜色之间的距离是否在minDist定义的公差范围内。 如果是这种情况则将值255白色分配给输出图像如果不是则分配0黑色。 要计算两个颜色值之间的距离请使用getDistance方法。 有多种计算此距离的方法。 例如可以计算包含 RGB 颜色值的 3 个向量之间的欧式距离。 在我们的案例中为使计算简单有效我们简单地将 RGB 值的绝对差求和也称为城市街区距离。 getDistance方法的定义如下 // Computes the distance from target color.int getDistance(const cv::Vec3b color) const {return abs(color[0]-target[0])abs(color[1]-target[1])abs(color[2]-target[2]);}请注意我们如何使用cv::Vec3d来保存代表颜色的 RGB 值的三个unsigned chars。 变量target显然是指指定的目标颜色正如我们将要看到的它在我们定义的类算法中被定义为类变量。 现在让我们完成处理方法的定义。 用户将提供输入图像图像扫描完成后将返回结果 cv::Mat ColorDetector::process(const cv::Mat image) {// re-allocate binary map if necessary// same size as input image, but 1-channelresult.create(image.rows,image.cols,CV_8U);processing loop above goes here…return result; }每次调用此方法时检查是否需要重新分配包含结果二进制映射的输出图像以适合输入图像的大小这一点很重要。 这就是为什么我们使用cv::Mat的create方法。 请记住只有在指定的大小和深度与当前图像结构不符时该图像才会继续进行重新分配。 现在我们已经定义了核心处理方法让我们看看应该添加哪些其他方法来部署此算法。 先前我们确定了算法需要哪些输入和输出数据。 因此我们首先定义将保存此数据的类属性 class ColorDetector {private:// minimum acceptable distanceint minDist; // target colorcv::Vec3b target; // image containing resulting binary mapcv::Mat result;为了创建封装我们的算法的类的实例并命名为ColorDetector我们需要定义一个构造器。 请记住策略设计模式的目标之一是使算法部署尽可能容易。 可以定义的最简单的构造器是一个空的构造器。 它将在有效状态下创建类算法的实例。 然后我们希望构造器将所有输入参数初始化为其默认值或通常能带来良好结果的已知值。 在我们的案例中我们认为距离 100 通常是可以接受的公差。 我们还设置了默认的目标颜色。 我们没有特殊原因选择黑色。 目的是确保我们始终从可预测和有效的输入值开始 // empty constructorColorDetector() : minDist(100) { // default parameter initialization heretarget[0] target[1] target[2] 0;}此时创建我们的类算法实例的用户可以立即使用有效图像调用process方法并获得有效输出。 这是“策略”模式的另一个目标即确保算法始终以有效参数运行。 显然此类的用户将想要使用自己的设置。 这是通过为用户提供适当的获取器和获取器来完成的。 让我们从颜色容差参数开始 // Sets the color distance threshold.// Threshold must be positive, // otherwise distance threshold is set to 0.void setColorDistanceThreshold(int distance) {if (distance0)distance0;minDist distance;}// Gets the color distance thresholdint getColorDistanceThreshold() const {return minDist;}注意我们如何首先检查输入的有效性。 同样这是为了确保我们的算法永远不会在无效状态下运行。 可以类似地设置目标颜色 // Sets the color to be detectedvoid setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {// BGR ordertarget[2] red;target[1] green;target[0] blue;}// Sets the color to be detectedvoid setTargetColor(cv::Vec3b color) {target color;}// Gets the color to be detectedcv::Vec3b getTargetColor() const {return target;}这次有趣的是我们为用户提供了setTagertColor方法的两个定义。 在第一个版本中将三个颜色分量指定为三个参数而在第二个版本中cv::Vec3b用于保存颜色值。 同样目标是促进使用我们的类算法。 用户只需选择最适合需求的安装员即可。 工作原理 一旦使用策略设计模式将算法封装到一个类中就可以通过创建此类的实例来进行部署。 通常实例将在程序初始化时创建。 可以读取和显示算法参数的默认值。 对于具有 GUI 的应用可以使用不同的小部件文本字段滑块等读取和设置参数值以便用户可以轻松地使用它们。 但是在介绍 GUI 之前这将在本章后面完成让我们首先编写一个简单的main函数该函数将运行我们的颜色检测算法 int main() {// 1. Create image processor objectColorDetector cdetect;// 2. Read input imagecv::Mat image cv::imread(boldt.jpg);if (!image.data)return 0; // 3. Set input parameterscdetect.setTargetColor(130,190,230); // here blue skycv::namedWindow(result);// 4. Process the image and display the resultcv::imshow(result,cdetect.process(image));cv::waitKey();return 0; }在上一章介绍的彩色图像上运行该程序会产生以下输出 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KfPYYB1-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_01.jpg)] 显然我们在此类中封装的算法相对简单只有一个扫描循环和一个公差参数。 当要实现的算法更加复杂具有多个步骤并包含多个参数时策略设计模式将变得非常强大。 更多 要计算两个颜色向量之间的距离我们使用以下简单公式 return abs(color[0]-target[0])abs(color[1]-target[1])abs(color[2]-target[2]);但是OpenCV 包含用于计算向量的欧几里得范数的函数。 因此我们可以计算出如下距离 return static_castint(cv::normint,3(cv::Vec3i(color[0]-target[0],color[1]-target[1],color[2]-target[2])));然后使用getDistance方法的此定义将获得非常相似的结果。 在此我们使用cv::Vec3i整数的 3 个向量因为相减的结果是整数值。 从第 2 章回忆起OpenCV 矩阵和向量数据结构包括基本算术运算符的定义这也很有趣。 例如如果要添加两个cv::Vec3i向量a和b并将结果分配给c则可以简单地编写 c ab;或者可以为距离计算提出以下定义 return static_castint(cv::normuchar,3(color-target);乍一看这个定义可能是正确的但是这是错误的。 这是因为所有这些运算符总是包含对saturate_cast的调用请参阅上一章中的秘籍“使用访问邻居扫描图像”以确保结果保持在输入类型的域内此处为uchar。 因此在目标值大于相应颜色值的情况下将分配值 0 而不是预期的负值。 另见 由 A. Alexandrescu 引入的基于策略的类设计是策略设计模式的一个有趣变体其中在编译时选择算法。 Erich Gamma 等人Addison-Wesley于 1994 年出版的《设计模式可重用的面向对象软件的元素》是关于该主题的经典书籍之一。 另请参阅“使用模型-视图-控制器模式”秘籍构建基于 GUI 的应用以了解如何在具有 GUI 的应用中使用策略模式。 使用控制器与处理模块通信 在构建更复杂的应用时您将需要创建可以组合在一起的多种算法以完成一些高级任务。 因此正确设置应用并让所有类一起通信将变得越来越复杂。 这样将应用的控制集中在一个类中就变得很有利。 这是控制器背后的想法。 它是应用中的一个特定对象起着重要的作用我们将在本秘籍中对其进行探讨。 准备 使用两个按钮创建一个基于对话框的简单应用一个按钮用于选择图像另一个按钮用于开始处理如下所示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqP46HGa-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_02.jpg)] 在这里我们使用先前秘籍的ColorDetector类。 操作步骤 控制器的角色是首先创建执行应用所需的类。 在这里它只是一堂课。 另外我们需要两个成员变量以保留对输入和输出结果的引用 class ColorDetectController {private:// the algorithm classColorDetector cdetect;cv::Mat image; // The image to be processedcv::Mat result; // The image resultpublic:ColorDetectController() { //setting up the applicationcdetect new ColorDetector();}然后您需要定义用户控制应用所需的所有设置器和获取器 // Sets the color distance thresholdvoid setColorDistanceThreshold(int distance) {cdetect-setColorDistanceThreshold(distance);}// Gets the color distance thresholdint getColorDistanceThreshold() const {return cdetect-getColorDistanceThreshold();}// Sets the color to be detectedvoid setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {cdetect-setTargetColor(red,green,blue);}// Gets the color to be detectedvoid getTargetColor(unsigned char red, unsigned char green, unsigned char blue) const {cv::Vec3b color cdetect-getTargetColor();red color[2];green color[1];blue color[0];}// Sets the input image. Reads it from file.bool setInputImage(std::string filename) {image cv::imread(filename);if (!image.data)return false;elsereturn true;}// Returns the current input image.const cv::Mat getInputImage() const {return image;}您还需要一种将被调用以启动该过程的方法 // Performs image processing.void process() {result cdetect-process(image);}以及获得处理结果的方法 // Returns the image result from the latest processing.const cv::Mat getLastResult() const {return result;}最后在应用终止并释放控制器时清理所有内容非常重要 // Deletes processor objects created by the controller.~ColorDetectController() {delete cdetect;}工作原理 使用上面的控制器类程序员可以轻松地为将执行算法的应用构建接口。 程序员无需了解所有类如何连接在一起也不必找出必须调用哪个类的方法才能使所有程序正常运行。 这全部由控制器类完成。 唯一的要求是创建该控制器类的实例。 控制器中定义的设置器和获取器是您认为部署算法所需的那些。 这些方法只是在适当的类中调用相应的方法。 同样这里的简单示例仅包含一种类算法但是在大多数情况下将涉及多个类实例。 因此控制器的作用是将请求重定向到适当的类并简化与这些类的接口。 作为这种简化的示例请考虑方法setTargetColor和getTargetColor 。 他们都使用uchar设置并获取感兴趣的颜色。 这消除了应用程序员了解cv::Vec3b类的任何知识。 在某些情况下控制器还准备应用程序员提供的数据。 这是我们在setInputImage方法的情况下所做的其中将与给定文件名相对应的图像加载到内存中。 该方法返回true或false取决于加载操作是否成功也可能引发异常来处理这种情况。 最后方法process是运行该算法的方法。 该方法不返回结果必须调用另一个方法才能获得最新处理结果。 现在要使用此控制器创建一个非常基本的基于对话框的应用只需将ColorDetectController成员变量添加到对话框类此处称为colordetect。 如果是 MFC 对话框则“打开”按钮将如下所示 // Callback method of Open button. void OnOpen() {// MFC widget to select a file of type bmp or jpgCFileDialog dlg(TRUE, _T(.bmp), NULL,OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST|OFN_HIDEREADONLY,_T(image files (*.bmp; .jpg) |.bmp;.jpg|All Files (.)|.*||),NULL);dlg.m_ofn.lpstrTitle _T(Open Image);// if a filename has been selectedif (dlg.DoModal() IDOK) {// get the path of the selected filenamestd::string filename dlg.GetPathName(); // set and display the input imagecolordetect.setInputImage(filename);cv::imshow(Input Image,colordetect.getInputImage());} }第二个按钮执行该过程并显示结果 // Callback method of Process button. void OnProcess() {// target color is hard-coded herecolordetect.setTargetColor(130,190,230);// process the input image and display resultcolordetect.process();cv::imshow(Output Result,colordetect.getLastResult()); }显然一个更完整的应用将包括其他小部件以允许用户设置算法参数。 另见 另请参见“使用模型视图控制器模式构建基于 GUI 的应用”的秘籍该模式提供了由 GUI 控制的应用的更多扩展示例。 使用单例设计模式 单例是另一种流行的设计模式用于促进对类实例的访问并确保在程序执行期间仅存在该类的一个实例。 在此秘籍中我们使用单例访问控制器对象。 准备 我们使用先前秘籍的ColorDetectController类。 为了获得单例类将对其进行修改。 操作步骤 首先要做的是添加一个私有静态成员变量该变量将保留对单个类实例的引用。 另外为了禁止构造其他类实例将构造器设为私有 class ColorDetectController {private:// pointer to the singletonstatic ColorDetectController *singleton; ColorDetector *cdetect;// private constructorColorDetectController() { //setting up the applicationcdetect new ColorDetector();}此外您还可以将副本构造器和operator设为私有以确保没有人可以创建单例唯一实例的副本。 当类的用户要求此类的实例时将按需创建单例对象。 这可以使用静态方法完成该方法会创建实例如果尚不存在然后返回指向该实例的指针 // Gets access to Singleton instancestatic ColorDetectController *getInstance() {// Creates the instance at first callif (singleton 0)singleton new ColorDetectController;return singleton;}请注意但是单例的此实现不是线程安全的。 因此当并发​​线程需要访问单例实例时不应使用它。 最后由于单例实例是动态创建的因此用户在不再需要它时必须将其删除。 同样这是通过静态方法完成的 // Releases the singleton instance of this controller.static void destroy() {if (singleton ! 0) {delete singleton;singleton 0;}}由于singleton是静态成员变量因此必须在.cpp文件中定义。 这样做如下 #include colorDetectController.hColorDetectController ColorDetectController::singleton0; 工作原理 由于可以通过公共静态方法获取单例因此所有包含单例类声明的类都可以访问单例对象。 这对于某些复杂 GUI 的几个小部件类可以访问的控制器对象特别有用。 无需前面的秘籍中的任何一个 GUI 类中的成员变量。 对话框类的两个回调方法将如下编写 // Callback method of Open button. void OnOpen() {…// if a filename has beed selectedif (dlg.DoModal() IDOK) {// get the path of the selected filenamestd::string filename dlg.GetPathName(); // set and display the input imageColorDetectController::getInstance()-setInputImage(filename);cv::imshow(Input Image,ColorDetectController::getInstance()-getInputImage());} }// Callback method of Process button. OnProcess() {// target color is hard-coded hereColorDetectController::getInstance()-setTargetColor(130,190,230);// process the input image and display resultColorDetectController::getInstance()-process();cv::imshow(Output Result,ColorDetectController::getInstance()-getLastResult()); }当应用关闭时必须释放单例实例 // Callback method of Close button. void OnClose() {// Releases the Singleton.ColorDetectController::getInstance()-destroy();OnOK(); }如此处所示将控制器封装在单例内时从任何类获取对此实例的访问变得更加容易。 但是此应用的更严格实现将需要更精细的 GUI。 这在下一个秘籍中完成该秘籍通过介绍模型-视图-控制器架构总结了在应用设计中使用模式的讨论。 使用模型-视图-控制器架构设计应用 前面的秘籍使您可以发现三种重要的设计模式策略控制器和单例模式。 本秘籍介绍了一种架构模式其中将这三种模式与其他类结合使用。 正是模型视图控制器或 MVC 的目的是产生一个将应用逻辑与用户界面清楚地分开的应用。 在本秘籍中我们将使用 MVC 模式使用 Qt 构建基于 GUI 的应用。 但是在实际操作之前我们先简要介绍一下该模式。 准备 顾名思义MVC 模式包含三个主要组件。 现在我们将看看它们各自的作用。 模型包含有关应用的信息。 它保存了应用处理的所有数据。 产生新数据时它将通知控制器控制器随后将要求视图显示新结果。 通常模型会将几种算法组合在一起可能按照策略模式实现。 所有这些算法都是模型的一部分。 视图对应于用户界面。 它由不同的小部件组成这些小部件将数据呈现给用户并允许用户与应用进行交互。 它的作用之一是将用户发出的命令发送到控制器。 当有新数据可用时它会刷新以显示新信息。 控制器是将视图和模型桥接在一起的模块。 它从视图接收请求并将请求中继到模型中的适当方法。 当模型更改其状态时也会通知它因此要求刷新视图以显示此新信息。 操作步骤 与前面的秘籍一样我们将使用ColorDetector类。 这将是我们的模型其中包含应用逻辑和基础数据。 我们还实现了一个控制器它是ColorDetectController类。 然后通过选择最合适的窗口小部件可以轻松构建更复杂的 GUI。 例如使用 Qt可以构建以下接口 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-toAkzqQN-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_03.jpg)] 打开图像按钮用于选择和打开图像。 可以通过按选择颜色按钮选择要检测的颜色。 这将打开一个颜色选择器小部件下面以黑白打印可轻松选择所需的颜色 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XW7LZjEv-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_04.jpg)] 然后使用滑块选择要使用的正确阈值。 然后通过按处理按钮处理图像并显示结果。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4q7xcgCQ-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_05.jpg)] 工作原理 在 MVC 架构下用户界面仅调用控制器方法。 它不包含任何应用数据也不实现任何应用逻辑。 因此很容易用另一个接口替换一个接口。 在这里添加了颜色选择器小部件QColorDialog一旦选择了颜色就会从选择颜色插槽中调用适当的控制器方法 QColor color QColorDialog::getColor(Qt::green, this); if (color.isValid()) { ColorDetectController::getInstance()-setTargetColor(color.red(),color.green(),color.blue()); }通过QSlider小部件设置阈值。 当单击处理按钮时将读取此值这还将触发处理并显示结果 ColorDetectController::getInstance()-setColorDistanceThreshold(ui-verticalSlider_Threshold-value()); ColorDetectController::getInstance()-process(); cv::Mat resulting ColorDetectController::getInstance()-getLastResult(); if (!resulting.empty())displayMat(resulting);实际上Qt 的 GUI 库大量使用了 MVC 模式。 它使用信号概念的概念以使 GUI 的所有小部件与数据模型保持同步。 另见 Qt 在线文档可以帮助您了解有关 MVC 模式的 Qt 实现的更多信息。 第 1 章的“使用 Qt 创建 GUI 应用”秘籍以简要介绍 Qt GUI 框架及其信号和插槽模型。 转换色彩空间 本章教您如何将算法封装到类中。 这样通过简化的接口该算法变得更易于使用。 封装还允许您修改算法的实现而不会影响使用该算法的类。 在此秘籍中说明了此原理在此秘籍中我们将修改ColorDetector类算法以使用其他颜色空间。 因此此秘籍将是引入 OpenCV 颜色转换的机会。 准备 RGB 颜色空间或 BGR取决于存储颜色的顺序基于红色绿色和蓝色加法原色的使用。 之所以选择这些是因为将它们组合在一起可以产生各种颜色的色域。 实际上人类视觉系统还基于三色感知的颜色视锥细胞敏感度位于红色绿色和蓝色光谱附近。 它通常是数字图像中的默认色彩空间因为这是获取色彩的方式。 捕获的光通过红色绿色和蓝色过滤器。 另外在数字图像中调节红色绿色和蓝色通道使得当以等量组合时获得灰度级强度即从黑色(0,0,0)到白色(255,255,255)。 不幸的是使用 RGB 颜色空间计算颜色之间的距离并不是衡量两种给定颜色相似度的最佳方法。 确实RGB 不是在感知上均匀的色彩空间。 这意味着给定距离处的两种颜色可能看起来非常相似而相隔相同距离的其他两种颜色看起来会非常不同。 为了解决该问题已经引入了具有感知上均匀的特性的其他色彩空间。 特别地CIE Lab 是一种这样的色彩空间。 通过将我们的图像转换到该空间图像像素和目标颜色之间的欧几里得距离将有意义地成为两种颜色之间视觉相似性的度量。 我们将在此秘籍中展示如何修改先前的应用以与 CIE Lab 一起使用。 操作步骤 通过使用 OpenCV 函数cv::cvtColor可以轻松完成不同颜色空间之间的转换。 让我们在处理方法开始时将输入图像转换为 CIE Lab 颜色空间 cv::Mat ColorDetector::process(const cv::Mat image) {// re-allocate binary map if necessary// same size as input image, but 1-channelresult.create(image.rows,image.cols,CV_8U);// re-allocate intermediate image if necessaryconverted.create(image.rows,image.cols,image.type());// Converting to Lab color space cv::cvtColor(image, converted, CV_BGR2Lab);// get the iterators of the converted image cv::Mat_cv::Vec3b::iterator it converted.begincv::Vec3b();cv::Mat_cv::Vec3b::iterator itend converted.endcv::Vec3b();// get the iterator of the output image cv::Mat_uchar::iterator itout result.beginuchar();// for each pixelfor ( ; it! itend; it, itout) {…变量converted包含颜色转换后的图像。 在ColorDetector类中将其定义为类属性 class ColorDetector {private:// image containing color converted imagecv::Mat converted;我们还需要转换输入的目标颜色。 为此我们创建了一个仅包含 1 个像素的临时图像。 请注意您需要保持与先前秘籍相同的签名即用户继续以 RGB 提供目标颜色 // Sets the color to be detectedvoid setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {// Temporary 1-pixel imagecv::Mat tmp(1,1,CV_8UC3);tmp.atcv::Vec3b(0,0)[0] blue;tmp.atcv::Vec3b(0,0)[1] green;tmp.atcv::Vec3b(0,0)[2] red;// Converting the target to Lab color space cv::cvtColor(tmp, tmp, CV_BGR2Lab);target tmp.atcv::Vec3b(0,0);}如果使用此修改后的类编译了先前秘籍的应用则现在它将使用 CIE Lab 颜色空间检测目标颜色的像素。 工作原理 当图像从一种颜色空间转换为另一种颜色空间时线性或非线性变换将应用于每个输入像素以产生输出像素。 输出图像的像素类型将与输入图像之一匹配。 即使大多数时候使用 8 位像素也可以对浮点图像使用色彩转换在这种情况下通常假定像素值在0和1.0之间变化或整数图像 像素通常在0和65535之间变化。 但是像素值的确切范围取决于特定的色彩空间。 例如对于 CIE Lab 颜色空间L通道在0和100之间变化而a和b色度分量在-127和127之间变化 。 可以使用最常用的色彩空间。 这只是为 OpenCV 函数提供正确的掩码的问题。 其中包括 YCrCb它是 JPEG 压缩中使用的色彩空间。 为了从 BGR​​转换为 YCrCb掩码应为CV_BGR2YCrCb。 请注意具有三种常规原色红色绿色和蓝色的表示形式按 RGB 顺序或 BRG 顺序可用。 HSV 和 HLS 颜色空间也很有趣因为它们将颜色分解为其色相和饱和度分量以及值或亮度分量这是人类描述颜色的一种更自然的方式。 您也可以将彩色图像转换为灰度图像。 输出将是一个 1 通道图像 cv::cvtColor(color, gray, CV_BGR2Gray);也可以在另一个方向上进行转换但是最终得到的彩色图像的 3 个通道将用灰度图像中的相应值完全填充。 另见 第 4 章中“使用平均移位算法找到对象”的秘籍使用 HSV 颜色空间在图像中找到对象。 关于色彩空间理论有许多很好的参考资料。 其中以下是完整且最新的参考文献E. DuboisMorgan 和 Claypool 于 2009 年 10 月发表的《色彩空间的结构和特性以及彩色图像的表示》。 四、使用直方图计算像素 在本章中我们将介绍 计算图像直方图应用查询表修改图像外观均衡图像直方图反投影直方图来检测特定图像内容使用均值平移算法查找对象使用直方图比较检索相似图像 简介 图像由具有不同值颜色的像素组成。 像素值在整个图像上的分布构成了此图像的重要特征。 本章介绍图像直方图的概念。 您将学习如何计算直方图以及如何使用它来修改图像的外观。 直方图还可以用于表征图像的内容并检测图像中的特定对象或纹理。 其中一些技巧将在本章中介绍。 计算图像直方图 图像由像素组成每个像素具有不同的值。 例如在 1 通道灰度图像中每个像素的值介于 0黑色和 255白色之间。 根据图片内容您会发现图像内部布置的每种灰色阴影的数量不同。 直方图是一个简单的表格它给出了图像或有时是一组图像中具有给定值的像素数。 因此灰度图像的直方图将具有 256 个条目或箱子。 箱子 0 给出值为 0 的像素数箱子 1 给出值为 1 的像素数依此类推。 显然如果将直方图的所有条目相加则应该获得像素总数。 直方图也可以归一化以使箱子的总和等于 1。在这种情况下每个箱子给出图像中具有该特定值的像素的百分比。 入门 定义一个简单的控制台项目并准备使用如下图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rUh4bnYD-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_01.jpg)] 操作步骤 使用cv::calcHist函数可以很容易地用 OpenCV 计算直方图。 这是一个通用函数可以计算任何像素值类型的多通道图像的直方图。 通过专门针对 1 通道灰度图像的类让它更易于使用 class Histogram1D {private:int histSize[1]; // number of binsfloat hranges[2]; // min and max pixel valueconst float ranges[1];int channels[1]; // only 1 channel used herepublic:Histogram1D() {// Prepare arguments for 1D histogramhistSize[0] 256;hranges[0] 0.0;hranges[1] 255.0;ranges[0] hranges; channels[0] 0; // by default, we look at channel 0}使用定义的成员变量可以使用以下方法来完成灰度直方图的计算 // Computes the 1D histogram.cv::MatND getHistogram(const cv::Mat image) {cv::MatND hist;// Compute histogramcv::calcHist(image, 1, // histogram from 1 image onlychannels, // the channel usedcv::Mat(), // no mask is usedhist, // the resulting histogram1, // it is a 1D histogramhistSize, // number of binsranges // pixel value range);return hist;}现在您的程序只需要打开一个图像创建一个Histogram1D实例然后调用getHistogram方法 // Read input imagecv::Mat image cv::imread(../group.jpg,0); // open in bw// The histogram objectHistogram1D h;// Compute the histogramcv::MatND histo h.getHistogram(image); 这里的histo对象是具有 256 个条目的简单一维数组。 因此您可以通过简单地遍历此数组来读取每个箱子 // Loop over each binfor (int i0; i256; i) cout Value i histo.atfloat(i) endl;
在本章开头显示的图像中某些显示的值将显示为 … Value 7 159 Value 8 208 Value 9 271 Value 10 288 Value 11 340 Value 12 418 Value 13 432 Value 14 472 Value 15 525 …从这一系列值中提取任何直观的含义显然很困难。 因此通常可以方便地将直方图显示为函数例如使用条形图。 下面的方法创建这样的图 // Computes the 1D histogram and returns an image of it.cv::Mat getHistogramImage(const cv::Mat image){// Compute histogram firstcv::MatND hist getHistogram(image);// Get min and max bin valuesdouble maxVal0;double minVal0;cv::minMaxLoc(hist, minVal, maxVal, 0, 0);// Image on which to display histogramcv::Mat histImg(histSize[0], histSize[0], CV_8U,cv::Scalar(255));// set highest point at 90% of nbinsint hpt static_castint(0.9*histSize[0]);// Draw a vertical line for each bin for( int h 0; h histSize[0]; h ) {float binVal hist.atfloat(h);int intensity static_castint(binValhpt/maxVal);// This function draws a line between 2 points cv::line(histImg,cv::Point(h,histSize[0]),cv::Point(h,histSize[0]-intensity),cv::Scalar::all(0));}return histImg;}使用此方法您可以获得以线条绘制的条形图形式的直方图特征图像 // Display a histogram as an imagecv::namedWindow(Histogram);cv::imshow(Histogram,h.getHistogramImage(image)); 结果如下图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NYo6I02U-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_02.jpg)] 从该直方图可以看出图像显示出中等灰度级值的大峰值和大量的较暗像素。 这两组主要分别对应于图像的背景和前景。 这可以通过在这两个组之间的过渡处对图像进行阈值化来验证。 为此可以使用方便的 OpenCV 函数即cv::threshold函数 。 这是必须在图像上应用阈值以创建二进制图像时使用的函数。 在这里我们将图像的阈值限制在直方图的高峰值灰度值 60增加之前的最小值 cv::Mat thresholded;cv::threshold(image,thresholded,60,255,cv::THRESH_BINARY);生成的二进制图像清楚地显示了背景/前景分割 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cof7RItq-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_03.jpg)] 工作原理 函数cv::calcHist具有许多参数可以在多种情况下使用。 大多数情况下直方图将是单个 1 通道或 3 通道图像之一。 但是该函数允许您指定分布在多个图像上的多通道图像。 这就是为什么要将图像数组输入此函数的原因。 第 6 个参数指定直方图的维数例如对于 1D 直方图为 1。 在具有指定维数的数组中列出要在直方图计算中考虑的通道。 在我们的类实现中该单个通道默认为通道 0第三个参数。 直方图本身由每个维度中的仓数第七个参数整数数组和每个维度中的最小值和最大值第八个参数2 元素数组组成的数组描述。 也可以定义不均匀的直方图在这种情况下您需要指定每个箱子的限制。 对于许多 OpenCV 函数可以指定一个掩码指示要在计数中包括哪些像素然后忽略掩码值为 0 的所有像素。 可以指定两个附加的可选参数它们都是布尔值。 第一个指示直方图是否均匀默认为均匀。 第二个选项使您可以累积多个直方图计算的结果。 如果最后一个参数为true则图像的像素数将添加到在输入直方图中找到的当前值。 当一个人想要计算一组图像的直方图时这很有用。 生成的直方图存储在cv::MatND实例中。 这是用于处理 N 维矩阵的通用类。 方便地此类为尺寸为 1、2 和 3 的矩阵定义了at方法。这就是为什么我们能够这样写 float binVal hist.atfloat(h);在getHistogramImage方法中访问 1D 直方图的每个箱子时。 注意直方图中的值存储为float。 更多 本秘籍中介绍的类别Histogram1D通过将cv::calcHist函数限制为一维直方图来简化了函数。 这对于灰度图像很有用。 类似地我们可以定义一个可用于计算彩色 BGR 图像直方图的类 class ColorHistogram {private:int histSize[3];float hranges[2];const float ranges[3];int channels[3];public:ColorHistogram() {// Prepare arguments for a color histogramhistSize[0] histSize[1] histSize[2] 256;hranges[0] 0.0; // BRG rangehranges[1] 255.0;ranges[0] hranges; // all channels have the same range ranges[1] hranges; ranges[2] hranges; channels[0] 0; // the three channels channels[1] 1; channels[2] 2; }在这种情况下直方图将是三维的。 因此我们需要为三个维度中的每个维度指定一个范围。 对于 BGR 图像这三个通道具有相同的[0,255]范围。 在准备好参数之后通过以下方法计算出颜色直方图 cv::MatND getHistogram(const cv::Mat image) {cv::MatND hist;// Compute histogramcv::calcHist(image, 1, // histogram of 1 image onlychannels, // the channel usedcv::Mat(), // no mask is usedhist, // the resulting histogram3, // it is a 3D histogramhistSize, // number of binsranges // pixel value range);return hist;}返回一个三维cv::Mat实例。 该矩阵具有256 * 3个元素表示超过 1600 万个条目。 在许多应用中最好在计算出如此大的直方图之前减少颜色的数量请参阅第 2 章。 或者您也可以使用cv::SparseMat数据结构该数据结构用于表示大型稀疏矩阵即非零元素很少的矩阵而不会占用太多内存。 cv::calcHist函数具有返回一个这样的矩阵的版本。 因此很容易修改先前的方法以使用cv::SparseMatrix cv::SparseMat getSparseHistogram(const cv::Mat image) {cv::SparseMat hist(3,histSize,CV_32F);// Compute histogramcv::calcHist(image, 1, // histogram of 1 image onlychannels, // the channel usedcv::Mat(), // no mask is usedhist, // the resulting histogram3, // it is a 3D histogramhistSize, // number of binsranges // pixel value range);return hist;}另见 本章稍后的秘籍“反投影直方图以检测特定的图像内容”该方法将使用颜色直方图来检测特定的图像内容。 应用查询表修改图像外观 图像直方图使用可用的像素强度值捕获渲染场景的方式。 通过分析图像上像素值的分布可以使用此信息来修改并可能改善图像。 本秘籍说明了如何使用由查找表表示的简单映射函数来修改图像的像素值。 操作步骤 查找表是简单的一对一或多对一函数用于定义如何将像素值转换为新值。 对于常规灰度图像它是一维数组具有 256 个条目。 该表的条目i给出了相应灰度的新强度值即 newIntensity lookup[oldIntensity];OpenCV 中的函数cv::LUT将查找表应用于图像以生成新图像。 我们可以将此函数添加到我们的Histogram1D类中 cv::Mat applyLookUp(const cv::Mat image, // input imageconst cv::Mat lookup) { // 1x256 uchar matrix// the output imagecv::Mat result;// apply lookup tablecv::LUT(image,lookup,result);return result;}工作原理 当将查找表应用于图像时会生成新图像其中像素强度值已按照查找表的规定进行了修改。 这样的简单转换如下 // Create an image inversion tableint dim(256);cv::Mat lut(1, // 1 dimensiondim, // 256 entriesCV_8U); // ucharfor (int i0; i256; i) {lut.atuchar(i) 255-i;}此转换仅使像素强度反转即强度 0 变为 255强度 1 变为 254依此类推。 在图像上应用这样的查找表将产生原始图像的底片。 在上一个秘籍的图像上在这里可以看到结果 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VnIgITrf-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_04.jpg)] 更多 您还可以定义一个查找表以尝试改善图像的对比度。 例如如果您观察到第一个秘籍中显示的上一张图像的原始直方图则很容易注意到未使用整个范围的可能的强度值特别是对于此图像在图片中未使用较亮的强度值。 因此可以拉伸直方图以产生具有扩大对比度的图像。 该程序旨在检测图像直方图中计数为非零的最低imin和最高imax强度值。 然后可以重新映射强度值以使imin值重新定位为强度 0并且为imax赋值 255。强度i之间的线性映射简单如下 255.0(i-imin)/(imax-imin)0.5);因此完整图像拉伸方法将如下所示 cv::Mat stretch(const cv::Mat image, int minValue0) {// Compute histogram firstcv::MatND hist getHistogram(image);// find left extremity of the histogramint imin 0;for( ; imin histSize[0]; imin ) {std::couthist.atfloat(imin)std::endl;if (hist.atfloat(imin) minValue)break;}// find right extremity of the histogramint imax histSize[0]-1;for( ; imax 0; imax– ) {if (hist.atfloat(imax) minValue)break;}// Create lookup tableint dim(256);cv::Mat lookup(1, // 1 dimensiondim, // 256 entriesCV_8U); // uchar// Build lookup tablefor (int i0; i256; i) {// stretch between imin and imaxif (i imin) lookup.atuchar(i) 0;else if (i imax) lookup.atuchar(i) 255;// linear mappingelse lookup.atuchar(i) static_castuchar(255.0(i-imin)/(imax-imin)0.5);}// Apply lookup tablecv::Mat result;result applyLookUp(image,lookup);return result;}一旦计算出该方法请注意对我们的applyLookUp方法的调用。 同样在实践中不仅忽略具有 0 值的箱子而且忽略计数。 例如小于给定值在此定义为minValue的条目也可能是有利的。 该方法称为 // ignore starting and ending bins with less than 100 pixels
cv::Mat streteched h.stretch(image,100);然后在这里看到生成的图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IS9ObYYf-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_05.jpg)] 如以下屏幕快照所示具有以下扩展的直方图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DWJBv8Ku-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_06.jpg)] 另见 “均衡图像直方图”秘籍为您提供了另一种改善图像对比度的方法。 均衡图像直方图 在先前的秘籍中我们展示了如何通过拉伸直方图来改善图像的对比度使其直达所有可用强度值范围。 这种策略确实构成了可以有效改善图像的简单解决方案。 但是在许多情况下图像的视觉缺陷并不是其使用的强度范围太窄。 而是某些强度值比其他强度值使用得更频繁。 本章第一章中显示的直方图就是这种现象的一个很好的例子。 确实可以很好地表现出中灰色强度而较暗和较亮的像素值却很少。 实际上人们可以认为高质量的图像应该平等地利用所有可用的像素强度。 这是直方图均衡概念的思想即使图像直方图尽可能平坦。 操作步骤 OpenCV 提供了易于使用的函数可以执行直方图均衡化。 可以这样调用 cv::Mat equalize(const cv::Mat image) {cv::Mat result;cv::equalizeHist(image,result);return result;}将以下屏幕截图应用于我们的图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TL6gHf5A-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_07.jpg)] 该图像具有以下直方图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xH83mm10-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_08.jpg)] 当然直方图不能完全平坦因为查询表是全局的多对一转换。 但是可以看出直方图的总体分布现在比原始分布更均匀。 工作原理 在完全一致的直方图中所有面元都有相同数量的像素。 这意味着 50% 的像素强度低于 12825% 的像素强度低于 64依此类推。 可以使用以下规则来表达该观察结果在均匀的直方图中像素的p%的强度值必须小于或等于255 * p%。 这是用于均衡直方图的规则强度i的映射应处于与强度值低于i的像素百分比相对应的强度。 因此可以根据以下公式构建所需的查询表 lookup.atuchar(i) static_castuchar(255.0*p[i]);其中p[i]是强度低于或等于i的像素数。 函数p[i]通常称为累积直方图即它是一个直方图其中包含小于或等于给定强度的像素数而不包含具有特定强度值的像素。 通常直方图均衡化可以大大改善图像的外观。 但是视视觉内容而定结果的质量可能因图像而异。 反投影直方图来检测特定图像内容 直方图是图像内容的重要特征。 如果查看显示特定纹理或特定对象的图像区域则该区域的直方图可以看作是一个函数给出给定像素属于该特定纹理或对象的概率。 在本秘籍中您将学习如何将图像直方图有利地用于检测特定图像内容。 操作步骤 假设您有一张图片并且希望检测其中的特定内容例如在下面的屏幕快照中是天空中的云彩。 首先要做的是选择一个兴趣区域其中包含您要寻找的样本。 此区域是在以下测试屏幕截图上绘制的矩形内的区域 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmzGyyDM-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_09.jpg)] 在我们的程序中兴趣区域的获取如下 cv::Mat imageROI;imageROI image(cv::Rect(360,55,40,50)); // Cloud region然后您提取此 ROI 的直方图。 使用本章第一部分中定义的Histogram1D类可以轻松完成此操作 Histogram1D h;cv::MatND hist h.getHistogram(imageROI);通过对该直方图进行归一化我们获得一个函数该函数给出给定强度值的像素属于定义区域的概率 cv::normalize(histogram,histogram,1.0);对直方图进行反投影包括将输入图像中的每个像素值替换为在归一化的直方图中读取的相应像素值。 cv::calcBackProject(image,1, // one imagechannels, // the channels usedhistogram, // the histogram we are backprojectingresult, // the resulting back projection imageranges, // the range of values, for each dimension255.0 // a scaling factor );结果是以下概率图具有从亮低概率到暗高概率的参考区域概率 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWN2bIVc-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_10.jpg)] 如果在此图像上应用阈值我们将获得最可能的“云”像素 cv::threshold(result, result, 255threshold, 255, cv::THRESH_BINARY);[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6o2dLaVv-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_11.jpg)] 工作原理 前面的结果可能令人失望因为除了云之外还错误地检测了其他区域。 重要的是要了解概率函数是从简单的灰度直方图中提取的。 图像中的许多其他像素与云像素共享相同的强度并且在反投影直方图时相同强度的像素将被相同的概率值替换。 改善检测结果的一种解决方案是使用颜色信息。 但是为此我们需要修改对cv::calBackProject的调用。 函数cv::calBackProject与cv::calcHist函数相似。 第一个参数指定输入图像。 然后您需要列出要使用的通道号。 这次传递给函数的直方图是一个输入参数。 应该对其进行规范化并且其尺寸应与通道列表数组之一以及ranges参数之一匹配。 如cv::calcHist中所述这是一个float数组每个float数组指定每个通道的范围最小和最大值。 结果输出是图像即计算的概率图。 由于每个像素都被在对应的箱子位置处的直方图中找到的值替换因此所得图像的值介于 0.0 和 1.0 之间假定已提供标准化的直方图作为输入。 最后一个参数允许您选择将这些值乘以给定因子来重新缩放这些值。 更多 现在让我们看看如何在直方图反投影算法中使用颜色信息。 我们首先定义一个封装反向投影过程的类。 首先我们定义所需的属性并初始化数据 class ContentFinder {private:float hranges[2];const float ranges[3];int channels[3];float threshold;cv::MatND histogram;public:ContentFinder() : threshold(-1.0f) {ranges[0] hranges; // all channels have same range ranges[1] hranges; ranges[2] hranges; }接下来我们定义一个阈值参数该参数将用于创建显示检测结果的二进制图。 如果此参数设置为负值则将返回原始概率图 // Sets the threshold on histogram values [0,1]void setThreshold(float t) {threshold t;}// Gets the thresholdfloat getThreshold() {return threshold;}输入直方图必须归一化 // Sets the reference histogramvoid setHistogram(const cv::MatND h) {histogram h;cv::normalize(histogram,histogram,1.0);}要对直方图进行背投您只需指定图像范围此处假设所有通道都具有相同的范围以及使用的通道列表 cv::Mat find(const cv::Mat image, float minValue, float maxValue, int *channels, int dim) {cv::Mat result;hranges[0] minValue;hranges[1] maxValue;for (int i0; idim; i)this-channels[i] channels[i];cv::calcBackProject(image, 1, // input imagechannels, // list of channels usedhistogram, // the histogram we are usingresult, // the resulting backprojectionranges, // the range of values255.0 // the scaling factor);}// Threshold back projection to obtain a binary imageif (threshold0.0)cv::threshold(result, result, 255*threshold, 255, cv::THRESH_BINARY);return result;}现在让我们在上面使用的图像的彩色版本上使用 BGR 直方图。 这次我们将尝试检测蓝天区域。 我们将首先加载彩色图像使用第 2 章的色彩缩减函数减少颜色数量然后定义关注区域 ColorHistogram hc;// load color imagecv::Mat color cv::imread(../waves.jpg);// reduce colorscolor hc.colorReduce(color,32);// blue sky areacv::Mat imageROI color(cv::Rect(0,0,165,75)); 接下来您计算直方图并使用find方法检测图像的天空部分 cv::MatND hist hc.getHistogram(imageROI);ContentFinder finder;finder.setHistogram(hist);finder.setThreshold(0.05f);// Get back-projection of color histogramCv::Mat result finder.find(color);上一部分的图像彩色版本的检测结果在此处显示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ky1z53Fn-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_12.jpg)] 另见 下一个秘籍将使用 HSV 颜色空间来检测图像中的对象。 这是可用于检测某些图像内容的许多替代解决方案中的另一个。 使用均值移动算法查找对象 直方图反投影的结果是一个概率图该概率图表示在特定图像位置找到给定图像内容的概率。 假设我们现在知道图像中某个对象的大概位置则可以使用概率图找到该对象的确切位置。 最有可能的是在给定窗口内最大化此概率的那个。 因此如果我们从一个初始位置开始并反复移动那么应该可以找到确切的对象位置。 这是通过均值平移算法完成的。 操作步骤 假设我们已经确定了一个感兴趣的对象这里是狒狒的脸如下面的彩色屏幕截图所示请参见本书的网站以查看此彩色图片 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CHFaPpwS-1681873909557)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_13.jpg)] 这次我们将通过使用 HSV 颜色空间的色相通道来描述此对象。 这意味着我们需要将图像转换为 HSV 图像然后提取色调通道并计算已定义 ROI 的 1D 色调直方图 // Read reference imagecv::Mat image cv::imread(../baboon1.jpg);// Baboons face ROIcv::Mat imageROI image(cv::Rect(110,260,35,40));// Get the Hue histogramint minSat65;ColorHistogram hc;cv::MatND colorhist hc.getHueHistogram(imageROI,minSat);可以看出色调直方图是使用我们添加到ColorHistogram类中的便捷方法获得的 // Computes the 1D Hue histogram with a mask.// BGR source image is converted to HSV// Pixels with low saturation are ignoredcv::MatND getHueHistogram(const cv::Mat image, int minSaturation0) {cv::MatND hist;// Convert to HSV color spacecv::Mat hsv;cv::cvtColor(image, hsv, CV_BGR2HSV);// Mask to be used (or not)cv::Mat mask;if (minSaturation0) {// Spliting the 3 channels into 3 imagesstd::vectorcv::Mat v;cv::split(hsv,v);// Mask out the low saturated pixelscv::threshold(v[1],mask,minSaturation,255,cv::THRESH_BINARY);}// Prepare arguments for a 1D hue histogramhranges[0] 0.0;hranges[1] 180.0;channels[0] 0; // the hue channel // Compute histogramcv::calcHist(hsv, 1, // histogram of 1 image onlychannels, // the channel usedmask, // binary maskhist, // the resulting histogram1, // it is a 1D histogramhistSize, // number of binsranges // pixel value range);return hist;}然后将生成的直方图输入到我们的ContentFinder类实例中 ContentFinder finder;finder.setHistogram(colorhist);现在让我们打开第二个图像我们要在其中定位新狒狒的脸部位置。 该图像需要转换为 HSV 空间 image cv::imread(../baboon3.jpg);// Display imagecv::namedWindow(Image 2);cv::imshow(Image 2,image);// Convert to HSV spacecv::cvtColor(image, hsv, CV_BGR2HSV);// Split the imagecv::split(hsv,v);// Identify pixels with low saturationcv::threshold(v[1],v[1],minSat,255,cv::THRESH_BINARY);接下来让我们使用先前获得的直方图获得该图像的色相通道的反投影 // Get back-projection of hue histogramresult finder.find(hsv,0.0f,180.0f,ch,1);// Eliminate low stauration pixelscv::bitwise_and(result,v[1],result);现在从初始矩形区域即原始图像中狒狒脸的位置开始OpenCV 的cv::meanShift算法将在新的狒狒脸部位置更新rect对象 cv::Rect rect(110,260,35,40);cv::rectangle(image, rect, cv::Scalar(0,0,255));cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER,10,0.01);cv::meanShift(result,rect,criteria);初始和新面部位置显示在以下屏幕截图中 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XVjVvFvF-1681873909557)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_14.jpg)] 工作原理 在此示例中我们使用了 HSV 颜色空间的色相成分来表征我们要寻找的对象。 因此必须先转换图像。 当使用CV_BGR2HSV标志时色相分量是所得图像的第一通道。 这是一个 8 位分量其中色相从 0 到 180 变化使用cv::cvtColor时转换后的图像与源图像的类型相同。 为了提取色调图像使用cv::split函数将 3 通道 HSV 图像分为三个 1 通道图像。 将这三个图像放入std::vector实例并且色调图像是向量的第一项即索引 0。 使用颜色的色相分量时考虑其饱和度这是向量的第二项总是很重要的。 实际上当颜色的饱和度低时色相信息变得不稳定且不可靠。 这是由于以下事实对于低饱和色BG 和 R 分量几乎相等。 这使得很难确定所代表的确切颜色。 因此我们决定忽略具有低饱和度的颜色的色相成分。 也就是说它们不计入直方图中使用方法getHueHistogram使用参数minSat掩盖饱和度低于此阈值的像素并且将它们从反投影结果中消除使用cv::bitwise_and运算符可在调用cv::meanShift之前消除所有具有低饱和度颜色的正检测像素。 均值平移算法是定位概率函数的局部最大值的迭代过程。 它通过找到预定义窗口内数据点的质心或加权均值来实现。 然后算法将窗口中心移动到质心位置并重复此过程直到窗口中心收敛到稳定点为止。 OpenCV 实现定义了两个停止条件最大迭代次数和窗口中心位移值在该值以下位置被认为已收敛到稳定点。 这两个条件存储在cv::TermCriteria实例中。 cv::meanShift函数返回执行的迭代次数。 显然结果的质量取决于所提供的概率图的质量以及给定的初始位置。 另见 均值漂移算法已广泛用于视觉跟踪。 第 10 章将更详细地探讨对象跟踪问题。 OpenCV 还提供了 CamShift 算法的实现该算法是均值偏移的改进版本其中窗口的大小和方向可以更改。 使用直方图比较检索相似的图像 基于内容的图像检索是计算机视觉中的重要问题。 它包括查找一组呈现类似于给定查询图像的内容的图像。 由于我们已经知道直方图是表征图像内容的有效方法因此有理由认为直方图可用于解决基于内容的检索问题。 这里的关键是能够通过简单地比较两个图像的直方图来测量两个图像之间的相似度。 需要定义一个测量函数该函数将估计两个直方图之间的差异或相似程度。 过去已经提出了各种这样的措施并且 OpenCV 在cv::compareHist函数的实现中提出了很少的措施。 操作步骤 为了将参考图像与图像集合进行比较并找到与该查询图像最相似的图像我们创建了ImageComparator类。 这包含对查询图像和输入图像的引用以及它们的直方图cv::MatND实例。 另外由于我们将使用颜色直方图进行比较因此使用了ColorHistogram类 class ImageComparator {private:cv::Mat reference;cv::Mat input;cv::MatND refH;cv::MatND inputH;ColorHistogram hist;int div;public:ImageComparator() : div(32) {}为了获得可靠的相似性度量必须减少颜色数量。 因此该类包括一个颜色减少因子该因子将应用于查询和输入图像 // Color reduction factor// The comparison will be made on images with// color space reduced by this factor in each dimensionvoid setColorReduction( int factor) {div factor;}int getColorReduction() {return div;}使用适当的设置器指定查询图像该设置器还对图像进行颜色还原 void setReferenceImage(const cv::Mat image) {reference hist.colorReduce(image,div);refH hist.getHistogram(reference);}最后compare方法将参考图像与给定的输入图像进行比较。 该方法返回一个分数指示两个图像的相似程度。 double compare(const cv::Mat image) {input hist.colorReduce(image,div);inputH hist.getHistogram(input);return cv::compareHist(refH,inputH,CV_COMP_INTERSECT);} };此类可用于检索类似于给定查询图像的图像。 后者最初提供给类实例 ImageComparator c;c.setReferenceImage(image);在这里我们使用的查询图像是本章前面的秘籍“将直方图反投影来检测特定图像内容”中显示的海滩图像的彩色版本。 将该图像与以下所示的一系列图像进行了比较。 图像从最相似到最小显示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lH5B1eH7-1681873909557)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_15.jpg)] 工作原理 大多数直方图比较措施都基于逐个箱比较即在比较直方图的箱时不使用相邻箱。 因此重要的是在测量两个颜色直方图的相似度之前减小颜色空间。 也可以使用其他色彩空间。 对cv::compareHist的调用非常简单。 您只需输入两个直方图函数就会返回测得的距离。 使用标志指定要使用的特定测量方法。 在ImageComparator类中使用相交方法带有标志CV_COMP_INTERSECT。 此方法仅针对每个箱子比较每个直方图中的两个值并保持最小值。 那么相似性度量只是这些最小值的总和。 因此具有没有共同颜色的直方图的两个图像的相交值将为 0而两个相同直方图的值将等于像素总数。 其他可用的方法是对方块之间的归一化平方差求和的卡方标志CV_COMP_CHISQR基于信号中使用的归一化互相关运算符的相关方法标志CV_COMP_CORREL 处理以测量两个信号之间的相似性以及统计中使用的 Bhattacharyya 度量标志CV_COMP_BHATTACHARYYA来估计两个概率分布之间的相似性。 另见 OpenCV 文档描述了不同直方图比较度量中使用的确切公式。 地球移动距离这也是另一种流行的直方图比较方法。 此方法的主要优点是它考虑了在相邻箱中找到的值来评估两个直方图的相似性。 在 Y.i RubnerC. TomasiL. 发表的文章《地球移动者的距离作为图像检索的度量》中进行了描述。 五、通过形态学运算转换图像 在本章中我们将介绍 使用形态学过滤器腐蚀和膨胀图像使用形态过滤器开放和闭合图像使用形态过滤器检测边缘和角点使用分水岭分割图像用 GrabCut 算法提取前景对象 简介 形态滤波是 1960 年代开发的一种用于分析和处理离散图像的理论。 它定义了一系列运算符这些运算符通过使用预定义的形状元素探测图像来变换图像。 该形状元素与像素邻域相交的方式决定了运算结果。 本章介绍最重要的形态运算符。 它还探讨了使用处理图像形态的算法进行图像分割的问题。 使用形态过滤器腐蚀和膨胀图像 侵蚀和膨胀是最基本的形态学操纵子。 因此我们将在第一个秘籍中介绍它们。 数学形态学的基本工具是结构元素。 简单地将结构元素定义为在其上定义了原点的像素形状的配置也称为定位点。 应用形态学过滤器包括使用此结构元素探测图像的每个像素。 当结构元素的原点与给定像素对齐时其与图像的交点定义了一组像素在这些像素上应用了特定的形态学运算。 原则上结构元素可以是任何形状但是最常见的是使用简单的形状例如以原点为中心的正方形圆形或菱形主要是出于效率方面的考虑。 准备 由于形态过滤器通常适用于二进制图像因此我们将使用在上一章的第一个秘籍中通过阈值处理生成的二进制图像。 但是由于在形态学上惯例是使前景对象由高白色像素值表示而背景由低黑色像素值表示因此我们对图像进行了否定。 用形态学术语来说以下图像是上一章中产生的图像的补充 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nSpiMTSN-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_01.jpg)] 操作步骤 侵蚀和膨胀在 OpenCV 中作为cv::erode和cv::dilate的简单函数实现。 它们的用法很简单 // Read input imagecv::Mat image cv::imread(binary.bmp);// Erode the imagecv::Mat eroded; // the destination imagecv::erode(image,eroded,cv::Mat());// Display the eroded imagecv::namedWindow(Eroded Image););cv::imshow(Eroded Image,eroded);// Dilate the imagecv::Mat dilated; // the destination imagecv::dilate(image,dilated,cv::Mat());// Display the dilated imagecv::namedWindow(Dilated Image);cv::imshow(Dilated Image,dilated);在下面的屏幕快照中可以看到这些函数调用产生的两个图像。 首先显示侵蚀 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sphCIlUx-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_02.jpg)] 其次是膨胀结果 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsSM1r3f-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_03.jpg)] 工作原理 与所有其他形态过滤器一样此秘籍的两个过滤器在每个像素周围的一组像素或邻域上运行这由结构元素定义。 回想一下当应用于给定像素时结构化元素的锚点与此像素位置对齐并且与结构化元素相交的所有像素都包含在当前集中。 侵蚀用定义的像素集中找到的最小像素值替换当前像素。 膨胀是互补运算符它用定义的像素集中找到的最大像素值替换当前像素。 由于输入的二进制图像仅包含黑色0和白色255像素因此每个像素都由白色或黑色像素替换。 描绘这两个运算符效果的一个好方法是根据背景黑色和前景白色对象进行思考。 对于腐蚀如果结构化元素放置在给定像素位置时接触背景即相交集中的像素之一是黑色则该像素将被发送到背景。 在散布的情况下如果背景像素上的结构元素触摸前景对象则将为该像素分配白色值。 这解释了为什么在侵蚀的图像中物体的尺寸减小了。 观察一些非常小的物体可以视为“嘈杂的”背景像素是如何被完全消除的。 类似地膨胀的对象现在更大并且其中的一些“孔”已被填充。 默认情况下OpenCV 使用3x3正方形结构元素。 当在函数调用中将空矩阵 cv::Mat()指定为第三个参数时将获得该默认结构元素就像在上一个示例中所做的那样。 您还可以通过提供一个矩阵其中非零元素定义结构元素来指定所需大小和形状的结构元素。 在以下示例中将应用7x7结构元素 cv::Mat element(7,7,CV_8U,cv::Scalar(1));cv::erode(image,eroded,element);在这种情况下效果显然更具破坏性如下所示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ixTsgEtm-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_04.jpg)] 获得相同结果的另一种方法是在图像上重复应用相同的结构元素。 这两个函数有一个可选参数来指定重复次数 // Erode the image 3 times.cv::erode(image,eroded,cv::Mat(),cv::Point(-1,-1),3);原点参数cv::Point(-1,-1)表示原点位于矩阵的中心默认值可以在结构元素上的任何位置进行定义。 获得的图像将与我们使用7x7结构元素获得的图像相同。 确实对图像进行两次腐蚀就好比对具有自身膨胀结构元素的图像进行腐蚀。 这也适用于扩张。 最后由于背景/前景的概念是任意的因此我们可以进行以下观察这是侵蚀/膨胀运算符的基本属性。 用结构元素腐蚀前景对象可以看作是图像背景部分的扩张。 或更正式地 图像的侵蚀等同于补充图像的膨胀的补充。图像的膨胀等效于补充图像的侵蚀的补充。 更多 重要的是要注意即使我们在这里对二进制图像应用了形态过滤器也可以将它们应用于具有相同定义的灰度图像。 另请注意OpenCV 形态函数支持原地处理。 这意味着您可以将输入图像用作目标图像。 所以你可以这样写 cv::erode(image,image,cv::Mat());OpenCV 为您创建所需的临时映像以使其正常工作。 另见 下一个秘籍将级联应用腐蚀和膨胀过滤器以产生新的运算符。 使用形态学过滤器检测边缘和角落以将形态学过滤器应用到灰度图像上。 使用形态过滤器开放和闭合图像 先前的秘籍介绍了两个基本的形态运算符膨胀和侵蚀。 由此可以定义其他运算符。 接下来的两个秘籍将介绍其中的一些。 此秘籍中介绍了开放和闭合运算符。 操作步骤 为了应用高级形态过滤器需要将cv::morphologyEx函数与相应的函数代码一起使用。 例如以下调用将应用结束运算符 cv::Mat element5(5,5,CV_8U,cv::Scalar(1));cv::Mat closed;cv::morphologyEx(image,closed,cv::MORPH_CLOSE,element5);请注意这里我们使用5x5的结构元素使过滤器的效果更加明显。 如果输入前面秘籍的二进制图像则可获得 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U9fMDXT8-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_05.jpg)] 同样应用形态学打开运算符将得到以下图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iC6lCqoY-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_06.jpg)] 这是从以下代码获得的 cv::Mat opened;cv::morphologyEx(image,opened,cv::MORPH_OPEN,element5);工作原理 开放和闭合过滤器仅根据基本腐蚀和膨胀操作进行定义 闭合被定义为图像膨胀的腐蚀。开放被定义为图像腐蚀的膨胀。 因此可以使用以下调用来计算图像的关闭 // dilate original imagecv::dilate(image,result,cv::Mat()); // in-place erosion of the dilated imagecv::erode(result,result,cv::Mat()); 通过反转这两个函数调用可以获得打开。 在检查关闭过滤器的结果时可以看到白色前景对象的小孔已被填充。 过滤器还将几个相邻的对象连接在一起。 基本上任何太小而不能完全容纳结构元素的孔或间隙都将被过滤器消除。 相反打开过滤器消除了场景中的一些小物体。 所有太小而无法包含结构元素的元素均已删除。 这些过滤器通常用于对象检测。 关闭过滤器将错误地分成较小碎片的对象连接在一起而打开过滤器则消除了由图像噪声引入的小斑点。 因此顺序使用它们是有利的。 如果我们的测试二进制图像是连续关闭和打开的则将获得仅显示场景中主要对象的图像如下所示。 如果希望优先进行噪声过滤也可以在关闭之前应用打开过滤器但这会以消除一些碎片对象为代价。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yiMIag9k-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_07.jpg)] 应该注意的是对图像多次应用相同的打开和类似的关闭操作符没有任何效果。 实际上在孔被第一开口填充的情况下对该相同过滤器的附加应用将不会对图像产生任何其他变化。 用数学术语来说这些运算符被认为是幂等的。 使用形态过滤器检测边缘和角点 形态过滤器也可以用于检测图像中的特定特征。 在本秘籍中我们将学习如何检测灰度图像中的线和角。 入门 在此秘籍中将使用以下图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FbarcGck-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_08.jpg)] 操作步骤 让我们定义一个名为MorphoFeatures的类它将使我们能够检测图像特征 class MorphoFeatures {private:// threshold to produce binary imageint threshold;// structuring elements used in corner detectioncv::Mat cross;cv::Mat diamond;cv::Mat square;cv::Mat x;使用cv::morphologyEx函数的适当过滤器检测线路非常容易 cv::Mat getEdges(const cv::Mat image) {// Get the gradient imagecv::Mat result;cv::morphologyEx(image,result,cv::MORPH_GRADIENT,cv::Mat());// Apply threshold to obtain a binary imageapplyThreshold(result);return result; }二进制边缘图像是通过该类的简单私有方法获得的 void applyThreshold(cv::Mat result) {// Apply threshold on resultif (threshold0)cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY); }然后在主要函数中使用此类然后按以下方式获取边缘图像 // Create the morphological features instance MorphoFeatures morpho; morpho.setThreshold(40);// Get the edges cv::Mat edges; edges morpho.getEdges(image); 结果如下图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rw7lEfz6-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_09.jpg)] 使用形态学角点检测角点有点复杂因为它不是直接在 OpenCV 中实现的。 这是使用非正方形结构元素的一个很好的例子。 实际上它需要定义四个不同的结构元素形状分别为正方形菱形十字形和 X 形。 这是在构造器中完成的为简单起见所有这些结构化元素都具有固定的5x5尺寸 MorphoFeatures() : threshold(-1), cross(5,5,CV_8U,cv::Scalar(0)),diamond(5,5,CV_8U,cv::Scalar(1)), square(5,5,CV_8U,cv::Scalar(1)),x(5,5,CV_8U,cv::Scalar(0)){// Creating the cross-shaped structuring elementfor (int i0; i5; i) {cross.atuchar(2,i) 1;cross.atuchar(i,2) 1; }// Creating the diamond-shaped structuring elementdiamond.atuchar(0,0) 0;diamond.atuchar(0,1) 0;diamond.atuchar(1,0) 0;diamond.atuchar(4,4) 0;diamond.atuchar(3,4) 0;diamond.atuchar(4,3) 0;diamond.atuchar(4,0) 0;diamond.atuchar(4,1) 0;diamond.atuchar(3,0) 0;diamond.atuchar(0,4) 0;diamond.atuchar(0,3) 0;diamond.atuchar(1,4) 0;// Creating the x-shaped structuring elementfor (int i0; i5; i) {x.atuchar(i,i) 1;x.atuchar(4-i,i) 1; }
}在检测角点特征时所有这些结构元素都会级联应用以获得最终的角点贴图 cv::Mat getCorners(const cv::Mat image) {cv::Mat result;// Dilate with a cross cv::dilate(image,result,cross);// Erode with a diamondcv::erode(result,result,diamond);cv::Mat result2;// Dilate with a X cv::dilate(image,result2,x);// Erode with a squarecv::erode(result2,result2,square);// Corners are obtained by differencing// the two closed imagescv::absdiff(result2,result,result);// Apply threshold to obtain a binary imageapplyThreshold(result);return result; }为了更好地可视化检测结果以下方法在二进制图上每个检测到的点上在图像上绘制一个圆 void drawOnImage(const cv::Mat binary, cv::Mat image) {cv::Mat_uchar::const_iterator it binary.beginuchar();cv::Mat_uchar::const_iterator itend binary.enduchar();// for each pixel for (int i0; it! itend; it,i) {if (!*it) cv::circle(image,cv::Point(i%image.step,i/image.step),5,cv::Scalar(255,0,0));} }然后使用以下代码在图像上检测角点 // Get the corners cv::Mat corners; corners morpho.getCorners(image);// Display the corner on the image morpho.drawOnImage(corners,image); cv::namedWindow(Corners on Image); cv::imshow(Corners on Image,image);然后检测到的角的图像如下。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ikDDH2US-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_10.jpg)] 工作原理 帮助理解形态运算符对灰度图像的影响的一种好方法是将图像视为拓扑浮雕其中灰度对应于海拔或海拔。 在这种情况下明亮的区域对应于山脉而较暗的区域则构成地形的山谷。 同样由于边缘对应于较暗像素和较亮像素之间的快速过渡因此可以将其描绘为陡峭的悬崖。 如果在这样的地形上应用腐蚀运算符最终结果将是用某个邻域中的最小值替换每个像素从而减小其高度。 结果随着山谷的扩大悬崖将被“侵蚀”。 扩张具有完全相反的效果即悬崖将在山谷上空获得地形。 但是在两种情况下平稳度即恒定强度的区域将保持相对不变。 上述观察结果导致了一种检测图像边缘或悬崖的简单方法。 这可以通过计算膨胀图像和侵蚀图像之间的差异来完成。 由于这两个变换后的图像大部分在边缘位置不同因此差异会突出图像的边缘。 输入cv::MORPH_GRADIENT自变量时这正是cv::morphologyEx函数所做的事情。 显然结构元素越大检测到的边缘将越厚。 该边缘检测运算符也称为 Beucher 梯度下一章将更详细地讨论图像梯度的概念。 注意也可以通过简单地从扩张后的图像中减去原始图像或从原始图像中减去侵蚀图像来获得类似的结果。 产生的边缘将更薄。 角点检测要复杂一些因为它使用了四个不同的结构元素。 该运算符未在 OpenCV 中实现但我们在这里展示它是为了演示如何定义和组合各种形状的结构化元素。 这个想法是通过使用两个不同的结构元素对图像进行扩张和腐蚀来封闭图像。 选择这些元素以使它们的直边保持不变但是由于它们各自的作用将影响角点的边缘。 让我们使用由单个白色正方形组成的以下简单图像更好地了解此非对称关闭操作的效果 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMGsfU66-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_11.jpg)] 第一个正方形是原始图像。 当用十字形结构元素进行扩张时方形边缘会扩大除了在十字形不会碰到方形的角点处。 这是中间的方块说明的结果。 然后这个扩张的图像被结构元素侵蚀这次该元素具有菱形形状。 这种侵蚀使大多数边缘恢复到其原始位置但由于它们没有膨胀因此将角进一步推向了另一端。 然后获得左方格可以看到它已经失去了尖角。 使用 X 形和方形结构元素重复相同的过程。 这两个元素是先前元素的旋转版本因此将以 45 度方向捕获角。 最后对两个结果求差将提取角点特征。 另见 The article, Morphological gradients by J.-F. Rivest, P. Soille, S. Beucher, ISETs symposium on electronic imaging science and technology, SPIE, Feb. 1992, for more on morphological gradient.The article A modified regulated morphological corner detector by F.Y. Shih, C.-F. Chuang, V. Gaddipati, Pattern Recognition Letters , volume 26, issue 7, May 2005, for more information on morphological corner detection.使用分水岭分割图像 分水岭变换是一种流行的图像处理算法用于将图像快速分割为同质区域。 它依赖于这样的想法当图像被视为拓扑浮雕时均匀区域对应于由陡峭边缘界定的相对平坦的盆地。 由于其简单性该算法的原始版本往往会过分分割图像从而产生多个小区域。 这就是 OpenCV 提出该算法的变体的原因该变体使用了一组预定义的标记来指导图像段的定义。 操作步骤 分水岭分割是通过使用cv::watershed函数获得的。 此函数的输入是一个 32 位带符号整数标记图像其中每个非零像素代表一个标签。 想法是标记图像的某些像素这些像素当然属于给定区域。 根据该初始标记分水岭算法将确定其他像素所属的区域。 在本秘籍中我们将首先将标记图像创建为灰度图像然后将其转换为整数图像。 我们方便地将此步骤封装到WatershedSegmenter类中 class WatershedSegmenter {private:cv::Mat markers;public:void setMarkers(const cv::Mat markerImage) {// Convert to image of intsmarkerImage.convertTo(markers,CV_32S);}cv::Mat process(const cv::Mat image) {// Apply watershedcv::watershed(image,markers);return markers;}获得这些标记的方式取决于应用。 例如某些预处理步骤可能导致识别出属于感兴趣对象的某些像素。 然后分水岭将用于从该初始检测中划定整个对象。 在本秘籍中我们将仅使用本章中使用的二进制图像来识别相应原始图像的动物这是在第 4 章开头显示的图像。 因此从二进制图像中我们需要确定肯定属于前景的像素动物和肯定属于背景的像素主要是草。 在这里我们将用标签 255 标记前景像素并用标签 128 标记背景像素此选择完全是任意的除 255 以外的任何标签编号都可以使用。 其他像素即标记未知的像素的赋值为 0。就目前而言二进制图像包含太多属于图像各个部分的白色像素。 然后我们将严重腐蚀该图像以便仅保留属于重要对象的像素 // Eliminate noise and smaller objectscv::Mat fg;cv::erode(binary,fg,cv::Mat(),cv::Point(-1,-1),6);结果如下图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7XNwEsBX-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_12.jpg)] 请注意仍然存在属于背景林的一些像素。 让我们简单地保留它们。 因此它们将被认为对应于感兴趣的对象。 类似地我们还通过对原始二进制图像进行大的扩张来选择背景的一些像素 // Identify image pixels without objectscv::Mat bg;cv::dilate(binary,bg,cv::Mat(),cv::Point(-1,-1),6);cv::threshold(bg,bg,1,128,cv::THRESH_BINARY_INV);产生的黑色像素对应于背景像素。 这就是为什么在膨胀后立即将阈值运算分配给这些像素的值 128 的原因。然后获得以下图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-otxv8zFP-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_13.jpg)] 这些图像被组合以形成标记图像 // Create markers imagecv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));markers fgbg;请注意我们在此处如何使用重载的operator来组合图像。 这是将用作分水岭算法输入的图像 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kQNh0p2k-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_14.jpg)] 然后按以下方式获得分段 // Create watershed segmentation objectWatershedSegmenter segmenter;// Set markers and processsegmenter.setMarkers(markers);segmenter.process(image);然后更新标记图像以便为每个零像素分配一个输入标签之一而属于找到的边界的像素的值为 -1。 标签的结果图像如下 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZvDNMTOr-1681873909561)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_15.jpg)] 边界图像为 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0PuhOhH4-1681873909561)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_16.jpg)] 工作原理 正如我们在前面的秘籍中所做的那样我们将在分水岭算法的描述中使用拓扑图类比。 为了创建分水岭分割其想法是从级别 0 开始逐渐淹没图像。随着“水”级别的逐渐增加达到级别 1、2、3 等形成了集水盆地。 这些流域的大小也逐渐增加因此两个不同流域的水最终将合并。 发生这种情况时将创建分水岭以使两个盆地保持分离。 一旦水位达到最大水位这些创建的盆地和集水区就构成了集水区分割。 如人们所料洪水过程最初会形成许多小的单个盆地。 当所有这些合并时会创建许多分水岭线从而导致图像过度分割。 为了克服该问题已经提出了对该算法的修改其中泛洪处理从预定的标记像素组开始。 由这些标记创建的盆地根据分配给初始标记的值进行标记。 当两个具有相同标签的盆地合并时不会创建分水岭从而防止了过度分割。 这就是调用cv::watershed函数时发生的情况。 输入的标记图像将更新以产生最终的分水岭分割。 用户可以输入带有任意数量标签的标记图像其中未知标签的像素保留为 0。标记图像被选择为 32 位带符号整数的图像以便能够定义 255 个以上的标签。 它还允许将特殊值 -1 分配给与分水岭相关的像素。 这是cv::watershed函数返回的内容。 为方便显示结果我们引入了两种特殊方法。 第一个返回标签的图像分水岭的值为 0。 这可以通过阈值轻松完成 // Return result in the form of an imagecv::Mat getSegmentation() {cv::Mat tmp;// all segment with label higher than 255// will be assigned value 255markers.convertTo(tmp,CV_8U);return tmp;}类似地第二种方法返回一个图像其中分水岭线的值设置为 0其余图像为 255。这一次cv::convertTo方法用于实现以下结果 // Return watershed in the form of an imagecv::Mat getWatersheds() {cv::Mat tmp;// Each pixel p is transformed into// 255p255 before conversionmarkers.convertTo(tmp,CV_8U,255,255);return tmp;}转换之前应用的线性变换允许将 -1 像素转换为 0因为-1 * 255 255 0。 值大于 255 的像素被分配值为 255。这是由于将有符号整数转换为无符号字符时应用了饱和操作。 另见 The article The viscous watershed transform by C. Vachier, F. Meyer, Journal of Mathematical Imaging and Vision, volume 22, issue 2-3, May 2005, for more information on the watershed transform.下一个秘籍介绍了另一个图像分割算法该算法也可以将图像分割为背景和前景对象。 使用 GrabCut 算法提取前景对象 OpenCV 提出了另一种流行的图像分割算法GrabCut 算法的实现。 该算法不是基于数学形态学的但是我们在这里介绍它因为它显示了与前面秘籍中提出的分水岭分割算法的一些相似之处。 GrabCut 在计算上比分水岭贵但通常可以产生更准确的结果。 当一个人想要在静止图像中提取前景对象例如将一个对象从一张图片剪切并粘贴到另一张图片时这是最好的算法。 操作步骤 cv::grabCut函数易于使用。 您只需要输入图像并将其某些像素标记为属于背景或前景。 基于此部分标记该算法将确定完整图像的前景/背景分割。 指定输入图像的部分前景/背景标签的一种方法是定义一个矩形在其中包含前景对象 // Open imageimage cv::imread(../group.jpg);// define bounding rectangle// the pixels outside this rectangle// will be labeled as background cv::Rect rectangle(10,100,380,180);然后此矩形之外的所有像素将被标记为背景。 除了输入图像及其分割图像之外调用cv::grabCut函数还需要定义两个矩阵其中包含该算法构建的模型 cv::Mat result; // segmentation (4 possible values)cv::Mat bgModel,fgModel; // the models (internally used)// GrabCut segmentationcv::grabCut(image, // input imageresult, // segmentation resultrectangle, // rectangle containing foreground bgModel,fgModel, // models5, // number of iterationscv::GC_INIT_WITH_RECT); // use rectangle注意我们如何使用cv::GC_INIT_WITH_RECT标志作为函数的最后一个参数来指定使用边界矩形模式下一节将讨论其他可用模式。 输入/输出分割图像可以具有四个值之一 cv::GC_BGD用于肯定属于背景的像素例如在我们的示例中矩形外部的像素cv::GC_FGD用于肯定属于前景的像素在我们的示例中没有cv::GC_PR_BGD用于可能属于背景的像素cv::GC_PR_FGD用于可能属于前景的像素在我们的示例中为矩形内像素的初始值。 通过提取值等于cv::GC_PR_FGD的像素我们得到了分割的二进制图像 // Get the pixels marked as likely foregroundcv::compare(result,cv::GC_PR_FGD,result,cv::CMP_EQ);// Generate output imagecv::Mat foreground(image.size(),CV_8UC3,cv::Scalar(255,255,255));image.copyTo(foreground,// bg pixels are not copiedresult); 要提取所有前景像素即其值等于cv::GC_PR_FGD或cv::GC_FGD可以简单地检查第一位的值 // checking first bit with bitwise-andresult result1; // will be 1 if FG 这是可能的因为这些常数定义为值 1 和 3而其他两个常数定义为 0 和 2。在我们的示例中由于分割图像不包含cv::GC_FGD像素仅cv::GC_BGD像素已输入。 最后通过以下带有遮罩的复制操作我们获得了前景对象的图像在白色背景上 // Generate output imagecv::Mat foreground(image.size(),CV_8UC3,cv::Scalar(255,255,255)); // all white imageimage.copyTo(foreground,result); // bg pixels not copied结果图像如下 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e1pFidVo-1681873909561)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_17.jpg)] 工作原理 在前面的示例中GrabCut 算法能够通过简单地指定在其中包含这些对象四个动物的矩形来提取前景对象。 备选地还可以将值cv::GC_BGD和cv::GC_FGD分配给作为cv::grabCut函数的第二自变量提供的分割图像的某些特定像素。 然后您可以将GC_INIT_WITH_MASK指定为输入模式标志。 这些输入标签可以例如通过要求用户交互式地标记图像的一些元素来获得。 也可以组合这两种输入模式。 使用此输入信息GrabCut 通过以下步骤创建背景/前景分割。 最初将前景标签cv::GC_PR_FGD临时分配给所有未标记的像素。 基于当前分类该算法将像素分为相似颜色的群集即背景为K群集前景为K群集。 下一步是通过在前景像素和背景像素之间引入边界来确定背景/前景分割。 这是通过优化过程来完成的该过程尝试将像素与相似的标签连接起来并对在强度相对均匀的区域中放置边界施加了惩罚。 通过使用图切割算法可以有效地解决此优化问题该方法可以通过将它表示为连通图在上面应用切割来组成最佳配置从而找到问题的最佳解决方案。 所获得的分割为像素产生新的标签。 然后可以重复聚类过程并再次找到新的最佳分割依此类推。 因此GrabCut 是一个迭代过程可逐步改善分割结果。 根据场景的复杂性可以在或多或少的迭代中找到一个好的解决方案在简单情况下一个迭代就足够了。 这解释了该函数的上一个最后一个参数用户可以在其中指定要应用的迭代次数。 算法维护的两个内部模型作为函数的参数传递并返回这样如果希望通过执行其他迭代来改善细分结果则可以再次使用上次运行的模型调用该函数。 另见 The article by C. Rother, V. Kolmogorov and A. Blake, GrabCut: Interactive Foreground Extraction using Iterated Graph Cuts in ACM Transactions on Graphics (SIGGRAPH) volume 23, issue 3, August 2004, that describes in detail the GrabCut algorithm.