# -*- python -*-
#
# image.serial: read/write images
#
# Copyright 2006 - 2011 INRIA - CIRAD - INRA
#
# File author(s): Eric Moscardi <eric.moscardi@inria.fr>
# Jerome Chopard <jerome.chopard@sophia.inria.fr>
# Daniel BARBEAU <daniel.barbeau@sophia.inria.fr>
#
# Distributed under the Cecill-C License.
# See accompanying file LICENSE.txt or copy at
# http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html
#
# OpenAlea WebSite : http://openalea.gforge.inria.fr
################################################################################
"""
This module redefine load and save to account for spatial images
"""
__license__= "Cecill-C"
__revision__=" $Id$ "
from os.path import exists, splitext, split as psplit, expanduser as expusr
import os, fnmatch
from scipy.misc import imsave as _imsave
from struct import pack,unpack,calcsize
from pickle import dumps,loads
import numpy as np
from openalea.image.spatial_image import SpatialImage
from openalea.image.pil import Image, ImageOps
#~ from inrimage import *
from openalea.image.serial.inrimage import read_inrimage, write_inrimage
#~ from lsm import *
from openalea.image.serial.lsm import read_lsm
#~ from tif import *
from openalea.image.serial.tif import read_tif, write_tif
__all__ = ["save", "load", "read_sequence", "imread", "imsave", "lazy_image_or_path"]
[docs]def save (filename, img, is_vectorial=False) :
"""Save an array to a binary file in numpy format with a |SpatialImage| header.
.. warning::
There is no way to distinguish a 2D RGB[A] image from a 3D scalar image.
If img is a 2D RGB[A] image, make sure its shape is similar to (SX,SY,1,3) for RGB or
(SX,SY,1,4) for RGBA.
:Parameters:
- `filename` (file or str) - Filename to which the data is saved.
If the filename does not already have a ".npy"
extension, it is added.
- `img` (array)
- `is_vectorial` (bool) - specifically deal with img as if it was
a 2D vectorial (RGB[A]) image so that it saves it as a 3D RGBA image
and conforms to the contract.
"""
if isinstance(filename,str) :
if filename.endswith(".npy") :
file_ = open(filename,'wb')
else :
file_ = open("%s.npy" % filename,'wb')
file_.write("SpatialImage")
lenShape = len(img.shape)
if isinstance(img,SpatialImage) :
resolution = img.resolution
info = img.info
elif isinstance(img, np.ndarray):
resolution = (1.,)*lenShape
info = {}
# if the data is of shape 2 then
# we can be sure it is a scalar 2D image
# and we can reshape to a 3D scalar image.
if lenShape==2:
print "openalea.image.serial.basics.save: assuming 2D scalar image"
img = img.reshape(img.shape+(1,))
if len(resolution) == 2:
resolution += (1.,)
elif lenShape == 3 and not is_vectorial:
print "openalea.image.serial.basics.save: assuming 3D scalar image"
elif lenShape == 3 and is_vectorial:
print "openalea.image.serial.basics.save: interpreting as 2D scalar RGB[A] image"
img = img.reshape( img.shape[:2] + (1, img.shape[2]) )
if len(resolution) == 2:
resolution += (1.,)
elif lenShape == 4 and not is_vectorial:
print "openalea.image.serial.basics.save: assuming 3D RGB[A] image"
else:
raise IOError("Unable to identify image shape and encoding")
header = dumps( (resolution, info) )
file_.write(pack('i',len(header) ) )
file_.write(header)
np.save(file_,img)
[docs]def load (file, mmap_mode=None, is_vectorial=False) :
"""Load a pickled, ``.npy``, or ``.npz`` binary file.
:Parameters:
- `file` (file or str)
- `mmap_mode` (None, 'r+', 'r', 'w+', 'c') - optional
If not None, then memory-map the file, using the given mode
(see `numpy.memmap`). The mode has no effect for pickled or
zipped files.
A memory-mapped array is stored on disk, and not directly loaded
into memory. However, it can be accessed and sliced like any
ndarray. Memory mapping is especially useful for accessing
small fragments of large files without reading the entire file
into memory.
- `is_vectorial` (bool) - specifically deal with file as if it was
a 2D vectorial (RGB[A]) image so that it returns a 3D RGBA image
and conforms to the contract.
:Returns Type: |SpatialImage|
"""
if isinstance(file,str) :
file = open(file,'rb')
header = file.read(12)
if header == "SpatialImage" :
nb, = unpack('i',file.read(calcsize('i') ) )
res,info = loads(file.read(nb) )
data = np.load(file,mmap_mode)
# if the read data is of shape 2 then
# we can be sure it is a scalar 2D image
# and we can reshape to a 3D scalar image.
if len(data.shape) == 2:
print "openalea.image.serial.basics.load: assuming 2D scalar image"
data = data.reshape(data.shape+(1,))
if len(res) == 2:
res += (1.,)
elif len(data.shape) == 3 and not is_vectorial:
print "openalea.image.serial.basics.load: assuming 3D scalar image"
elif len(data.shape) == 3 and is_vectorial:
print "openalea.image.serial.basics.load: interpreting as 2D scalar RGB[A] image"
data = data.reshape( data.shape[:2] + (1, data.shape[2]) )
if len(res) == 2:
res += (1.,)
elif len(data.shape) == 4 and not is_vectorial:
print "openalea.image.serial.basics.load: assuming 3D RGB[A] image"
else:
raise IOError("Unable to identify image shape and encoding")
if len(res) == len(data.shape) :
vdim = 1
else :
vdim = data.shape[-1]
return SpatialImage(data,res,vdim,info)
else :
file.seek(0)
return SpatialImage(np.load(file,mmap_mode))
##################################################
# TODO : Read voxels size in xlm file if provided #
##################################################
[docs]def read_sequence ( directory, grayscale=True, number_images=None, start=0, increment=1, filename_contains="", voxels_size=None, verbose=True) :
"""
Convert a sequence of images in a folder as a numpy array.
The images must all be the same size and type.
They can be in TIFF, .... format.
:Parameters:
- `grayscale` (bool) - convert the image to grayscale
- `number_images` (int) - specify how many images to open
- `start` (int) - used to start with the nth image in the folder (default = 0 for the first image)
- `increment` (int) - set to "n" to open every "n" image (default = 1 for opening all images)
- `filename_contains` (str) - only files whose name contains that string are opened
- `voxels_size (tuple) - specify voxels size
- `verbose` (bool) - verbose mode
"""
_images = []
_files = []
if verbose : print "Loading : "
for f in os.listdir(directory):
if fnmatch.fnmatch(f, '*%s*' %filename_contains):
try :
im = Image.open(os.path.join(directory, f))
_files.append(f)
if grayscale is True :
_images.append(ImageOps.grayscale(im))
else:
_images.append(im)
except :
if verbose : print "\t warning : cannot open %s" %f
if len(_images) == 0 :
if verbose : print "\t no images loaded"
return -1
xdim, ydim = _images[0].size
if number_images is None :
zdim = round(float(len(_images) - start)/ increment)
_nmax = len(_images) - start
else :
zdim = number_images
_nmax = number_images * increment
if _images[0].mode == 'RGB':
nd_image = np.zeros((xdim,ydim,zdim, 3), dtype=np.uint8)
nd_image = np.zeros((xdim,ydim,zdim))
j = 0
for i in _images[start:_nmax+start:increment] :
if i.size == _images[start].size :
nd_image[:,:,j] = i
if verbose : print "\t ./%s" %_files[_images.index(i)]
j += 1
else :
if verbose : print "%s : wrong size - %s expected, %s found" %(_files[_images.index(i)], _images[start].size, i.size)
result = nd_image.transpose(1,0,2)
if voxels_size is None :
return SpatialImage(result)
else :
return SpatialImage(result, voxels_size)
[docs]def imread (filename, dimension=3) :
"""Reads an image file completely into memory.
It uses the file extension to determine how to read the file. It first tries
some specific readers for volume images (Inrimages, TIFFs, LSMs, NPY) or falls
back on PIL readers for common formats if installed.
In all cases the returned image is 3D (2D is upgraded to single slice 3D).
If it has colour or is a vector field it is even 4D.
:Parameters:
- `filename` (str)
:Returns Type:
|SpatialImage|
"""
filename = expusr(filename)
if not exists(filename) :
raise IOError("The requested file do not exist: %s" % filename)
root, ext = splitext(filename)
ext = ext.lower()
if ext == ".gz":
root, ext = splitext(root)
ext = ext.lower()
if ext == ".inr":
return read_inrimage(filename)
elif ext == ".lsm":
return read_lsm(filename)
elif ext in [".tif", ".tiff"]:
return read_tif(filename)
elif ext in [".npz", ".npy"]:
return load(filename)
else:
# -- We use the normal numpy reader. It returns 2D images.
# If len(shape) == 2 : scalar image.
# If len(shape) == 3 and shape[2] == 3 : rgb image
# If len(shape) == 3 and shape[3] == 4 : rgba image.
# Return a SpatialImage please! --
# Use the array protocol to convert a PIL image to an array.
# Don't use pylab'es PIL_to_array conversion as it flips images vertically.
im_array = np.array(Image.open(filename))
shape = im_array.shape
if len(shape)==2:
newShape = (shape[0], shape[1], 1, 1)
elif len(shape) == 3:
newShape = (shape[0], shape[1], 1, shape[2])
else:
raise IOError("unhandled image shape : %s, %s"%(filename, str(shape)))
#newarr = np.zeros(newShape, dtype=im_array.dtype, order="C")
#newarr[:,:,0] = im_array[:,:]
vdim = 1 if( len(shape) < 3 ) else shape[2]
return SpatialImage(im_array.reshape(newShape), None, vdim)
[docs]def imsave(filename, img):
"""Save a |SpatialImage| to filename.
.. note: `img` **must** be a |SpatialImage|.
The filewriter is choosen according to the file extension. However all file extensions
will not match the data held by img, in dimensionnality or encoding, and might raise `IOError`s.
For real volume data, Inrimage and NPY are currently supported.
For |SpatialImage|s that are actually 2D, PNG, BMP, JPG among others are supported if PIL is installed.
:Parameters:
- `filename` (str)
- `img` (|SpatialImage|)
"""
assert isinstance(img, SpatialImage)
# -- images are always at least 3D! If the size of dimension 3 (indexed 2) is 1, then it is actually
# a 2D image. If it is 4D it has vectorial or RGB[A] data. --
filename = expusr(filename)
head, tail = psplit(filename)
head = head or "."
if not exists(head):
raise IOError("The directory do not exist: %s" % head)
root, ext = splitext(filename)
is2D = img.shape[2] == 1
ext = ext.lower()
if ext == ".gz":
root, ext = splitext(root)
ext = ext.lower()
if ext == ".inr":
write_inrimage(filename, img)
elif ext in [".npz", ".npy"]:
save(filename, img)
elif ext in [".tiff", ".tif"]:
write_tif(filename, img)
else:
if not is2D:
raise IOError("No writer found for format of 3D image %s"%filename)
else:
# -- fallback on Pylab.
# WARNING: Careful, this can fail in many ways still!
# For example, many formats wont support writing scalar floats, or
# vector floats, or encodings different from uchar8 --
#WARNING 2: Still this damn transposition thing that may appear.
#the problem is that what we write doesn't look like what is shown
#with "display()". display() is broken, not the write functions.
if len(img.shape) == 4: # RGB[A] images
_imsave(filename,img[:,:,0,:])
elif len(img.shape) == 3: #scalar images
_imsave(filename, img[:,:,0])
else:
raise IOError("Unhandled image shape %s"%str(img.shape))
###################
# UTILITY METHODS #
###################
[docs]def lazy_image_or_path(image):
""" Takes an image or a path to an image and returns the image.
Extensively used in other functions to make them accept images given as paths.
If `image` is already a |SpatialImage| this method is a pass-thru. If it looks
like a path, it will load the image at that path and return it.
:Parameters:
- `image` (|SpatialImage|, str) - [Path] or image.
:Returns:
- image or imread(image)
:Returns Type:
|SpatialImage|
"""
wp = False
if isinstance(image, (str, unicode)):
image = imread(image)
wp = True
else:
assert isinstance(image, SpatialImage)
return image, wp