6. OpenCV 4.2.0 图像矩阵的掩膜运算(OpenCV 官方文档翻译)

官方文档链接:https://docs.opencv.org/4.2.0/d7/d37/tutorial_mat_mask_operations.html


矩阵上的掩膜操作非常简单。目的是根据 掩膜矩阵(也成为 )重新计算图像中每个像素的值。此掩膜矩阵中的值代表着相邻像素(和当前像素)对新像素值得影响程度。从数学的角度看,即是我们用指定的值作加权平均。


测试用例 (Our test case)

现在考虑图像对比度增强方法的问题。基本上,我们希望对图像的每个像素应用以下公式:

在这里插入图片描述

第一种表示方式是使用公式,而第二种表示法是使用掩膜的表示法。使用掩膜时,将掩膜矩阵的中心(即矩阵中元素 5 的位置)放在要计算的像素上,并将像素值与重叠矩阵值相乘求和。两种方法的结果是一样的,但对于大型矩阵,后一种方法更容易检查。


完整代码

#include <iostream>

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>
#include <opencv2/imgproc/imgproc.hpp>

void Sharpen(const cv::Mat& myImage, cv::Mat& Result);

int main(int argc, char** argv)
{
	const char* filename = "lena.jpg";

	cv::Mat src, dst0, dst1;
	src = cv::imread(cv::samples::findFile(filename), cv::IMREAD_GRAYSCALE);

	if (src.empty())
	{
		std::cerr << "Can't open image [" << filename << "]" << std::endl;
		return EXIT_FAILURE;
	}

	cv::namedWindow("Input", cv::WINDOW_AUTOSIZE);
	cv::namedWindow("Output", cv::WINDOW_AUTOSIZE);

	cv::imshow("Input", src);
	double t = (double)cv::getTickCount();

	Sharpen(src, dst0);

	t = ((double)cv::getTickCount() - t) / cv::getTickFrequency();
	std::cout << "Hand written function time passed in seconds: " << t << std::endl;

	cv::imshow("Output", dst0);
	cv::waitKey(0);

	cv::Mat kernel = (cv::Mat_<char>(3, 3) << 0, -1, 0,
																		-1, 5, -1,
																		0, -1, 0);

	t = (double)cv::getTickCount();

	cv::filter2D(src, dst1, src.depth(), kernel);
	t = ((double)cv::getTickCount() - t) / cv::getTickFrequency();

	std::cout << "Built-in filter2D time passed in seconds: " << t << std::endl;

	cv::imshow("Output", dst1);

	cv::waitKey(0);

	return 0;
}

void Sharpen(const cv::Mat& myImage, cv::Mat& Result)
{
	CV_Assert(myImage.depth() == CV_8U);

	const int nChannels = myImage.channels();
	Result.create(myImage.size(), myImage.type());

	for (int j = 1; j < myImage.rows - 1; ++j)
	{
		const uchar* previous = myImage.ptr<uchar>(j - 1);
		const uchar* current = myImage.ptr<uchar>(j);
		const uchar* next = myImage.ptr<uchar>(j + 1);

		uchar* output = Result.ptr<uchar>(j);

		for (int i = nChannels; i < nChannels * (myImage.cols - 1); ++i)
		{
			*output++ = cv::saturate_cast<uchar>(5 * current[i] - current[i - nChannels] - current[i + nChannels]
				- previous[i] - next[i]);
		}
	}

	Result.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));
}
输出结果

在这里插入图片描述


现在来看一下如何使用 基本像素访问方法filter2D() 函数来实现这一掩膜操作。

基本方法 (The Basic Method)

void Sharpen(const cv::Mat& myImage, cv::Mat& Result)
{
	CV_Assert(myImage.depth() == CV_8U);

	const int nChannels = myImage.channels();
	Result.create(myImage.size(), myImage.type());

	for (int j = 1; j < myImage.rows - 1; ++j)
	{
		const uchar* previous = myImage.ptr<uchar>(j - 1);
		const uchar* current = myImage.ptr<uchar>(j);
		const uchar* next = myImage.ptr<uchar>(j + 1);

		uchar* output = Result.ptr<uchar>(j);

		for (int i = nChannels; i < nChannels * (myImage.cols - 1); ++i)
		{
			*output++ = cv::saturate_cast<uchar>(5 * current[i] - current[i - nChannels] - current[i + nChannels]
				- previous[i] - next[i]);
		}
	}

	Result.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));
}

首先,我们要确保输入的图像数据是无符号字符格式。为此,我们使用 cv::CV_Assert 函数,当其中的表达式为 false 时,该函数将抛出错误。

    cv::CV_Assert(myImage.depth() == CV_8U);  // accept only uchar images

我们创建一个输出图像,其大小和类型与我们的输入相同。根据通道的数量,图像可能有一个或多个子列。

我们将通过指针遍历它们,因此元素的总数取决于这个数字。

    const int nChannels = myImage.channels();
    Result.create(myImage.size(),myImage.type());

我们将使用普通 C[] 运算符来访问像素。因为我们需要同时访问多个行,所以我们将获取每个行的指针(前一行、当前行和下一行)。我们需要另一个指针指向我们要保存计算的位置。然后简单地使用 [] 运算符访问正确的项。为了向前移动输出指针,只需在每次操作后增加一个字节:

    for(int j = 1 ; j < myImage.rows-1; ++j)
    {
        const uchar* previous = myImage.ptr<uchar>(j - 1);
        const uchar* current  = myImage.ptr<uchar>(j    );
        const uchar* next     = myImage.ptr<uchar>(j + 1);
        uchar* output = Result.ptr<uchar>(j);
        for(int i= nChannels;i < nChannels*(myImage.cols-1); ++i)
        {
            *output++ = cv::saturate_cast<uchar>(5*current[i]
                         -current[i-nChannels] - current[i+nChannels] - previous[i] - next[i]);
        }
    }

对于图像的边界,公式中并未给出定义,一个简单的解决方案是不在这些点应用掩膜,例如,将边框上的像素设置为 0:

    Result.row(0).setTo(Scalar(0));
    Result.row(Result.rows-1).setTo(Scalar(0));
    Result.col(0).setTo(Scalar(0));
    Result.col(Result.cols-1).setTo(Scalar(0));

filter2D 函数 (The filter2D function)

在图像处理中应用这种滤波器非常常见,以至于在 OpenCV 中有一个函数负责应用掩膜(在某些地方也称为内核)。为此,首先需要定义一个包含掩膜的对象:

    cv::Mat kernel = (cv::Mat_<char>(3,3) <<  0, -1,  0,
                                           -1,  5, -1,
                                            0, -1,  0);

然后调用 filter2D() 函数并指定输入和输出图像以及使用的掩膜:

    cv::filter2D( src, dst1, src.depth(), kernel );

该函数甚至有第 5 个可选参数来指定内核的中心,第 6 个参数用于在将筛选的像素存储为 K 之前向其添加可选值,第 7 个参数用于确定在未定义操作的区域(边界)中要执行的操作。

这个函数相比第一种使用指针的方法要更加简单,但是时间消耗可能因计算机配置而已,官方教程中开发者的机器上 filter2D() 的运行速度更快,但是在本人的机器上,使用指针的方法运行速度更快。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值