Source code for xrayutilities.io.imagereader

# This file is part of xrayutilities.
#
# xrayutilities is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
#
# Copyright (c) 2012-2023 Dominik Kriegner <dominik.kriegner@gmail.com>

import os.path
import time

import numpy

from .. import config
from ..exception import InputError
# relative imports from xrayutilities
from .helper import xu_open


[docs] class ImageReader: """ parse CCD frames in the form of tiffs or binary data (``*.bin``) to numpy arrays. ignore the header since it seems to contain no useful data The routine was tested so far with 1. RoperScientific files with 4096x4096 pixels created at Hasylab Hamburg, which save an 16bit integer per point. 2. Perkin Elmer images created at Hasylab Hamburg with 2048x2048 pixels. """
[docs] def __init__(self, nop1, nop2, hdrlen=0, flatfield=None, darkfield=None, dtype=numpy.int16, byte_swap=False): """ initialize the ImageReader reader, which includes setting the dimension of the images as well as defining the data used for flat- and darkfield correction! Parameters ---------- nop1, nop2 : int number of pixels in the first and second dimension of the image hdrlen : int, optional length of the file header which should be ignored flatfield : str or ndarray, optional filename or data for flatfield correction. supported file types include (.bin/.tif (also compressed .xz or .gz) and .npy files). otherwise a 2D numpy array should be given darkfield : str or ndarray, optional filename or data for darkfield correction. same types as for flat field are supported. dtype : numpy.dtype, optional datatype of the stored values (default: numpy.int16) byte_swap : bool, optional flag which determines bytes are swapped after reading """ # save number of pixels per image self.nop1 = nop1 self.nop2 = nop2 self.dtype = dtype self.hdrlen = hdrlen self.byteswap = byte_swap # read flatfield if flatfield: if isinstance(flatfield, str): if os.path.splitext(flatfield)[1] == '.npy': self.flatfield = numpy.load(flatfield) elif os.path.splitext(flatfield)[1] in \ ['.gz', '.xz', '.bin', '.tif']: # read without flatc and darkc self.flatfield = self.readImage(flatfield) else: raise InputError("Error: unknown filename for " "flatfield correction!") elif isinstance(flatfield, numpy.ndarray): self.flatfield = flatfield else: raise InputError("Error: unsupported type for " "flatfield correction!") # read darkfield if darkfield: if isinstance(darkfield, str): if os.path.splitext(darkfield)[1] == '.npy': self.darkfield = numpy.load(darkfield) elif os.path.splitext(darkfield)[1] in \ ['.gz', '.xz', '.bin', '.tif']: # read without flatc and darkc self.darkfield = self.readImage(darkfield) else: raise InputError("Error: unknown filename for " "darkfield correction!") elif isinstance(darkfield, numpy.ndarray): self.darkfield = darkfield else: raise InputError("Error: unsupported type for " "darkfield correction!") if flatfield: self.flatc = True if config.VERBOSITY >= config.INFO_ALL: print("XU.io.ImageReader: flatfield correction enabled") else: self.flatc = False if darkfield: self.darkc = True if config.VERBOSITY >= config.INFO_ALL: print("XU.io.ImageReader: darkfield correction enabled") else: self.darkc = False
[docs] def readImage(self, filename, path=None): """ read image file and correct for dark- and flatfield in case the necessary data are available. returned data = ((image data)-(darkfield))/flatfield*average(flatfield) Parameters ---------- filename : str filename of the image to be read. so far only single filenames are supported. The data might be compressed. supported extensions: .tif, .bin and .bin.xz path : str, optional path of the data files """ if path: full_filename = os.path.join(path, filename) else: full_filename = filename if config.VERBOSITY >= config.INFO_ALL: print(f"XU.io.ImageReader.readImage: file {full_filename}") t1 = time.time() with xu_open(full_filename) as fh: # jump over header fh.seek(self.hdrlen) # read image rlen = numpy.dtype(self.dtype).itemsize * self.nop1 * self.nop2 img = numpy.frombuffer(fh.read(rlen), dtype=self.dtype) if self.byteswap: img = img.byteswap() img.shape = (self.nop1, self.nop2) # reshape the data # darkfield correction if self.darkc: img = (img - self.darkfield).astype(numpy.float32) # flatfield correction if self.flatc: img = img.astype(numpy.float32) / self.flatfield if config.VERBOSITY >= config.INFO_ALL: t2 = time.time() print(f"XU.io.ImageReader.readImage: parsing time {t2 - t1:8.3f}") return img
dlen = {'char': 1, 'byte': 1, 'word': 2, 'dword': 4, 'rational': 8, # not implemented correctly 'float': 4, 'double': 8} dtypes = {1: 'byte', 2: 'char', 3: 'word', 4: 'dword', 5: 'rational', # not implemented correctly 6: 'byte', 7: 'byte', 8: 'word', 9: 'dword', 10: 'rational', # not implemented correctly 11: 'float', 12: 'double'} nptyp = {1: numpy.byte, 2: numpy.char, 3: numpy.uint16, 4: numpy.uint32, 5: numpy.uint32, 6: numpy.int8, 7: numpy.byte, 8: numpy.int16, 9: numpy.int32, 10: numpy.int32, 11: numpy.float32, 12: numpy.float64} tiffdtype = {1: {8: numpy.uint8, 16: numpy.uint16, 32: numpy.uint32}, 2: {8: numpy.int8, 16: numpy.int16, 32: numpy.int32}, 3: {16: numpy.float16, 32: numpy.float32}} tifftags = {256: 'ImageWidth', # width 257: 'ImageLength', # height 258: 'BitsPerSample', 259: 'Compression', 262: 'PhotometricInterpretation', 270: 'ImageDescription', 272: 'Model', 273: 'StripOffsets', 278: 'RowsPerStrip', 279: 'StripByteCount', 282: 'XResolution', 283: 'YResolution', 305: 'Software', 306: 'DateTime', 315: 'Artist', 339: 'SampleFormat', 36864: 'ExifVersion'}
[docs] class TIFFRead(ImageReader): """ class to Parse a TIFF file including extraction of information from the file header in order to determine the image size and data type The data stored in the image are available in the 'data' property. """
[docs] def __init__(self, filename, path=None): """ initialization of the class which will prepare the parser and parse the files content into class properties Parameters ---------- filename : str file name of the TIFF-like image file path : str, optional path of the data file """ if path: full_filename = os.path.join(path, filename) else: full_filename = filename with xu_open(full_filename, 'rb') as fh: self.byteorder = fh.read(2*dlen['char']) self.version = numpy.frombuffer(fh.read(dlen['word']), dtype=numpy.uint16)[0] if self.byteorder not in (b'II', b'MM') or self.version != 42: raise TypeError(f"Not a TIFF file ({filename})") if self.byteorder != b'II': raise NotImplementedError("The 'MM' byte order is not yet " "implemented, please file a bug!") fh.seek(4) self.ifdoffset = numpy.frombuffer(fh.read(dlen['dword']), dtype=numpy.uint32)[0] fh.seek(self.ifdoffset) self.ntags = numpy.frombuffer(fh.read(dlen['word']), dtype=numpy.uint16)[0] self._parseImgTags(fh, self.ntags) fh.seek(self.ifdoffset + 2 + 12 * self.ntags) nextimgoffset = numpy.frombuffer(fh.read(dlen['dword']), dtype=numpy.uint32)[0] if nextimgoffset != 0: raise NotImplementedError("Multiple images per file are not " "supported, please file a bug!") # check if image type is supported if self.imgtags.get('Compression', 1) != 1: raise NotImplementedError("Compression is not supported, " "please file a bug report!") if self.imgtags.get('PhotometricInterpretation', 0) not in (0, 1): raise NotImplementedError("RGB and colormap is not supported") sf = self.imgtags.get('SampleFormat', 1) bs = self.imgtags.get('BitsPerSample', 1) if isinstance(self.imgtags['StripOffsets'], numpy.ndarray): hdrlen = self.imgtags['StripOffsets'][0] else: hdrlen = self.imgtags['StripOffsets'] ImageReader.__init__(self, self.imgtags['ImageLength'], self.imgtags['ImageWidth'], hdrlen=hdrlen, dtype=tiffdtype[sf][bs], byte_swap=False) self.data = self.readImage(filename, path)
def _parseImgTags(self, fh, ntags): """ parse TIFF image tags from Image File Directory header Parameters ---------- fh : file-handle file handle of the TIFF file ntags : int number of tags in the Image File Directory """ self.imgtags = {} for _ in range(ntags): ftag = numpy.frombuffer(fh.read(dlen['word']), dtype=numpy.uint16)[0] ftype = numpy.frombuffer(fh.read(dlen['word']), dtype=numpy.uint16)[0] flength = numpy.frombuffer(fh.read(dlen['dword']), dtype=numpy.uint32)[0] fdoffset = numpy.frombuffer(fh.read(dlen['dword']), dtype=numpy.uint32)[0] pos = fh.tell() if flength*dlen[dtypes[ftype]] <= 4: fdoffset = pos - dlen['dword'] fh.seek(fdoffset) if ftype == 2: fdata = fh.read(flength * dlen[dtypes[ftype]]) fdata = fdata.split(b'\x00')[0].decode("ascii", errors="replace") else: rlen = flength * dlen[dtypes[ftype]] fdata = numpy.frombuffer(fh.read(rlen), dtype=nptyp[ftype]) if flength == 1: fdata = fdata[0] fh.seek(pos) # add field to tags self.imgtags[tifftags.get(ftag, ftag)] = fdata
[docs] class PerkinElmer(ImageReader): """ parse PerkinElmer CCD frames (``*.tif``) to numpy arrays Ignore the header since it seems to contain no useful data The routine was tested only for files with 2048x2048 pixel images created at Hasylab Hamburg which save an 32bit float per point. """
[docs] def __init__(self, **keyargs): """ initialize the PerkinElmer reader, which includes setting the dimension of the images as well as defining the data used for flat- and darkfield correction! Parameters ---------- flatfield : str or ndarray, optional filename or data for flatfield correction. supported file types include (.bin .bin.xz and .npy files). otherwise a 2D numpy array should be given darkfield : str or ndarray, optional filename or data for darkfield correction. same types as for flat field are supported. """ ImageReader.__init__(self, 2048, 2048, hdrlen=8, dtype=numpy.float32, byte_swap=False, **keyargs)
[docs] class RoperCCD(ImageReader): """ parse RoperScientific CCD frames (``*.bin``) to numpy arrays Ignore the header since it seems to contain no useful data The routine was tested only for files with 4096x4096 pixel images created at Hasylab Hamburg which save an 16bit integer per point. """
[docs] def __init__(self, **keyargs): """ initialize the RoperCCD reader, which includes setting the dimension of the images as well as defining the data used for flat- and darkfield correction! Parameters ---------- flatfield : str or ndarray, optional filename or data for flatfield correction. supported file types include (.bin .bin.xz and .npy files). otherwise a 2D numpy array should be given darkfield : str or ndarray, optional filename or data for darkfield correction. same types as for flat field are supported. """ ImageReader.__init__(self, 4096, 4096, hdrlen=216, dtype=numpy.int16, byte_swap=False, **keyargs)
[docs] class Pilatus100K(ImageReader): """ parse Dectris Pilatus 100k frames (``*.tiff``) to numpy arrays Ignore the header since it seems to contain no useful data """
[docs] def __init__(self, **keyargs): """ initialize the Piulatus100k reader, which includes setting the dimension of the images as well as defining the data used for flat- and darkfield correction! Parameters ---------- flatfield : str or ndarray, optional filename or data for flatfield correction. supported file types include (.bin .bin.xz and .npy files). otherwise a 2D numpy array should be given darkfield : str or ndarray, optional filename or data for darkfield correction. same types as for flat field are supported. """ ImageReader.__init__(self, 195, 487, hdrlen=4096, dtype=numpy.int32, byte_swap=False, **keyargs)
[docs] def get_tiff(filename, path=None): """ read tiff image file and return the data Parameters ---------- filename : str filename of the image to be read. so far only single filenames are supported. The data might be compressed. path : str, optional path of the data file """ if config.VERBOSITY >= config.INFO_ALL: print(f"XU.io.get_tiff: file {filename}") t1 = time.time() t = TIFFRead(filename, path=path) if config.VERBOSITY >= config.INFO_ALL: t2 = time.time() print(f"XU.io.get_tiff: parsing time {t2 - t1:8.3f}") return t.data