5

I have an array with 600×600×40 dimension that each band(from 40 band) represent a 600×600 image I want to save it to a multiple band .tif image. I have tried this functions from scikit-image and openCV but they can not save more than 3 band(as RGB).

import cv2
cv2.imwrite('image.tif',600by600_just3band_array)
Dan Mašek
  • 17,852
  • 6
  • 57
  • 85
user10482281
  • 53
  • 1
  • 5
  • I don't think you can use use openCV or scikit to save images with more than 4 channel. Better option will be to use pickle.dump or numpy.save or you can save for all combination of 2 or 3 channel – Garvita Tiwari Dec 14 '18 at 10:08
  • you are right and as far as I tried I agree with you, please help and instruct me with sample code – user10482281 Dec 14 '18 at 10:10

3 Answers3

10

tifffile (https://pypi.org/project/tifffile/) supports multi-channel .tiff's and has an API similar to the one of scikit-image or OpenCV:

In [1]: import numpy as np

In [2]: import tifffile

In [3]: # Channel dimension should come first

In [4]: x = np.random.randint(0, 255, 4*100*100).reshape((4, 100, 100))

In [5]: tifffile.imsave('test.tiff', x)

In [6]: y = tifffile.imread('test.tiff')

In [7]: np.all(np.equal(x, y))
Out[7]: True
soupault
  • 6,089
  • 4
  • 24
  • 35
  • Excellent! I think you are inadvertently saving a 64-bit TIFF if you don't use `x = np.random.randint(0, 255, 4*100*100, dtype=np.uint8)` though. – Mark Setchell Dec 14 '18 at 17:42
  • 2
    Nice! You can get multiband (rather than multipage) tiffs like this: `x = np.random.randint(0, 255, 100*100*40, dtype='uint8').reshape((100, 100, 40)); tifffile.imsave("y.tif", x, planarconfig='contig')` ie. band dimension last for contig. – jcupitt Dec 14 '18 at 17:55
  • any way to name the bands? – Flash Thunder Nov 10 '20 at 10:50
5

You could save multiple images, each representing a single band (greyscale), or even multiple bands (colour) in a single TIFF file with PIL/Pillow like this:

from PIL import Image
# Synthesize 8 dummy images, all greyscale, all same size but with varying brightness
size=(480,640)  
b1 = Image.new('L', size, color=10)                                                         
b2 = Image.new('L', size, color=20)                                                        
b3 = Image.new('L', size, color=30)                                                       
b4 = Image.new('L', size, color=40)                                                        
b5 = Image.new('L', size, color=50)                                                        
b6 = Image.new('L', size, color=60)                                                        
b7 = Image.new('L', size, color=70)                                                        
b8 = Image.new('L', size, color=80)                                                        

# Save all 8 to single TIFF file
b1.save('multi.tif', save_all=True, append_images=[b2,b3,b4,b5,b6,b7,b8]) 

If you now examine that file with ImageMagick at the command line, you can see all 8 bands are present:

magick identify multi.tif 
multi.tif[0] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000
multi.tif[1] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000
multi.tif[2] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000
multi.tif[3] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000
multi.tif[4] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000
multi.tif[5] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000
multi.tif[6] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000
multi.tif[7] TIFF 480x640 480x640+0+0 8-bit Grayscale Gray 2.34473MiB 0.000u 0:00.000

In case you are using OpenCV or Numpy arrays for your processing, you can make an OpenCV or Numpy array into a PIL/Pillow image with:

PILimage = Image.fromarray(numpyImage)

and, going the other way, from a PIL/Pillow image to Numpy array:

NumpyImage = np.array(PILimage)

If you then want to read them back, you can do this:

# Open the multi image
im = Image.open('multi.tif')                                                               

# Iterate through frames
for frame in ImageSequence.Iterator(im):  
    frame.show() 

enter image description here


If you want to move to a specific band, you can seek like this:

im = Image.open('multi.tif')                                                               

im.seek(3) 
im.show()

You can also extract band3 from the TIF and save as a PNG with ImageMagick at the command line with:

magick multi.tif[3] band3.png

Or make a band 1, 2, 7 RGB composite with:

magick multi.tif[1] multi.tif[2] multi.tif[7] -colorspace RGB -combine 127rgb.png

which will look dark blue because the red and the green channels are very low and only the blue channel has a large-ish value.


I am not the world's best on Python, so am uncertain of any implications/errors, but I think if you have a 600x600x40 numpy array of images, you can do what I am suggesting like this:

# Synthesize dummy array of 40 images, each 600x600
nparr = np.random.randint(0,256,(600,600,40), dtype=np.uint8)

# Make PIL/Pillow image of first
a = Image.fromarray(nparr[:,:,0])

# Save whole lot in one TIF
a.save('multi.tif', save_all=True, append_images=[Image.fromarray(nparr[:,:,x]) for x in range(1,40)]) 

Keywords: Multi-band, multi band, multi-spectral, multi spectral, satellite image, imagery, image processing, Python, Numpy, PIL, Pillow, TIFF, TIF, NDVI

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • as i have numpy.ndarray 600×600×40 i should do this 40 times: b1=Image.fromarray(array[:,:,0]) and so on untill b40, then prepare each band for final b1.save(.....append_images=[b2,b3,...b40] , doesn't exist short simple way? – user10482281 Dec 14 '18 at 12:22
  • That would work, but I think you can make a *"list comprehension"* in there and do it in one line. I am not the world's best on Python, but it is something like `append_images=[Image.fromarray(yourarray[:,:,x]) for x in range(40)]` – Mark Setchell Dec 14 '18 at 12:30
  • See here... https://www.pythonforbeginners.com/basics/list-comprehensions-in-python and have a try, as you have your Numpy array and I don't, and let me know if you work it out or need a hand in a while. – Mark Setchell Dec 14 '18 at 12:32
  • I really appreciate your help, but the real problem is multi.tif is sigle band I had tried your final recomend before maybe you aren't #1 Python developer but you're one of the best – user10482281 Dec 14 '18 at 12:42
  • I have added some code that works with my dummy image array at the end of my answer. – Mark Setchell Dec 14 '18 at 12:47
  • Thank you very much – user10482281 Dec 14 '18 at 13:05
1

Mark's clever answer is making a multi-page TIFF. Unfortunately, imagemagick and PIL are really MONO / RGB / RGBA / CMYK libraries and they don't have direct support for multiband images.

pyvips has true multiband support. For example:

import sys
import pyvips
import numpy as np

# make a (100, 100, 40) numpy image
array = np.zeros((100, 100, 40), dtype=sys.argv[2])

# convert to vips and save
image = numpy2vips(array)
image.write_to_file(sys.argv[1])

# read it back, convert to numpy, and show info
image2 = pyvips.Image.new_from_file(sys.argv[1])
array = vips2numpy(image2)

print("shape =", array.shape)
print("format =", array.dtype)

I can run it like this:

$ ./try284.py x.tif uint8
shape = (100, 100, 40)
format = uint8
$ vipsheader x.tif
x.tif: 100x100 uchar, 40 bands, srgb, tiffload
$ identify x.tif
x.tif TIFF 100x100 100x100+0+0 8-bit sRGB 400KB 0.000u 0:00.000

It supports other dtypes as well:

$ ./try284.py x.tif uint32
shape = (100, 100, 40)
format = uint32
$ ./try284.py x.tif float32
shape = (100, 100, 40)
format = float32

etc. etc.

You can load these TIFFs in gdal. I guess gdal can be used to write them as well, though I've not tried. Annoyingly, it moves the 40 to the outermost dimension.

$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from osgeo import gdal
>>> x = gdal.Open("x.tif")
>>> a = x.ReadAsArray()
>>> a.shape
(40, 100, 100)

vips2numpy() and numpy2vips() are defined here:

https://github.com/libvips/pyvips/blob/master/examples/pil-numpy-pyvips.py

Copy-pasted for reference:

# map vips formats to np dtypes
format_to_dtype = {
    'uchar': np.uint8,
    'char': np.int8,
    'ushort': np.uint16,
    'short': np.int16,
    'uint': np.uint32,
    'int': np.int32,
    'float': np.float32,
    'double': np.float64,
    'complex': np.complex64,
    'dpcomplex': np.complex128,
}

# map np dtypes to vips
dtype_to_format = {
    'uint8': 'uchar',
    'int8': 'char',
    'uint16': 'ushort',
    'int16': 'short',
    'uint32': 'uint',
    'int32': 'int',
    'float32': 'float',
    'float64': 'double',
    'complex64': 'complex',
    'complex128': 'dpcomplex',
}

# numpy array to vips image
def numpy2vips(a):
    height, width, bands = a.shape
    linear = a.reshape(width * height * bands)
    vi = pyvips.Image.new_from_memory(linear.data, width, height, bands,
                                      dtype_to_format[str(a.dtype)])
    return vi

# vips image to numpy array
def vips2numpy(vi):
    return np.ndarray(buffer=vi.write_to_memory(),
                      dtype=format_to_dtype[vi.format],
    shape=[vi.height, vi.width, vi.bands])
jcupitt
  • 10,213
  • 2
  • 23
  • 39
  • That's very nifty, John. Do you happen to know what software can read a 40-band TIFF, please - other than `vips`. Thank you. – Mark Setchell Dec 14 '18 at 14:01
  • Good point. TIFF itself can, of course, you can run `tiffinfo`, `tiffcp` etc on these files. libvips was originally written for hyperspectral imaging, so it's been a necessary feature. You're right, I don't know of any other packages which can read these monsters. – jcupitt Dec 14 '18 at 14:42
  • Ok, thank you. Are you familiar with GDAL - maybe that can? – Mark Setchell Dec 14 '18 at 14:58
  • 1
    Oh, you're right gdal can do it. I'll update my answer. Thanks! – jcupitt Dec 14 '18 at 15:45