Qt-5-and-OpenCV-4-Computer-Vision-Projects 学习笔记 - 编辑图像

发布时间:2023-03-10 09:30

blurImage

void MainWindow::blurImage()
{
    if (currentImage == nullptr) {
        QMessageBox::information(this, \"Information\", \"No image to edit.\");
        return;
    }
    //将`QImage`转换为`Mat`,但是这里有些复杂。 我们正在打开的图像可以是任何格式-它可以是单色图像,灰度图像或深度不同的彩色图像。 要模糊它,我们必须知道它的格式,因此尽管它是原始格式,我们仍将其转换为具有 8 位深度和三个通道的常规格式。 这由 Qt 中的`QImage::Format_RGB888`和 OpenCV 中的`CV_8UC3`表示。 现在让我们看看如何进行转换并构造`Mat`对象:
    QPixmap pixmap = currentImage->pixmap();
    QImage image = pixmap.toImage();
    image = image.convertToFormat(QImage::Format_RGB888);
    cv::Mat mat = cv::Mat(
        image.height(),
        image.width(),
        CV_8UC3,
        image.bits(),
        image.bytesPerLine());

    //最后,这是一段可编辑的代码。 现在我们有了`Mat`对象,让我们对其进行模糊处理:
    //OpenCV 在其imgproc模块中提供blur函数。 它使用带有核的归一化框过滤器来模糊图像。 第一个参数是我们要模糊的图像,而第二个参数是我们要放置模糊的图像的位置。 我们使用临时矩阵存储模糊的图像,并在模糊结束后将其分配回原始图像。 第三个参数是核的大小。 在这里,核用于告诉 OpenCV 如何通过将其与不同数量的相邻像素组合来更改任何给定像素的值。
    cv::Mat tmp;
    cv::blur(mat, tmp, cv::Size(8, 8));
    mat = tmp;

    //对我们来说,前面代码的新部分是从mat对象构造QImage对象image_blurred,然后使用QPixmap::fromImage静态方法将QImage对象转换为QPixmap。 尽管这是新的,但很明显。 这段代码的其余部分对我们来说并不陌生,它与我们在MainWindow类的showImage方法中使用的代码相同。
    QImage image_blurred(
        mat.data,
        mat.cols,
        mat.rows,
        mat.step,
        QImage::Format_RGB888);
    pixmap = QPixmap::fromImage(image_blurred);
    imageScene->clear();
    imageView->resetMatrix();
    currentImage = imageScene->addPixmap(pixmap);
    imageScene->update();
    imageView->setSceneRect(pixmap.rect());
    
    //现在我们已经显示了模糊的图像,我们可以更新状态栏上的消息以告诉用户他们正在查看的该图像是已编辑的图像,而不是他们打开的原始图像:
    QString status = QString(\"(editted image), %1x%2\")
        .arg(pixmap.width()).arg(pixmap.height());
    mainStatusLabel->setText(status);
}

\"Qt-5-and-OpenCV-4-Computer-Vision-Projects

ErodePlugin腐蚀图像

     //这也很简单-我们只调用 OpenCV 库提供的erode函数。 该函数的作用称为图像腐蚀。 它是数学形态学领域中的两个基本运算符之一。 侵蚀是缩小图像前景或 1 值对象的过程。 它可以平滑对象边界并去除半岛,手指和小物体。 在下一部分中将插件加载到应用中后,我们将看到此效果。
     void ErodePlugin::edit(const cv::Mat &input, cv::Mat &output)
     {
         erode(input, output, cv::Mat());
     }

\"Qt-5-and-OpenCV-4-Computer-Vision-Projects

锐化图像

图像锐化是由许多著名的图像编辑软件(例如 GIMP 和 Photoshop)实现的常见功能。 锐化图像的原理是我们从原始版本中减去图像的平滑版本,以得到这两个版本之间的差异,然后将该差异添加到原始图像中。 我们可以通过对图像的副本应用高斯平滑过滤器来获得平滑版本。 稍后我们将看到如何使用 OpenCV 进行此操作,但是第一步是创建一个新的 Qt 插件项目。

     //虽然仅在第一行中更改了类名,但我们在此方法的主体中进行了很多更改以进行锐化工作。 首先,我们定义两个变量。 intensity变量是一个整数,它将指示我们将锐化图像的强度,而smoothed是cv::Mat的实例,将用于保存图像的平滑版本。 然后,我们调用GaussianBlur函数对作为cv::Mat实例传递到我们的方法的图像进行平滑处理,并将平滑后的版本存储在smoothed变量中。

	//在图像处理中,高斯模糊是一种被广泛采用的算法,尤其是当您要减少图像的噪点或细节时。 它以出色的数学家和科学家卡尔·弗里德里希·高斯(Carl Friedrich Gauss)的名字命名,因为它使用高斯函数来模糊图像。 有时也称为高斯平滑。

//您可以在这个页面中找到有关此算法的更多信息。 在 OpenCV 中,我们使用GaussianBlur函数来实现此效果。 与大多数 OpenCV 函数一样,此函数接受许多参数。 第一个和第二个是输入和输出图像。 第三个参数是cv::Size对象,代表核的大小。 第四个是double类型的变量,它表示 X 方向上的高斯核标准差。 它还有两个带有默认值的额外参数。 我们在代码中使用其默认值以使该方法易于理解,但是您可以在这个页面上参考GaussianBlur函数的文档,了解更多信息。

//在获得原始图像的平滑版本之后,可以通过从原始版本中减去平滑版本input - smoothed来找到原始版本和平滑版本之间的良好区别。 此表达式中的减法运算在 OpenCV 中称为按元素矩阵运算。 逐元素矩阵运算是计算机视觉中的数学函数和算法,可对矩阵的各个元素(即图像的像素)起作用。 重要的是要注意,可以逐个元素并行化操作,这从根本上意味着矩阵元素的处理顺序并不重要。 通过执行此减法,我们得到了区别-它也是cv::Mat实例,因此如果您要查看它,可以在应用中显示它。 由于这种区别很小,因此即使显示出来,您也会看到黑色图像,尽管它不是完全黑色的-其中有一些无块像素。 为了锐化原始图像,我们可以通过使用附加的逐元素运算将这个区分矩阵叠加到原始图像上一次或多次。 在我们的代码中,次数是我们定义的intensity变量。 首先,我们将intensity标量乘以区分矩阵(这也是标量和矩阵之间的元素操作),然后将结果添加到原始图像矩阵中:

     //input + (input - smoothed) * intensity
//最后,我们将结果矩阵分配给输出变量cv::Mat的引用,以out参数的方式返回锐化的图像。

     void SharpenPlugin::edit(const cv::Mat &input, cv::Mat &output)
     {
         int intensity = 2;
         cv::Mat smoothed;
         GaussianBlur(input, smoothed, cv::Size(9, 9), 0);
         output = input + (input - smoothed) * intensity;
     }

\"Qt-5-and-OpenCV-4-Computer-Vision-Projects

卡通效果

	//第一项任务是减少调色板。 为此,我们可以使用 OpenCV 库提供的双边过滤器。 尽管双边过滤器效果很好,并通过平滑平坦区域并保持锐利边缘为普通的 RGB 图像提供了卡通外观,但是它比其他平滑算法(例如,我们之前使用的高斯模糊算法)慢得多。 但是,在我们的应用中,速度很重要-为了使代码易于理解,我们不会创建单独的辅助线程来进行编辑工作。 如果编辑过程太慢,它将冻结我们应用的用户界面-也就是说,在编辑时,我们的应用将不是交互式的,用户界面也不会被更新。
    //幸运的是,我们有两种方法可以加快这一过程,从而缩短冻结时间:
	//缩小原始图像,然后将过滤器应用于该缩小的版本。
	//代替一次对图像应用大的双边过滤器,我们可以多次应用小双边的过滤器。

void CartoonPlugin::edit(const cv::Mat &input, cv::Mat &output)
{
    int num_down = 2;
    int num_bilateral = 7;
	//首先,我们定义两个Mat类对象copy1和copy2,然后将input的副本分配给copy1。
    cv::Mat copy1, copy2;
    cv::Mat image_gray, image_edge;
	//然后,我们使用cv::pyrDown重复缩小copy1的大小(两次通过int num_down = 2;)。 在此循环中,我们对两个定义的矩阵copy1和copy2进行操作。 由于cv::pyrDown函数不支持原地操作,因此对于输出,我们必须使用与输入矩阵不同的矩阵。 为了实现重复操作,我们应在每次操作后将所得矩阵的copy2克隆为copy1。
    copy1 = input.clone();
    for(int i = 0; i < num_down; i++) {
        cv::pyrDown(copy1, copy2);
        copy1 = copy2.clone();
    }
    //缩小操作后,我们在copy1中获得了原始图像的降采样版本。 现在,就像缩小过程一样,我们反复对copy1应用一个小的双边过滤器(通过int num_bilateral = 7;进行七次)。 此函数也不支持原地,因此我们将copy1用作其输入图像,并将copy2用作其输出图像。 我们传递给cv::bilateralFilter函数的最后三个参数指定像素邻域的直径,其值为9,色彩空间中的过滤器σ,其值也为9,以及坐标中的过滤器σ空间,其值分别为7。 您可以参考这里了解如何在过滤器中使用这些值。
    for(int i = 0; i < num_bilateral; i++) {
        cv::bilateralFilter(copy1, copy2, 9, 9, 7);
        copy1 = copy2.clone();
    }
    //缩小调色板后,我们应该将向下采样的图像放大到其原始大小。 这是通过在copy1上调用cv::pyrUp的次数与在其上调用cv::pyrDown相同的次数来完成的。
    for(int i = 0; i < num_down; i++) {
        cv::pyrUp(copy1, copy2);
        copy1 = copy2.clone();
    }
	//因为在缩小时将结果图像的大小计算为Size((input.cols + 1) / 2, (input.rows + 1) / 2),而在放大时将结果图像的大小计算为Size(input.cols * 2, (input.rows * 2),所以copy1矩阵的大小可能与原始图像不同。 它可能等于或大于原始像素几个像素。 在此,如果copy1在尺寸上与原始图片不同,则应将copy1调整为原始图片的尺寸:
    if (input.cols != copy1.cols  || input.rows != copy1.rows) {
        cv::Rect rect(0, 0, input.cols, input.rows);
        copy1(rect).copyTo(copy2);
        copy1 = copy2;
    }
	//至此,我们得到了原始图像的副本,该副本的调色板减小且尺寸不变。 现在,让我们继续前进,检测边缘并生成一些大胆的轮廓。 OpenCV 提供了许多检测边缘的函数。 在这里,我们选择cv::adaptiveThreshold函数并以cv::THRESH_BINARY作为其阈值类型进行调用以执行边缘检测。 在自适应阈值算法中,不是使用全局值作为阈值,而是使用动态阈值,该阈值由当前像素周围较小区域中的像素确定。 这样,我们可以检测每个小区域中最显着的特征,并据此计算阈值。 这些函数正是我们应该在图像中的对象周围绘制粗体和黑色轮廓的地方。 同时,自适应算法也有其弱点-容易受到噪声的影响。 因此,最好在检测边缘之前对图像应用中值过滤器,因为中值过滤器会将每个像素的值设置为周围所有像素的中值,这样可以减少噪声。 让我们看看如何做到这一点:
    
    //首先,我们通过调用cvtColor函数将输入图像转换为灰度图像,然后将cv::COLOR_RGB2GRAY作为颜色空间转换代码作为其第三个参数。 此函数也不能原地工作,因此我们使用另一个与输入矩阵不同的矩阵image_gray作为输出矩阵。 此后,我们在image_gray矩阵中获得原始图像的灰度版本。 然后,我们调用cv::medianBlur将中值过滤器应用于灰度图像。 如您所见,在此函数调用中,我们将image_gray矩阵用作其输入和输出矩阵。 这是因为此函数支持原地操作。 它可以原地处理输入矩阵的数据; 也就是说,它从输入读取数据,进行计算,然后将结果写入输入矩阵,而不会干扰图像。


    cv::cvtColor(input, image_gray, cv::COLOR_RGB2GRAY);
    cv::medianBlur(image_gray, image_gray, 5);
    //应用中值过滤器后,我们在灰度图像上调用cv::adaptiveThreshold以检测图像中的边缘。 我们在灰度图像上进行此操作,因此,在执行此操作后,灰度图像将变为仅包含边缘的二进制图像。 然后,我们将二进制边缘转换为 RGB 图像,并通过调用cvtColor将其存储在image_edge矩阵中。

    cv::adaptiveThreshold(image_gray, image_gray, 255,
        cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 9, 2);

    cv::cvtColor(image_gray, image_edge, cv::COLOR_GRAY2RGB);
	//现在,调色板已缩小并且边缘图像已准备就绪,让我们通过按位and操作合并它们并将其分配给output矩阵以返回它们:
    output = copy1 & image_edge;

    /*
    cv::GaussianBlur(image_edge, image_edge, cv::Size(5, 5), 0);
    cv::Mat mask(input.rows, input.cols, CV_8UC3, cv::Scalar(90, 90, 90));
    mask = mask & (~image_edge);
    output = (copy1 & image_edge) | mask;
    */
}

\"Qt-5-and-OpenCV-4-Computer-Vision-Projects

旋转图像

在前面的部分中,我们已将许多编辑功能作为插件添加,所有这些功能都利用了 OpenCV 提供的图像过滤器。 从本节开始,我们将添加一些利用 OpenCV 库的转换函数的功能。

根据 OpenCV 库的文档,OpenCV 中有两个图像转换类别:

  • 几何变换
  • 杂项变换(除几何变换外的所有变换)

在本节和下一部分中,我们将研究几何变换。 我们可以从它们的名称猜测得出,几何变换主要处理图像的几何属性,例如图像的大小,方向和形状。 它们不更改图像的内容,而是根据几何变换的性质,通过在周围移动图像的像素来更改图像的形式和形状。

让我们首先从简单的几何变换开始-旋转图像。 使用 OpenCV 旋转图像有多种方法。 例如,我们可以在矩阵上应用转置和翻转的复合操作,也可以使用适当的变换矩阵进行仿射变换。 在本节中,我们将使用后一种方法。

现在是时候开始一个新的动手项目来开发旋转插件了。 我们可以通过使用以前的插件项目作为模板来做到这一点。 以下是此过程的重点列表:

  1. 使用RotatePlugin作为项目名称。
  2. 创建项目文件和源文件(.h文件和.cpp文件)。
  3. 更改项目文件中的相关设置。
  4. 使用RotatePlugin作为插件类名称。
  5. name方法中返回Rotate作为插件名称。
  6. 更改edit方法的实现。

除了最后一步,每个步骤都非常简单明了。 因此,让我们跳过前五个步骤,直接进入最后一步-这是我们在此插件中实现edit方法的方式:

如前所述,我们使用仿射变换来进行旋转,这是通过调用 OpenCV 库提供的cv::warpAffine函数来实现的。 此函数不支持原地操作,因此我们将定义一个新的临时矩阵result来存储输出。

//warpAffine函数将称为转换矩阵的矩阵作为其第三个参数。 该变换矩阵包含描述仿射变换应如何完成的数据。 手工编写此转换矩阵有点复杂,因此 OpenCV 提供了生成该转换矩阵的函数。 为了生成旋转的变换矩阵,我们可以使用cv::getRotationMatrix2D函数,为其指定一个点作为轴点,一个角度和一个缩放比例。
//在我们的代码中,我们将输入图像的中心点用作旋转的轴点,并使用正数 45 表示旋转将逆时针旋转 45 度这一事实。 由于我们只想旋转图像,因此我们使用 1.0 作为缩放比例。 准备好这些参数后,我们通过调用cv::getRotationMatrix2D函数获得rotateMatrix,然后将其传递给第三位置的cv::warpAffine。

//cv::warpAffine的第四个参数是输出图像的大小。 我们在这里使用输入图像的大小来确保图像的大小在编辑过程中不会改变。 第五个参数是插值方法,因此在这里我们只使用cv::INTER_LINEAR。 第六个参数是输出图像边界的像素外推方法。 我们在这里使用cv::BORDER_CONSTANT,以便在旋转后,如果某些区域未被原始图像覆盖,则将用恒定的颜色填充它们。 我们可以将此颜色指定为第七个参数,否则默认使用黑色。
void RotatePlugin::edit(const cv::Mat &input, cv::Mat &output)
{
    double angle = 45.0;
    double scale = 1.0;
    cv::Point2f center = cv::Point(input.cols/2, input.rows/2);
    cv::Mat rotateMatrix = cv::getRotationMatrix2D(center, angle, scale);

    cv::Mat result;
    cv::warpAffine(input, result,
        rotateMatrix, input.size(),
        cv::INTER_LINEAR, cv::BORDER_CONSTANT);
    output = result;
}

\"Qt-5-and-OpenCV-4-Computer-Vision-Projects

仿射变换

//这次,在edit方法中,我们将使用另一种方法来获取warpAffine函数的转换矩阵。 首先,我们准备两个三角形-一个用于输入图像,另一个用于输出图像。 在我们的代码中,我们使用下图中显示的三角形:
//左边的一个用于输入,而右边的一个用于输出。 我们可以很容易地看到,在此变换中,图像的顶部将保持不变,而图像的底部将向右移动与图像宽度相同的距离。

//在代码中,我们将使用三个Point2f类的数组表示每个三角形,然后将它们传递给getAffineTransform函数以获得转换矩阵。 一旦获得了转换矩阵,就可以调用warpAffine函数,就像在RotatePlugin项目中所做的那样。 这是我们在代码中执行此操作的方式:
void AffinePlugin::edit(const cv::Mat &input, cv::Mat &output)
{

    cv::Point2f triangleA[3];
    cv::Point2f triangleB[3];

    triangleA[0] = cv::Point2f(0 , 0);
    triangleA[1] = cv::Point2f(1 , 0);
    triangleA[2] = cv::Point2f(0 , 1);

    triangleB[0] = cv::Point2f(0, 0);
    triangleB[1] = cv::Point2f(1, 0);
    triangleB[2] = cv::Point2f(1, 1);

    cv::Mat affineMatrix = cv::getAffineTransform(triangleA, triangleB);
    cv::Mat result;
    cv::warpAffine(
        input, result,
        affineMatrix, input.size(), // output image size, same as input
        cv::INTER_CUBIC, // Interpolation method
        cv::BORDER_CONSTANT  // Extrapolation method
        //BORDER_WRAP  // Extrapolation method
    );

    output = result;
}

\"Qt-5-and-OpenCV-4-Computer-Vision-Projects

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号