diff --git a/fabio/fabioformats.py b/fabio/fabioformats.py index 3e5e5e8e5..53c338582 100644 --- a/fabio/fabioformats.py +++ b/fabio/fabioformats.py @@ -82,6 +82,7 @@ def importer(module_name): ("raxisimage", "RaxisImage"), ("numpyimage", "NumpyImage"), ("eigerimage", "EigerImage"), + ("soleilimage", "SoleilImage"), ("hdf5image", "Hdf5Image"), ("fit2dimage", "Fit2dImage"), ("speimage", "SpeImage"), diff --git a/fabio/openimage.py b/fabio/openimage.py index 8479de66f..084d950fb 100644 --- a/fabio/openimage.py +++ b/fabio/openimage.py @@ -46,7 +46,7 @@ import logging logger = logging.getLogger(__name__) from . import fabioutils -from .fabioutils import FilenameObject, BytesIO +from .fabioutils import FilenameObject, BytesIO, NotGoodReader from .fabioimage import FabioImage # Make sure to load all formats @@ -105,18 +105,40 @@ def do_magic(byts, filename): if "/" in format_type: if format_type == "eiger/hdf5": if "::" in filename: - return "hdf5" + return ["hdf5"] else: - return "eiger" + return ["eiger", "soleil"] elif format_type == "marccd/tif": if "mccd" in filename.split("."): - return "marccd" + return ["marccd"] else: - return "tif" - return format_type - raise Exception("Could not interpret magic string") + return ["tif"] + return [format_type] + return None +def openimage_str(filename, frame): + logger.debug("Attempting to open %s" % (filename)) + objs = _openimage(filename) + for obj in objs: + try: + return obj.read(filename, frame) + except NotGoodReader as ex: + pass + raise NotGoodReader("Did not found a good reader for {}".format(filename)) + +def openimage_PathTypes(filename, frame): + if not isinstance(filename, fabioutils.StringTypes): + filename = str(filename) + openimage_str(filename, frame) + +def openimage_FilenameObject(filename, frame): + try: + return openimage_str(filename.tostring(), frame) + except Exception as ex: + logger.debug("Exception %s, trying name %s" % (ex, filename.stem)) + return openimage_str(filename.stem, filename.num) + def openimage(filename, frame=None): """Open an image. @@ -134,31 +156,14 @@ def openimage(filename, frame=None): :param Union[int,None] frame: A specific frame inside this file. :rtype: FabioImage """ - if isinstance(filename, fabioutils.PathTypes): - if not isinstance(filename, fabioutils.StringTypes): - filename = str(filename) - - if isinstance(filename, FilenameObject): - try: - logger.debug("Attempting to open %s" % (filename.tostring())) - obj = _openimage(filename.tostring()) - logger.debug("Attempting to read frame %s from %s with reader %s" % (frame, filename.tostring(), obj.classname)) - obj = obj.read(filename.tostring(), frame) - except Exception as ex: - # multiframe file - # logger.debug( "DEBUG: multiframe file, start # %d"%( - # filename.num) - logger.debug("Exception %s, trying name %s" % (ex, filename.stem)) - obj = _openimage(filename.stem) - logger.debug("Reading frame %s from %s" % (filename.num, filename.stem)) - obj.read(filename.stem, frame=filename.num) - else: - logger.debug("Attempting to open %s" % (filename)) - obj = _openimage(filename) - logger.debug("Attempting to read frame %s from %s with reader %s" % (frame, filename, obj.classname)) - obj = obj.read(obj.filename, frame) - return obj - + if isinstance(filename, str): + return openimage_str(filename, frame) + elif isinstance(filename, unicode): + return openimage_str(str(filename), frame) + elif isinstance(filename, fabioutils.PathTypes): + return openimage_PathTypes(filename, frame) + elif isinstance(filename, FilenameObject): + return openimage_FilenameObject(filename, frame) def openheader(filename): """ return only the header""" @@ -166,10 +171,13 @@ def openheader(filename): if not isinstance(filename, fabioutils.StringTypes): filename = str(filename) - obj = _openimage(filename) - obj.readheader(obj.filename) - return obj - + objs = _openimage(filename) + for obj in objs: + try: + obj.readheader(obj.filename) + return obj + except NotGoodReader: + pass def _openimage(filename): """ @@ -205,10 +213,8 @@ def _openimage(filename): else: imo = None - filetype = None - try: - filetype = do_magic(magic_bytes, filename) - except Exception: + filetypes = do_magic(magic_bytes, filename) + if filetypes is None: logger.debug("Backtrace", exc_info=True) try: file_obj = FilenameObject(filename=filename) @@ -219,27 +225,33 @@ def _openimage(filename): isinstance(file_obj.format, list): # one of OXD/ADSC - should have got in previous raise Exception("openimage failed on magic bytes & name guess") - filetype = file_obj.format - + if file_obj.format is not None: + filetypes = [file_obj.format] except Exception: logger.debug("Backtrace", exc_info=True) raise IOError("Fabio could not identify " + filename) - if filetype is None: + if filetypes is None: raise IOError("Fabio could not identify " + filename) - klass_name = "".join(filetype) + 'image' + objs = [] + for filetype in filetypes: + klass_name = "".join(filetype) + 'image' + try: + obj = fabioformats.factory(klass_name) + objs.append(obj) + except (RuntimeError, Exception): + pass - try: - obj = fabioformats.factory(klass_name) - except (RuntimeError, Exception): + if len(objs) == 0: logger.debug("Backtrace", exc_info=True) - raise IOError("Filename %s can't be read as format %s" % (filename, klass_name)) + raise IOError("Filename %s can't be read as format %s" % (filename, filetypes)) - obj.filename = filename - # skip the read for read header - return obj + for obj in objs: + obj.filename = filename + # skip the read for read header + return objs def open_series(filenames=None, first_filename=None, single_frame=None, fixed_frames=None, fixed_frame_number=None): diff --git a/fabio/soleilimage.py b/fabio/soleilimage.py new file mode 100644 index 000000000..637729f40 --- /dev/null +++ b/fabio/soleilimage.py @@ -0,0 +1,193 @@ +# coding: utf-8 +# +# Project: FabIO X-ray image reader +# +# Copyright (C) 2010-2016 European Synchrotron Radiation Facility +# Grenoble, France +# +# Copyright (C) 2019 Synchrotron-SOLEIL +# Gif-sur-Yvette, France +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +"""Soleil HDF5 image for FabIO + +Authors: Jerome Kieffer, Picca Frédéric-Emmanuel +email: Jerome.Kieffer@terre-adelie.org, picca@synchrotron-soleil.fr + +Specifications: +input should being the form: + +filename + +Only supports ndim=2 or 3 (exposed as a stack of images) +""" + +from __future__ import with_statement, print_function, division + +__authors__ = ["Jérôme Kieffer", "Picca Frédéric-Emmanuel"] +__contact__ = "Jerome.Kieffer@terre-adelie.org" +__license__ = "MIT" +__copyright__ = "Jérôme Kieffer" +__date__ = "01/03/2019" + +import logging + +logger = logging.getLogger(__name__) + +from collections import namedtuple +from functools import partial + +try: + import h5py + from h5py import Dataset, File +except ImportError: + h5py = None + +from fabio.fabioimage import FabioFrame, FabioImage +from .fabioutils import previous_filename, next_filename + +# Generic hdf5 access types. + +DatasetPathContains = namedtuple("DatasetPathContains", "path") +DatasetPathWithAttribute = namedtuple("DatasetPathWithAttribute", "attribute value") + +def _v_attrs(attribute, value, _name, obj): + """extract all the images and accumulate them in the acc variable""" + if isinstance(obj, Dataset): + if attribute in obj.attrs and obj.attrs[attribute] == value: + return obj + + +def _v_item(key, name, obj): + if key in name: + return obj + + +def get_dataset(h5file, path): + res = None + if isinstance(path, DatasetPathContains): + res = h5file.visititems(partial(_v_item, path.path)) + elif isinstance(path, DatasetPathWithAttribute): + res = h5file.visititems(partial(_v_attrs, + path.attribute, path.value)) + return res + + +class SoleilFrame(FabioFrame): + """Identify a slice of dataset from an HDF5 file""" + + def __init__(self, soleil_image, frame_num): + if not isinstance(soleil_image, SoleilImage): + raise TypeError("Expected class {SoleilImage}".format(SoleilImage)) + data = soleil_image.dataset[frame_num, :, :] + super(SoleilFrame, self).__init__(data=data, header=soleil_image.header) + self.hdf5 = soleil_image.hdf5 + self.dataset = soleil_image.dataset + self.filename = soleil_image.filename + self._nframes = soleil_image.nframes + self.header = soleil_image.header + self.currentframe = frame_num + + +class SoleilImage(FabioImage): + """ + FabIO image class for Images from an Soleil HDF file + + filename::dataset + """ + + DESCRIPTION = "Soleil Hierarchical Data Format HDF5 flat reader" + + DEFAULT_EXTENSIONS = ["nxs", "h5"] + + def __init__(self, *arg, **kwargs): + if not h5py: + raise RuntimeError("fabio.SoleilImage cannot be used without h5py. Please install h5py and restart") + super(SoleilImage, self).__init__(*arg, **kwargs) + self.hdf5 = None + self.dataset = None + + def read(self, filename, frame=None): + """ + try to read image + :param fname: filename + """ + self.resetvals() + + path = DatasetPathWithAttribute("interpretation", b"image") + self.filename = filename + self.hdf5 = File(self.filename, "r") + self.dataset = get_dataset(self.hdf5, path) + + # ndim does not exist for external links ? + ndim = len(self.dataset.shape) + if ndim == 3: + self._nframes = self.dataset.shape[0] + if frame is not None: + self.currentframe = int(frame) + else: + self.currentframe = 0 + self.data = self.dataset[self.currentframe, :, :] + elif ndim == 2: + self.data = self.dataset[:, :] + else: + err = "Only 2D and 3D datasets are supported by FabIO, here %sD" % self.dataset.ndim + logger.error(err) + raise RuntimeError(err) + return self + + def getframe(self, num): + """ + Returns a frame as a new FabioImage object + :param num: frame number + """ + if num < 0 or num > self.nframes: + raise RuntimeError("Requested frame number %i is out of range [0, %i[ " % (num, self.nframes)) + # Do a deep copy of the header to make a new one + return SoleilFrame(self, num) + + def next(self): + """ + Get the next image in a series as a fabio image + """ + if self.currentframe < (self.nframes - 1): + return self.getframe(self.currentframe + 1) + else: + newobj = SoleilImage() + newobj.read(next_filename(self.filename)) + return newobj + + def previous(self): + """ + Get the previous image in a series as a fabio image + """ + if self.currentframe > 0: + return self.getframe(self.currentframe - 1) + else: + newobj = SoleilImage() + newobj.read(previous_filename(self.filename)) + return newobj + + def close(self): + if self.hdf5 is not None: + self.hdf5.close() + self.dataset = None