fabio.fabioimage

Package fabio :: Module fabioimage |
[hide private]
[frames] | no frames]

# Source Code for Module fabio.fabioimage

      1  #!/usr/bin/env python
      2
      3  """
      4
      5  Authors: Henning O. Sorensen & Erik Knudsen
      6           Center for Fundamental Research: Metal Structures in Four Dimensions
      7           Risoe National Laboratory
      8           Frederiksborgvej 399
      9           DK-4000 Roskilde
     10           email:erik.knudsen@risoe.dk
     11
     12           and Jon Wright, Jerome Kieffer: ESRF
     13
     14  """
     15  from __future__ import with_statement
     16  import os, types, logging, sys, tempfile
     17  logger = logging.getLogger("fabioimage")
     18  import numpy
     19  try:
     20      import Image
     21  except ImportError:
     22      logger.warning("PIL is not installed ... trying to do without")
     23      Image = None
     24  import fabioutils, converters
     25
     26
     27 -class fabioimage(object):
     28      """
     29      A common object for images in fable
     30      Contains a numpy array (.data) and dict of meta data (.header)
     31      """
     32
     33      _need_a_seek_to_read = False
     34      _need_a_real_file = False
     35
     36 -    def __init__(self, data=None , header=None):
     37          """
     38          Set up initial values
     39          """
     40          self._classname = None
     41          if type(data) in types.StringTypes:
     42              raise Exception("fabioimage.__init__ bad argument - " + \
     43                              "data should be numpy array")
     44          self.data = self.checkData(data)
     45          self.pilimage = None
     46          if header is None:
     47              self.header = {}
     48          else:
     49              self.header = self.checkHeader(header)
     50          self.header_keys = self.header.keys() # holds key ordering
     51          if self.data is not None:
     52              self.dim2, self.dim1 = self.data.shape
     53          else:
     54              self.dim1 = self.dim2 = 0
     55          self.bytecode = None     # numpy typecode
     56          self.bpp = 2             # bytes per pixel
     57          # cache for image statistics
     58          self.mean = self.maxval = self.stddev = self.minval = None
     59          # Cache roi
     60          self.roi = None
     61          self.area_sum = None
     62          self.slice = None
     63          # New for multiframe files
     64          self.nframes = 1
     65          self.currentframe = 0
     66          self.filename = None
     67          self.filenumber = None
     68
     69      @staticmethod
     70 -    def checkHeader(header=None):
     71          """
     72          Empty for fabioimage but may be populated by others classes
     73          """
     74          if header is None:
     75              return {}
     76          else:
     77              return header
     78
     79      @staticmethod
     80 -    def checkData(data=None):
     81          """
     82          Empty for fabioimage but may be populated by others classes, especially for format accepting only integers
     83          """
     84          return data
     85
     86 -    def getclassname(self):
     87          """
     88          Retrieves the name of the class
     89          @return: the name of the class
     90          """
     91          if self._classname is None:
     92              self._classname = str(self.__class__).replace("<class '", "").replace("'>", "").split(".")[-1]
     93          return self._classname
     94      classname = property(getclassname)
     95
     96 -    def getframe(self, num):
     97          """ returns the file numbered 'num' in the series as a fabioimage """
     98          if self.nframes == 1:
     99              # single image per file
    100              import openimage
    101              return openimage.openimage(
    102                  fabioutils.jump_filename(self.filename, num))
    103          raise Exception("getframe out of range")
    104
    105 -    def previous(self):
    106          """ returns the previous file in the series as a fabioimage """
    107          import openimage
    108          return openimage.openimage(
    109              fabioutils.previous_filename(self.filename))
    110
    111 -    def next(self):
    112          """ returns the next file in the series as a fabioimage """
    113          import openimage
    114          return openimage.openimage(
    115              fabioutils.next_filename(self.filename))
    116
    117 -    def toPIL16(self, filename=None):
    118          """
    119          Convert to Python Imaging Library 16 bit greyscale image
    120
    121          FIXME - this should be handled by the libraries now
    122          """
    123          if not Image:
    124              raise RuntimeError("PIL is not installed !!! ")
    125          if filename:
    126              self.read(filename)
    127          if self.pilimage is not None:
    128              return self.pilimage
    129          # mode map
    130          size = self.data.shape[:2][::-1]
    131          typmap = {
    132              'float32' : "F"     ,
    133              'int32'   : "F;32S" ,
    134              'uint32'  : "F;32"  ,
    135              'int16'   : "F;16S" ,
    136              'uint16'  : "F;16"  ,
    137              'int8'    : "F;8S"  ,
    138              'uint8'   : "F;8"  }
    139          if typmap.has_key(self.data.dtype.name):
    140              mode2 = typmap[ self.data.dtype.name ]
    141              mode1 = mode2[0]
    142          else:
    143              raise Exception("Unknown numpy type " + str(self.data.dtype.type))
    144          #
    145          # hack for byteswapping for PIL in MacOS
    146          testval = numpy.array((1, 0), numpy.uint8).view(numpy.uint16)[0]
    147          if  testval == 1:
    148              dats = self.data.tostring()
    149          elif testval == 256:
    150              dats = self.data.byteswap().tostring()
    151          else:
    152              raise Exception("Endian unknown in fabioimage.toPIL16")
    153
    154          self.pilimage = Image.frombuffer(mode1,
    155                                           size,
    156                                           dats,
    157                                           "raw",
    158                                           mode2,
    159                                           0,
    160                                           1)
    161
    162          return self.pilimage
    163
    164 -    def getheader(self):
    165          """ returns self.header """
    166          return self.header
    167
    168 -    def getmax(self):
    169          """ Find max value in self.data, caching for the future """
    170          if self.maxval is None:
    171              self.maxval = self.data.max()
    172          return self.maxval
    173
    174 -    def getmin(self):
    175          """ Find min value in self.data, caching for the future """
    176          if self.minval is None:
    177              self.minval = self.data.min()
    178          return self.minval
    179
    180 -    def make_slice(self, coords):
    181          """
    182          Convert a len(4) set of coords into a len(2)
    183          tuple (pair) of slice objects
    184          the latter are immutable, meaning the roi can be cached
    185          """
    186          assert len(coords) == 4
    187          if len(coords) == 4:
    188              # fabian edfimage preference
    189              if coords[0] > coords[2]:
    190                  coords[0:3:2] = [coords[2], coords[0]]
    191              if coords[1] > coords[3]:
    192                  coords[1:4:2] = [coords[3], coords[1]]
    193              #in fabian: normally coordinates are given as (x,y) whereas
    194              # a matrix is given as row,col
    195              # also the (for whichever reason) the image is flipped upside
    196              # down wrt to the matrix hence these tranformations
    197              fixme = (self.dim2 - coords[3] - 1,
    198                       coords[0] ,
    199                       self.dim2 - coords[1] - 1,
    200                       coords[2])
    201          return (slice(int(fixme[0]), int(fixme[2]) + 1) ,
    202                   slice(int(fixme[1]), int(fixme[3]) + 1))
    203
    204
    205 -    def integrate_area(self, coords):
    206          """
    207          Sums up a region of interest
    208          if len(coords) == 4 -> convert coords to slices
    209          if len(coords) == 2 -> use as slices
    210          floor -> ? removed as unused in the function.
    211          """
    212          if self.data == None:
    213              # This should return NAN, not zero ?
    214              return 0
    215          if len(coords) == 4:
    216              sli = self.make_slice(coords)
    217          elif len(coords) == 2 and isinstance(coords[0], slice) and \
    218                                    isinstance(coords[1], slice):
    219              sli = coords
    220
    221          if sli == self.slice and self.area_sum is not None:
    222              pass
    223          elif sli == self.slice and self.roi is not None:
    224              self.area_sum = self.roi.sum(dtype=numpy.float)
    225          else:
    226              self.slice = sli
    227              self.roi = self.data[ self.slice ]
    228              self.area_sum = self.roi.sum(dtype=numpy.float)
    229          return self.area_sum
    230
    231 -    def getmean(self):
    232          """ return the mean """
    233          if self.mean is None:
    234              self.mean = self.data.mean(dtype=numpy.double)
    235          return self.mean
    236
    237 -    def getstddev(self):
    238          """ return the standard deviation """
    239          if self.stddev == None:
    240              self.stddev = self.data.std(dtype=numpy.double)
    241          return self.stddev
    242
    243 -    def add(self, other):
    244          """
    245          Add another Image - warning, does not clip to 16 bit images by default
    246          """
    247          if not hasattr(other, 'data'):
    248              logger.warning('edfimage.add() called with something that ' + \
    249                  'does not have a data field')
    250          assert self.data.shape == other.data.shape , \
    251                    'incompatible images - Do they have the same size?'
    252          self.data = self.data + other.data
    253          self.resetvals()
    254
    255
    256 -    def resetvals(self):
    257          """ Reset cache - call on changing data """
    258          self.mean = self.stddev = self.maxval = self.minval = None
    259          self.roi = self.slice = self.area_sum = None
    260
    261 -    def rebin(self, x_rebin_fact, y_rebin_fact, keep_I=True):
    262          """
    263          Rebin the data and adjust dims
    264          @param x_rebin_fact: x binning factor
    265          @param y_rebin_fact: y binning factor
    266          @param keep_I: shall the signal increase ?
    267          @type x_rebin_fact: int
    268          @type y_rebin_fact: int
    269          @type keep_I: boolean
    270
    271
    272          """
    273          if self.data == None:
    274              raise Exception('Please read in the file you wish to rebin first')
    275
    276          if (self.dim1 % x_rebin_fact != 0) or (self.dim2 % y_rebin_fact != 0):
    277              raise RuntimeError('image size is not divisible by rebin factor - ' + \
    278                    'skipping rebin')
    279          else:
    280              dataIn = self.data.astype("float64")
    281              shapeIn = self.data.shape
    282              shapeOut = (shapeIn[0] / y_rebin_fact, shapeIn[1] / x_rebin_fact)
    283              binsize = y_rebin_fact * x_rebin_fact
    284              if binsize < 50: #method faster for small binning (4x4)
    285                  out = numpy.zeros(shapeOut, dtype="float64")
    286                  for j in range(x_rebin_fact):
    287                      for i in range(y_rebin_fact):
    288                          out += dataIn[i::y_rebin_fact, j::x_rebin_fact]
    289              else: #method faster for large binning (8x8)
    290                  temp = self.data.astype("float64")
    291                  temp.shape = (shapeOut[0], y_rebin_fact, shapeOut[1], x_rebin_fact)
    292                  out = temp.sum(axis=3).sum(axis=1)
    293          self.resetvals()
    294          if keep_I:
    295              self.data = (out / (y_rebin_fact * x_rebin_fact)).astype(self.data.dtype)
    296          else:
    297              self.data = out.astype(self.data.dtype)
    298
    299          self.dim1 = self.dim1 / x_rebin_fact
    300          self.dim2 = self.dim2 / y_rebin_fact
    301
    302          #update header
    303          self.update_header()
    304
    305 -    def write(self, fname):
    306          """
    307          To be overwritten - write the file
    308          """
    309          raise Exception("Class has not implemented readheader method yet")
    310
    311 -    def save(self, fname):
    312          'wrapper for write'
    313          self.write(fname)
    314
    315 -    def readheader(self, filename):
    316          """
    317          Call the _readheader function...
    318          """
    319          # Override the needs asserting that all headers can be read via python modules
    320          save_state = self._need_a_real_file , self._need_a_seek_to_read
    321          self._need_a_real_file , self._need_a_seek_to_read = False, False
    322          fin = self._open(filename)
    323          self._readheader(fin)
    324          fin.close()
    325          self._need_a_real_file , self._need_a_seek_to_read = save_state
    326
    327 -    def _readheader(self, fik_obj):
    328          """
    329          Must be overridden in classes
    330          """
    331          raise Exception("Class has not implemented _readheader method yet")
    332
    333 -    def update_header(self , **kwds):
    334          """
    335          update the header entries
    336          by default pass in a dict of key, values.
    337          """
    338          self.header.update(kwds)
    339
    340 -    def read(self, filename, frame=None):
    341          """
    342          To be overridden - fill in self.header and self.data
    343          """
    344          raise Exception("Class has not implemented read method yet")
    345  #        return self
    346
    347 -    def load(self, *arg, **kwarg):
    348          "Wrapper for read"
    349          return self.read(*arg, **kwarg)
    350
    351 -    def readROI(self, filename, frame=None, coords=None):
    352          """
    353          Method reading Region of Interest.
    354          This implementation is the trivial one, just doing read and crop
    355          """
    356          self.read(filename, frame)
    357          if len(coords) == 4:
    358              self.slice = self.make_slice(coords)
    359          elif len(coords) == 2 and isinstance(coords[0], slice) and \
    360                                    isinstance(coords[1], slice):
    361              self.slice = coords
    362          else:
    363              logger.warning('readROI: Unable to understand Region Of Interest: got %s', coords)
    364          self.roi = self.data[ self.slice ]
    365          return self.roi
    366
    367
    368 -    def _open(self, fname, mode="rb"):
    369          """
    370          Try to handle compressed files, streams, shared memory etc
    371          Return an object which can be used for "read" and "write"
    372          ... FIXME - what about seek ?
    373          """
    374          fileObject = None
    375          self.filename = fname
    376          self.filenumber = fabioutils.extract_filenumber(fname)
    377
    378          if hasattr(fname, "read") and hasattr(fname, "write"):
    379              # It is already something we can use
    380              return fname
    381          if isinstance(fname, (str, unicode)):
    382              self.header["filename"] = fname
    383              if os.path.splitext(fname)[1] == ".gz":
    384                  fileObject = self._compressed_stream(fname,
    385                                         fabioutils.COMPRESSORS['.gz'],
    386                                         fabioutils.GzipFile,
    387                                         mode)
    388              elif os.path.splitext(fname)[1] == '.bz2':
    389                  fileObject = self._compressed_stream(fname,
    390                                         fabioutils.COMPRESSORS['.bz2'],
    391                                         fabioutils.BZ2File,
    392                                         mode)
    393              #
    394              # Here we return the file even though it may be bzipped or gzipped
    395              # but named incorrectly...
    396              #
    397              # FIXME - should we fix that or complain about the daft naming?
    398              else:
    399                  fileObject = fabioutils.File(fname, mode)
    400              if "name" not in dir(fileObject):
    401                  fileObject.name = fname
    402
    403          return fileObject
    404
    405 -    def _compressed_stream(self,
    406                             fname,
    407                             system_uncompress,
    408                             python_uncompress,
    409                             mode='rb'):
    410          """
    411          Try to transparently handle gzip / bzip without always getting python
    412          performance
    413          """
    414          # assert that python modules are always OK based on performance benchmark
    415          # Try to fix the way we are using them?
    416          fobj = None
    417          if self._need_a_real_file and mode[0] == "r":
    418              fo = python_uncompress(fname, mode)
    419  #            fobj = os.tmpfile()
    420              #problem when not administrator under certain flavors of windows
    421              tmpfd, tmpfn = tempfile.mkstemp()
    422              os.close(tmpfd)
    423              fobj = fabioutils.File(tmpfn, "w+b")
    424              fobj.write(fo.read())
    425              fo.close()
    426              fobj.seek(0)
    427          elif self._need_a_seek_to_read and mode[0] == "r":
    428              fo = python_uncompress(fname, mode)
    429              fobj = fabioutils.StringIO(fo.read(), fname, mode)
    430          else:
    431              fobj = python_uncompress(fname, mode)
    432          return fobj
    433
    434 -    def convert(self, dest):
    435          """
    436          Convert a fabioimage object into another fabioimage object (with possible conversions)
    437          @param dest: destination type "EDF", "edfimage" or the class itself
    438          """
    439          if type(dest) in types.StringTypes:
    440              dest = dest.lower()
    441              modules = []
    442              for val  in fabioutils.FILETYPES.values():
    443                  modules += [i + "image" for i in val if i not in modules]
    444              klass = None
    445              module = None
    446              klass_name = None
    447              for klass_name in modules:
    448                  if  klass_name.startswith(dest):
    449                      try:
    450                          module = sys.modules["fabio." + klass_name]
    451                      except KeyError:
    452                          try:
    453                              module = __import__(klass_name)
    454                          except:
    455                              logger.error("Failed to import %s", klass_name)
    456                          else:
    457                              logger.debug("imported %simage", klass_name)
    458                      if module is not None:
    459                          break
    460              if module is not None:
    461                  if hasattr(module, klass_name):
    462                      klass = getattr(module, klass_name)
    463                  else:
    464                      logger.error("Module %s has no image class" % module)
    465          elif isinstance(dest, self.__class__):
    466              klass = dest.__class__
    467          elif ("__new__" in dir(dest)) and isinstance(dest(), fabioimage):
    468              klass = dest
    469          else:
    470              logger.warning("Unrecognized destination format: %s " % dest)
    471              return self
    472          if klass is None:
    473              logger.warning("Unrecognized destination format: %s " % dest)
    474              return self
    475          other = klass() #temporary instance (to be overwritten)
    476          other = klass(data=converters.convert_data(self.classname, other.classname, self.data),
    477                      header=converters.convert_header(self.classname, other.classname, self.header))
    478          return other
    479
    480 -def test():
    481      """
    482      check some basic fabioimage functionality
    483      """
    484      import time
    485      start = time.time()
    486
    487      dat = numpy.ones((1024, 1024), numpy.uint16)
    488      dat = (dat * 50000).astype(numpy.uint16)
    489      assert dat.dtype.char == numpy.ones((1), numpy.uint16).dtype.char
    490      hed = {"Title":"50000 everywhere"}
    491      obj = fabioimage(dat, hed)
    492
    493      assert obj.getmax() == 50000
    494      assert obj.getmin() == 50000
    495      assert obj.getmean() == 50000 , obj.getmean()
    496      assert obj.getstddev() == 0.
    497
    498      dat2 = numpy.zeros((1024, 1024), numpy.uint16, savespace=1)
    499      cord = [ 256, 256, 790, 768 ]
    500      slic = obj.make_slice(cord)
    501      dat2[slic] = dat2[slic] + 100
    502
    503      obj = fabioimage(dat2, hed)
    504
    505      # New object, so...
    506      assert obj.maxval is None
    507      assert obj.minval is None
    508
    509      assert obj.getmax() == 100, obj.getmax()
    510      assert obj.getmin() == 0 , obj.getmin()
    511      npix = (slic[0].stop - slic[0].start) * (slic[1].stop - slic[1].start)
    512      obj.resetvals()
    513      area1 = obj.integrate_area(cord)
    514      obj.resetvals()
    515      area2 = obj.integrate_area(slic)
    516      assert area1 == area2
    517      assert obj.integrate_area(cord) == obj.integrate_area(slic)
    518      assert obj.integrate_area(cord) == npix * 100, obj.integrate_area(cord)
    519
    520
    521      def clean():
    522          """ clean up the created testfiles"""
    523          for name in ["testfile", "testfile.gz", "testfile.bz2"]:
    524              try:
    525                  os.remove(name)
    526              except:
    527                  continue
    528
    529
    530      clean()
    531      import gzip, bz2
    532      gzip.open("testfile.gz", "wb").write("{ hello }")
    533      fout = obj._open("testfile.gz")
    534      readin = fout.read()
    535      assert readin == "{ hello }", readin + " gzipped file"
    536
    537
    538      bz2.BZ2File("testfilebz", "wb").write("{ hello }")
    539      fout = obj._open("testfile.bz2")
    540      readin = fout.read()
    541      assert readin == "{ hello }", readin + " bzipped file"
    542
    543      ftest = open("testfile", "wb")
    544      ftest.write("{ hello }")
    545      assert ftest == obj._open(ftest)
    546      ftest.close()
    547      fout = obj._open("testfile")
    548      readin = fout.read()
    549      assert readin == "{ hello }", readin + "plain file"
    550      fout.close()
    551      ftest.close()
    552      clean()
    553
    554      print "Passed in", time.time() - start, "s"
    555
    556  if __name__ == '__main__':
    557      test()
    558