diff --git a/docs/supported_formats/hamamatsu.rst b/docs/supported_formats/hamamatsu.rst new file mode 100644 index 000000000..1d747f958 --- /dev/null +++ b/docs/supported_formats/hamamatsu.rst @@ -0,0 +1,22 @@ +.. _hamamatsu-format: + +Hamamatsu +--------- + +Reader for spectroscopy data saved in ``.img`` (ITEX) files from the HPD-TA +(High Performance Digital Temporal Analyzer) or HiPic (High Performance image control) +softwares from Hamamatsu, e.g. for streak cameras or high performance CCD cameras. + +If `LumiSpy `_ is installed, ``Luminescence`` will be +used as the ``signal_type``. + +.. Note:: + + Reading files containing multiple channels or multiple images per channel + is not implemented. + +API functions +^^^^^^^^^^^^^ + +.. automodule:: rsciio.hamamatsu + :members: \ No newline at end of file diff --git a/docs/supported_formats/index.rst b/docs/supported_formats/index.rst index 2b6b42da2..8d950df91 100644 --- a/docs/supported_formats/index.rst +++ b/docs/supported_formats/index.rst @@ -24,6 +24,7 @@ big datasets is supported. edax emd empad + hamamatsu hspy image jeol diff --git a/docs/supported_formats/supported_formats.rst b/docs/supported_formats/supported_formats.rst index a12d9e3df..7a52f817c 100644 --- a/docs/supported_formats/supported_formats.rst +++ b/docs/supported_formats/supported_formats.rst @@ -30,6 +30,8 @@ +---------------------------------------------------------------------+-------------------------+--------+--------+--------+ | :ref:`FEI TIA ` | emi & ser | Yes | No | Yes | +---------------------------------------------------------------------+-------------------------+--------+--------+--------+ + | :ref:`Hamamatsu ` | img | Yes | No | No | + +---------------------------------------------------------------------+-------------------------+--------+--------+--------+ | :ref:`Horiba Jobin Yvon LabSpec ` | xml | Yes | No | No | +---------------------------------------------------------------------+-------------------------+--------+--------+--------+ | :ref:`HSpy - HyperSpy hdf5 ` | hspy | Yes | Yes | Yes | diff --git a/rsciio/hamamatsu/__init__.py b/rsciio/hamamatsu/__init__.py new file mode 100644 index 000000000..d4de92f67 --- /dev/null +++ b/rsciio/hamamatsu/__init__.py @@ -0,0 +1,10 @@ +from ._api import file_reader + + +__all__ = [ + "file_reader", +] + + +def __dir__(): + return sorted(__all__) diff --git a/rsciio/hamamatsu/_api.py b/rsciio/hamamatsu/_api.py new file mode 100644 index 000000000..82b6164db --- /dev/null +++ b/rsciio/hamamatsu/_api.py @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2023 The HyperSpy developers +# +# This file is part of RosettaSciIO. +# +# RosettaSciIO 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 3 of the License, or +# (at your option) any later version. +# +# RosettaSciIO 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 RosettaSciIO. If not, see . + +import logging +import importlib.util +from pathlib import Path +from copy import deepcopy +from enum import IntEnum, EnumMeta + +import numpy as np +from numpy.polynomial.polynomial import polyfit + +from rsciio._docstrings import FILENAME_DOC, LAZY_DOC, RETURNS_DOC + +_logger = logging.getLogger(__name__) + + +def _str2numeric(input, type): + """Handle None-values when converting strings to float.""" + try: + if type == "float": + return float(input) + elif type == "int": + return int(input) + else: + return None + except (ValueError, TypeError): + return None + + +def _str2bool(input): + if input == "-1": + return True + elif input == "0": + return False + else: + return None + + +def _remove_none_from_dict(dict_in): + """Recursive removal of None-values from a dictionary.""" + for key, value in list(dict_in.items()): + if isinstance(value, dict): + _remove_none_from_dict(value) + elif value is None: + del dict_in[key] + + +## < specifies little endian +TypeNames = { + "int8": " problems with indexing + # this leads to problems with uin64, because there is no int128 in numpy + if type in ["uint8", "uint16", "uint32", "uint64"] and convert: + data = data.astype(np.dtype(" data is read incorrectly + w_px = int(self.__read_numeric("int16")) + header["image_width_px"] = w_px + self._h_lines = int(self.__read_numeric("int16")) + header["image_height_lines"] = self._h_lines + header["offset_x"] = int(self.__read_numeric("int16")) + header["offset_y"] = int(self.__read_numeric("int16")) + file_type = FileType(int(self.__read_numeric("int16"))).name + header["file_type"] = file_type + header["num_images_in_channel"] = int(self.__read_numeric("int32")) + header["num_additional_channels"] = int(self.__read_numeric("int16")) + header["channel_number"] = int(self.__read_numeric("int16")) + header["timestamp"] = self.__read_numeric("double") + header["marker"] = self.__read_utf8(4) + ## according to the manual, additional_info is one byte shorter + ## however, there is also an unexplained 1 byte gap between marker and additional info + ## so this extra byte is absorbed in additional_info + ## in the testfiles both marker and additional_info contain only zeros + header["additional_info"] = self.__read_utf8(30) + comment = self.__read_utf8(com_len) + if file_type == "bit8": + dtype = "uint8" + elif file_type == "bit16": + dtype = "uint16" + elif file_type == "bit32": + dtype = "uint32" + else: + raise RuntimeError(f"reading type: {file_type} not implemented") + data = self.__read_numeric(dtype, size=w_px * self._h_lines) + self.original_metadata.update(header) + return data, comment + + @staticmethod + def _get_scaling_entry(scaling_dict, attr_name): + x_val = scaling_dict.get("ScalingX" + attr_name) + y_val = scaling_dict.get("ScalingY" + attr_name) + if y_val == "us": + y_val = "µs" + return x_val, y_val + + def _extract_calibration_data(self, cal): + if cal[0] == "#": + pos, size = map(int, cal[1:].split(",")) + self._file_obj.seek(pos) + return self.__read_numeric("float", size=size) + else: + raise RuntimeError( + f"Cannot read axis data (invalid start for address {cal})" + ) + + def _set_axis(self, name, scale_type, unit, cal_addr): + axis = {"units": unit, "name": name, "navigate": False} + if scale_type == 1: + ## in this mode (focus mode) the y-axis does not correspond to time + ## photoelectrons are not deflected here -> natural spread + axis["units"] = "px" + axis["scale"] = 1 + axis["offset"] = 0 + axis["size"] = self._h_lines + axis["name"] = "Vertical CCD Position" + elif scale_type == 2: + data = self._extract_calibration_data(cal_addr) + # in testfile wavelength is exactly uniform + # time is close + if name == "Wavelength": + if data[0] > data[1]: + self._reverse_signal = True + data = np.ascontiguousarray(data[::-1]) + else: + self._reverse_signal = False + if self._use_uniform_signal_axes: + offset, scale = polyfit(np.arange(data.size), data, deg=1) + axis["offset"] = offset + axis["scale"] = scale + axis["size"] = data.size + scale_compare = 100 * np.max(np.abs(np.diff(data) - scale) / scale) + if scale_compare > 1: + _logger.warning( + f"The relative variation of the signal-axis-scale ({scale_compare:.2f}%) exceeds 1%.\n" + " " + "Using a non-uniform-axis is recommended." + ) + else: + axis["axis"] = data + else: + raise ValueError( + f"Cannot extract {name}-axis information (invalid scale-type)." + ) + return axis + + def _get_axes(self): + scaling_md = self.original_metadata.get("Comment", {}).get("Scaling", {}) + x_cal_address, y_cal_address = self._get_scaling_entry( + scaling_md, "ScalingFile" + ) + x_unit, y_unit = self._get_scaling_entry(scaling_md, "Unit") + x_type, y_type = map(int, self._get_scaling_entry(scaling_md, "Type")) + x_axis = self._set_axis("Wavelength", x_type, x_unit, x_cal_address) + y_axis = self._set_axis("Time", y_type, y_unit, y_cal_address) + y_axis["index_in_array"] = 0 + x_axis["index_in_array"] = 1 + axes_list = sorted([x_axis, y_axis], key=lambda item: item["index_in_array"]) + return axes_list + + def _reshape_data(self): + axes_sizes = [] + for ax in self.axes: + try: + axes_sizes.append(ax["axis"].size) + except KeyError: + axes_sizes.append(ax["size"]) + + self.data = np.reshape(self.data, axes_sizes) + if self._reverse_signal: + self.data = np.ascontiguousarray(self.data[:, ::-1]) + + @staticmethod + def _split_sections_from_comment(input): + initial_split = input[1:].split("[") # ignore opening bracket at start + result = {} + for entry in initial_split: + sep_idx = entry.index("]") + header = entry[:sep_idx] + body = entry[sep_idx + 2 :].rstrip() + result[header] = body + return result + + @staticmethod + def _get_range_for_val(v, sep, count, num_entries, str_len): + if v[sep + 1] == '"': + end_val = v.index('"', sep + 2) + start_val = sep + 2 + total_end = end_val + 2 + else: + if count == num_entries: + end_val = str_len + else: + end_val = v.index(",", sep) + start_val = sep + 1 + total_end = end_val + 1 + return start_val, end_val, total_end + + def _extract_entries_from_section(self, entries_str): + result = {} + str_len = len(entries_str) + cur_pos = 0 + counter = 0 + num_entries = entries_str.count("=") + if num_entries == 0: + return entries_str + while cur_pos < str_len: + counter += 1 + sep_idx = entries_str.index("=", cur_pos) + key = entries_str[cur_pos:sep_idx] + start_val, end_val, cur_pos = self._get_range_for_val( + entries_str, sep_idx, counter, num_entries, str_len + ) + val = entries_str[start_val:end_val] + result[key] = val + return result + + def _process_comment(self, comment): + section_split = self._split_sections_from_comment(comment) + result = {} + for k, v in section_split.items(): + result[k] = self._extract_entries_from_section(v) + return result + + def _map_general_md(self): + general = {} + general["title"] = self._original_filename.split(".")[0] + general["original_filename"] = self._original_filename + try: + date = self.original_metadata["Comment"]["Application"]["Date"] + time = self.original_metadata["Comment"]["Application"]["Time"] + except KeyError: # pragma: no cover + pass # pragma: no cover + else: + delimiters = ["/", "."] + for d in delimiters: + date_split = date.split(d) + if len(date_split) == 3: + general["date"] = ( + date_split[2] + "-" + date_split[1] + "-" + date_split[0] + ) + break + else: + _logger.warning("Unknown date format, cannot transfrom to ISO.") + general["date"] = date + general["time"] = time.split(".")[0] + return general + + def _map_signal_md(self): + signal = {} + + if importlib.util.find_spec("lumispy") is None: + _logger.warning( + "Cannot find package lumispy, using BaseSignal as signal_type." + ) + signal["signal_type"] = "" + else: + signal["signal_type"] = "Luminescence" # pragma: no cover + + try: + quantity = self.original_metadata["Comment"]["Acquisition"]["ZAxisLabel"] + quantity_unit = self.original_metadata["Comment"]["Acquisition"][ + "ZAxisUnit" + ] + except KeyError: # pragma: no cover + pass # pragma: no cover + else: + if quantity_unit == "Count": + quantity_unit = "Counts" + signal["quantity"] = f"{quantity} ({quantity_unit})" + return signal + + def _map_detector_md(self): + detector = {} + acq_dict = self.original_metadata.get("Comment", {}).get("Acquisition", {}) + streak_dict = self.original_metadata.get("Comment", {}).get("Streak camera", {}) + + detector["frames"] = _str2numeric(acq_dict.get("NrExposure"), "int") + try: + exp_time_str = acq_dict["ExposureTime"] + except KeyError: + pass + else: + exp_time_split = exp_time_str.split(" ") + if len(exp_time_split) == 2: + exp_time, exp_time_units = exp_time_split + exp_time = _str2numeric(exp_time, "float") + if exp_time_units == "s": + pass + elif exp_time_units == "ms": + exp_time /= 1000 + else: + _logger.warning( + f"integration_time is given in {exp_time_units} instead of seconds." + ) + detector["integration_time"] = exp_time * detector["frames"] + else: + _logger.warning("integration_time could not be extracted") + + try: + binning_str = acq_dict["pntBinning"] + except KeyError: + pass + else: + if len(binning_str.split(",")) == 2: + detector["binning"] = tuple(map(int, binning_str.split(","))) + + detector["processing"] = { + "shading_correction": _str2bool(acq_dict.get("ShadingCorr")), + "background_correction": _str2bool(acq_dict.get("BacksubCorr")), + "curvature_correction": _str2bool(acq_dict.get("CurveCorr")), + "defect_correction": _str2bool(acq_dict.get("DefectCorrection")), + } + detector["detector_type"] = "StreakCamera" + detector["model"] = streak_dict.get("DeviceName") + + detector["mcp_gain"] = _str2numeric(streak_dict.get("MCP Gain"), "float") + try: + time_range_str = streak_dict["Time Range"] + except KeyError: + pass + else: + time_range_split = time_range_str.split(" ") + if len(time_range_split) == 2: + time_range, time_range_units = time_range_split + time_range = _str2numeric(time_range, "float") + if time_range_units == "us": + time_range_units = "µs" + detector["time_range"] = time_range + detector["time_range_units"] = time_range_units + else: + # TODO: add warning? only occurs for shading file + time_range = _str2numeric(time_range_str, "float") + detector["time_range"] = time_range + detector["acquisition_mode"] = AcqMode(int(acq_dict.get("AcqMode"))).name + return detector + + def _map_spectrometer_md(self): + spectrometer = {} + spectro_dict = self.original_metadata.get("Comment", {}).get("Spectrograph", {}) + try: + groove_density_str = spectro_dict["Grating"] + except KeyError: + groove_density = None + else: + groove_density_split = groove_density_str.split(" ") + if len(groove_density_split) == 2: + groove_density, groove_density_units = groove_density_str.split(" ") + groove_density = _str2numeric(groove_density, "int") + if groove_density_units != "g/mm": + _logger.warning( + f"groove_density is given in {groove_density_units}" + ) + else: + groove_density = groove_density_str + ## Remove grating when no unit (->1 or 2, but not lines per mm) + ## Same for blaze + ## warning for these cases? + if spectro_dict.get("Ruling") != "0" and spectro_dict.get("Blaze") != 0: + spectrometer["Grating"] = { + "blazing_wavelength": _str2numeric(spectro_dict.get("Blaze"), "float"), + "groove_density": _str2numeric(groove_density, "float"), + } + spectrometer["model"] = spectro_dict.get("DeviceName") + spectrometer["entrance_slit_width"] = _str2numeric( + spectro_dict.get("Side Ent. Slitw."), "float" + ) ## TODO: units?, side entry iris? + spectrometer["central_wavelength"] = _str2numeric( + spectro_dict.get("Wavelength"), "float" + ) + return spectrometer + + def map_metadata(self): + """Maps original_metadata to metadata.""" + general = self._map_general_md() + signal = self._map_signal_md() + detector = self._map_detector_md() + spectrometer = self._map_spectrometer_md() + + acquisition_instrument = { + "Detector": detector, + "Spectrometer": spectrometer, + } + + metadata = { + "Acquisition_instrument": acquisition_instrument, + "General": general, + "Signal": signal, + } + _remove_none_from_dict(metadata) + return metadata + + +def file_reader(filename, lazy=False, use_uniform_signal_axes=False, **kwds): + """Reads Hamamatsu's ``.img`` file. + + Parameters + ---------- + %s + %s + use_uniform_signal_axis : bool, default=False + Can be specified to choose between non-uniform or uniform signal axis. + If ``True``, the ``scale`` attribute is calculated from the average delta + along the signal axis and a warning is raised in case the delta varies + by more than 1 percent. + + %s + """ + filesize = Path(filename).stat().st_size + original_filename = Path(filename).name + result = {} + with open(str(filename), "rb") as f: + img = IMGReader( + f, + filesize=filesize, + filename=original_filename, + use_uniform_signal_axes=use_uniform_signal_axes, + ) + + result["data"] = img.data + result["axes"] = img.axes + result["metadata"] = deepcopy(img.metadata) + result["original_metadata"] = deepcopy(img.original_metadata) + + return [ + result, + ] + + +file_reader.__doc__ %= (FILENAME_DOC, LAZY_DOC, RETURNS_DOC) diff --git a/rsciio/hamamatsu/specifications.yaml b/rsciio/hamamatsu/specifications.yaml new file mode 100644 index 000000000..e17f8d030 --- /dev/null +++ b/rsciio/hamamatsu/specifications.yaml @@ -0,0 +1,8 @@ +name: 'Hamamatsu' +name_aliases: [] +description: "Read data from Hamamatsu's .img (ITEX) files." +full_support: False +file_extensions: ['img', 'IMG'] +default_extension: 0 +writes: False +non_uniform_axis: True \ No newline at end of file diff --git a/rsciio/tests/data/hamamatsu/focus_mode.img b/rsciio/tests/data/hamamatsu/focus_mode.img new file mode 100644 index 000000000..2dc33a265 Binary files /dev/null and b/rsciio/tests/data/hamamatsu/focus_mode.img differ diff --git a/rsciio/tests/data/hamamatsu/operate_mode.img b/rsciio/tests/data/hamamatsu/operate_mode.img new file mode 100644 index 000000000..bbeaf75f2 Binary files /dev/null and b/rsciio/tests/data/hamamatsu/operate_mode.img differ diff --git a/rsciio/tests/data/hamamatsu/photon_counting.img b/rsciio/tests/data/hamamatsu/photon_counting.img new file mode 100644 index 000000000..033a3ec24 Binary files /dev/null and b/rsciio/tests/data/hamamatsu/photon_counting.img differ diff --git a/rsciio/tests/data/hamamatsu/shading_file.img b/rsciio/tests/data/hamamatsu/shading_file.img new file mode 100644 index 000000000..21dad3a18 Binary files /dev/null and b/rsciio/tests/data/hamamatsu/shading_file.img differ diff --git a/rsciio/tests/registry.txt b/rsciio/tests/registry.txt index ff1e88c2b..9fd57ac82 100644 --- a/rsciio/tests/registry.txt +++ b/rsciio/tests/registry.txt @@ -160,6 +160,10 @@ 'empad/map128x128_version1.2.0.xml' b1cd0dfedc348c9e03ac10e32e3b98a0a0502f87e72069423e7d5f78d40ccae5 'empad/map4x4.xml' ff1a1a6488c7e525c1386f04d791bf77425e27b334c4301a9e6ec85c7628cbeb 'empad/stack_images.xml' 7047717786c3773735ff751a3ca797325d7944fe7b70f110cdda55d455a38a55 +'hamamatsu/focus_mode.img' 9c6994e078e8daf941a6a46a0061a543405887e9f61f62618b01b0f754ce5c93 +'hamamatsu/operate_mode.img' 4833dd8e13dc908bf4a716708372ca51f2083a64f874cca0443fad4c03b289c3 +'hamamatsu/photon_counting.img' 899043b308fe736797060fea471acb733e7e23e1bc2367c1bed06c327d5a92ce +'hamamatsu/shading_file.img' d075910ca724cd9c999c4d5391b61bfde20fd0ee5348e3836b1c5b1239324353 'hspy/example1_v1.0.hdf5' 54ddd193e336b15909d33fcafdfec617f20e1d285a69d407beece83faa2df6fd 'hspy/example1_v1.1.hdf5' 9dca0b4ffd196ca823d10770a2867c08c824ec8f36adfd77ebae6d1d61d5b203 'hspy/example1_v1.2.hdf5' 03296024288ad12f04857abe4de3effa9420ef5609e606559f168e6e99354c5b diff --git a/rsciio/tests/test_hamamatsu.py b/rsciio/tests/test_hamamatsu.py new file mode 100644 index 000000000..666b88697 --- /dev/null +++ b/rsciio/tests/test_hamamatsu.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2023 The HyperSpy developers +# +# This file is part of RosettaSciIO. +# +# RosettaSciIO 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 3 of the License, or +# (at your option) any later version. +# +# RosettaSciIO 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 RosettaSciIO. If not, see . + +import gc +import numpy as np +import pytest +from pathlib import Path + +hs = pytest.importorskip("hyperspy.api", reason="hyperspy not installed") + +testfile_dir = (Path(__file__).parent / "data" / "hamamatsu").resolve() + +testfile_focus_mode_path = (testfile_dir / "focus_mode.img").resolve() +testfile_operate_mode_path = (testfile_dir / "operate_mode.img").resolve() +testfile_photon_count_path = (testfile_dir / "photon_counting.img").resolve() +testfile_shading_path = (testfile_dir / "shading_file.img").resolve() + + +class TestOperate: + @classmethod + def setup_class(cls): + cls.s = hs.load(testfile_operate_mode_path, reader="Hamamatsu") + + @classmethod + def teardown_class(cls): + del cls.s + gc.collect() + + def test_data_deflection(self): + expected_data_start = [ + [0, 0, 715], + [246, 161, 475], + [106, 899, 0], + ] + np.testing.assert_allclose(expected_data_start, self.s.isig[:3, :3].data) + + def test_axes_deflection(self): + axes = self.s.axes_manager + assert axes.signal_dimension == 2 + assert axes.navigation_dimension == 0 + ax0 = axes[0] + ax1 = axes[1] + assert ax0.name == "Wavelength" + assert ax1.name == "Time" + assert ax0.size == 672 + assert ax1.size == 512 + assert ax0.units == "nm" + assert ax1.units == "µs" + + expected_data_start_X = [472.252, 472.33337, 472.41473, 472.4961, 472.57745] + expected_data_start_Y = [0.0, 0.031080816, 0.062164314, 0.09325048, 0.12433932] + np.testing.assert_allclose(ax0.axis[:5], expected_data_start_X) + np.testing.assert_allclose(ax1.axis[:5], expected_data_start_Y) + + def test_original_metadata_no_comment(self): + original_metadata_no_comment = { + k: v + for (k, v) in self.s.original_metadata.as_dictionary().items() + if k != "Comment" + } + expected_metadata = { + "character_im": "IM", + "offset_x": 0, + "offset_y": 0, + "file_type": "bit32", + "num_images_in_channel": 0, + "num_additional_channels": 0, + "channel_number": 0, + "timestamp": 0.0, + "marker": "", + "additional_info": "", + "image_width_px": 672, + "image_height_lines": 512, + "comment_length": 3311, + } + assert expected_metadata == original_metadata_no_comment + + def test_original_metadata_comment(self): + original_metadata = self.s.original_metadata.Comment.as_dictionary() + expected_metadata = { + "Application": { + "Enconding": "UTF-8", + "Date": "20/01/2023", + "Time": "11:34:55.147", + "Software": "HPD-TA", + "Application": "2", + "ApplicationTitle": "High Performance Digital Temporal Analyzer", + "SoftwareVersion": "9.5 pf10", + "SoftwareDate": "15.12.2020", + }, + "Camera": { + "CameraName": "Orca R2", + "SerialNumber": "S/N: 891892", + "Type": "37", + "ScanMode": "1", + "Subarray": "0", + "Binning": "2", + "BitsPerChannel": "0", + "HighDynamicRangeMode": "0", + "ScanSpeed": "0", + "SubarrayHpos": "0", + "SubarrayHsize": "0", + "SubarrayVpos": "0", + "SubarrayVsize": "0", + "TriggerMode": "3", + "TriggerModeKeyVal": "Internal", + "TriggerPolarity": "1", + "TriggerPolarityKeyVal": "neg.", + "CoolerSwitch": "-1", + "Gain": "0", + "Prop_SensorMode": "1", + "Prop_Colortype": "1", + "Prop_TriggerTimes": "1", + "Prop_ExposureTimeControl": "2", + "Prop_TimingMinTriggerInterval": "0.500139", + "Prop_TimingExposure": "2", + "Prop_ImageTopOffsetBytes": "0", + "Prop_ImagePixelType": "2", + "Prop_BufferRowbytes": "1344", + "Prop_BufferFramebytes": "688128", + "Prop_BufferTopOffsetBytes": "0", + "Prop_BufferPixelType": "2", + "Prop_RecordFixedBytesPerFile": "256", + "Prop_RecordFixedBytesPerSession": "784", + "Prop_RecordFixedBytesPerFrame": "688176", + "Prop_NumberOfOutputTriggerConnector": "1", + "Prop_OutputTriggerPolarity": "1", + "Prop_OutputTriggerActive": "1", + "Prop_OutputTriggerDelay": "0", + "Prop_OutputTriggerPeriod": "0.0001", + "Prop_SystemAlive": "2", + "Prop_ImageDetectorPixelWidth": "6.45", + "Prop_ImageDetectorPixelHeight": "6.45", + "Prop_TimeStampProducer": "2", + "Prop_FrameStampProducer": "2", + }, + "Acquisition": { + "NrExposure": "60", + "NrTrigger": "0", + "ExposureTime": "5 s", + "AcqMode": "4", + "DataType": "8", + "DataTypeOfSingleImage": "7", + "CurveCorr": "0", + "DefectCorrection": "0", + "areSource": "0,0,672,512", + "areGRBScan": "0,0,672,512", + "pntOrigCh": "0,0", + "pntOrigFB": "0,0", + "pntBinning": "2,2", + "BytesPerPixel": "4", + "IsLineData": "0", + "BacksubCorr": "-1", + "ShadingCorr": "0", + "ZAxisLabel": "Intensity", + "ZAxisUnit": "Count", + "miMirrorRotate": "0", + }, + "Grabber": {"Type": "5", "SubType": "0"}, + "DisplayLUT": { + "EntrySize": "9", + "LowerValue": "764", + "UpperValue": "3562", + "LowerValueEx": "0", + "UpperValueEx": "32767", + "BitRange": "16x bit", + "Color": "2", + "LUTType": "0", + "LUTInverted": "0", + "AutoLutInLiveMode": "0", + "DisplayNegative": "0", + "Gamma": "1", + "First812OvlCol": "1", + "Lut16xShift": "7", + "Lut16xOvlVal": "3932100", + }, + "ExternalDevices": { + "TriggerDelay": "150", + "PostTriggerTime": "10", + "ExposureTime": "10", + "TDStatusCableConnected": "0", + "ConnectMonitorOut": "0", + "ConnectResetIn": "0", + "TriggerMethod": "2", + "UseDTBE": "0", + "ExpTimeAddMultiple": "-1", + "DontSendReset": "0", + "MultipleOfSweep": "1", + "A6538Connected": "0", + "CounterBoardInstalled": "0", + "MotorizedSlitInstalled": "0", + "UseSpecAsMono": "0", + "GPIBInstalled": "-1", + "CounterBoardIOBase": "0", + "MotorizedSlitPortID": "1", + "GPIBIOBase": "0", + }, + "Streak camera": { + "UseDevice": "-1", + "DeviceName": "C5680", + "PluginName": "M5677", + "GPIBCableConnected": "-1", + "GPIBBase": "8", + "Time Range": "20 us", + "Mode": "Operate", + "Gate Mode": "Normal", + "MCP Gain": "50", + "Shutter": "Open", + "Gate Time": "0", + "Trig. Mode": "Cont", + "Trigger status": "Ready", + "Trig. level": "1", + "Trig. slope": "Rising", + "FocusTimeOver": "11", + }, + "Spectrograph": { + "UseDevice": "-1", + "DeviceName": "Andor SG", + "PluginName": "Kymera 328i", + "GPIBCableConnected": "0", + "GPIBBase": "13", + "Wavelength": "500", + "Grating": "300 g/mm", + "Blaze": "500", + "Ruling": "300", + "Exit Mirror": "Front", + "Side Ent. Slitw.": "10", + "Turret": "1", + "Focus Mirror": "234", + "Side Entry Iris": "26", + }, + "Delay box": { + "UseDevice": "-1", + "DeviceName": "C4792-01", + "PluginName": "", + "GPIBCableConnected": "0", + "Trig. Mode": "Int. Trig.", + "Repetition Rate": "1 Hz", + "L-Pulsewidth": "300 ns", + "Delay Int.Trig": "-999999999900", + "Delay Ext.Trig": "0", + "Dly Mode-Lock": "0", + "Dly1 DmpMode": "0", + "Dly2 DmpMode": "0", + }, + "Delay2 box": {"UseDevice": "0"}, + "Filter wheel": {"UseDevice": "0"}, + "Scaling": { + "ScalingXType": "2", + "ScalingXScale": "1", + "ScalingXUnit": "nm", + "ScalingXScalingFile": "#1379631,0672", + "ScalingYType": "2", + "ScalingYScale": "1", + "ScalingYUnit": "us", + "ScalingYScalingFile": "#1382319,0512", + }, + "Comment": {"UserComment": ""}, + } + + assert expected_metadata == original_metadata + + def test_metadata(self): + metadata = self.s.metadata + + detector = self.s.metadata.Acquisition_instrument.Detector + spectrometer = self.s.metadata.Acquisition_instrument.Spectrometer + + assert metadata.General.date == "2023-01-20" + assert metadata.General.time == "11:34:55" + assert metadata.General.original_filename == "operate_mode.img" + assert metadata.General.title == metadata.General.original_filename[:-4] + + assert metadata.Signal.quantity == "Intensity (Counts)" + assert metadata.Signal.signal_type == "" + + assert isinstance(detector.binning, tuple) + assert len(detector.binning) == 2 + assert detector.binning[0] == 2 + assert detector.binning[1] == 2 + assert detector.detector_type == "StreakCamera" + assert detector.model == "C5680" + assert detector.frames == 60 + np.testing.assert_allclose(detector.integration_time, 300) + assert detector.processing.background_correction == True + assert detector.processing.curvature_correction == False + assert detector.processing.defect_correction == False + assert detector.processing.shading_correction == False + np.testing.assert_allclose(detector.time_range, 20) + assert detector.time_range_units == "µs" + np.testing.assert_allclose(detector.mcp_gain, 50) + assert detector.acquisition_mode == "analog_integration" + + np.testing.assert_allclose(spectrometer.entrance_slit_width, 10) + assert spectrometer.model == "Andor SG" + assert spectrometer.Grating.blazing_wavelength == 500 + assert spectrometer.Grating.groove_density == 300 + np.testing.assert_allclose(spectrometer.central_wavelength, 500) + + +class TestFocus: + @classmethod + def setup_class(cls): + cls.s_focus = hs.load(testfile_focus_mode_path, reader="Hamamatsu") + + @classmethod + def teardown_class(cls): + del cls.s_focus + gc.collect() + + def test_data_focus(self): + expected_data_end = [[0, 0, 36], [0, 0, 0], [21, 0, 0]] + np.testing.assert_allclose(expected_data_end, self.s_focus.isig[-3:, -3:].data) + + def test_axes_focus(self): + axes = self.s_focus.axes_manager + assert axes.signal_dimension == 2 + assert axes.navigation_dimension == 0 + ax0 = axes[0] + ax1 = axes[1] + assert ax0.name == "Wavelength" + assert ax1.name == "Vertical CCD Position" + assert ax1.units == "px" + assert ax0.units == "nm" + assert ax0.size == 672 + assert ax1.size == 512 + + np.testing.assert_allclose(ax1.scale, 1) + np.testing.assert_allclose(ax1.offset, 0) + + expected_data_start_X = [472.252, 472.33337, 472.41473, 472.4961, 472.57745] + np.testing.assert_allclose(ax0.axis[:5], expected_data_start_X) + + +class TestPhotonCount: + @classmethod + def setup_class(cls): + cls.s = hs.load(testfile_photon_count_path, reader="Hamamatsu") + + @classmethod + def teardown_class(cls): + del cls.s + gc.collect() + + def test_data(self): + expected_data = [0, 0, 0] + np.testing.assert_allclose(self.s.isig[-3:, 0].data, expected_data) + + def test_metadata(self): + metadata = self.s.metadata + assert metadata.General.date == "2018-08-29" + assert ( + metadata.Acquisition_instrument.Detector.acquisition_mode + == "photon_counting" + ) + assert "Grating" not in list( + self.s.metadata.Acquisition_instrument.Spectrometer.as_dictionary() + ) + assert self.s.original_metadata.file_type == "bit16" + + +class TestShading: + @classmethod + def setup_class(cls): + cls.s = hs.load(testfile_shading_path, reader="Hamamatsu") + + @classmethod + def teardown_class(cls): + del cls.s + gc.collect() + + def test_metadata(self): + np.testing.assert_allclose( + self.s.metadata.Acquisition_instrument.Detector.time_range, 4 + ) + + def test_data(self): + expected_data = [9385, 8354, 7658] + np.testing.assert_allclose(self.s.isig[:3, 0].data, expected_data) diff --git a/rsciio/tests/test_jeol.py b/rsciio/tests/test_jeol.py index d9628686e..1db355a9c 100644 --- a/rsciio/tests/test_jeol.py +++ b/rsciio/tests/test_jeol.py @@ -82,7 +82,7 @@ def teardown_module(module): def test_load_project(): # test load all elements of the project rawdata.ASW filename = TESTS_FILE_PATH / TEST_FILES[0] - s = hs.load(filename) + s = hs.load(filename, reader="JEOL") # first file is always a 16bit image of the work area assert s[0].data.dtype == np.uint8 assert s[0].data.shape == (512, 512) @@ -118,12 +118,12 @@ def test_load_project(): # check scale (image) filename = TESTS_FILE_PATH / "Sample" / "00_View000" / TEST_FILES[1] - s1 = hs.load(filename) + s1 = hs.load(filename, reader="JEOL") np.testing.assert_allclose(s[0].axes_manager[0].scale, s1.axes_manager[0].scale) assert s[0].axes_manager[0].units == s1.axes_manager[0].units # check scale (pts) filename = TESTS_FILE_PATH / "Sample" / "00_View000" / TEST_FILES[7] - s2 = hs.load(filename) + s2 = hs.load(filename, reader="JEOL") np.testing.assert_allclose(s[6].axes_manager[0].scale, s2.axes_manager[0].scale) assert s[6].axes_manager[0].units == s2.axes_manager[0].units @@ -131,7 +131,7 @@ def test_load_project(): def test_load_image(): # test load work area haadf image filename = TESTS_FILE_PATH / "Sample" / "00_View000" / TEST_FILES[1] - s = hs.load(filename) + s = hs.load(filename, reader="JEOL") assert s.data.dtype == np.uint8 assert s.data.shape == (512, 512) assert s.axes_manager.signal_dimension == 2 @@ -147,7 +147,7 @@ def test_load_image(): def test_load_datacube(SI_dtype): # test load eds datacube filename = TESTS_FILE_PATH / "Sample" / "00_View000" / TEST_FILES[7] - s = hs.load(filename, SI_dtype=SI_dtype, cutoff_at_kV=5) + s = hs.load(filename, SI_dtype=SI_dtype, cutoff_at_kV=5, reader="JEOL") assert s.data.dtype == SI_dtype assert s.data.shape == (512, 512, 596) assert s.axes_manager.signal_dimension == 1 @@ -166,28 +166,28 @@ def test_load_datacube(SI_dtype): def test_load_datacube_rebin_energy(): filename = TESTS_FILE_PATH / "Sample" / "00_View000" / TEST_FILES[7] - s = hs.load(filename, cutoff_at_kV=0.1) + s = hs.load(filename, cutoff_at_kV=0.1, reader="JEOL") s_sum = s.sum() ref_data = hs.signals.Signal1D(np.array([3, 23, 77, 200, 487, 984, 1599, 2391])) np.testing.assert_allclose(s_sum.data[88:96], ref_data.data) rebin_energy = 8 - s2 = hs.load(filename, rebin_energy=rebin_energy) + s2 = hs.load(filename, rebin_energy=rebin_energy, reader="JEOL") s2_sum = s2.sum() np.testing.assert_allclose(s2_sum.data[11:12], ref_data.data.sum()) with pytest.raises(ValueError, match="must be a divisor"): - _ = hs.load(filename, rebin_energy=10) + _ = hs.load(filename, rebin_energy=10, reader="JEOL") def test_load_datacube_cutoff_at_kV(): gc.collect() cutoff_at_kV = 10.0 filename = TESTS_FILE_PATH / "Sample" / "00_View000" / TEST_FILES[7] - s = hs.load(filename, cutoff_at_kV=None) - s2 = hs.load(filename, cutoff_at_kV=cutoff_at_kV) + s = hs.load(filename, cutoff_at_kV=None, reader="JEOL") + s2 = hs.load(filename, cutoff_at_kV=cutoff_at_kV, reader="JEOL") assert s2.axes_manager[-1].size == 1096 np.testing.assert_allclose(s2.axes_manager[2].scale, 0.00999866) @@ -199,8 +199,8 @@ def test_load_datacube_cutoff_at_kV(): def test_load_datacube_downsample(): downsample = 8 filename = TESTS_FILE_PATH / TEST_FILES[0] - s = hs.load(filename, downsample=1)[-1] - s2 = hs.load(filename, downsample=downsample)[-1] + s = hs.load(filename, downsample=1, reader="JEOL")[-1] + s2 = hs.load(filename, downsample=downsample, reader="JEOL")[-1] s_sum = s.sum(-1).rebin(scale=(downsample, downsample)) s2_sum = s2.sum(-1) @@ -217,32 +217,34 @@ def test_load_datacube_downsample(): np.testing.assert_allclose(s_sum.data, s2_sum.data) with pytest.raises(ValueError, match="must be a divisor"): - _ = hs.load(filename, downsample=10)[-1] + _ = hs.load(filename, downsample=10, reader="JEOL")[-1] with pytest.raises( ValueError, match="`downsample` can't be an iterable of length different from 2.", ): - _ = hs.load(filename, downsample=[2, 2, 2])[-1] + _ = hs.load(filename, downsample=[2, 2, 2], reader="JEOL")[-1] downsample = [8, 16] - s = hs.load(filename, downsample=downsample)[-1] + s = hs.load(filename, downsample=downsample, reader="JEOL")[-1] assert s.axes_manager["x"].size * downsample[0] == 512 assert s.axes_manager["y"].size * downsample[1] == 512 with pytest.raises(ValueError, match="must be a divisor"): - _ = hs.load(filename, downsample=[256, 100])[-1] + _ = hs.load(filename, downsample=[256, 100], reader="JEOL")[-1] with pytest.raises(ValueError, match="must be a divisor"): - _ = hs.load(filename, downsample=[100, 256])[-1] + _ = hs.load(filename, downsample=[100, 256], reader="JEOL")[-1] def test_load_datacube_frames(): rebin_energy = 2048 filename = TESTS_FILE_PATH / "Sample" / "00_View000" / TEST_FILES[7] - s = hs.load(filename, sum_frames=True, rebin_energy=rebin_energy) + s = hs.load(filename, sum_frames=True, rebin_energy=rebin_energy, reader="JEOL") assert s.data.shape == (512, 512, 2) - s_frame = hs.load(filename, sum_frames=False, rebin_energy=rebin_energy) + s_frame = hs.load( + filename, sum_frames=False, rebin_energy=rebin_energy, reader="JEOL" + ) assert s_frame.data.shape == (14, 512, 512, 2) np.testing.assert_allclose(s_frame.sum(axis="Frame").data, s.data) np.testing.assert_allclose( @@ -273,7 +275,7 @@ def test_load_eds_file(filename_as_string): filename = TESTS_FILE_PATH / "met03.EDS" if filename_as_string: filename = str(filename) - s = hs.load(filename) + s = hs.load(filename, reader="JEOL") assert isinstance(s, hs.signals.EDSTEMSpectrum) assert s.data.shape == (2048,) axis = s.axes_manager[0] @@ -322,7 +324,7 @@ def test_shift_jis_encoding(): with open(filename, "br"): pass try: - _ = hs.load(filename) + _ = hs.load(filename, reader="JEOL") except FileNotFoundError: # we don't have the other files required to open the data pass @@ -351,6 +353,7 @@ def test_number_of_frames(): downsample=[32, 32], rebin_energy=512, SI_dtype=np.int32, + reader="JEOL", ) assert data.axes_manager["Frame"].size == frames @@ -362,6 +365,7 @@ def test_number_of_frames(): downsample=[32, 32], rebin_energy=512, SI_dtype=np.int32, + reader="JEOL", ) assert data.axes_manager["Frame"].size == valid @@ -373,12 +377,20 @@ def test_em_image_in_pts(): # no SEM/STEM image s = hs.load( - dir1 / TEST_FILES[0], read_em_image=False, only_valid_data=False, cutoff_at_kV=1 + dir1 / TEST_FILES[0], + read_em_image=False, + only_valid_data=False, + cutoff_at_kV=1, + reader="JEOL", ) assert len(s) == 7 s = hs.load( - dir1 / TEST_FILES[0], read_em_image=True, only_valid_data=False, cutoff_at_kV=1 + dir1 / TEST_FILES[0], + read_em_image=True, + only_valid_data=False, + cutoff_at_kV=1, + reader="JEOL", ) assert len(s) == 7 @@ -388,10 +400,15 @@ def test_em_image_in_pts(): read_em_image=False, only_valid_data=False, cutoff_at_kV=1, + reader="JEOL", ) assert len(s) == 22 s = hs.load( - dir2 / TEST_FILES2[0], read_em_image=True, only_valid_data=False, cutoff_at_kV=1 + dir2 / TEST_FILES2[0], + read_em_image=True, + only_valid_data=False, + cutoff_at_kV=1, + reader="JEOL", ) assert len(s) == 25 assert ( @@ -409,6 +426,7 @@ def test_em_image_in_pts(): sum_frames=True, cutoff_at_kV=1, frame_list=[0, 0, 0, 1], + reader="JEOL", ) assert s[1].data[0, 0] == 87 * 4 assert s[1].data[63, 63] == 87 * 3 @@ -419,6 +437,7 @@ def test_em_image_in_pts(): only_valid_data=False, sum_frames=False, cutoff_at_kV=1, + reader="JEOL", ) s2 = hs.load( dir2p / TEST_FILES2[16], @@ -426,6 +445,7 @@ def test_em_image_in_pts(): only_valid_data=False, sum_frames=True, cutoff_at_kV=1, + reader="JEOL", ) s1 = [s[0].data.sum(axis=0), s[1].data.sum(axis=0)] assert np.array_equal(s1[0], s2[0].data) @@ -441,6 +461,7 @@ def test_pts_lazy(): only_valid_data=False, sum_frames=False, lazy=True, + reader="JEOL", ) s1 = [s[0].data.sum(axis=0).compute(), s[1].data.sum(axis=0).compute()] s2 = hs.load( @@ -449,6 +470,7 @@ def test_pts_lazy(): only_valid_data=False, sum_frames=True, lazy=False, + reader="JEOL", ) assert np.array_equal(s1[0], s2[0].data) assert np.array_equal(s1[1], s2[1].data) @@ -459,7 +481,12 @@ def test_pts_frame_shift(): # without frame shift ref = hs.load( - file, read_em_image=True, only_valid_data=False, sum_frames=False, lazy=False + file, + read_em_image=True, + only_valid_data=False, + sum_frames=False, + lazy=False, + reader="JEOL", ) # x, y, en points = [[24, 23, 106], [21, 16, 106]] @@ -489,6 +516,7 @@ def test_pts_frame_shift(): sum_frames=False, frame_shifts=shifts, lazy=False, + reader="JEOL", ) for frame in range(s0[0].axes_manager["Frame"].size): @@ -506,6 +534,7 @@ def test_pts_frame_shift(): sum_frames=False, frame_shifts=shifts, lazy=True, + reader="JEOL", ) dt = s1[0].data.compute() for frame in range(s0[0].axes_manager["Frame"].size): @@ -520,7 +549,9 @@ def test_pts_frame_shift(): max_sfts = sfts.max(axis=0) min_sfts = sfts.min(axis=0) fs = sfts - max_sfts - s = hs.load(file, frame_shifts=sfts, sum_frames=False, only_valid_data=False) + s = hs.load( + file, frame_shifts=sfts, sum_frames=False, only_valid_data=False, reader="JEOL" + ) sz = min_sfts - max_sfts + ref[0].data.shape[1:3] assert s.data.shape == (2, sz[0], sz[1], 4096) for fr, sft in enumerate(fs): @@ -539,10 +570,10 @@ def test_broken_files(tmp_path): if file.suffix == ".asw": # in case of asw, valid data can not be obtained with pytest.raises(ValueError, match="Not a valid JEOL asw format"): - _ = hs.load(file) + _ = hs.load(file, reader="JEOL") else: # just skipping broken files - s = hs.load(file) + s = hs.load(file, reader="JEOL") assert s == [] @@ -560,7 +591,7 @@ def test_seq_eds_files(tmp_path): zipped.extractall(tmp_path) # test reading sequential acuired EDS spectrum - s = hs.load(tmp_path / "1" / "1.ASW") + s = hs.load(tmp_path / "1" / "1.ASW", reader="JEOL") # check if three subfiles are in file (img, eds, eds) assert len(s) == 3 # check positional information in subfiles @@ -589,7 +620,7 @@ def test_seq_eds_files(tmp_path): data2[0x42D] = 0x30 with open(fname2, "wb") as f: f.write(data2) - dat = hs.load(fname2) + dat = hs.load(fname2, reader="JEOL") assert len(dat) == 0 # No ViewInfo @@ -597,7 +628,7 @@ def test_seq_eds_files(tmp_path): data2[0x1AD] = 0x30 with open(fname2, "wb") as f: f.write(data2) - dat = hs.load(fname2) + dat = hs.load(fname2, reader="JEOL") assert len(dat) == 0 # No SampleInfo @@ -605,7 +636,7 @@ def test_seq_eds_files(tmp_path): data2[0x6E] = 0x30 with open(fname2, "wb") as f: f.write(data2) - dat = hs.load(fname2) + dat = hs.load(fname2, reader="JEOL") assert len(dat) == 0 # test read for pseudo SEM eds/img data @@ -620,7 +651,7 @@ def test_seq_eds_files(tmp_path): data[0x75BD] = 0x41 with open(sub_dir / ("x" + test_files[0]), "wb") as f: f.write(data) - s = hs.load(sub_dir / ("x" + test_files[0])) + s = hs.load(sub_dir / ("x" + test_files[0]), reader="JEOL") assert "SEM" in s.metadata["Acquisition_instrument"] # .eds @@ -629,7 +660,7 @@ def test_seq_eds_files(tmp_path): data[0x4B13] = 0x34 with open(sub_dir / ("x" + test_files[1]), "wb") as f: f.write(data) - s = hs.load(sub_dir / ("x" + test_files[1])) + s = hs.load(sub_dir / ("x" + test_files[1]), reader="JEOL") assert isinstance(s, hs.signals.EDSSEMSpectrum) assert "SEM" in s.metadata["Acquisition_instrument"] @@ -655,7 +686,12 @@ def test_frame_start_index(tmp_path): ] ref = hs.load( - file, sum_frames=False, downsample=[32, 32], rebin_energy=512, SI_dtype=np.int32 + file, + sum_frames=False, + downsample=[32, 32], + rebin_energy=512, + SI_dtype=np.int32, + reader="JEOL", ) frame_start_index = ref.original_metadata.jeol_pts_frame_start_index assert np.array_equal(frame_start_index, frame_start_index_ref) @@ -666,6 +702,7 @@ def test_frame_start_index(tmp_path): downsample=[32, 32], rebin_energy=512, SI_dtype=np.int32, + reader="JEOL", ) frame_start_index = s.original_metadata.jeol_pts_frame_start_index assert np.array_equal(frame_start_index[0:6], frame_start_index_ref[0:6]) @@ -678,6 +715,7 @@ def test_frame_start_index(tmp_path): downsample=[32, 32], rebin_energy=512, SI_dtype=np.int32, + reader="JEOL", ) frame_start_index = s.original_metadata.jeol_pts_frame_start_index assert np.array_equal(frame_start_index[0:10], frame_start_index_ref[0:10]) @@ -691,6 +729,7 @@ def test_frame_start_index(tmp_path): downsample=[32, 32], rebin_energy=512, SI_dtype=np.int32, + reader="JEOL", ) assert s.data.shape == (2, 16, 16, 8) @@ -703,5 +742,11 @@ def test_frame_start_index(tmp_path): data[0x1117] = 0x41 with open(test_file, "wb") as f: f.write(data) - s = hs.load(test_file, downsample=[32, 32], rebin_energy=512, SI_dtype=np.int32) + s = hs.load( + test_file, + downsample=[32, 32], + rebin_energy=512, + SI_dtype=np.int32, + reader="JEOL", + ) assert s.metadata["Signal"]["signal_type"] == "EDS_SEM" diff --git a/upcoming_changes/87.new.rst b/upcoming_changes/87.new.rst new file mode 100644 index 000000000..c1d0d2c73 --- /dev/null +++ b/upcoming_changes/87.new.rst @@ -0,0 +1 @@ +Add support for reading the ``.img``-format from Hamamatsu. \ No newline at end of file