用Python对带有NaN的图像进行高斯滤波

32

从一个二维坐标列表和第三个变量(速度)中,我创建了一个覆盖整个采样区域的二维numpy数组。我的意图是创建一幅图像,在其中每个像素包含在其中的点的平均速度。然后使用高斯滤波器对该图像进行过滤。

问题在于采样区域不均匀。因此,图像中间有几个没有信息(Nan)的像素。当我尝试通过高斯滤波器过滤数组时,Nan会传播并破坏整个图像。

我需要过滤这个图像,但要拒绝所有没有信息的像素。换句话说,如果一个像素不包含信息,则应该不考虑它进行过滤。

以下是我的求平均的代码示例:

Mean_V = np.zeros([len(x_bins), len(y_bins)])

for i, x_bin in enumerate(x_bins[:-1]):
    bin_x = (x > x_bins[i]) & (x <= x_bins[i+1])
    for j, y_bin in enumerate(y_bins[:-1]):
        bin_xy = (y[bin_x] > y_bins[j]) & (y[bin_x] <= y_bins[j+1])
        if (sum(x > 0 for x in bin_xy) > 0) :
            Mean_V[i,j]=np.mean(V[bin_x][bin_xy])
        else:
            Mean_V[i,j]=np.nan

编辑:

我在浏览网络时发现了我在2013年提出的这个问题。这个问题的解决方案可以在astropy库中找到:

http://docs.astropy.org/en/stable/convolution/

Astropy的卷积将NaN像素替换为从它们的邻居进行核加权插值。

谢谢大家!


