# -*- coding: utf-8 -*- # imageio is distributed under the terms of the (new) BSD License. """ Read SPE files. Backend: internal This plugin supports reading files saved in the Princeton Instruments SPE file format. Parameters for reading ---------------------- char_encoding : str Character encoding used to decode strings in the metadata. Defaults to "latin1". check_filesize : bool The number of frames in the file is stored in the file header. However, this number may be wrong for certain software. If this is `True` (default), derive the number of frames also from the file size and raise a warning if the two values do not match. sdt_meta : bool If set to `True` (default), check for special metadata written by the `SDT-control` software. Does not have an effect for files written by other software. Metadata for reading -------------------- ROIs : list of dict Regions of interest used for recording images. Each dict has the "top_left" key containing x and y coordinates of the top left corner, the "bottom_right" key with x and y coordinates of the bottom right corner, and the "bin" key with number of binned pixels in x and y directions. comments : list of str The SPE format allows for 5 comment strings of 80 characters each. controller_version : int Hardware version logic_output : int Definition of output BNC amp_hi_cap_low_noise : int Amp switching mode mode : int Timing mode exp_sec : float Alternative exposure in seconds date : str Date string detector_temp : float Detector temperature detector_type : int CCD / diode array type st_diode : int Trigger diode delay_time : float Used with async mode shutter_control : int Normal, disabled open, or disabled closed absorb_live : bool on / off absorb_mode : int Reference strip or file can_do_virtual_chip : bool True or False whether chip can do virtual chip threshold_min_live : bool on / off threshold_min_val : float Threshold minimum value threshold_max_live : bool on / off threshold_max_val : float Threshold maximum value time_local : str Experiment local time time_utc : str Experiment UTC time adc_offset : int ADC offset adc_rate : int ADC rate adc_type : int ADC type adc_resolution : int ADC resolution adc_bit_adjust : int ADC bit adjust gain : int gain sw_version : str Version of software which created this file spare_4 : bytes Reserved space readout_time : float Experiment readout time type : str Controller type clockspeed_us : float Vertical clock speed in microseconds readout_mode : ["full frame", "frame transfer", "kinetics", ""] Readout mode. Empty string means that this was not set by the Software. window_size : int Window size for Kinetics mode file_header_ver : float File header version chip_size : [int, int] x and y dimensions of the camera chip virt_chip_size : [int, int] Virtual chip x and y dimensions pre_pixels : [int, int] Pre pixels in x and y dimensions post_pixels : [int, int], Post pixels in x and y dimensions geometric : list of {"rotate", "reverse", "flip"} Geometric operations sdt_major_version : int (only for files created by SDT-control) Major version of SDT-control software sdt_minor_version : int (only for files created by SDT-control) Minor version of SDT-control software sdt_controller_name : str (only for files created by SDT-control) Controller name exposure_time : float (only for files created by SDT-control) Exposure time in seconds color_code : str (only for files created by SDT-control) Color channels used detection_channels : int (only for files created by SDT-control) Number of channels background_subtraction : bool (only for files created by SDT-control) Whether background subtraction war turned on em_active : bool (only for files created by SDT-control) Whether EM was turned on em_gain : int (only for files created by SDT-control) EM gain modulation_active : bool (only for files created by SDT-control) Whether laser modulation (“attenuate”) was turned on pixel_size : float (only for files created by SDT-control) Camera pixel size sequence_type : str (only for files created by SDT-control) Type of sequnce (standard, TOCCSL, arbitrary, …) grid : float (only for files created by SDT-control) Sequence time unit (“grid size”) in seconds n_macro : int (only for files created by SDT-control) Number of macro loops delay_macro : float (only for files created by SDT-control) Time between macro loops in seconds n_mini : int (only for files created by SDT-control) Number of mini loops delay_mini : float (only for files created by SDT-control) Time between mini loops in seconds n_micro : int (only for files created by SDT-control) Number of micro loops delay_micro : float (only for files created by SDT-control) Time between micro loops in seconds n_subpics : int (only for files created by SDT-control) Number of sub-pictures delay_shutter : float (only for files created by SDT-control) Camera shutter delay in seconds delay_prebleach : float (only for files created by SDT-control) Pre-bleach delay in seconds bleach_time : float (only for files created by SDT-control) Bleaching time in seconds recovery_time : float (only for files created by SDT-control) Recovery time in seconds comment : str (only for files created by SDT-control) User-entered comment. This replaces the "comments" field. datetime : datetime.datetime (only for files created by SDT-control) Combines the "date" and "time_local" keys. The latter two plus "time_utc" are removed. modulation_script : str (only for files created by SDT-control) Laser modulation script. Replaces the "spare_4" key. """ from datetime import datetime import logging import os from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Union import numpy as np from ..core import Format logger = logging.getLogger(__name__) class Spec: """SPE file specification data Tuples of (offset, datatype, count), where offset is the offset in the SPE file and datatype is the datatype as used in `numpy.fromfile`() `data_start` is the offset of actual image data. `dtypes` translates SPE datatypes (0...4) to numpy ones, e. g. dtypes[0] is dtype(" Union[Dict, None]: """Extract SDT-control metadata from comments Parameters ---------- comments List of SPE file comments, typically ``metadata["comments"]``. Returns ------- If SDT-control comments were detected, return a dict of metadata, else `None`. """ sdt_md = {} if comments[4][70:] != "COMVER0500": logger.debug("SDT-control comments not found.") return None sdt_md = {} for name, spec in SDTControlSpec.comments.items(): try: v = spec.cvt(comments[spec.n][spec.slice]) if spec.scale is not None: v *= spec.scale except Exception as e: logger.debug( "Failed to decode SDT-control metadata " f'field "{name}": {e}' ) sdt_md[name] = v comment = comments[0] + comments[2] sdt_md["comment"] = comment.strip() return sdt_md @staticmethod def get_datetime(date: str, time: str) -> Union[datetime, None]: """Turn date and time saved by SDT-control into proper datetime object Parameters ---------- date SPE file date, typically ``metadata["date"]``. time SPE file date, typically ``metadata["time_local"]``. Returns ------- File's datetime if parsing was succsessful, else None. """ try: month = __class__.months[date[2:5]] return datetime( int(date[5:9]), month, int(date[0:2]), int(time[0:2]), int(time[2:4]), int(time[4:6]), ) except Exception as e: logger.info(f"Failed to decode date from SDT-control metadata: {e}.") @staticmethod def extract_metadata(meta: Mapping, char_encoding: str = "latin1"): """Extract SDT-control metadata from SPE metadata SDT-control stores some metadata in comments and other fields. Extract them and remove unused entries. Parameters ---------- meta SPE file metadata. Modified in place. char_encoding Character encoding used to decode strings in the metadata. """ sdt_meta = __class__.parse_comments(meta["comments"]) if not sdt_meta: return # This file has SDT-control metadata meta.pop("comments") meta.update(sdt_meta) # Get date and time in a usable format dt = __class__.get_datetime(meta["date"], meta["time_local"]) if dt: meta["datetime"] = dt meta.pop("date") meta.pop("time_local") sp4 = meta["spare_4"] try: meta["modulation_script"] = sp4.decode(char_encoding) meta.pop("spare_4") except UnicodeDecodeError: logger.warning( "Failed to decode SDT-control laser " "modulation script. Bad char_encoding?" ) # Get rid of unused data meta.pop("time_utc") meta.pop("exposure_sec") class SpeFormat(Format): """See :mod:`imageio.plugins.spe`""" def _can_read(self, request): return request.extension in self.extensions def _can_write(self, request): return False class Reader(Format.Reader): def _open(self, char_encoding="latin1", check_filesize=True, sdt_meta=True): self._file = self.request.get_file() self._char_encoding = char_encoding info = self._parse_header(Spec.basic) self._file_header_ver = info["file_header_ver"] self._dtype = Spec.dtypes[info["datatype"]] self._shape = (info["ydim"], info["xdim"]) self._len = info["NumFrames"] self._sdt_meta = sdt_meta if check_filesize: # Some software writes incorrect `NumFrames` metadata. # To determine the number of frames, check the size of the data # segment -- until the end of the file for SPE<3, until the # xml footer for SPE>=3. data_end = ( info["xml_footer_offset"] if info["file_header_ver"] >= 3 else os.path.getsize(self.request.get_local_filename()) ) line = data_end - Spec.data_start line //= self._shape[0] * self._shape[1] * self._dtype.itemsize if line != self._len: logger.warning( "The file header of %s claims there are %s frames, " "but there are actually %s frames.", self.request.filename, self._len, line, ) self._len = min(line, self._len) self._meta = None def _get_meta_data(self, index): if self._meta is None: if self._file_header_ver < 3: self._init_meta_data_pre_v3() else: self._init_meta_data_post_v3() return self._meta def _close(self): # The file should be closed by `self.request` pass def _init_meta_data_pre_v3(self): self._meta = self._parse_header(Spec.metadata) nr = self._meta.pop("NumROI", None) nr = 1 if nr < 1 else nr self._meta["ROIs"] = roi_array_to_dict(self._meta["ROIs"][:nr]) # chip sizes self._meta["chip_size"] = [ self._meta.pop("xDimDet", None), self._meta.pop("yDimDet", None), ] self._meta["virt_chip_size"] = [ self._meta.pop("VChipXdim", None), self._meta.pop("VChipYdim", None), ] self._meta["pre_pixels"] = [ self._meta.pop("XPrePixels", None), self._meta.pop("YPrePixels", None), ] self._meta["post_pixels"] = [ self._meta.pop("XPostPixels", None), self._meta.pop("YPostPixels", None), ] # comments self._meta["comments"] = [str(c) for c in self._meta["comments"]] # geometric operations g = [] f = self._meta.pop("geometric", 0) if f & 1: g.append("rotate") if f & 2: g.append("reverse") if f & 4: g.append("flip") self._meta["geometric"] = g # Make some additional information more human-readable t = self._meta["type"] if 1 <= t <= len(Spec.controllers): self._meta["type"] = Spec.controllers[t - 1] else: self._meta["type"] = "" m = self._meta["readout_mode"] if 1 <= m <= len(Spec.readout_modes): self._meta["readout_mode"] = Spec.readout_modes[m - 1] else: self._meta["readout_mode"] = "" # bools for k in ( "absorb_live", "can_do_virtual_chip", "threshold_min_live", "threshold_max_live", ): self._meta[k] = bool(self._meta[k]) # frame shape self._meta["frame_shape"] = self._shape # Extract SDT-control metadata if desired if self._sdt_meta: SDTControlSpec.extract_metadata(self._meta, self._char_encoding) def _parse_header(self, spec): ret = {} # Decode each string from the numpy array read by np.fromfile decode = np.vectorize(lambda x: x.decode(self._char_encoding)) for name, sp in spec.items(): self._file.seek(sp[0]) cnt = 1 if len(sp) < 3 else sp[2] v = np.fromfile(self._file, dtype=sp[1], count=cnt) if v.dtype.kind == "S" and name not in Spec.no_decode: # Silently ignore string decoding failures try: v = decode(v) except Exception: logger.warning( 'Failed to decode "{}" metadata ' "string. Check `char_encoding` " "parameter.".format(name) ) try: # For convenience, if the array contains only one single # entry, return this entry itself. v = v.item() except ValueError: v = np.squeeze(v) ret[name] = v return ret def _init_meta_data_post_v3(self): info = self._parse_header(Spec.basic) self._file.seek(info["xml_footer_offset"]) xml = self._file.read() self._meta = {"__xml": xml} def _get_length(self): if self.request.mode[1] in "vV": return 1 else: return self._len def _get_data(self, index): if index < 0: raise IndexError("Image index %i < 0" % index) if index >= self._len: raise IndexError("Image index %i > %i" % (index, self._len)) if self.request.mode[1] in "vV": if index != 0: raise IndexError("Index has to be 0 in v and V modes") self._file.seek(Spec.data_start) data = np.fromfile( self._file, dtype=self._dtype, count=self._shape[0] * self._shape[1] * self._len, ) data = data.reshape((self._len,) + self._shape) else: self._file.seek( Spec.data_start + index * self._shape[0] * self._shape[1] * self._dtype.itemsize ) data = np.fromfile( self._file, dtype=self._dtype, count=self._shape[0] * self._shape[1] ) data = data.reshape(self._shape) return data, self._get_meta_data(index) def roi_array_to_dict(a): """Convert the `ROIs` structured arrays to :py:class:`dict` Parameters ---------- a : numpy.ndarray: Structured array containing ROI data Returns ------- list of dict One dict per ROI. Keys are "top_left", "bottom_right", and "bin", values are tuples whose first element is the x axis value and the second element is the y axis value. """ dict_list = [] a = a[["startx", "starty", "endx", "endy", "groupx", "groupy"]] for sx, sy, ex, ey, gx, gy in a: roi_dict = { "top_left": [int(sx), int(sy)], "bottom_right": [int(ex), int(ey)], "bin": [int(gx), int(gy)], } dict_list.append(roi_dict) return dict_list