470 lines
15 KiB
Python
470 lines
15 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# imageio is distributed under the terms of the (new) BSD License.
|
||
|
|
||
|
""" SPE file reader
|
||
|
"""
|
||
|
|
||
|
import os
|
||
|
import logging
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
from .. import formats
|
||
|
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("<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 SpeFormat(Format):
|
||
|
""" Some CCD camera software produces images in the Princeton Instruments
|
||
|
SPE file format. This plugin supports reading such files.
|
||
|
|
||
|
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.
|
||
|
|
||
|
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
|
||
|
"""
|
||
|
|
||
|
def _can_read(self, request):
|
||
|
return (
|
||
|
request.mode[1] in self.modes + "?" and 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):
|
||
|
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"]
|
||
|
|
||
|
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())
|
||
|
)
|
||
|
l = data_end - Spec.data_start
|
||
|
l //= self._shape[0] * self._shape[1] * self._dtype.itemsize
|
||
|
if l != 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,
|
||
|
l,
|
||
|
)
|
||
|
self._len = min(l, 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
|
||
|
|
||
|
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.
|
||
|
"""
|
||
|
l = []
|
||
|
a = a[["startx", "starty", "endx", "endy", "groupx", "groupy"]]
|
||
|
for sx, sy, ex, ey, gx, gy in a:
|
||
|
d = {
|
||
|
"top_left": [int(sx), int(sy)],
|
||
|
"bottom_right": [int(ex), int(ey)],
|
||
|
"bin": [int(gx), int(gy)],
|
||
|
}
|
||
|
l.append(d)
|
||
|
return l
|
||
|
|
||
|
|
||
|
fmt = SpeFormat("spe", "SPE file format", ".spe", "iIvV")
|
||
|
formats.add_format(fmt, overwrite=True)
|