scipy.stats包提供了nanmean和nanstd函数,它们会忽略nan而不是返回nan。在您的代码中将numpy.mean / numpy.std替换为它们,一切都应该没问题(; - Faultier
1
你能贴出你的平均算法代码示例吗? - Daniel
你可能需要自己编写循环来执行卷积操作并进行检查。 - tacaswell
嗨!我的平均代码没有任何神秘之处。但如果您愿意,我可以添加一个示例。 - user2761243
请查看此问题的Matlab版本:https://dev59.com/VF0a5IYBdhLWcg3w48JP - eric
显示剩余2条评论
3个回答

45

用语言表述:

通过对辅助数组VW应用标准高斯滤波器,并取两者的比值得到结果Z,可以轻松地获得一个忽略给定数组U中NaN值的高斯滤波器。

这里,V是将原始的U数组中的NaN值替换为零得到的副本,而W是由1组成的数组,在原始U数组中的NaN位置上填充0。

这个方法的思想是,将NaN值替换为零会导致滤波后的数组产生误差,但可以通过将同样的高斯滤波器应用于另一个辅助数组并将两者组合来进行补偿。

用 Python 语言实现:

import numpy as np
import scipy as sp
import scipy.ndimage

sigma=2.0                  # standard deviation for Gaussian kernel
truncate=4.0               # truncate filter at this many sigmas

U=sp.randn(10,10)          # random array...
U[U>2]=np.nan              # ...with NaNs for testing

V=U.copy()
V[np.isnan(U)]=0
VV=sp.ndimage.gaussian_filter(V,sigma=sigma,truncate=truncate)

W=0*U.copy()+1
W[np.isnan(U)]=0
WW=sp.ndimage.gaussian_filter(W,sigma=sigma,truncate=truncate)

Z=VV/WW

以数字表示:

这里高斯滤波器的系数被设置为[0.25,0.50,0.25],仅用于演示目的,并且它们的总和为1,即0.25 + 0.50 + 0.25 = 1,不失一般性。

在将NaN替换为零并应用高斯滤波器(参见下文的VV)之后,很明显零会引入误差,即由于"缺失"数据导致的系数0.25 + 0.50 = 0.75不再总和为一,因此低估了"真实"值。

然而,这可以通过使用第二个辅助数组(参见下文的WW)来进行补偿,该数组在使用相同的高斯滤波器进行过滤后仅包含系数的总和。

因此,将两个过滤后的辅助数组相除会重新调整系数,使它们总和为一,同时忽略NaN位置。

array U         1   2   NaN 1   2    
auxiliary V     1   2   0   1   2    
auxiliary W     1   1   0   1   1
position        a   b   c   d   e

filtered VV_b   = 0.25*V_a  + 0.50*V_b  + 0.25*V_c
                = 0.25*1    + 0.50*2    + 0
                = 1.25

filtered WW_b   = 0.25*W_a  + 0.50*W_b  + 0.25*W_c
                = 0.25*1    + 0.50*1    + 0
                = 0.75

ratio Z         = VV_b / WW_b  
                = (0.25*1 + 0.50*2) / (0.25*1    + 0.50*1)
                = 0.333*1 + 0.666*2
                = 1.666

更新 - 除以零:

以下内容整合了来自下方评论区中 @AndyL 和 @amain 的有用问题和答案,感谢他们的贡献!

如果高斯核支持区域内只包含 NaN 条目,则大量的 NaN 可能导致某些位置分母为零(WW=0)(理论上支持区域是无限的,但在实践中通常会截断核,参见上述代码示例中的 'truncate' 参数)。在这种情况下,分子也变为零(VV=0),因此 numpy 会抛出“RuntimeWarning: invalid value encountered in true_divide”并返回相应位置的 NaN。

这可能是最一致/有意义的结果,如果您可以接受 numpy 警告,则不需要进行进一步的调整。


3
我不确定完全理解其原因,但这种方法效果很好,几乎与使用astropy.convolution.Gaussian2DKernelastropy.convolve函数得到相同的结果,并且速度快了10倍。 - j08lue
1
这非常聪明 - 我也不完全理解它为什么起作用。 - Sealander
太棒了!WW是跟踪高斯核有效部分的巧妙方法。也许您可以将“V [U!= U] = 0”更改为“V [np.isnan(U)]”以提高清晰度? - Scott Staniewicz
1
谢谢@ScottStaniewicz,我已经实现了你的建议! - David
1
@David 理论上是可以的,但实际上大多数高斯滤波器的实现都默认被截断。例如,scipy的ndimage.gaussian_filter默认的截断值为4.0个标准偏差。为了避免类似于numpy警告的“除以零”问题,在除法之前可以将WW中的零转换为NaN。 - amain
显示剩余6条评论

11

enter image description here

我之前跳过了这个问题并使用了davids answer(感谢!)。然而,后来事实证明,对于一个带有NaN的数组应用高斯滤波器的任务并不像我想象的那样明确定义。
正如ndimage.gaussian_filter所描述的那样,在图像边界处理值时有不同的选项(反射,常数外推等)。类似的决定也必须针对图像中的NaN值做出。
  • 有些想法可能是线性插值nan值,但问题是,如何处理图像边界处的nan值。
  • filter_nan_gaussian_david:David的方法等同于假设每个nan点都有一些平均邻域值。这会导致总强度的变化(请参见第3列中的sum值),但在其他方面表现出色。
  • filter_nan_gaussian_conserving:此方法是通过高斯滤波器扩展每个点的强度。映射到nan像素的强度将被重新调整回原点。这是否有意义取决于应用程序。我有一个由nan包围的封闭区域,希望保留总强度+避免边界失真。
  • filter_nan_gaussian_conserving2:通过高斯滤波器扩展每个点的强度。映射到nan像素的强度被重定向到具有相同高斯加权的其他像素。这导致在许多nan /边界像素附近的原点处强度的相对降低。这在最后一行的最右侧有说明。

代码

import numpy as np
from scipy import ndimage
import matplotlib as mpl
import matplotlib.pyplot as plt

def filter_nan_gaussian_conserving(arr, sigma):
    """Apply a gaussian filter to an array with nans.

    Intensity is only shifted between not-nan pixels and is hence conserved.
    The intensity redistribution with respect to each single point
    is done by the weights of available pixels according
    to a gaussian distribution.
    All nans in arr, stay nans in gauss.
    """
    nan_msk = np.isnan(arr)

    loss = np.zeros(arr.shape)
    loss[nan_msk] = 1
    loss = ndimage.gaussian_filter(
            loss, sigma=sigma, mode='constant', cval=1)

    gauss = arr.copy()
    gauss[nan_msk] = 0
    gauss = ndimage.gaussian_filter(
            gauss, sigma=sigma, mode='constant', cval=0)
    gauss[nan_msk] = np.nan

    gauss += loss * arr

    return gauss

def filter_nan_gaussian_conserving2(arr, sigma):
    """Apply a gaussian filter to an array with nans.

    Intensity is only shifted between not-nan pixels and is hence conserved.
    The intensity redistribution with respect to each single point
    is done by the weights of available pixels according
    to a gaussian distribution.
    All nans in arr, stay nans in gauss.
    """
    nan_msk = np.isnan(arr)

    loss = np.zeros(arr.shape)
    loss[nan_msk] = 1
    loss = ndimage.gaussian_filter(
            loss, sigma=sigma, mode='constant', cval=1)

    gauss = arr / (1-loss)
    gauss[nan_msk] = 0
    gauss = ndimage.gaussian_filter(
            gauss, sigma=sigma, mode='constant', cval=0)
    gauss[nan_msk] = np.nan

    return gauss

def filter_nan_gaussian_david(arr, sigma):
    """Allows intensity to leak into the nan area.
    According to Davids answer:
        https://dev59.com/AGMl5IYBdhLWcg3wFjoC#36307291
    """
    gauss = arr.copy()
    gauss[np.isnan(gauss)] = 0
    gauss = ndimage.gaussian_filter(
            gauss, sigma=sigma, mode='constant', cval=0)

    norm = np.ones(shape=arr.shape)
    norm[np.isnan(arr)] = 0
    norm = ndimage.gaussian_filter(
            norm, sigma=sigma, mode='constant', cval=0)

    # avoid RuntimeWarning: invalid value encountered in true_divide
    norm = np.where(norm==0, 1, norm)
    gauss = gauss/norm
    gauss[np.isnan(arr)] = np.nan
    return gauss



fig, axs = plt.subplots(3, 4)
fig.suptitle('black: 0, white: 1, red: nan')
cmap = mpl.cm.get_cmap('gist_yarg_r')
cmap.set_bad('r')
def plot_info(ax, arr, col):
    kws = dict(cmap=cmap, vmin=0, vmax=1)
    if col == 0:
        title = 'input'
    elif col == 1:
        title = 'filter_nan_gaussian_conserving'
    elif col == 2:
        title = 'filter_nan_gaussian_david'
    elif col == 3:
        title = 'filter_nan_gaussian_conserving2'
    ax.set_title(title + '\nsum: {:.4f}'.format(np.nansum(arr)))
    ax.imshow(arr, **kws)

sigma = (1,1)

arr0 = np.zeros(shape=(6, 10))
arr0[2:, :] = np.nan
arr0[2, 1:3] = 1

arr1 = np.zeros(shape=(6, 10))
arr1[2, 1:3] = 1
arr1[3, 2] = np.nan

arr2 = np.ones(shape=(6, 10)) *.5
arr2[3, 2] = np.nan

plot_info(axs[0, 0], arr0, 0)
plot_info(axs[0, 1], filter_nan_gaussian_conserving(arr0, sigma), 1)
plot_info(axs[0, 2], filter_nan_gaussian_david(arr0, sigma), 2)
plot_info(axs[0, 3], filter_nan_gaussian_conserving2(arr0, sigma), 3)

plot_info(axs[1, 0], arr1, 0)
plot_info(axs[1, 1], filter_nan_gaussian_conserving(arr1, sigma), 1)
plot_info(axs[1, 2], filter_nan_gaussian_david(arr1, sigma), 2)
plot_info(axs[1, 3], filter_nan_gaussian_conserving2(arr1, sigma), 3)

plot_info(axs[2, 0], arr2, 0)
plot_info(axs[2, 1], filter_nan_gaussian_conserving(arr2, sigma), 1)
plot_info(axs[2, 2], filter_nan_gaussian_david(arr2, sigma), 2)
plot_info(axs[2, 3], filter_nan_gaussian_conserving2(arr2, sigma), 3)

plt.show()

这是否直接适用于高斯滤波器的更高阶?我尝试过,但是边框上出现了几个伪像。 - Liris

1
如何将 Z=VV/WW 替换为 Z=VV/(WW+epsilon),其中 epsilon=0.000001,以自动处理上一个提议中没有任何观测值的情况。

我不确定这是否回答了问题。如果原发帖人有 NaN 值,那么这并没有什么帮助。具有 NaN 值的操作返回 NaN 值。也许你应该详细说明一下你的答案。 - Alain Merigot
VV和WW都不包含nan。VV和WW都将变为零,因此epsilon将使除法可计算,并给出零作为结果。 - Christopher Grimm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接