如何将一个非常大的 numpy 数组保存为图像,尽可能少地加载到内存中

How to save a very large numpy array as an image, loading as little as possible into memory

我的程序经常使用非常大的 numpy 数组 ((819200, 460800, 4), uint8)。要将其存储在内存中(作为纯零),我需要超过 1.3TB 的内存,这是荒谬的。 我的目标是能够将这些 numpy 数组保存为图像。我也希望它尽可能快,但速度不是问题。

我一开始做的是将numpy数组存储在HDF5文件中(使用H5PY),然后我对该数组进行处理,然后使用CV2保存。不仅速度慢,CV2 似乎将图像加载到内存中,所以这个想法很快就消失了 window。 现在,我已经尝试了 20 多种不同的方法来保存这些大型数组,所以为了缩短 post,我只提一些最新的方法。

使用 CV2 后,我发现了一个名为“numpngw”的库。它是一个基于 numpy 和 python 的 png 编写器。这是我的代码:

f = h5py.File("mytestfile.hdf5", "w")
dset = f.create_dataset("mydataset", (100000,100000,4), dtype=np.uint8, compression='gzip')

shp = dset.shape    
step = 10000


png = open("new.png", "wb")
numpngw._write_header_and_meta(png, 8, shp, color_type=6, bitdepth=8, palette=None, #i'm manually writing to the png file rather than writing all data at once, so i can append data over and over again.
                            interlace=0, text_list=None, timestamp=None, sbit=None, gamma=None, iccp=None,
                            chromaticity=None, trans=None, background=None, phys=None)


for i in range(0, shp[0]+step, step): #from step to 
    numpngw._write_data(png, dset[i:i+step, i:i+step], bitdepth=8, max_chunk_len=step, #writing the data in largest chunks I can
                    filter_type=None, interlace=0)
    png.flush()
    #gc.collect()

    numpngw._write_iend(png)

png.close()
f.close()

这样做的想法是,它只是一遍又一遍地写入 numpy 数组的块,直到写入整个数组。 我什至不知道这个版本是否有效,因为它非常慢。

然后我用PIL尝试了同样的chunk写法。我没有使用 PNG,而是使用了 TIFF,因为它看起来更快。不幸的是,PIL 不支持以块的形式附加到 TIFF。 "append" 参数用于动画 TIFF,所以我不能那样做。

我最近使用的库是 tifffile。它似乎 可以满足我的所有需求。它还有一个 memmap 实现,可以从内存映射的 numpy 数组中创建一个 TIFF 文件。

blank = numpy.zeros((256,256,3))
memmap_image = tifffile.memmap('temp.tif', shape=blank.shape, dtype='uint8')
memmap_image[:] = blank[:]
memmap_image.flush()
del memmap_image

这将创建一个空白的 TIFF 文件。将它与 H5PY 结合使用可以让我保存大图像 - 或者我是这么认为的。大型 TIFF 文件似乎已损坏。我尝试在 (Windows) Photos、Adobe Acrobat Reader DC 和 Affinity Photo 中打开它们。所有人都说该文件未被识别(有时 Affinity Photo 甚至在打开时崩溃 - 虽然可能是内存问题)。 我不知道什么会导致图像损坏,因为它似乎适用于较小的阵列。第二天我回到它并开始在这条线上出现内存错误(无处不在)memmap_image[:] = blank[:]

我最后尝试的是将 chunk 方法与 tifffile 相结合:

f = h5py.File("mytestfile.hdf5", "w")
dset = f.create_dataset("mydataset", (100000,100000,3), dtype=np.uint8)

shp = dset.shape    
step = 10000

a = tiffile.memmap('temp.tif', shape=(100000,100000,3), dtype=np.uint8)

for i in range(0, shp[0]+step, step):
    a[i:i+step,i:i+step] = dset[i:i+step,i:i+step]
    a.flush()
del a

大约需要 2 分钟(不错!),它创建了一个大文件(~29GB,压缩会使它变小),但是,它再次损坏,无法读取 TIFF 文件。

我真的不想放弃这个项目,但我坚持我还能尝试什么。 谁能推荐一个支持附加到图像但又不想将其加载到内存中的 TIFF/PNG 库?

标准 TIFF 不能用于存储 100000x100000 RGB 图像,除非它具有极高的可压缩性。由于使用 32 位偏移量,TIFF 文件的大小限制为 4 GB。 BigTIFF 使用 64 位偏移量。要启用 tifffile 写入 BigTIFF,请将 bigtiff=True 参数与 memmapimwrite 一起使用。但是,没有多少 software/libraries 能够读取文件,因为不支持 BigTIFF and/or 大条带尺寸。

这么大的图像通常是平铺存储的,通常带有压缩和多种分辨率(金字塔)。 Tifffile 可以从内存映射的 numpy 数组或图块生成器创建图块(大)TIFF,例如:

import numpy
import h5py
import tifffile

dtype = 'uint8'
shape = 100000, 100000, 3
tileshape = 1024, 1024

f = h5py.File('test.hdf5', 'w')
data = f.create_dataset('test', shape, dtype=dtype, compression='gzip')


def tile_generator(data, tileshape):
    for y in range(0, data.shape[0], tileshape[0]):
        for x in range(0, data.shape[1], tileshape[1]):
            tile = data[y: y+tileshape[0], x: x+tileshape[1], :]
            if tile.shape[:2] != tileshape:
                pad = (
                    (0, tileshape[0] - tile.shape[0]),
                    (0, tileshape[1] - tile.shape[1]),
                    (0, 0)
                )
                tile = numpy.pad(tile, pad, 'constant')
            yield tile


tifffile.imwrite(
    'temp.tif', 
    tile_generator(data, tileshape),
    dtype=dtype, 
    shape=shape, 
    tile=tileshape,
    bigtiff=True,
    # compress='jpeg'
)

tifffile 通过 imagecodecs 库支持多种压缩选项,例如DEFLATE、LZMA、ZStd、JPEG、JPEG2000、JPEGXR、WebP...

有专门的 TIFF "sub-formats"、库和工具来处理金字塔形 TIFF,通常取决于应用领域,例如libvips, OpenSlide, GDAL, or BioFormats

这里有一个 libvips 示例,用于创建一个巨大的 TIFF 文件而不需要加载内存:

import pyvips

# - make a set of pyvips images from a set of pointers to memory mapped files
# - the pointer objects need to support the buffer protocol, ie. refcounts,
# and will not be copied
# - format is something like "char" or "float"
images = [pyvips.Image.new_from_memory(pointer, width, height, bands, format)
          for pointer in my_set_of_pointers]

# join into a huge image, eg. 100 tiles across
# you can set margins, alignment, spacing, background, etc.
huge = pyvips.Image.arrayjoin(images, across=100)

# write to a file ... you can set a range of options, see eg. the 
# tiffsave docs
huge.write_to-file("thing.tif", compression="jpeg", tile=True, bigtiff=True)

可以高效读写TIFF金字塔,设置pyramid选项。 libvips 8.10 还支持生物格式金字塔。

libvips GUI nip2 可以显示任何大小的图像,包括巨大的 bigtiff。如果其他观众正在苦苦挣扎,这可能值得一试。我经常在这台普通笔记本电脑上处理 300,000 x 300,000 像素的图像。

现在 tifffile 有一些功能可以处理非常大的文件。

from tifffile import TiffWriter, memmap

# reading
original_image = memmap('/path/to/uncompressed/3D_image.tif')

# writing
with TiffWriter('/path/to/save/3D_image.tif', bigtiff=True) as tif:
    for i in range(rotated.shape[0]):
        tif.write(rotated[i], photometric='minisblack')