【问题标题】:Weird behavior of cv2.fastNlMeansDenoising()cv2.fastNlMeansDenoising() 的奇怪行为
【发布时间】:2019-12-21 02:31:40
【问题描述】:

同样根据this documentation of opencvthis linkthis link

C++:

void fastNlMeansDenoising(InputArray src, OutputArray dst, float h=3, int templateWindowSize=7, int searchWindowSize=21 )  

Python:

cv2.fastNlMeansDenoising(src[, dst[, h[, templateWindowSize[, searchWindowSize]]]]) → dst

参数(简要)如下:

  • src - 输入图像。

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

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

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

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

据我所知,在 Python 中,我们可以将 dst/output 变量从方法中取出: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') 

这是输入图像:

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

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

对此有什么解释吗?为什么要在fastNlMeansDenoising参数中添加None

【问题讨论】:

  • bilateralFilter 中,dst 是第五个参数(第一个是可选的)。您使用 4 个参数调用它(所以 dst 是第五个,可选获取默认值,一切都很好),或者使用 5 个参数(再次确定,因为你没有混淆它们)。因此,它的行为方式相同,但由于 dst 在参数列表中的位置更靠后,因此混淆的可能性较小。
  • 至于为什么混合不会导致显式错误 - Python 绑定以某种神秘的方式工作(特别是对于输出参数,重载函数会增加一些复杂性)。在我能够正确解释在这种特殊情况下发生了什么之前,我需要一些时间来刷新我的记忆。
  • 当 C++ 参数为 Input/OutputArray 时,python 函数将接受任何 python 对象。然后它尝试将convert it 转换为cv::Mat。 (如果失败,它会尝试转换为UMat。如果失败,并且存在重载,它会尝试以相同的方式解决它们。如果没有匹配项,则会出现错误。)现在,由于 C++ API 允许标量用于一些需要输入/输出数组的操作,转换函数也允许标量。例如,一个整数将变成一个 4x1 float64 mat,其值为..
  • 在第一行,其余为零。单浮子也是一样。元组(数字)成为单列垫,其行数与元组中的元素一样多,同样是 float64。最后是数组,但我们不要讨论这个。因此,调用实际 C++ 函数之前的一切都正常。接下来是 C++ API 的一个特性。当给定的输出 cv::Mat 具有不正确的形状或数据类型时,它会自动重新分配以满足要求。
  • 我认为(尽管现在我有一些疑问,让我检查一下)......这种行为对 Python 有轻微的副作用。假设在cv2.fastNlMeansDenoising(gray, test_3, 31, 7, 21) 中,test_3 numpy 数组的形状或类型错误。在这种情况下,函数可以正常运行,但由于新分配的 Mat 不再引用底层 numpy 缓冲区,test_3 将保持不变。而且由于您没有捕获返回值,因此您会丢失实际结果。 |更新:检查过,是的,就是这样。

标签: python opencv parameters


【解决方案1】:

函数fastNlMeansDenoising

我们先来看看the Python function的签名:

cv2.fastNlMeansDenoising(src[, dst[, h[, templateWindowSize[, searchWindowSize]]]]) → dst

括号 ([]) 的嵌套方式意味着第 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

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

void fastNlMeansDenoising(InputArray src, OutputArray dst, float h=3, int templateWindowSize=7, int searchWindowSize=21)

对于最后 3 个参数,很明显 -- h=3templateWindowSize=7searchWindowSize=21。在 Python 绑定中,OutputArray 参数隐式具有 None(与 C++ API 不同,Python 变体也返回输出)。


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

test_1 = cv2.fastNlMeansDenoising(gray, 31, 7, 21)

意思

test_1 = cv2.fastNlMeansDenoising(src=gray, dst=31, h=7, templateWindowSize=21, searchWindowSize=21)

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

我们将探讨为什么将dst 设置为 31 不会导致稍后在答案中引发任何显式错误。


第四个变种是恕我直言,最好的方法是跳过dst

test_4 = cv2.fastNlMeansDenoising(gray, h=31, templateWindowSize=7, searchWindowSize=21)

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

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

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

当您继续阅读时,我们会变得很明显。


函数bilateralFilter

你提到bilateralFilter是为了比较,所以我们也来看看。

cv.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]]) → dst

这意味着只有使用位置参数可以调用这 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

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

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

总结一下:这些函数的行为一致,但dst 后面的可选参数越少,对自己开枪的机会就越少。


Python 绑定的工作原理

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

让我们看一下生成包装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;
}

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

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

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

转换为cv::Mat 的过程如下:

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

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


OutputArrays 的工作原理

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

这解释了为什么 31dst 是可以的 - 它产生了错误形状和类型的 Mat,但它刚刚被重新分配,一切都很好。

但是,这个不错的功能在 Python API 中引入了一些障碍。当为 Input/OutputArray 参数提供 numpy 数组时,将创建一个 Mat 实例,该实例共享保存值的底层缓冲区。这意味着操作很快(因为没有数据被复制),并且 numpy 数组会自动反映在 Mat 上所做的更改。但是,如果 OpenCV 由于不正确的形状/类型而重新分配了Mat,则会分配一个新的缓冲区并且原始的 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)

【讨论】:

  • @singrium 好了。让我知道是否有任何需要改进的地方。
  • 这绝对是我在 Stack Overflow 中见过的解释最清楚的答案之一(如果不是最好的话)。我不能说更多。
  • 这是一个令人难以置信的答案
猜你喜欢
  • 1970-01-01
  • 2020-11-16
  • 1970-01-01
  • 2022-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多