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.