cv2.fastNlMeansDenoising() 的奇怪行为

sin*_*ium 1 python parameters opencv

根据opencv 的这个文档这个链接这个链接也是:

C++:

void fastNlMeansDenoising(InputArray src, OutputArray dst, float h=3, int templateWindowSize=7, int searchWindowSize=21 )  
Run Code Online (Sandbox Code Playgroud)

Python:

cv2.fastNlMeansDenoising(src[, dst[, h[, templateWindowSize[, searchWindowSize]]]]) ? dst
Run Code Online (Sandbox Code Playgroud)

参数(简要)如下:

  • src – 输入图像。

  • dst – 与 src 具有相同大小和类型的输出图像。

  • templateWindowSize – 模板补丁的大小(以像素为单位)。应该是奇葩。

  • searchWindowSize – 窗口的大小(以像素为单位)。应该是奇葩。

  • h – 调节过滤强度的参数。

据我所知,在Python中,我们可以采取DST /输出变量的方法的是:dst = cv2.method(input, param1, param2, ..., paramx)。而且我们不需要在方法中放置任何东西(即我们不需要这样做:dst = cv2.method(input, None, param1, param2, ..., paramx)
虽然这适用于不同的 OpenCV 方法,但它不适用于fastNlMeansDenoising
以下代码将澄清我的问题:

import cv2
import numpy as np


def thresh(filename):
    img = cv2.imread(filename)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    #without adding None instead of dst
    test_1 = cv2.fastNlMeansDenoising(gray, 31, 7, 21)
    cv2.imwrite('test_1.jpg', test_1)

    # Adding None instead of dst
    test_2 = cv2.fastNlMeansDenoising(gray, None, 31, 7, 21)
    cv2.imwrite('test_2.jpg', test_2)

    # putting dst inside the method
    test_3 = np.empty(gray.shape, np.uint8)
    cv2.fastNlMeansDenoising(gray, test_3, 31, 7, 21)
    cv2.imwrite('test_3.jpg', test_3)

    # Adding the input params
    test_4 = cv2.fastNlMeansDenoising(gray, h=31, templateWindowSize=7,
                                      searchWindowSize=21)
    cv2.imwrite('test_4.jpg', test_4)

    blur = cv2.bilateralFilter(gray, 31, 7, 21)
    cv2.imwrite('blur.jpg', blur)
    blur_ = cv2.bilateralFilter(gray, 31, 7, 21, None)
    cv2.imwrite('blur_.jpg', blur_)
    blur__ = np.empty(gray.shape, np.uint8)
    cv2.bilateralFilter(gray, 31, 7, 21, blur__)
    cv2.imwrite('blur__.jpg', blur__)


thresh('test.png') 
Run Code Online (Sandbox Code Playgroud)

这是输入图像:

在此处输入图片说明

如果您运行代码,您会注意到,test_2.jpg、test_3.jpg 和 test_4.jpg 是相似的。和 test_1.jpg 一样gray(好像test_1没有收到 的输出fastNlMeansDenoising)。

然而,情况并非如此bilateralFilter:blur.jpg、blur_.jpg 和 blur__.jpg 都是相同的,尽管我重复了与fastNlMeansDenoising.

对此有什么解释吗?我们为什么要添加NonefastNlMeansDenoising参数?

Dan*_*šek 6

功能 fastNlMeansDenoising

我们先来看看Python 函数的签名:

cv2.fastNlMeansDenoising(src[, dst[, h[, templateWindowSize[, searchWindowSize]]]]) ? dst
Run Code Online (Sandbox Code Playgroud)

括号的方式([] ) 的嵌套方式意味着第 2-5 个参数是可选的,但只要它们作为位置参数传入,序列就需要保持不变(即您不能跳过任何一个)。

这意味着仅使用位置参数,有 5 种可能性:

cv2.fastNlMeansDenoising(src) ? dst
cv2.fastNlMeansDenoising(src, dst) ? dst
cv2.fastNlMeansDenoising(src, dst, h) ? dst
cv2.fastNlMeansDenoising(src, dst, h, templateWindowSize) ? dst
cv2.fastNlMeansDenoising(src, dst, h, templateWindowSize, searchWindowSize) ? dst
Run Code Online (Sandbox Code Playgroud)

未提供的任何可选参数将使用默认值。使用的默认值可以从相应的 C++ 函数签名中推导出来。

void fastNlMeansDenoising(InputArray src, OutputArray dst, float h=3, int templateWindowSize=7, int searchWindowSize=21)
Run Code Online (Sandbox Code Playgroud)

在过去的3个参数,这是显而易见的- h=3templateWindowSize=7searchWindowSize=21。在 Python 绑定中,OutputArray参数隐式具有None(与 C++ API 不同,Python 变体也返回输出)。


考虑到这一点,您的第一个变体

test_1 = cv2.fastNlMeansDenoising(gray, 31, 7, 21)
Run Code Online (Sandbox Code Playgroud)

方法

test_1 = cv2.fastNlMeansDenoising(src=gray, dst=31, h=7, templateWindowSize=21, searchWindowSize=21)
Run Code Online (Sandbox Code Playgroud)

h比您预期的要小得多,并且templateWindowSize要大得多。这就是结果不同的原因。

我们将探讨为什么设置dst为 31 不会在后面的答案中引起任何明确的错误。


恕我直言,第四个变体是跳过的最佳方式dst

test_4 = cv2.fastNlMeansDenoising(gray, h=31, templateWindowSize=7, searchWindowSize=21)
Run Code Online (Sandbox Code Playgroud)

显式使用关键字参数时,您不太可能混淆。

第二个变体(None作为第二个参数传递)是可以的。

第三个变体在循环中很有用,它允许您在后续迭代中重用临时数组并避免重新分配(这可能代价高昂)。但是,有一个问题——数组必须完全具有所需的形状和数据类型。如果不是,它不会被修改(但该函数仍将返回一个新分配的数组,该数组包含您需要捕获的结果)。

当您继续阅读时,我们就会明白这样做的原因。


功能 bilateralFilter

你提到是bilateralFilter为了比较,所以让我们也来研究一下。

cv.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]]) ? dst
Run Code Online (Sandbox Code Playgroud)

