diff --git a/format/FormatSER.py b/format/FormatSER.py
index 21ae645c5..ed23f6643 100644
--- a/format/FormatSER.py
+++ b/format/FormatSER.py
@@ -1,13 +1,18 @@
"""
Experimental format for TIA .ser files used by some FEI microscopes. See
-http://www.er-c.org/cbb/info/TIAformat/
+https://personal.ntu.edu.sg/cbb/info/TIAformat/index.html
"""
from __future__ import absolute_import, division, print_function
+import os
+import re
import struct
-import sys
-from builtins import range
+import xml.etree.ElementTree as ET
+from math import sqrt
+from pathlib import Path
+
+import numpy as np
from boost_adaptbx.boost.python import streambuf
from scitbx.array_family import flex
@@ -18,14 +23,157 @@
from dxtbx.format.FormatMultiImage import FormatMultiImage
-class FormatSER(FormatMultiImage, Format):
- def __init__(self, image_file, **kwargs):
+# The read_emi and _parseEntry_emi functions are taken from openNCEM project
+# under the terms of the MIT license.
+#
+# Copyright 2019 the Ncempy developers
+#
+# 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.
+def read_emi(filename):
+ """Read the meta data from an emi file.
+ Parameters
+ ----------
+ filename: str or pathlib.Path
+ Path to the emi file.
+ Returns
+ -------
+ : dict
+ Dictionary of experimental metadata stored in the EMI file.
+ """
+
+ # check filename type
+ if isinstance(filename, str):
+ pass
+ elif isinstance(filename, Path):
+ filename = str(filename)
+ else:
+ raise TypeError("Filename is supposed to be a string or pathlib.Path")
+
+ # try opening the file
+ try:
+ # open file for reading bytes, as binary and text are intermixed
+ with open(filename, "rb") as f_emi:
+ emi_data = f_emi.read()
+ except IOError:
+ print('Error reading file: "{}"'.format(filename))
+ raise
+ except Exception:
+ raise
+
+ # dict to store _emi stuff
+ _emi = {}
+
+ # need anything readable from to
+ # collect = False
+ # data = b''
+ # for line in f_emi:
+ # if b'' in line:
+ # collect = True
+ # if collect:
+ # data += line.strip()
+ # if b'' in line:
+ # collect = False
+
+ # close the file
+ # f_emi.close()
+
+ metaStart = emi_data.find(b"")
+ metaEnd = emi_data.find(
+ b""
+ ) # need to add len('') = 13 to encompass this final tag
+
+ root = ET.fromstring(emi_data[metaStart : metaEnd + 13])
+
+ # strip of binary stuff still around
+ # data = data.decode('ascii', errors='ignore')
+ # matchObj = re.search('(.+?)' + data + '')
+
+ # single items
+ _emi["Uuid"] = root.findtext("Uuid")
+ _emi["AcquireDate"] = root.findtext("AcquireDate")
+ _emi["Manufacturer"] = root.findtext("Manufacturer")
+ _emi["DetectorPixelHeight"] = root.findtext("DetectorPixelHeight")
+ _emi["DetectorPixelWidth"] = root.findtext("DetectorPixelWidth")
+
+ # Microscope Conditions
+ grp = root.find("ExperimentalConditions/MicroscopeConditions")
+
+ for elem in grp:
+ _emi[elem.tag] = _parseEntry_emi(elem.text)
+
+ # Experimental Description
+ grp = root.find("ExperimentalDescription/Root")
+
+ for elem in grp:
+ _emi[
+ "{} [{}]".format(elem.findtext("Label"), elem.findtext("Unit"))
+ ] = _parseEntry_emi(elem.findtext("Value"))
+
+ # AcquireInfo
+ grp = root.find("AcquireInfo")
+
+ for elem in grp:
+ _emi[elem.tag] = _parseEntry_emi(elem.text)
+
+ # DetectorRange
+ grp = root.find("DetectorRange")
+
+ for elem in grp:
+ _emi["DetectorRange_" + elem.tag] = _parseEntry_emi(elem.text)
+
+ return _emi
+
+
+def _parseEntry_emi(value):
+ """Auxiliary function to parse string entry to int, float or np.string_().
+ Parameters
+ ----------
+ value : str
+ String containing an int, float or string.
+ Returns
+ -------
+ : int or float or str
+ Entry value as int, float or string.
+ """
+
+ # try to parse as int
+ try:
+ p = int(value)
+ except ValueError:
+ # if not int, then try float
+ try:
+ p = float(value)
+ except ValueError:
+ # if neither int nor float, stay with string
+ p = np.string_(str(value))
- if not self.understand(image_file):
- raise IncorrectFormatError(self, image_file)
- FormatMultiImage.__init__(self, **kwargs)
- Format.__init__(self, image_file, **kwargs)
+ return p
+
+class FormatSER(Format):
@staticmethod
def understand(image_file):
try:
@@ -106,41 +254,95 @@ def _read_metadata(image_file):
hd["ArraySizeX"] = struct.unpack(" 1
+
+ def __init__(self, image_file, **kwargs):
+
+ if not self.understand(image_file):
+ raise IncorrectFormatError(self, image_file)
+ FormatMultiImage.__init__(self, **kwargs)
+ Format.__init__(self, image_file, **kwargs)
+
+ def _scan(self):
+ """Dummy scan for this stack"""
+
+ nframes = self.get_num_images()
+ image_range = (1, nframes)
+ exposure_times = 0.0
+ oscillation = (0, 0.5)
+ epochs = [0] * nframes
+
+ return self._scan_factory.make_scan(
+ image_range, exposure_times, oscillation, epochs, deg=True
+ )
+
+ def get_num_images(self):
+ return self._header_dictionary["ValidNumberElements"]
+
+ def get_goniometer(self, index=None):
+ return Format.get_goniometer(self)
+
+ def get_detector(self, index=None):
+ return Format.get_detector(self)
+
+ def get_beam(self, index=None):
+ return Format.get_beam(self)
+
+ def get_scan(self, index=None):
+ if index is None:
+ return Format.get_scan(self)
+ else:
+ scan = Format.get_scan(self)
+ return scan[index]
+
+ def get_image_file(self, index=None):
+ return Format.get_image_file(self)
+
+ def get_raw_data(self, index):
+ return self._get_raw_data(index)
diff --git a/format/FormatSEReBIC.py b/format/FormatSEReBIC.py
deleted file mode 100644
index 9224116d8..000000000
--- a/format/FormatSEReBIC.py
+++ /dev/null
@@ -1,106 +0,0 @@
-"""Experimental format for TIA .ser files used by FEI microscope at eBIC."""
-
-from __future__ import absolute_import, division, print_function
-
-import sys
-
-from dxtbx import IncorrectFormatError
-from dxtbx.format.FormatSER import FormatSER
-
-
-class FormatSEReBIC(FormatSER):
- @staticmethod
- def understand(image_file):
-
- # check the header looks right
- try:
- fmt = FormatSER(image_file)
- except IncorrectFormatError:
- return False
-
- # Read metadata and check the image dimension is supported
- d = fmt._read_metadata(image_file)
- image_size = d["ArraySizeX"], d["ArraySizeY"]
- if image_size not in [(4096, 4096), (2048, 2048)]:
- return False
-
- return True
-
- def _goniometer(self):
- """Dummy goniometer, 'vertical' as the images are viewed. Not completely
- sure about the handedness yet"""
-
- return self._goniometer_factory.known_axis((0, -1, 0))
-
- def _detector(self):
- """Dummy detector"""
-
- image_size = (
- self._header_dictionary["ArraySizeX"],
- self._header_dictionary["ArraySizeY"],
- )
- if image_size == (4096, 4096):
- pixel_size = 0.014, 0.014
- binning = 1
- if image_size == (2048, 2048):
- pixel_size = 0.028, 0.028
- binning = 2
-
- distance = 2000
- # For Ceta binning=1, complete saturation occurs ~8000 counts. Negative values
- # are common and may require setting a negative pedestal to make most
- # pixels positive.
- trusted_range = (-1000, 8000 * binning ** 2)
- beam_centre = [(p * i) / 2 for p, i in zip(pixel_size, image_size)]
- d = self._detector_factory.simple(
- "PAD",
- distance,
- beam_centre,
- "+x",
- "-y",
- pixel_size,
- image_size,
- trusted_range,
- )
- # The gain of the CetaD is thought to be > 26.0 at 200 keV. That of
- # the standard Ceta is about 7.
- for p in d:
- p.set_gain(26)
- return d
-
- def _beam(self):
- """Dummy unpolarized beam, energy 200 keV"""
-
- wavelength = 0.02508
- return self._beam_factory.make_polarized_beam(
- sample_to_source=(0.0, 0.0, 1.0),
- wavelength=wavelength,
- polarization=(0, 1, 0),
- polarization_fraction=0.5,
- )
-
- def _scan(self):
- """Dummy scan for this stack"""
-
- nframes = self.get_num_images()
- image_range = (1, nframes)
- exposure_times = 0.0
- oscillation = (0, 0.5)
-
- # FIXME we do actually have acquisition times, might as well use them
- epochs = [0] * nframes
-
- return self._scan_factory.make_scan(
- image_range, exposure_times, oscillation, epochs, deg=True
- )
-
- def get_raw_data(self, index):
-
- raw_data = super(FormatSEReBIC, self).get_raw_data(index)
-
- return raw_data
-
-
-if __name__ == "__main__":
- for arg in sys.argv[1:]:
- print(FormatSEReBIC.understand(arg))
diff --git a/newsfragments/345.feature b/newsfragments/345.feature
new file mode 100644
index 000000000..bcc3ff136
--- /dev/null
+++ b/newsfragments/345.feature
@@ -0,0 +1 @@
+Improved support for TIA (Emispec) .ser files.