Source code for neuralib.scanbox.core

from __future__ import annotations

import dataclasses
import json
from dataclasses import dataclass, field
from pathlib import Path
from pprint import pprint
from typing import Any, TypedDict, TypeVar, cast

import numpy as np
from neuralib.io import JsonEncodeHandler
from neuralib.typing import PathLike
from scipy.io import loadmat

__all__ = [
    'read_scanbox',
    'SBXInfo',
    'sbx_to_json',
    #
    'screenshot_to_tiff'
]


[docs] def read_scanbox(file: PathLike) -> SBXInfo: """ load scanbox file :param file: scanbox .mat file :return: SBXInfo """ return SBXInfo.load(file)
T = TypeVar('T') # dataclass def copy_from(t: type[T], o: T, **specials) -> T: a = [] for f in dataclasses.fields(cast(Any, t)): if f.name in specials: a.append(specials[f.name]) else: a.append(getattr(o, f.name)) return t(*a)
[docs] @dataclass(frozen=True) class SBXInfo: """for each recording session, `exp.mat` from scanbox""" scanbox_version: str objective: str abort_bit: int area_line: int ballmotion: np.ndarray bytesPerBuffer: int scanmode: int config: ConfigInfo calibration: list[CalibrationInfo] channels: int messages: np.ndarray opto2pow: np.ndarray otparam: np.ndarray # ETL setting? otwave: np.ndarray otwave_um: np.ndarray otwavestyle: int postTriggerSamples: int power_depth_link: int recordsPerBuffer: int resfreq: int sz: np.ndarray # line/frames? usernotes: np.ndarray volscan: int # volumetric img? bool? # opt nchan: int | None @property def magidx(self) -> int: """info.config.magnification, used for `CalibrationInfo` idx""" return self.config.magnification - 1
[docs] def asdict(self): from dataclasses import asdict return asdict(self)
# =============================== # # attributes from SBX .mat output # # =============================== #
[docs] @dataclass(frozen=True) class CalibrationInfo: """attr from calibration""" x: float y: float gain_resonant_mult: int uv: list = field(default_factory=list) delta: list = field(default_factory=list)
[docs] @dataclass(frozen=True) class AgcInfo: """attr from info.config.agc""" agc_prctile: np.ndarray enable: int threshold: int
[docs] @dataclass(frozen=True) class KnobbyPosInfo: """attr from info.config.knobby.pos manipulator coordinates """ a: float x: float y: float z: float
# noinspection PyUnresolvedReferences
[docs] @dataclass(frozen=True) class KnobbyInfo: """attr from info.config.knobby""" pos: SBXInfo.KnobbyPosInfo schedule: np.ndarray
[docs] @dataclass(frozen=True) class ObjectiveInfo: name: str
# noinspection PyUnresolvedReferences
[docs] @dataclass(frozen=True) class ConfigInfo: """attr from config""" agc: SBXInfo.AgcInfo # calibration: np.ndarray coord_abs: np.ndarray coord_rel: np.ndarray frame_times: np.ndarray frames: int # total frames host_name: str # BSTATION6 knobby: SBXInfo.KnobbyInfo laser_power: float laser_power_perc: str # 75% lines: int # 528 magnification: int # idx from 1 in magnification_list magnification_list: np.ndarray objective: SBXInfo.ObjectiveInfo # directly get useful attr `name` objective_type: int pmt0_gain: float # green channel pmt1_gain: float # red channel wavelength: int # laser wavelength. i.e., 920 nm
[docs] @classmethod def load(cls, file: PathLike) -> SBXInfo: info = loadmat(file, squeeze_me=True, struct_as_record=False)['info'] try: nchan = info.chan.nchan # version >= 3 except AttributeError: nchan = None return copy_from(SBXInfo, info, scanbox_version=str(info.scanbox_version), config=copy_from(cls.ConfigInfo, (config := info.config), agc=copy_from(cls.AgcInfo, config.agc), host_name=getattr(config, 'host_name', ''), knobby=copy_from(cls.KnobbyInfo, (knobby := config.knobby), pos=copy_from(cls.KnobbyPosInfo, (pos := knobby.pos), a=pos.x)), objective=copy_from(cls.ObjectiveInfo, config.objective)), calibration=[copy_from(cls.CalibrationInfo, cali) for cali in info.calibration], nchan=nchan)
# =============================== # # attributes from SBX .mat output # # =============================== # @property def fov_distance(self) -> tuple[float, float]: """(X, Y) in um. Note this value might be hardware dependent. value return is internal usage for the lab """ lines = self.config.lines if self.objective == 'Nikon 16x/0.8w/WD3.0': obj = 16 else: raise NotImplementedError('') zoom = float(self.config.magnification_list[self.magidx]) return _get_default_scanbox_fov_dimension(lines, obj, zoom) def _validate_fov_distance(self) -> bool: """due to this is the info only seen in GUI""" return ( self.objective == 'Nikon 16x/0.8w/WD3.0' and self.config.lines == 528 and self.config.magnification_list[self.magidx] == '1.7' )
def _get_default_scanbox_fov_dimension(lines: int, obj_type: int, zoom: float) -> tuple[float, float]: """ Hardware/settings dependent fov size according to recording configuration :param lines: number of lines for the scanning fov :param obj_type: objective magnification. i.e., 16X :param zoom: zoom setting during acquisition :return: (X, Y) in um """ # ~ 30hz if lines == 528 and obj_type == 16: if zoom == 1: return 1396, 1056 elif zoom == 1.2: return 1284, 978 elif zoom == 1.4: return 1023, 765 elif zoom == 1.7: return 892, 667 elif zoom == 2.0: return 716, 531 elif zoom == 2.4: return 632, 463 raise NotImplementedError('check scanbox GUI directly')
[docs] def sbx_to_json(matfile: PathLike, output: Path | None = None, verbose: bool = True) -> None: """ save .mat scanbox output file as json file :param matfile: .mat filepath :param output: output filepath, if None, create a json file in the same directory :param verbose: pprint as dict :return: """ if isinstance(matfile, str): matfile = Path(matfile) else: matfile = Path(matfile) mat = SBXInfo.load(matfile) if output is None: output = matfile.with_name('sbx').with_suffix('.json') dy = dataclasses.asdict(mat) if verbose: pprint(dy) with open(output, "w") as outfile: json.dump(dy, outfile, sort_keys=True, indent=4, cls=JsonEncodeHandler)
# ========== # # ScreenShot # # ========== # class SBXScreenShot(TypedDict): __header__: str __version__: str __globals__: str img: np.ndarray
[docs] def screenshot_to_tiff(mat_file: PathLike, output: PathLike | None = None) -> None: """ Scanbox screenshot result (.mat) to tif file :param mat_file: :param output: save output path, otherwise show :return: """ dat = cast(SBXScreenShot, loadmat(mat_file)) img = dat['img'] if output is None: import matplotlib.pyplot as plt plt.imshow(img) plt.show() else: import tifffile tifffile.imwrite(output, img)