Skip to content

矩阵的掩码操作

矩阵的掩码操作(mask operation)相当简单。其思想是根据一个掩码矩阵(也称为卷积核/kernel)来重新计算图像中每个像素的值。该掩码保存的值将调整相邻像素(以及当前像素)对新像素值的影响程度。从数学角度来看,我们用指定的值做了一个加权平均。

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

I(i,j)=5∗I(i,j)−[I(i−1,j)+I(i+1,j)+I(i,j−1)+I(i,j+1)]I(i,j) = 5*I(i,j) - [ I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)]

  ⟺  I(i,j)∗M,其中 M=[0−10−15−10−10]\iff I(i,j)*M, \text{其中 } M = \begin{bmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{bmatrix}

(上述矩阵的行、列索引对应 -1、0、+1。)

第一种记法使用的是公式,而第二种是使用掩码对第一种记法的紧凑版本。使用掩码时,将掩码矩阵的中心(在上例中由 0-0 索引标注)放在你要计算的像素上,然后将像素值与重叠的矩阵值相乘后求和。这是一回事,不过在矩阵较大的情况下,后一种记法要容易查看得多。

C++

你可以从此处下载此源代码,或在 OpenCV 源码库的 samples 目录 samples/cpp/tutorial_code/core/mat_mask_operations/mat_mask_operations.cpp 中查看。

(完整代码见C++ 源代码。)

Java

你可以从此处下载此源代码,或在 OpenCV 源码库的 samples 目录 samples/java/tutorial_code/core/mat_mask_operations/MatMaskOperations.java 中查看。

(完整代码见Java 源代码。)

Python

你可以从此处下载此源代码,或在 OpenCV 源码库的 samples 目录 samples/python/tutorial_code/core/mat_mask_operations/mat_mask_operations.py 中查看。

(完整代码见Python 源代码。)

现在让我们看看如何使用基本的像素访问方法或 filter2D() 函数来实现这一点。

以下是一个能完成此任务的函数:

(基本方法的代码实现见源代码。)

首先,我们确保输入图像的数据为 unsigned char 格式。为此,我们使用 CV_Assert 函数(宏),当其中的表达式为假时会抛出错误。

(8 位格式断言的代码实现见源代码。)

我们创建一个与输入尺寸和类型相同的输出图像。正如你可以在存储一节中所看到的,根据通道数的不同,我们可能有一个或多个子列。

C++

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

(创建通道的代码实现见源代码。)

Python

height, width, n_channels = my_image.shape
result = np.zeros(my_image.shape, my_image.dtype)

resultMatMaskFilter2D.png

C++

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

(基本方法循环的代码实现见源代码。)

在图像的边界处,上述记法会导致不存在的像素位置(比如 -1、-1)。在这些点上我们的公式是未定义的。一个简单的解决方案是不在这些点上应用卷积核,例如,将边界上的像素设为零:

(边界处理的代码实现见源代码。)

Java / Python

我们需要访问多行和多列,这可以通过在当前中心 (i,j) 上加或减 1 来完成。然后我们应用求和并将新值放入 Result 矩阵。

(基本方法循环的代码实现见源代码。)

在图像的边界处,上述记法会导致不存在的像素位置(比如 (-1,-1))。在这些点上我们的公式是未定义的。一个简单的解决方案是不在这些点上应用卷积核,例如,将边界上的像素设为零:

(边界处理的代码实现见源代码。)

在图像处理中,应用此类滤波器非常常见,因此 OpenCV 中有一个函数专门负责应用掩码(在某些地方也称为卷积核)。为此,你首先需要定义一个保存掩码的对象:

(定义卷积核的代码实现见源代码。)

然后调用 filter2D() 函数,指定输入、输出图像和要使用的卷积核:

(调用 filter2D 的代码实现见源代码。)

该函数甚至还有第五个可选参数用于指定卷积核的中心,第六个用于在将滤波后的像素存入 K 之前添加一个可选值,第七个用于确定在操作未定义的区域(边界)该怎么做。

这个函数更短、更简洁,而且由于有一些优化,它通常比手工编码的方法更快。例如在我的测试中,第二种方法只用了 13 毫秒,而第一种大约用了 31 毫秒。差距相当大。

例如:

你可以在我们的 YouTube 频道上查看程序运行实例。