矩阵的掩码操作
矩阵的掩码操作(mask operation)相当简单。其思想是根据一个掩码矩阵(也称为卷积核/kernel)来重新计算图像中每个像素的值。该掩码保存的值将调整相邻像素(以及当前像素)对新像素值的影响程度。从数学角度来看,我们用指定的值做了一个加权平均。
让我们考虑图像对比度增强方法的问题。基本上,我们想对图像的每个像素应用以下公式:
(上述矩阵的行、列索引对应 -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.shaperesult = np.zeros(my_image.shape, my_image.dtype)
C++
我们将使用普通的 C [] 运算符来访问像素。因为我们需要同时访问多行,所以我们要获取每一行的指针(前一行、当前行和下一行)。我们还需要另一个指针指向我们要保存计算结果的位置。然后用 [] 运算符简单地访问正确的项。为了使输出指针前移,我们在每次操作后简单地递增它(一个字节):
(基本方法循环的代码实现见源代码。)
在图像的边界处,上述记法会导致不存在的像素位置(比如 -1、-1)。在这些点上我们的公式是未定义的。一个简单的解决方案是不在这些点上应用卷积核,例如,将边界上的像素设为零:
(边界处理的代码实现见源代码。)
Java / Python
我们需要访问多行和多列,这可以通过在当前中心 (i,j) 上加或减 1 来完成。然后我们应用求和并将新值放入 Result 矩阵。
(基本方法循环的代码实现见源代码。)
在图像的边界处,上述记法会导致不存在的像素位置(比如 (-1,-1))。在这些点上我们的公式是未定义的。一个简单的解决方案是不在这些点上应用卷积核,例如,将边界上的像素设为零:
(边界处理的代码实现见源代码。)
filter2D 函数
Section titled “filter2D 函数”在图像处理中,应用此类滤波器非常常见,因此 OpenCV 中有一个函数专门负责应用掩码(在某些地方也称为卷积核)。为此,你首先需要定义一个保存掩码的对象:
(定义卷积核的代码实现见源代码。)
然后调用 filter2D() 函数,指定输入、输出图像和要使用的卷积核:
(调用 filter2D 的代码实现见源代码。)
该函数甚至还有第五个可选参数用于指定卷积核的中心,第六个用于在将滤波后的像素存入 K 之前添加一个可选值,第七个用于确定在操作未定义的区域(边界)该怎么做。
这个函数更短、更简洁,而且由于有一些优化,它通常比手工编码的方法更快。例如在我的测试中,第二种方法只用了 13 毫秒,而第一种大约用了 31 毫秒。差距相当大。
例如:
你可以在我们的 YouTube 频道上查看程序运行实例。