1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4""" Read SPE files.
5
6This plugin supports reading files saved in the Princeton Instruments
7SPE file format.
8
9Parameters
10----------
11check_filesize : bool
12 The number of frames in the file is stored in the file header. However,
13 this number may be wrong for certain software. If this is `True`
14 (default), derive the number of frames also from the file size and
15 raise a warning if the two values do not match.
16char_encoding : str
17 Deprecated. Exists for backwards compatibility; use ``char_encoding`` of
18 ``metadata`` instead.
19sdt_meta : bool
20 Deprecated. Exists for backwards compatibility; use ``sdt_control`` of
21 ``metadata`` instead.
22
23Methods
24-------
25.. note::
26 Check the respective function for a list of supported kwargs and detailed
27 documentation.
28
29.. autosummary::
30 :toctree:
31
32 SpePlugin.read
33 SpePlugin.iter
34 SpePlugin.properties
35 SpePlugin.metadata
36
37"""
38
39from datetime import datetime
40import logging
41import os
42from typing import (
43 Any,
44 Callable,
45 Dict,
46 Iterator,
47 List,
48 Mapping,
49 Optional,
50 Sequence,
51 Tuple,
52 Union,
53)
54import warnings
55
56import numpy as np
57
58from ..core.request import Request, IOMode, InitializationError
59from ..core.v3_plugin_api import PluginV3, ImageProperties
60
61
62logger = logging.getLogger(__name__)
63
64
65class Spec:
66 """SPE file specification data
67
68 Tuples of (offset, datatype, count), where offset is the offset in the SPE
69 file and datatype is the datatype as used in `numpy.fromfile`()
70
71 `data_start` is the offset of actual image data.
72
73 `dtypes` translates SPE datatypes (0...4) to numpy ones, e. g. dtypes[0]
74 is dtype("<f") (which is np.float32).
75
76 `controllers` maps the `type` metadata to a human readable name
77
78 `readout_modes` maps the `readoutMode` metadata to something human readable
79 although this may not be accurate since there is next to no documentation
80 to be found.
81 """
82
83 basic = {
84 "datatype": (108, "<h"), # dtypes
85 "xdim": (42, "<H"),
86 "ydim": (656, "<H"),
87 "xml_footer_offset": (678, "<Q"),
88 "NumFrames": (1446, "<i"),
89 "file_header_ver": (1992, "<f"),
90 }
91
92 metadata = {
93 # ROI information
94 "NumROI": (1510, "<h"),
95 "ROIs": (
96 1512,
97 np.dtype(
98 [
99 ("startx", "<H"),
100 ("endx", "<H"),
101 ("groupx", "<H"),
102 ("starty", "<H"),
103 ("endy", "<H"),
104 ("groupy", "<H"),
105 ]
106 ),
107 10,
108 ),
109 # chip-related sizes
110 "xDimDet": (6, "<H"),
111 "yDimDet": (18, "<H"),
112 "VChipXdim": (14, "<h"),
113 "VChipYdim": (16, "<h"),
114 # other stuff
115 "controller_version": (0, "<h"),
116 "logic_output": (2, "<h"),
117 "amp_high_cap_low_noise": (4, "<H"), # enum?
118 "mode": (8, "<h"), # enum?
119 "exposure_sec": (10, "<f"),
120 "date": (20, "<10S"),
121 "detector_temp": (36, "<f"),
122 "detector_type": (40, "<h"),
123 "st_diode": (44, "<h"),
124 "delay_time": (46, "<f"),
125 # shutter_control: normal, disabled open, disabled closed
126 # But which one is which?
127 "shutter_control": (50, "<H"),
128 "absorb_live": (52, "<h"),
129 "absorb_mode": (54, "<H"),
130 "can_do_virtual_chip": (56, "<h"),
131 "threshold_min_live": (58, "<h"),
132 "threshold_min_val": (60, "<f"),
133 "threshold_max_live": (64, "<h"),
134 "threshold_max_val": (66, "<f"),
135 "time_local": (172, "<7S"),
136 "time_utc": (179, "<7S"),
137 "adc_offset": (188, "<H"),
138 "adc_rate": (190, "<H"),
139 "adc_type": (192, "<H"),
140 "adc_resolution": (194, "<H"),
141 "adc_bit_adjust": (196, "<H"),
142 "gain": (198, "<H"),
143 "comments": (200, "<80S", 5),
144 "geometric": (600, "<H"), # flags
145 "sw_version": (688, "<16S"),
146 "spare_4": (742, "<436S"),
147 "XPrePixels": (98, "<h"),
148 "XPostPixels": (100, "<h"),
149 "YPrePixels": (102, "<h"),
150 "YPostPixels": (104, "<h"),
151 "readout_time": (672, "<f"),
152 "xml_footer_offset": (678, "<Q"),
153 "type": (704, "<h"), # controllers
154 "clockspeed_us": (1428, "<f"),
155 "readout_mode": (1480, "<H"), # readout_modes
156 "window_size": (1482, "<H"),
157 "file_header_ver": (1992, "<f"),
158 }
159
160 data_start = 4100
161
162 dtypes = {
163 0: np.dtype(np.float32),
164 1: np.dtype(np.int32),
165 2: np.dtype(np.int16),
166 3: np.dtype(np.uint16),
167 8: np.dtype(np.uint32),
168 }
169
170 controllers = [
171 "new120 (Type II)",
172 "old120 (Type I)",
173 "ST130",
174 "ST121",
175 "ST138",
176 "DC131 (PentaMax)",
177 "ST133 (MicroMax/Roper)",
178 "ST135 (GPIB)",
179 "VTCCD",
180 "ST116 (GPIB)",
181 "OMA3 (GPIB)",
182 "OMA4",
183 ]
184
185 # This was gathered from random places on the internet and own experiments
186 # with the camera. May not be accurate.
187 readout_modes = ["full frame", "frame transfer", "kinetics"]
188
189 # Do not decode the following metadata keys into strings, but leave them
190 # as byte arrays
191 no_decode = ["spare_4"]
192
193
194class SDTControlSpec:
195 """Extract metadata written by the SDT-control software
196
197 Some of it is encoded in the comment strings
198 (see :py:meth:`parse_comments`). Also, date and time are encoded in a
199 peculiar way (see :py:meth:`get_datetime`). Use :py:meth:`extract_metadata`
200 to update the metadata dict.
201 """
202
203 months = {
204 # Convert SDT-control month strings to month numbers
205 "Jän": 1,
206 "Jan": 1,
207 "Feb": 2,
208 "Mär": 3,
209 "Mar": 3,
210 "Apr": 4,
211 "Mai": 5,
212 "May": 5,
213 "Jun": 6,
214 "Jul": 7,
215 "Aug": 8,
216 "Sep": 9,
217 "Okt": 10,
218 "Oct": 10,
219 "Nov": 11,
220 "Dez": 12,
221 "Dec": 12,
222 }
223
224 sequence_types = {
225 # TODO: complete
226 "SEQU": "standard",
227 "SETO": "TOCCSL",
228 "KINE": "kinetics",
229 "SEAR": "arbitrary",
230 }
231
232 class CommentDesc:
233 """Describe how to extract a metadata entry from a comment string"""
234
235 n: int
236 """Which of the 5 SPE comment fields to use."""
237 slice: slice
238 """Which characters from the `n`-th comment to use."""
239 cvt: Callable[[str], Any]
240 """How to convert characters to something useful."""
241 scale: Union[None, float]
242 """Optional scaling factor for numbers"""
243
244 def __init__(
245 self,
246 n: int,
247 slice: slice,
248 cvt: Callable[[str], Any] = str,
249 scale: Optional[float] = None,
250 ):
251 self.n = n
252 self.slice = slice
253 self.cvt = cvt
254 self.scale = scale
255
256 comment_fields = {
257 (5, 0): {
258 "sdt_major_version": CommentDesc(4, slice(66, 68), int),
259 "sdt_minor_version": CommentDesc(4, slice(68, 70), int),
260 "sdt_controller_name": CommentDesc(4, slice(0, 6), str),
261 "exposure_time": CommentDesc(1, slice(64, 73), float, 10**-6),
262 "color_code": CommentDesc(4, slice(10, 14), str),
263 "detection_channels": CommentDesc(4, slice(15, 16), int),
264 "background_subtraction": CommentDesc(4, 14, lambda x: x == "B"),
265 "em_active": CommentDesc(4, 32, lambda x: x == "E"),
266 "em_gain": CommentDesc(4, slice(28, 32), int),
267 "modulation_active": CommentDesc(4, 33, lambda x: x == "A"),
268 "pixel_size": CommentDesc(4, slice(25, 28), float, 0.1),
269 "sequence_type": CommentDesc(
270 4, slice(6, 10), lambda x: __class__.sequence_types[x]
271 ),
272 "grid": CommentDesc(4, slice(16, 25), float, 10**-6),
273 "n_macro": CommentDesc(1, slice(0, 4), int),
274 "delay_macro": CommentDesc(1, slice(10, 19), float, 10**-3),
275 "n_mini": CommentDesc(1, slice(4, 7), int),
276 "delay_mini": CommentDesc(1, slice(19, 28), float, 10**-6),
277 "n_micro": CommentDesc(1, slice(7, 10), int),
278 "delay_micro": CommentDesc(1, slice(28, 37), float, 10**-6),
279 "n_subpics": CommentDesc(1, slice(7, 10), int),
280 "delay_shutter": CommentDesc(1, slice(73, 79), float, 10**-6),
281 "delay_prebleach": CommentDesc(1, slice(37, 46), float, 10**-6),
282 "bleach_time": CommentDesc(1, slice(46, 55), float, 10**-6),
283 "recovery_time": CommentDesc(1, slice(55, 64), float, 10**-6),
284 },
285 (5, 1): {
286 "bleach_piezo_active": CommentDesc(4, slice(34, 35), lambda x: x == "z")
287 },
288 }
289
290 @staticmethod
291 def get_comment_version(comments: Sequence[str]) -> Tuple[int, int]:
292 """Get the version of SDT-control metadata encoded in the comments
293
294 Parameters
295 ----------
296 comments
297 List of SPE file comments, typically ``metadata["comments"]``.
298
299 Returns
300 -------
301 Major and minor version. ``-1, -1`` if detection failed.
302 """
303 if comments[4][70:76] != "COMVER":
304 return -1, -1
305 try:
306 return int(comments[4][76:78]), int(comments[4][78:80])
307 except ValueError:
308 return -1, -1
309
310 @staticmethod
311 def parse_comments(
312 comments: Sequence[str], version: Tuple[int, int]
313 ) -> Dict[str, Any]:
314 """Extract SDT-control metadata from comments
315
316 Parameters
317 ----------
318 comments
319 List of SPE file comments, typically ``metadata["comments"]``.
320 version
321 Major and minor version of SDT-control metadata format
322
323 Returns
324 -------
325 Dict of metadata
326 """
327 sdt_md = {}
328 for minor in range(version[1] + 1):
329 # Metadata with same major version is backwards compatible.
330 # Fields are specified incrementally in `comment_fields`.
331 # E.g. if the file has version 5.01, `comment_fields[5, 0]` and
332 # `comment_fields[5, 1]` need to be decoded.
333 try:
334 cmt = __class__.comment_fields[version[0], minor]
335 except KeyError:
336 continue
337 for name, spec in cmt.items():
338 try:
339 v = spec.cvt(comments[spec.n][spec.slice])
340 if spec.scale is not None:
341 v *= spec.scale
342 sdt_md[name] = v
343 except Exception as e:
344 warnings.warn(
345 f"Failed to decode SDT-control metadata field `{name}`: {e}"
346 )
347 sdt_md[name] = None
348 if version not in __class__.comment_fields:
349 supported_ver = ", ".join(
350 map(lambda x: f"{x[0]}.{x[1]:02}", __class__.comment_fields)
351 )
352 warnings.warn(
353 f"Unsupported SDT-control metadata version {version[0]}.{version[1]:02}. "
354 f"Only versions {supported_ver} are supported. "
355 "Some or all SDT-control metadata may be missing."
356 )
357 comment = comments[0] + comments[2]
358 sdt_md["comment"] = comment.strip()
359 return sdt_md
360
361 @staticmethod
362 def get_datetime(date: str, time: str) -> Union[datetime, None]:
363 """Turn date and time saved by SDT-control into proper datetime object
364
365 Parameters
366 ----------
367 date
368 SPE file date, typically ``metadata["date"]``.
369 time
370 SPE file date, typically ``metadata["time_local"]``.
371
372 Returns
373 -------
374 File's datetime if parsing was succsessful, else None.
375 """
376 try:
377 month = __class__.months[date[2:5]]
378 return datetime(
379 int(date[5:9]),
380 month,
381 int(date[0:2]),
382 int(time[0:2]),
383 int(time[2:4]),
384 int(time[4:6]),
385 )
386 except Exception as e:
387 logger.info(f"Failed to decode date from SDT-control metadata: {e}.")
388
389 @staticmethod
390 def extract_metadata(meta: Mapping, char_encoding: str = "latin1"):
391 """Extract SDT-control metadata from SPE metadata
392
393 SDT-control stores some metadata in comments and other fields.
394 Extract them and remove unused entries.
395
396 Parameters
397 ----------
398 meta
399 SPE file metadata. Modified in place.
400 char_encoding
401 Character encoding used to decode strings in the metadata.
402 """
403 comver = __class__.get_comment_version(meta["comments"])
404 if any(c < 0 for c in comver):
405 # This file most likely was not created by SDT-control
406 logger.debug("SDT-control comments not found.")
407 return
408
409 sdt_meta = __class__.parse_comments(meta["comments"], comver)
410 meta.pop("comments")
411 meta.update(sdt_meta)
412
413 # Get date and time in a usable format
414 dt = __class__.get_datetime(meta["date"], meta["time_local"])
415 if dt:
416 meta["datetime"] = dt
417 meta.pop("date")
418 meta.pop("time_local")
419
420 sp4 = meta["spare_4"]
421 try:
422 meta["modulation_script"] = sp4.decode(char_encoding)
423 meta.pop("spare_4")
424 except UnicodeDecodeError:
425 warnings.warn(
426 "Failed to decode SDT-control laser "
427 "modulation script. Bad char_encoding?"
428 )
429
430 # Get rid of unused data
431 meta.pop("time_utc")
432 meta.pop("exposure_sec")
433
434
435class SpePlugin(PluginV3):
436 def __init__(
437 self,
438 request: Request,
439 check_filesize: bool = True,
440 char_encoding: Optional[str] = None,
441 sdt_meta: Optional[bool] = None,
442 ) -> None:
443 """Instantiate a new SPE file plugin object
444
445 Parameters
446 ----------
447 request : Request
448 A request object representing the resource to be operated on.
449 check_filesize : bool
450 If True, compute the number of frames from the filesize, compare it
451 to the frame count in the file header, and raise a warning if the
452 counts don't match. (Certain software may create files with
453 char_encoding : str
454 Deprecated. Exists for backwards compatibility; use ``char_encoding`` of
455 ``metadata`` instead.
456 sdt_meta : bool
457 Deprecated. Exists for backwards compatibility; use ``sdt_control`` of
458 ``metadata`` instead.
459
460 """
461
462 super().__init__(request)
463 if request.mode.io_mode == IOMode.write:
464 raise InitializationError("cannot write SPE files")
465
466 if char_encoding is not None:
467 warnings.warn(
468 "Passing `char_encoding` to the constructor is deprecated. "
469 "Use `char_encoding` parameter of the `metadata()` method "
470 "instead.",
471 DeprecationWarning,
472 )
473 self._char_encoding = char_encoding
474 if sdt_meta is not None:
475 warnings.warn(
476 "Passing `sdt_meta` to the constructor is deprecated. "
477 "Use `sdt_control` parameter of the `metadata()` method "
478 "instead.",
479 DeprecationWarning,
480 )
481 self._sdt_meta = sdt_meta
482
483 self._file = self.request.get_file()
484
485 try:
486 # Spec.basic contains no string, no need to worry about character
487 # encoding.
488 info = self._parse_header(Spec.basic, "latin1")
489 self._file_header_ver = info["file_header_ver"]
490 self._dtype = Spec.dtypes[info["datatype"]]
491 self._shape = (info["ydim"], info["xdim"])
492 self._len = info["NumFrames"]
493
494 if check_filesize:
495 # Some software writes incorrect `NumFrames` metadata.
496 # To determine the number of frames, check the size of the data
497 # segment -- until the end of the file for SPE<3, until the
498 # xml footer for SPE>=3.
499 if info["file_header_ver"] >= 3:
500 data_end = info["xml_footer_offset"]
501 else:
502 self._file.seek(0, os.SEEK_END)
503 data_end = self._file.tell()
504 line = data_end - Spec.data_start
505 line //= self._shape[0] * self._shape[1] * self._dtype.itemsize
506 if line != self._len:
507 warnings.warn(
508 f"The file header of {self.request.filename} claims there are "
509 f"{self._len} frames, but there are actually {line} frames."
510 )
511 self._len = min(line, self._len)
512 self._file.seek(Spec.data_start)
513 except Exception:
514 raise InitializationError("SPE plugin cannot read the provided file.")
515
516 def read(self, *, index: int = ...) -> np.ndarray:
517 """Read a frame or all frames from the file
518
519 Parameters
520 ----------
521 index : int
522 Select the index-th frame from the file. If index is `...`,
523 select all frames and stack them along a new axis.
524
525 Returns
526 -------
527 A Numpy array of pixel values.
528
529 """
530
531 if index is Ellipsis:
532 read_offset = Spec.data_start
533 count = self._shape[0] * self._shape[1] * self._len
534 out_shape = (self._len, *self._shape)
535 elif index < 0:
536 raise IndexError(f"Index `{index}` is smaller than 0.")
537 elif index >= self._len:
538 raise IndexError(
539 f"Index `{index}` exceeds the number of frames stored in this file (`{self._len}`)."
540 )
541 else:
542 read_offset = (
543 Spec.data_start
544 + index * self._shape[0] * self._shape[1] * self._dtype.itemsize
545 )
546 count = self._shape[0] * self._shape[1]
547 out_shape = self._shape
548
549 self._file.seek(read_offset)
550 data = np.fromfile(self._file, dtype=self._dtype, count=count)
551 return data.reshape(out_shape)
552
553 def iter(self) -> Iterator[np.ndarray]:
554 """Iterate over the frames in the file
555
556 Yields
557 ------
558 A Numpy array of pixel values.
559 """
560
561 return (self.read(index=i) for i in range(self._len))
562
563 def metadata(
564 self,
565 index: int = ...,
566 exclude_applied: bool = True,
567 char_encoding: str = "latin1",
568 sdt_control: bool = True,
569 ) -> Dict[str, Any]:
570 """SPE specific metadata.
571
572 Parameters
573 ----------
574 index : int
575 Ignored as SPE files only store global metadata.
576 exclude_applied : bool
577 Ignored. Exists for API compatibility.
578 char_encoding : str
579 The encoding to use when parsing strings.
580 sdt_control : bool
581 If `True`, decode special metadata written by the
582 SDT-control software if present.
583
584 Returns
585 -------
586 metadata : dict
587 Key-value pairs of metadata.
588
589 Notes
590 -----
591 SPE v3 stores metadata as XML, whereas SPE v2 uses a binary format.
592
593 .. rubric:: Supported SPE v2 Metadata fields
594
595 ROIs : list of dict
596 Regions of interest used for recording images. Each dict has the
597 "top_left" key containing x and y coordinates of the top left corner,
598 the "bottom_right" key with x and y coordinates of the bottom right
599 corner, and the "bin" key with number of binned pixels in x and y
600 directions.
601 comments : list of str
602 The SPE format allows for 5 comment strings of 80 characters each.
603 controller_version : int
604 Hardware version
605 logic_output : int
606 Definition of output BNC
607 amp_hi_cap_low_noise : int
608 Amp switching mode
609 mode : int
610 Timing mode
611 exp_sec : float
612 Alternative exposure in seconds
613 date : str
614 Date string
615 detector_temp : float
616 Detector temperature
617 detector_type : int
618 CCD / diode array type
619 st_diode : int
620 Trigger diode
621 delay_time : float
622 Used with async mode
623 shutter_control : int
624 Normal, disabled open, or disabled closed
625 absorb_live : bool
626 on / off
627 absorb_mode : int
628 Reference strip or file
629 can_do_virtual_chip : bool
630 True or False whether chip can do virtual chip
631 threshold_min_live : bool
632 on / off
633 threshold_min_val : float
634 Threshold minimum value
635 threshold_max_live : bool
636 on / off
637 threshold_max_val : float
638 Threshold maximum value
639 time_local : str
640 Experiment local time
641 time_utc : str
642 Experiment UTC time
643 adc_offset : int
644 ADC offset
645 adc_rate : int
646 ADC rate
647 adc_type : int
648 ADC type
649 adc_resolution : int
650 ADC resolution
651 adc_bit_adjust : int
652 ADC bit adjust
653 gain : int
654 gain
655 sw_version : str
656 Version of software which created this file
657 spare_4 : bytes
658 Reserved space
659 readout_time : float
660 Experiment readout time
661 type : str
662 Controller type
663 clockspeed_us : float
664 Vertical clock speed in microseconds
665 readout_mode : ["full frame", "frame transfer", "kinetics", ""]
666 Readout mode. Empty string means that this was not set by the
667 Software.
668 window_size : int
669 Window size for Kinetics mode
670 file_header_ver : float
671 File header version
672 chip_size : [int, int]
673 x and y dimensions of the camera chip
674 virt_chip_size : [int, int]
675 Virtual chip x and y dimensions
676 pre_pixels : [int, int]
677 Pre pixels in x and y dimensions
678 post_pixels : [int, int],
679 Post pixels in x and y dimensions
680 geometric : list of {"rotate", "reverse", "flip"}
681 Geometric operations
682 sdt_major_version : int
683 (only for files created by SDT-control)
684 Major version of SDT-control software
685 sdt_minor_version : int
686 (only for files created by SDT-control)
687 Minor version of SDT-control software
688 sdt_controller_name : str
689 (only for files created by SDT-control)
690 Controller name
691 exposure_time : float
692 (only for files created by SDT-control)
693 Exposure time in seconds
694 color_code : str
695 (only for files created by SDT-control)
696 Color channels used
697 detection_channels : int
698 (only for files created by SDT-control)
699 Number of channels
700 background_subtraction : bool
701 (only for files created by SDT-control)
702 Whether background subtraction war turned on
703 em_active : bool
704 (only for files created by SDT-control)
705 Whether EM was turned on
706 em_gain : int
707 (only for files created by SDT-control)
708 EM gain
709 modulation_active : bool
710 (only for files created by SDT-control)
711 Whether laser modulation (“attenuate”) was turned on
712 pixel_size : float
713 (only for files created by SDT-control)
714 Camera pixel size
715 sequence_type : str
716 (only for files created by SDT-control)
717 Type of sequnce (standard, TOCCSL, arbitrary, …)
718 grid : float
719 (only for files created by SDT-control)
720 Sequence time unit (“grid size”) in seconds
721 n_macro : int
722 (only for files created by SDT-control)
723 Number of macro loops
724 delay_macro : float
725 (only for files created by SDT-control)
726 Time between macro loops in seconds
727 n_mini : int
728 (only for files created by SDT-control)
729 Number of mini loops
730 delay_mini : float
731 (only for files created by SDT-control)
732 Time between mini loops in seconds
733 n_micro : int (only for files created by SDT-control)
734 Number of micro loops
735 delay_micro : float (only for files created by SDT-control)
736 Time between micro loops in seconds
737 n_subpics : int
738 (only for files created by SDT-control)
739 Number of sub-pictures
740 delay_shutter : float
741 (only for files created by SDT-control)
742 Camera shutter delay in seconds
743 delay_prebleach : float
744 (only for files created by SDT-control)
745 Pre-bleach delay in seconds
746 bleach_time : float
747 (only for files created by SDT-control)
748 Bleaching time in seconds
749 recovery_time : float
750 (only for files created by SDT-control)
751 Recovery time in seconds
752 comment : str
753 (only for files created by SDT-control)
754 User-entered comment. This replaces the "comments" field.
755 datetime : datetime.datetime
756 (only for files created by SDT-control)
757 Combines the "date" and "time_local" keys. The latter two plus
758 "time_utc" are removed.
759 modulation_script : str
760 (only for files created by SDT-control)
761 Laser modulation script. Replaces the "spare_4" key.
762 bleach_piezo_active : bool
763 (only for files created by SDT-control)
764 Whether piezo for bleaching was enabled
765 """
766
767 if self._file_header_ver < 3:
768 if self._char_encoding is not None:
769 char_encoding = self._char_encoding
770 if self._sdt_meta is not None:
771 sdt_control = self._sdt_meta
772 return self._metadata_pre_v3(char_encoding, sdt_control)
773 return self._metadata_post_v3()
774
775 def _metadata_pre_v3(self, char_encoding: str, sdt_control: bool) -> Dict[str, Any]:
776 """Extract metadata from SPE v2 files
777
778 Parameters
779 ----------
780 char_encoding
781 String character encoding
782 sdt_control
783 If `True`, try to decode special metadata written by the
784 SDT-control software.
785
786 Returns
787 -------
788 dict mapping metadata names to values.
789
790 """
791
792 m = self._parse_header(Spec.metadata, char_encoding)
793
794 nr = m.pop("NumROI", None)
795 nr = 1 if nr < 1 else nr
796 m["ROIs"] = roi_array_to_dict(m["ROIs"][:nr])
797
798 # chip sizes
799 m["chip_size"] = [m.pop(k, None) for k in ("xDimDet", "yDimDet")]
800 m["virt_chip_size"] = [m.pop(k, None) for k in ("VChipXdim", "VChipYdim")]
801 m["pre_pixels"] = [m.pop(k, None) for k in ("XPrePixels", "YPrePixels")]
802 m["post_pixels"] = [m.pop(k, None) for k in ("XPostPixels", "YPostPixels")]
803
804 # convert comments from numpy.str_ to str
805 m["comments"] = [str(c) for c in m["comments"]]
806
807 # geometric operations
808 g = []
809 f = m.pop("geometric", 0)
810 if f & 1:
811 g.append("rotate")
812 if f & 2:
813 g.append("reverse")
814 if f & 4:
815 g.append("flip")
816 m["geometric"] = g
817
818 # Make some additional information more human-readable
819 t = m["type"]
820 if 1 <= t <= len(Spec.controllers):
821 m["type"] = Spec.controllers[t - 1]
822 else:
823 m["type"] = None
824 r = m["readout_mode"]
825 if 1 <= r <= len(Spec.readout_modes):
826 m["readout_mode"] = Spec.readout_modes[r - 1]
827 else:
828 m["readout_mode"] = None
829
830 # bools
831 for k in (
832 "absorb_live",
833 "can_do_virtual_chip",
834 "threshold_min_live",
835 "threshold_max_live",
836 ):
837 m[k] = bool(m[k])
838
839 # Extract SDT-control metadata if desired
840 if sdt_control:
841 SDTControlSpec.extract_metadata(m, char_encoding)
842
843 return m
844
845 def _metadata_post_v3(self) -> Dict[str, Any]:
846 """Extract XML metadata from SPE v3 files
847
848 Returns
849 -------
850 dict with key `"__xml"`, whose value is the XML metadata
851 """
852
853 info = self._parse_header(Spec.basic, "latin1")
854 self._file.seek(info["xml_footer_offset"])
855 xml = self._file.read()
856 return {"__xml": xml}
857
858 def properties(self, index: int = ...) -> ImageProperties:
859 """Standardized ndimage metadata.
860
861 Parameters
862 ----------
863 index : int
864 If the index is an integer, select the index-th frame and return
865 its properties. If index is an Ellipsis (...), return the
866 properties of all frames in the file stacked along a new batch
867 dimension.
868
869 Returns
870 -------
871 properties : ImageProperties
872 A dataclass filled with standardized image metadata.
873 """
874
875 if index is Ellipsis:
876 return ImageProperties(
877 shape=(self._len, *self._shape),
878 dtype=self._dtype,
879 n_images=self._len,
880 is_batch=True,
881 )
882 return ImageProperties(shape=self._shape, dtype=self._dtype, is_batch=False)
883
884 def _parse_header(
885 self, spec: Mapping[str, Tuple], char_encoding: str
886 ) -> Dict[str, Any]:
887 """Get information from SPE file header
888
889 Parameters
890 ----------
891 spec
892 Maps header entry name to its location, data type description and
893 optionally number of entries. See :py:attr:`Spec.basic` and
894 :py:attr:`Spec.metadata`.
895 char_encoding
896 String character encoding
897
898 Returns
899 -------
900 Dict mapping header entry name to its value
901 """
902
903 ret = {}
904 # Decode each string from the numpy array read by np.fromfile
905 decode = np.vectorize(lambda x: x.decode(char_encoding))
906
907 for name, sp in spec.items():
908 self._file.seek(sp[0])
909 cnt = 1 if len(sp) < 3 else sp[2]
910 v = np.fromfile(self._file, dtype=sp[1], count=cnt)
911 if v.dtype.kind == "S" and name not in Spec.no_decode:
912 # Silently ignore string decoding failures
913 try:
914 v = decode(v)
915 except Exception:
916 warnings.warn(
917 f'Failed to decode "{name}" metadata '
918 "string. Check `char_encoding` parameter."
919 )
920
921 try:
922 # For convenience, if the array contains only one single
923 # entry, return this entry itself.
924 v = v.item()
925 except ValueError:
926 v = np.squeeze(v)
927 ret[name] = v
928 return ret
929
930
931def roi_array_to_dict(a: np.ndarray) -> List[Dict[str, List[int]]]:
932 """Convert the `ROIs` structured arrays to :py:class:`dict`
933
934 Parameters
935 ----------
936 a
937 Structured array containing ROI data
938
939 Returns
940 -------
941 One dict per ROI. Keys are "top_left", "bottom_right", and "bin",
942 values are tuples whose first element is the x axis value and the
943 second element is the y axis value.
944 """
945
946 dict_list = []
947 a = a[["startx", "starty", "endx", "endy", "groupx", "groupy"]]
948 for sx, sy, ex, ey, gx, gy in a:
949 roi_dict = {
950 "top_left": [int(sx), int(sy)],
951 "bottom_right": [int(ex), int(ey)],
952 "bin": [int(gx), int(gy)],
953 }
954 dict_list.append(roi_dict)
955 return dict_list