这意味着仅使用位置参数有 3 种可能性来调用它:

cv.bilateralFilter(src, d, sigmaColor, sigmaSpace) ? dst
cv.bilateralFilter(src, d, sigmaColor, sigmaSpace, dst) ? dst
cv.bilateralFilter(src, d, sigmaColor, sigmaSpace, dst, borderType) ? dst
Run Code Online (Sandbox Code Playgroud)

请注意,由于dst参数在序列中出现得更晚,因此您可能只会犯一个错误——而是传入边框类型。

在您的代码示例中,您只使用了 4 个或 5 个参数,甚至从未使用过borderType,并且在所有情况下都dst获得了有意义的值。

总结一下:函数的行为一致,但后面的可选参数dst越少,让自己陷入困境的机会就越少。


Python 绑定如何工作

由于需要向 Python 公开的 OpenCV 代码库的大小,C++ 函数的包装器会自动生成。由于 API 的复杂性,除非您详细研究实现,否则某些行为可能不会立即显现。(并且由于实际绑定代码是在构建时自动生成的,最好在本地编译 OpenCV 以检查生成的实现)

让我们看一下为 wrap 生成的一段代码fastNlMeansDenoising

static PyObject* pyopencv_cv_fastNlMeansDenoising(PyObject* , PyObject* args, PyObject* kw)
{
    using namespace cv;

    {
    PyObject* pyobj_src = NULL;
    Mat src;
    PyObject* pyobj_dst = NULL;
    Mat dst;
    float h=3;
    int templateWindowSize=7;
    int searchWindowSize=21;

    const char* keywords[] = { "src", "dst", "h", "templateWindowSize", "searchWindowSize", NULL };
    if( PyArg_ParseTupleAndKeywords(args, kw, "O|Ofii:fastNlMeansDenoising", (char**)keywords, &pyobj_src, &pyobj_dst, &h, &templateWindowSize, &searchWindowSize) &&
        pyopencv_to(pyobj_src, src, ArgInfo("src", 0)) &&
        pyopencv_to(pyobj_dst, dst, ArgInfo("dst", 1)) )
    {
        ERRWRAP2(cv::fastNlMeansDenoising(src, dst, h, templateWindowSize, searchWindowSize));
        return pyopencv_from(dst);
    }
    }

    // Clear Python error, try the same for UMat

    // Clear Python error, try overload with Mat

    // Clear Python error, try overload with UMat

    return NULL;
}
Run Code Online (Sandbox Code Playgroud)

