956 lines
31 KiB
Python
956 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
|
# imageio is distributed under the terms of the (new) BSD License.
|
|
|
|
""" Read SPE files.
|
|
|
|
This plugin supports reading files saved in the Princeton Instruments
|
|
SPE file format.
|
|
|
|
Parameters
|
|
----------
|
|
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.
|
|
char_encoding : str
|
|
Deprecated. Exists for backwards compatibility; use ``char_encoding`` of
|
|
``metadata`` instead.
|
|
sdt_meta : bool
|
|
Deprecated. Exists for backwards compatibility; use ``sdt_control`` of
|
|
``metadata`` instead.
|
|
|
|
Methods
|
|
-------
|
|
.. note::
|
|
Check the respective function for a list of supported kwargs and detailed
|
|
documentation.
|
|
|
|
.. autosummary::
|
|
:toctree:
|
|
|
|
SpePlugin.read
|
|
SpePlugin.iter
|
|
SpePlugin.properties
|
|
SpePlugin.metadata
|
|
|
|
"""
|
|
|
|
from datetime import datetime
|
|
import logging
|
|
import os
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Dict,
|
|
Iterator,
|
|
List,
|
|
Mapping,
|
|
Optional,
|
|
Sequence,
|
|
Tuple,
|
|
Union,
|
|
)
|
|
import warnings
|
|
|
|
import numpy as np
|
|
|
|
from ..core.request import Request, IOMode, InitializationError
|
|
from ..core.v3_plugin_api import PluginV3, ImageProperties
|
|
|
|
|
|
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("<f") (which is np.float32).
|
|
|
|
`controllers` maps the `type` metadata to a human readable name
|
|
|
|
`readout_modes` maps the `readoutMode` metadata to something human readable
|
|
although this may not be accurate since there is next to no documentation
|
|
to be found.
|
|
"""
|
|
|
|
basic = {
|
|
"datatype": (108, "<h"), # dtypes
|
|
"xdim": (42, "<H"),
|
|
"ydim": (656, "<H"),
|
|
"xml_footer_offset": (678, "<Q"),
|
|
"NumFrames": (1446, "<i"),
|
|
"file_header_ver": (1992, "<f"),
|
|
}
|
|
|
|
metadata = {
|
|
# ROI information
|
|
"NumROI": (1510, "<h"),
|
|
"ROIs": (
|
|
1512,
|
|
np.dtype(
|
|
[
|
|
("startx", "<H"),
|
|
("endx", "<H"),
|
|
("groupx", "<H"),
|
|
("starty", "<H"),
|
|
("endy", "<H"),
|
|
("groupy", "<H"),
|
|
]
|
|
),
|
|
10,
|
|
),
|
|
# chip-related sizes
|
|
"xDimDet": (6, "<H"),
|
|
"yDimDet": (18, "<H"),
|
|
"VChipXdim": (14, "<h"),
|
|
"VChipYdim": (16, "<h"),
|
|
# other stuff
|
|
"controller_version": (0, "<h"),
|
|
"logic_output": (2, "<h"),
|
|
"amp_high_cap_low_noise": (4, "<H"), # enum?
|
|
"mode": (8, "<h"), # enum?
|
|
"exposure_sec": (10, "<f"),
|
|
"date": (20, "<10S"),
|
|
"detector_temp": (36, "<f"),
|
|
"detector_type": (40, "<h"),
|
|
"st_diode": (44, "<h"),
|
|
"delay_time": (46, "<f"),
|
|
# shutter_control: normal, disabled open, disabled closed
|
|
# But which one is which?
|
|
"shutter_control": (50, "<H"),
|
|
"absorb_live": (52, "<h"),
|
|
"absorb_mode": (54, "<H"),
|
|
"can_do_virtual_chip": (56, "<h"),
|
|
"threshold_min_live": (58, "<h"),
|
|
"threshold_min_val": (60, "<f"),
|
|
"threshold_max_live": (64, "<h"),
|
|
"threshold_max_val": (66, "<f"),
|
|
"time_local": (172, "<7S"),
|
|
"time_utc": (179, "<7S"),
|
|
"adc_offset": (188, "<H"),
|
|
"adc_rate": (190, "<H"),
|
|
"adc_type": (192, "<H"),
|
|
"adc_resolution": (194, "<H"),
|
|
"adc_bit_adjust": (196, "<H"),
|
|
"gain": (198, "<H"),
|
|
"comments": (200, "<80S", 5),
|
|
"geometric": (600, "<H"), # flags
|
|
"sw_version": (688, "<16S"),
|
|
"spare_4": (742, "<436S"),
|
|
"XPrePixels": (98, "<h"),
|
|
"XPostPixels": (100, "<h"),
|
|
"YPrePixels": (102, "<h"),
|
|
"YPostPixels": (104, "<h"),
|
|
"readout_time": (672, "<f"),
|
|
"xml_footer_offset": (678, "<Q"),
|
|
"type": (704, "<h"), # controllers
|
|
"clockspeed_us": (1428, "<f"),
|
|
"readout_mode": (1480, "<H"), # readout_modes
|
|
"window_size": (1482, "<H"),
|
|
"file_header_ver": (1992, "<f"),
|
|
}
|
|
|
|
data_start = 4100
|
|
|
|
dtypes = {
|
|
0: np.dtype(np.float32),
|
|
1: np.dtype(np.int32),
|
|
2: np.dtype(np.int16),
|
|
3: np.dtype(np.uint16),
|
|
8: np.dtype(np.uint32),
|
|
}
|
|
|
|
controllers = [
|
|
"new120 (Type II)",
|
|
"old120 (Type I)",
|
|
"ST130",
|
|
"ST121",
|
|
"ST138",
|
|
"DC131 (PentaMax)",
|
|
"ST133 (MicroMax/Roper)",
|
|
"ST135 (GPIB)",
|
|
"VTCCD",
|
|
"ST116 (GPIB)",
|
|
"OMA3 (GPIB)",
|
|
"OMA4",
|
|
]
|
|
|
|
# This was gathered from random places on the internet and own experiments
|
|
# with the camera. May not be accurate.
|
|
readout_modes = ["full frame", "frame transfer", "kinetics"]
|
|
|
|
# Do not decode the following metadata keys into strings, but leave them
|
|
# as byte arrays
|
|
no_decode = ["spare_4"]
|
|
|
|
|
|
class SDTControlSpec:
|
|
"""Extract metadata written by the SDT-control software
|
|
|
|
Some of it is encoded in the comment strings
|
|
(see :py:meth:`parse_comments`). Also, date and time are encoded in a
|
|
peculiar way (see :py:meth:`get_datetime`). Use :py:meth:`extract_metadata`
|
|
to update the metadata dict.
|
|
"""
|
|
|
|
months = {
|
|
# Convert SDT-control month strings to month numbers
|
|
"Jän": 1,
|
|
"Jan": 1,
|
|
"Feb": 2,
|
|
"Mär": 3,
|
|
"Mar": 3,
|
|
"Apr": 4,
|
|
"Mai": 5,
|
|
"May": 5,
|
|
"Jun": 6,
|
|
"Jul": 7,
|
|
"Aug": 8,
|
|
"Sep": 9,
|
|
"Okt": 10,
|
|
"Oct": 10,
|
|
"Nov": 11,
|
|
"Dez": 12,
|
|
"Dec": 12,
|
|
}
|
|
|
|
sequence_types = {
|
|
# TODO: complete
|
|
"SEQU": "standard",
|
|
"SETO": "TOCCSL",
|
|
"KINE": "kinetics",
|
|
"SEAR": "arbitrary",
|
|
}
|
|
|
|
class CommentDesc:
|
|
"""Describe how to extract a metadata entry from a comment string"""
|
|
|
|
n: int
|
|
"""Which of the 5 SPE comment fields to use."""
|
|
slice: slice
|
|
"""Which characters from the `n`-th comment to use."""
|
|
cvt: Callable[[str], Any]
|
|
"""How to convert characters to something useful."""
|
|
scale: Union[None, float]
|
|
"""Optional scaling factor for numbers"""
|
|
|
|
def __init__(
|
|
self,
|
|
n: int,
|
|
slice: slice,
|
|
cvt: Callable[[str], Any] = str,
|
|
scale: Optional[float] = None,
|
|
):
|
|
self.n = n
|
|
self.slice = slice
|
|
self.cvt = cvt
|
|
self.scale = scale
|
|
|
|
comment_fields = {
|
|
(5, 0): {
|
|
"sdt_major_version": CommentDesc(4, slice(66, 68), int),
|
|
"sdt_minor_version": CommentDesc(4, slice(68, 70), int),
|
|
"sdt_controller_name": CommentDesc(4, slice(0, 6), str),
|
|
"exposure_time": CommentDesc(1, slice(64, 73), float, 10**-6),
|
|
"color_code": CommentDesc(4, slice(10, 14), str),
|
|
"detection_channels": CommentDesc(4, slice(15, 16), int),
|
|
"background_subtraction": CommentDesc(4, 14, lambda x: x == "B"),
|
|
"em_active": CommentDesc(4, 32, lambda x: x == "E"),
|
|
"em_gain": CommentDesc(4, slice(28, 32), int),
|
|
"modulation_active": CommentDesc(4, 33, lambda x: x == "A"),
|
|
"pixel_size": CommentDesc(4, slice(25, 28), float, 0.1),
|
|
"sequence_type": CommentDesc(
|
|
4, slice(6, 10), lambda x: __class__.sequence_types[x]
|
|
),
|
|
"grid": CommentDesc(4, slice(16, 25), float, 10**-6),
|
|
"n_macro": CommentDesc(1, slice(0, 4), int),
|
|
"delay_macro": CommentDesc(1, slice(10, 19), float, 10**-3),
|
|
"n_mini": CommentDesc(1, slice(4, 7), int),
|
|
"delay_mini": CommentDesc(1, slice(19, 28), float, 10**-6),
|
|
"n_micro": CommentDesc(1, slice(7, 10), int),
|
|
"delay_micro": CommentDesc(1, slice(28, 37), float, 10**-6),
|
|
"n_subpics": CommentDesc(1, slice(7, 10), int),
|
|
"delay_shutter": CommentDesc(1, slice(73, 79), float, 10**-6),
|
|
"delay_prebleach": CommentDesc(1, slice(37, 46), float, 10**-6),
|
|
"bleach_time": CommentDesc(1, slice(46, 55), float, 10**-6),
|
|
"recovery_time": CommentDesc(1, slice(55, 64), float, 10**-6),
|
|
},
|
|
(5, 1): {
|
|
"bleach_piezo_active": CommentDesc(4, slice(34, 35), lambda x: x == "z")
|
|
},
|
|
}
|
|
|
|
@staticmethod
|
|
def get_comment_version(comments: Sequence[str]) -> Tuple[int, int]:
|
|
"""Get the version of SDT-control metadata encoded in the comments
|
|
|
|
Parameters
|
|
----------
|
|
comments
|
|
List of SPE file comments, typically ``metadata["comments"]``.
|
|
|
|
Returns
|
|
-------
|
|
Major and minor version. ``-1, -1`` if detection failed.
|
|
"""
|
|
if comments[4][70:76] != "COMVER":
|
|
return -1, -1
|
|
try:
|
|
return int(comments[4][76:78]), int(comments[4][78:80])
|
|
except ValueError:
|
|
return -1, -1
|
|
|
|
@staticmethod
|
|
def parse_comments(
|
|
comments: Sequence[str], version: Tuple[int, int]
|
|
) -> Dict[str, Any]:
|
|
"""Extract SDT-control metadata from comments
|
|
|
|
Parameters
|
|
----------
|
|
comments
|
|
List of SPE file comments, typically ``metadata["comments"]``.
|
|
version
|
|
Major and minor version of SDT-control metadata format
|
|
|
|
Returns
|
|
-------
|
|
Dict of metadata
|
|
"""
|
|
sdt_md = {}
|
|
for minor in range(version[1] + 1):
|
|
# Metadata with same major version is backwards compatible.
|
|
# Fields are specified incrementally in `comment_fields`.
|
|
# E.g. if the file has version 5.01, `comment_fields[5, 0]` and
|
|
# `comment_fields[5, 1]` need to be decoded.
|
|
try:
|
|
cmt = __class__.comment_fields[version[0], minor]
|
|
except KeyError:
|
|
continue
|
|
for name, spec in cmt.items():
|
|
try:
|
|
v = spec.cvt(comments[spec.n][spec.slice])
|
|
if spec.scale is not None:
|
|
v *= spec.scale
|
|
sdt_md[name] = v
|
|
except Exception as e:
|
|
warnings.warn(
|
|
f"Failed to decode SDT-control metadata field `{name}`: {e}"
|
|
)
|
|
sdt_md[name] = None
|
|
if version not in __class__.comment_fields:
|
|
supported_ver = ", ".join(
|
|
map(lambda x: f"{x[0]}.{x[1]:02}", __class__.comment_fields)
|
|
)
|
|
warnings.warn(
|
|
f"Unsupported SDT-control metadata version {version[0]}.{version[1]:02}. "
|
|
f"Only versions {supported_ver} are supported. "
|
|
"Some or all SDT-control metadata may be missing."
|
|
)
|
|
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.
|
|
"""
|
|
comver = __class__.get_comment_version(meta["comments"])
|
|
if any(c < 0 for c in comver):
|
|
# This file most likely was not created by SDT-control
|
|
logger.debug("SDT-control comments not found.")
|
|
return
|
|
|
|
sdt_meta = __class__.parse_comments(meta["comments"], comver)
|
|
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:
|
|
warnings.warn(
|
|
"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 SpePlugin(PluginV3):
|
|
def __init__(
|
|
self,
|
|
request: Request,
|
|
check_filesize: bool = True,
|
|
char_encoding: Optional[str] = None,
|
|
sdt_meta: Optional[bool] = None,
|
|
) -> None:
|
|
"""Instantiate a new SPE file plugin object
|
|
|
|
Parameters
|
|
----------
|
|
request : Request
|
|
A request object representing the resource to be operated on.
|
|
check_filesize : bool
|
|
If True, compute the number of frames from the filesize, compare it
|
|
to the frame count in the file header, and raise a warning if the
|
|
counts don't match. (Certain software may create files with
|
|
char_encoding : str
|
|
Deprecated. Exists for backwards compatibility; use ``char_encoding`` of
|
|
``metadata`` instead.
|
|
sdt_meta : bool
|
|
Deprecated. Exists for backwards compatibility; use ``sdt_control`` of
|
|
``metadata`` instead.
|
|
|
|
"""
|
|
|
|
super().__init__(request)
|
|
if request.mode.io_mode == IOMode.write:
|
|
raise InitializationError("cannot write SPE files")
|
|
|
|
if char_encoding is not None:
|
|
warnings.warn(
|
|
"Passing `char_encoding` to the constructor is deprecated. "
|
|
"Use `char_encoding` parameter of the `metadata()` method "
|
|
"instead.",
|
|
DeprecationWarning,
|
|
)
|
|
self._char_encoding = char_encoding
|
|
if sdt_meta is not None:
|
|
warnings.warn(
|
|
"Passing `sdt_meta` to the constructor is deprecated. "
|
|
"Use `sdt_control` parameter of the `metadata()` method "
|
|
"instead.",
|
|
DeprecationWarning,
|
|
)
|
|
self._sdt_meta = sdt_meta
|
|
|
|
self._file = self.request.get_file()
|
|
|
|
try:
|
|
# Spec.basic contains no string, no need to worry about character
|
|
# encoding.
|
|
info = self._parse_header(Spec.basic, "latin1")
|
|
self._file_header_ver = info["file_header_ver"]
|
|
self._dtype = Spec.dtypes[info["datatype"]]
|
|
self._shape = (info["ydim"], info["xdim"])
|
|
self._len = info["NumFrames"]
|
|
|
|
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.
|
|
if info["file_header_ver"] >= 3:
|
|
data_end = info["xml_footer_offset"]
|
|
else:
|
|
self._file.seek(0, os.SEEK_END)
|
|
data_end = self._file.tell()
|
|
line = data_end - Spec.data_start
|
|
line //= self._shape[0] * self._shape[1] * self._dtype.itemsize
|
|
if line != self._len:
|
|
warnings.warn(
|
|
f"The file header of {self.request.filename} claims there are "
|
|
f"{self._len} frames, but there are actually {line} frames."
|
|
)
|
|
self._len = min(line, self._len)
|
|
self._file.seek(Spec.data_start)
|
|
except Exception:
|
|
raise InitializationError("SPE plugin cannot read the provided file.")
|
|
|
|
def read(self, *, index: int = ...) -> np.ndarray:
|
|
"""Read a frame or all frames from the file
|
|
|
|
Parameters
|
|
----------
|
|
index : int
|
|
Select the index-th frame from the file. If index is `...`,
|
|
select all frames and stack them along a new axis.
|
|
|
|
Returns
|
|
-------
|
|
A Numpy array of pixel values.
|
|
|
|
"""
|
|
|
|
if index is Ellipsis:
|
|
read_offset = Spec.data_start
|
|
count = self._shape[0] * self._shape[1] * self._len
|
|
out_shape = (self._len, *self._shape)
|
|
elif index < 0:
|
|
raise IndexError(f"Index `{index}` is smaller than 0.")
|
|
elif index >= self._len:
|
|
raise IndexError(
|
|
f"Index `{index}` exceeds the number of frames stored in this file (`{self._len}`)."
|
|
)
|
|
else:
|
|
read_offset = (
|
|
Spec.data_start
|
|
+ index * self._shape[0] * self._shape[1] * self._dtype.itemsize
|
|
)
|
|
count = self._shape[0] * self._shape[1]
|
|
out_shape = self._shape
|
|
|
|
self._file.seek(read_offset)
|
|
data = np.fromfile(self._file, dtype=self._dtype, count=count)
|
|
return data.reshape(out_shape)
|
|
|
|
def iter(self) -> Iterator[np.ndarray]:
|
|
"""Iterate over the frames in the file
|
|
|
|
Yields
|
|
------
|
|
A Numpy array of pixel values.
|
|
"""
|
|
|
|
return (self.read(index=i) for i in range(self._len))
|
|
|
|
def metadata(
|
|
self,
|
|
index: int = ...,
|
|
exclude_applied: bool = True,
|
|
char_encoding: str = "latin1",
|
|
sdt_control: bool = True,
|
|
) -> Dict[str, Any]:
|
|
"""SPE specific metadata.
|
|
|
|
Parameters
|
|
----------
|
|
index : int
|
|
Ignored as SPE files only store global metadata.
|
|
exclude_applied : bool
|
|
Ignored. Exists for API compatibility.
|
|
char_encoding : str
|
|
The encoding to use when parsing strings.
|
|
sdt_control : bool
|
|
If `True`, decode special metadata written by the
|
|
SDT-control software if present.
|
|
|
|
Returns
|
|
-------
|
|
metadata : dict
|
|
Key-value pairs of metadata.
|
|
|
|
Notes
|
|
-----
|
|
SPE v3 stores metadata as XML, whereas SPE v2 uses a binary format.
|
|
|
|
.. rubric:: Supported SPE v2 Metadata fields
|
|
|
|
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.
|
|
bleach_piezo_active : bool
|
|
(only for files created by SDT-control)
|
|
Whether piezo for bleaching was enabled
|
|
"""
|
|
|
|
if self._file_header_ver < 3:
|
|
if self._char_encoding is not None:
|
|
char_encoding = self._char_encoding
|
|
if self._sdt_meta is not None:
|
|
sdt_control = self._sdt_meta
|
|
return self._metadata_pre_v3(char_encoding, sdt_control)
|
|
return self._metadata_post_v3()
|
|
|
|
def _metadata_pre_v3(self, char_encoding: str, sdt_control: bool) -> Dict[str, Any]:
|
|
"""Extract metadata from SPE v2 files
|
|
|
|
Parameters
|
|
----------
|
|
char_encoding
|
|
String character encoding
|
|
sdt_control
|
|
If `True`, try to decode special metadata written by the
|
|
SDT-control software.
|
|
|
|
Returns
|
|
-------
|
|
dict mapping metadata names to values.
|
|
|
|
"""
|
|
|
|
m = self._parse_header(Spec.metadata, char_encoding)
|
|
|
|
nr = m.pop("NumROI", None)
|
|
nr = 1 if nr < 1 else nr
|
|
m["ROIs"] = roi_array_to_dict(m["ROIs"][:nr])
|
|
|
|
# chip sizes
|
|
m["chip_size"] = [m.pop(k, None) for k in ("xDimDet", "yDimDet")]
|
|
m["virt_chip_size"] = [m.pop(k, None) for k in ("VChipXdim", "VChipYdim")]
|
|
m["pre_pixels"] = [m.pop(k, None) for k in ("XPrePixels", "YPrePixels")]
|
|
m["post_pixels"] = [m.pop(k, None) for k in ("XPostPixels", "YPostPixels")]
|
|
|
|
# convert comments from numpy.str_ to str
|
|
m["comments"] = [str(c) for c in m["comments"]]
|
|
|
|
# geometric operations
|
|
g = []
|
|
f = m.pop("geometric", 0)
|
|
if f & 1:
|
|
g.append("rotate")
|
|
if f & 2:
|
|
g.append("reverse")
|
|
if f & 4:
|
|
g.append("flip")
|
|
m["geometric"] = g
|
|
|
|
# Make some additional information more human-readable
|
|
t = m["type"]
|
|
if 1 <= t <= len(Spec.controllers):
|
|
m["type"] = Spec.controllers[t - 1]
|
|
else:
|
|
m["type"] = None
|
|
r = m["readout_mode"]
|
|
if 1 <= r <= len(Spec.readout_modes):
|
|
m["readout_mode"] = Spec.readout_modes[r - 1]
|
|
else:
|
|
m["readout_mode"] = None
|
|
|
|
# bools
|
|
for k in (
|
|
"absorb_live",
|
|
"can_do_virtual_chip",
|
|
"threshold_min_live",
|
|
"threshold_max_live",
|
|
):
|
|
m[k] = bool(m[k])
|
|
|
|
# Extract SDT-control metadata if desired
|
|
if sdt_control:
|
|
SDTControlSpec.extract_metadata(m, char_encoding)
|
|
|
|
return m
|
|
|
|
def _metadata_post_v3(self) -> Dict[str, Any]:
|
|
"""Extract XML metadata from SPE v3 files
|
|
|
|
Returns
|
|
-------
|
|
dict with key `"__xml"`, whose value is the XML metadata
|
|
"""
|
|
|
|
info = self._parse_header(Spec.basic, "latin1")
|
|
self._file.seek(info["xml_footer_offset"])
|
|
xml = self._file.read()
|
|
return {"__xml": xml}
|
|
|
|
def properties(self, index: int = ...) -> ImageProperties:
|
|
"""Standardized ndimage metadata.
|
|
|
|
Parameters
|
|
----------
|
|
index : int
|
|
If the index is an integer, select the index-th frame and return
|
|
its properties. If index is an Ellipsis (...), return the
|
|
properties of all frames in the file stacked along a new batch
|
|
dimension.
|
|
|
|
Returns
|
|
-------
|
|
properties : ImageProperties
|
|
A dataclass filled with standardized image metadata.
|
|
"""
|
|
|
|
if index is Ellipsis:
|
|
return ImageProperties(
|
|
shape=(self._len, *self._shape),
|
|
dtype=self._dtype,
|
|
n_images=self._len,
|
|
is_batch=True,
|
|
)
|
|
return ImageProperties(shape=self._shape, dtype=self._dtype, is_batch=False)
|
|
|
|
def _parse_header(
|
|
self, spec: Mapping[str, Tuple], char_encoding: str
|
|
) -> Dict[str, Any]:
|
|
"""Get information from SPE file header
|
|
|
|
Parameters
|
|
----------
|
|
spec
|
|
Maps header entry name to its location, data type description and
|
|
optionally number of entries. See :py:attr:`Spec.basic` and
|
|
:py:attr:`Spec.metadata`.
|
|
char_encoding
|
|
String character encoding
|
|
|
|
Returns
|
|
-------
|
|
Dict mapping header entry name to its value
|
|
"""
|
|
|
|
ret = {}
|
|
# Decode each string from the numpy array read by np.fromfile
|
|
decode = np.vectorize(lambda x: x.decode(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:
|
|
warnings.warn(
|
|
f'Failed to decode "{name}" metadata '
|
|
"string. Check `char_encoding` parameter."
|
|
)
|
|
|
|
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 roi_array_to_dict(a: np.ndarray) -> List[Dict[str, List[int]]]:
|
|
"""Convert the `ROIs` structured arrays to :py:class:`dict`
|
|
|
|
Parameters
|
|
----------
|
|
a
|
|
Structured array containing ROI data
|
|
|
|
Returns
|
|
-------
|
|
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
|