我需要使用半径为17或更大的3D结构元素计算3D形状阵列(400,401,401)的形态开口,大小为64320400字节。结构元素ndarray的大小是
42875字节。使用scipy.ndimage.morphology.binary_opening
,整个过程消耗8GB RAM。
我在GitHub上读过scipy/ndimage/morphology.py
,据我所知,形态侵蚀算子是用纯C实现的。我很难理解ni_morphology.c
来源,所以我没有发现这段代码的任何部分导致如此巨大的内存利用率。添加更多RAM并不是一个可行的解决方案,因为内存使用量可能会随着结构元素半径呈指数增长。
重现问题:
import numpy as np
from scipy import ndimage
arr_3D = np.ones((400,401,401),dtype="bool")
str_3D = ndimage.morphology.generate_binary_structure(3,1)
big_str_3D = ndimage.morphology.iterate_structure(str_3D,20)
arr_out_3D = ndimage.morphology.binary_opening(arr_3D, big_str_3D)
这需要大约7GB RAM。
有没有人对上述示例中的计算形态学有一些建议?
答案 0 :(得分:4)
我也做过加大半径的开口以进行粒度分析,并且遇到了同样的问题。实际上,内存使用量大约增加了R ^ 6,其中R是球形核的半径。这是一个相当大的增长速度!我进行了一些内存分析,包括将开孔分成侵蚀然后是膨胀(开孔的定义),然后发现大的内存使用量来自SciPy的二进制文件,并在结果返回到调用的Python脚本后立即清除。 SciPy的形态学代码大部分用C语言实现,因此修改它们是一个困难的前景。
无论如何,OP的最后评论:“经过一些研究,我转向使用卷积实现Opening实现->傅立叶变换的乘法-O(n log n),而没有那么大的内存开销。” 帮助我想出解决方案,所以谢谢。但是,起初执行起来并不明显。对于遇到此问题的其他任何人,我将在此处发布实现。
我将开始讨论膨胀,因为二进制腐蚀只是二进制图像的补码(逆)的膨胀,然后将结果求逆。
简而言之:根据this white paper by Kosheleva et al,扩张可以看作是数据集A与结构元素(球形核)B的卷积,阈值高于某个值。卷积也可以在频率空间中进行(通常快得多),因为频率空间中的乘法与实际空间中的卷积相同。因此,通过先对A和B进行傅立叶变换,然后将它们相乘,然后对结果进行逆变换,然后对值<0.5的阈值 进行阈值化,即可得出A与B的关系。我链接的白皮书说阈值大于0,但是大量测试表明,给出的错误结果有很多伪影; another white paper by Kukal et al。给出的阈值大于0.5,并且得出的结果与scipy.ndimage.binary_dilation相同对我来说。我不确定为什么会出现差异,而且我想知道我是否错过了ref 1命名法的某些细节)
正确的实现涉及填充大小,但是对我们来说幸运的是,它已经在scipy.signal.fftconvolve(A,B,'same')
中完成了-该功能完成了我刚刚描述的工作,并为您完成了填充工作。将第三个选项设置为“ same”将返回与我们想要的大小A相同的结果(否则它将被B的大小填充)。
所以扩张是
from scipy.signal import fftconvolve
def dilate(A,B):
return fftconvolve(A,B,'same')>0.5
腐蚀的原理是:您将A反转,如上所述按B进行扩张,然后重新反转结果。但这需要一些技巧,才能与scipy.ndimage.binary_erosion的结果完全匹配-您必须将反演填充至少1到球形核B的半径R的1s。因此可以实施腐蚀以获得与scipy相同的结果.ndimage.binary_erosion。 (请注意,代码可以用更少的行完成,但是我试图在此处进行说明。)
from scipy.signal import fftconvolve
import numpy as np
def erode_v1(A,B,R):
#R should be the radius of the spherical kernel, i.e. half the width of B
A_inv = np.logical_not(A)
A_inv = np.pad(A_inv, R, 'constant', constant_values=1)
tmp = fftconvolve(A_inv, B, 'same') > 0.5
#now we must un-pad the result, and invert it again
return np.logical_not(tmp[R:-R, R:-R, R:-R])
您可以通过另一种方式获得相同的腐蚀结果,如the white paper by Kukal et al所示-他们指出,可以通过将阈值> m-0.5来使A和B的卷积变为腐蚀,其中m是“ B”(结果是球体的体积,而不是阵列的体积)。我首先展示了erode_v1,因为它稍微容易理解,但是结果是一样的:
from scipy.signal import fftconvolve
import numpy as np
def erode_v2(A,B):
thresh = np.count_nonzero(B)-0.5
return fftconvolve(A,B,'same') > thresh
我希望这对其他有此问题的人有所帮助。关于我得到的结果的注释:
另外两个快速注释:
首先:考虑一下我在中间部分讨论的关于erode_v1的填充。用1填充反数基本上可以从数据集的边缘以及数据集中的任何接口发生腐蚀。根据您的系统和您要执行的操作,您可能需要考虑这是否真正代表了您要处理的方式。如果不是,则可以考虑使用“反射”边界条件进行填充,该边界条件将模拟边缘附近所有特征的延续。我建议您在不同的边界条件下(在膨胀和腐蚀方面)进行试验,并对结果进行可视化和量化,以确定哪种最适合您的系统和目标。
第二:大多数情况下,这种基于频率的方法不仅在内存方面更好,而且在速度方面也更好。对于小内核B,原始方法更快。但是,无论如何小内核运行都非常快,因此出于我自己的目的,我不在乎。如果这样做(就像您多次执行小内核一样),则可能需要找到B的临界大小并在此时切换方法。
参考文献,尽管我很抱歉,因为它们都不提供年份,所以不容易被引用:
答案 1 :(得分:1)
一个疯狂的猜测是代码试图以某种方式分解结构元素并进行多个并行计算。每个计算都有自己的原始数据副本。 400x400x400不是那么大......
AFAIK,因为你只进行一次开/关,它最多应该使用原始数据的3倍内存:原始+扩张/侵蚀+最终结果......
你可以尝试自己动手实施......它可能会更慢,但代码很简单,应该能够对问题有所了解......