首先,PyArg_ParseTupleAndKeywords用于解析函数参数并将它们的值(如果可选和缺失,则保留预设的默认值)分配给相应的 C++ 变量。

需要注意的是,当相应的 C++ 参数的类型为 时Input/OutputArray,它会被解析为 Python 对象(O格式字符串中的 )——这意味着它在这个阶段可以是任何东西。

解析参数后,pyopencv_to用于将 Python 对象转换为cv::Mat. 由于许多 OpenCV 函数(例如cv::add)允许一些输入参数(以及潜在的输出参数)既是数组又是标量,因此 Python 绑定也支持这一点。

转换cv::Mat工作如下:

  • 如果参数是None,则留空Mat
  • 如果参数是整数(标量),则创建一个Mat4 行 1 列和 64 位浮点值的数据类型。将第一行设置为提供的整数值,其余设置为 0。
  • 如果参数是浮点数(标量),则执行与整数(以上)相同的操作。
  • 如果参数是数字元组,则创建一个Mat具有n行和 1 列且数据类型为 64 位浮点值n的元组,其中是元组中的元素数。每一行按顺序保存一个元素。
  • 最后,处理数组(超出本答案的范围)。

这意味着当您调用 时cv2.fastNlMeansDenoising(gray, 31, 7, 21),整数31变成了Mat具有 64 位浮点元素的 4x1 单通道。因此,可以毫无问题地调用底层 C++ 函数。现在,它为什么不抱怨Mat存储输出的大小和数据类型不正确?


如何OutputArray工的

由于 C++ API 使用输出数组参数来支持返回值,因此它需要能够支持在调用函数之前无法确定结果大小的情况。为了解决这个问题,在给定一个空的Mat,或者Mat不正确的形状或数据类型的情况下,Mat重新创建(分配一个新的缓冲区等)以满足要求。由于Mat它基本上是一个指向底层图像缓冲区的智能指针,因此它可以正常工作,并且在 C++ 中可以完全预测(恕我直言)——即使发生重新分配,Mat您作为输出参数提供的实例也将正确引用新数据。

这解释了为什么31as 没问题dst——它产生Mat了错误的形状和类型,但只是重新分配了,一切都很好。

然而,这个不错的特性在 Python API 中引入了一些障碍。当为Input/OutputArray参数提供 numpy 数组时,将Mat创建一个实例,该实例共享保存值的底层缓冲区。这意味着操作很快(因为没有复制数据),并且 numpy 数组会自动反映对Mat. 但是,如果 OpenCVMat由于形状/类型不正确而重新分配,则会分配一个新缓冲区,而原始 numpy 数组保持不变。

这可以很容易地证明:

>>> a = np.ones((3,3), np.uint8)
>>> b = a + 1
>>> c = np.zeros(a.shape, np.float32)
>>> c
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.],
       [ 0.,  0.,  0.]], dtype=float32)
>>> cv2.add(a, b, c)
array([[3, 3, 3],
       [3, 3, 3],
       [3, 3, 3]], dtype=uint8)
>>> c
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.],
       [ 0.,  0.,  0.]], dtype=float32)
>>> d = np.zeros_like(a)
>>> cv2.add(a, b, d)
array([[3, 3, 3],
       [3, 3, 3],
       [3, 3, 3]], dtype=uint8)
>>> d
array([[3, 3, 3],
       [3, 3, 3],
       [3, 3, 3]], dtype=uint8)
Run Code Online (Sandbox Code Playgroud)

  • 这绝对是我在 Stack Overflow 上见过的最(如果不是最好的)解释得最清楚的答案之一。我不能再说更多了。 (2认同)
  • 这是一个令人难以置信的答案 (2认同)