diff --git a/.codespellignore b/.codespellignore index 0672eda0b..7e3714bf4 100644 --- a/.codespellignore +++ b/.codespellignore @@ -3,3 +3,6 @@ afterAll xdescribe optinist Optinist +nnumber +nNumber +NNUMBER diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed3599e55..9311b7000 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,5 +28,6 @@ repos: rev: v2.2.4 hooks: - id: codespell + args: [-I, .codespellignore] additional_dependencies: - tomli diff --git a/studio/app/optinist/microscopes/.gitignore b/studio/app/optinist/microscopes/.gitignore new file mode 100644 index 000000000..1c54ff51d --- /dev/null +++ b/studio/app/optinist/microscopes/.gitignore @@ -0,0 +1,7 @@ +dll/ +testdata/ + +/*.out.json +/*.out.tiff +/*.out.*.json +/*.out.*.tiff diff --git a/studio/app/optinist/microscopes/IsxdReader.py b/studio/app/optinist/microscopes/IsxdReader.py new file mode 100644 index 000000000..c0bc36415 --- /dev/null +++ b/studio/app/optinist/microscopes/IsxdReader.py @@ -0,0 +1,99 @@ +import sys + +try: + import isx +except ModuleNotFoundError: + pass + +from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel + + +class IsxdReader(MicroscopeDataReaderBase): + """Inscopix isxd data reader""" + + SDK_MODULE_NAME = "isx" + + @staticmethod + def get_library_path() -> str: + """Returns the path of the library (dll) file""" + # Note: In inscopix SDK, library (ddl) files are not used directly. + return None # do nothing. + + @staticmethod + def is_available() -> bool: + """Determine if library is available""" + return __class__.SDK_MODULE_NAME in sys.modules + + def _init_library(self): + # Note: in inscopix sdk, there is no library (ddl) + # initialization process. (using pip module) + pass # do nothing. + + def _load_file(self, data_file_path: str) -> object: + handle = isx.Movie.read(data_file_path) + return (handle,) + + def _build_original_metadata(self, data_name: str) -> dict: + movie: isx.Movie = None + (movie,) = self.resource_handles + + spacing: isx.Spacing = movie.spacing + timing: isx.Timing = movie.timing + + original_metadata = { + "data_name": data_name, + "spacing": { + "width": spacing.num_pixels[0], + "height": spacing.num_pixels[1], + }, + "timing": { + "start": timing.start.to_datetime().strftime("%Y-%m-%d %H:%M:%S"), + "period_msec": timing.period.to_msecs(), + "num_samples": timing.num_samples, + "dropped": timing.dropped, + "cropped": timing.cropped, + "blank": timing.blank, + }, + } + + return original_metadata + + def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: + """ + @link OME/NativeND2Reader + """ + + spacing = original_metadata["spacing"] + timing = original_metadata["timing"] + + omeData = OMEDataModel( + image_name=original_metadata["data_name"], + size_x=spacing["width"], + size_y=spacing["height"], + size_t=timing["num_samples"], + size_z=0, # Note: currently unsettled + size_c=0, # Note: currently unsettled + acquisition_date=timing["start"], + objective_model=None, # Note: currently unsettled + fps=(1000 / timing["period_msec"]), + ) + + return omeData + + def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: + # Note: Not currently supported + return None + + def _release_resources(self) -> None: + # Note: in inscopix sdk, there is no library (ddl) release process. + pass # do nothing. + + def _get_image_stacks(self) -> list: + movie: isx.Movie = None + (movie,) = self.resource_handles + + image_frames = [ + movie.get_frame_data(i) for i in range(movie.timing.num_samples) + ] + + return image_frames diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py new file mode 100644 index 000000000..2006126ec --- /dev/null +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -0,0 +1,137 @@ +import os +from abc import ABCMeta, abstractmethod +from dataclasses import dataclass + + +@dataclass +class OMEDataModel: + """OME(Open Microscopy Environment) aware metadata + @link https://ome-model.readthedocs.io/en/stable/ome-xml/ + """ + + image_name: str + size_x: int # width + size_y: int # height + size_t: int # time frames + size_z: int # axis z frames + size_c: int # channels + acquisition_date: str + objective_model: str # objective lens model + fps: int # frames_per_second # Note: extended from OME format + + +class MicroscopeDataReaderBase(metaclass=ABCMeta): + """Microscope data reader base class""" + + LIBRARY_DIR_KEY = "MICROSCOPES_LIBRARY_DIR" + + def __init__(self): + """ + Initialization + """ + self._init_library() + + # init members + self.__data_file_path = None + self.__resource_handles = None + self.__original_metadata = None + self.__ome_metadata = None + self.__lab_specific_metadata = None + + def __del__(self): + """ + Destructor + """ + if self.__resource_handles is not None: + self._release_resources() + self.__resource_handles = None + + def load(self, data_file_path: str): + """ + Release resources + """ + if self.__resource_handles is not None: + raise Exception("Reader module already initialized.") + + """ + Reset data + """ + self.__original_metadata = None + self.__ome_metadata = None + self.__lab_specific_metadata = None + + """ + Load data file + """ + handles = self._load_file(data_file_path) + self.__resource_handles = handles + self.__data_file_path = data_file_path + data_name = os.path.basename(data_file_path) + + """ + Read metadata + """ + self.__original_metadata = self._build_original_metadata(data_name) + self.__ome_metadata = self._build_ome_metadata(self.__original_metadata) + self.__lab_specific_metadata = self._build_lab_specific_metadata( + self.__original_metadata + ) + + def get_image_stacks(self) -> list: + """Return microscope image stacks""" + return self._get_image_stacks() + + @abstractmethod + def _init_library(self) -> dict: + """Initialize microscope library""" + pass + + @abstractmethod + def _load_file(self, data_file_path: str) -> object: + """Return metadata specific to microscope instruments""" + pass + + @abstractmethod + def _build_original_metadata(self, data_name: str) -> dict: + """Build metadata specific to microscope instruments""" + pass + + @abstractmethod + def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: + """Build OME(Open Microscopy Environment) aware metadata""" + pass + + @abstractmethod + def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: + """Build metadata in lab-specific format""" + pass + + @abstractmethod + def _release_resources() -> None: + """Release microscope library resources""" + pass + + @abstractmethod + def _get_image_stacks(self) -> list: + """Return microscope image stacks""" + pass + + @property + def data_file_path(self) -> str: + return self.__data_file_path + + @property + def resource_handles(self) -> list: + return self.__resource_handles + + @property + def original_metadata(self) -> dict: + return self.__original_metadata + + @property + def ome_metadata(self) -> dict: + return self.__ome_metadata + + @property + def lab_specific_metadata(self) -> dict: + return self.__lab_specific_metadata diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py new file mode 100644 index 000000000..0a12ccef1 --- /dev/null +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -0,0 +1,383 @@ +import ctypes +import json +import os +import platform +import re +from enum import Enum, IntEnum + +import numpy as np +from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel + + +class LimCode(IntEnum): + LIM_OK = 0 + LIM_ERR_UNEXPECTED = -1 + + +class ExperimentLoopType(Enum): + Unknown = "Unknown" + TimeLoop = "TimeLoop" + XYPosLoop = "XYPosLoop" + ZStackLoop = "ZStackLoop" + NETimeLoop = "NETimeLoop" + + +class DimensionType(Enum): + D_2D_SINGLE = "2Ds" + D_2D_MULTI = "2Dm" + D_3D_SINGLE = "3Ds" + D_3D_MULTI = "3Dm" + + +class LIMPICTURE(ctypes.Structure): + _fields_ = [ + ("uiWidth", ctypes.c_uint), + ("uiHeight", ctypes.c_uint), + ("uiBitsPerComp", ctypes.c_uint), + ("uiComponents", ctypes.c_uint), + ("uiWidthBytes", ctypes.c_size_t), + ("uiSize", ctypes.c_size_t), + ("pImageData", ctypes.c_void_p), + ] + + +class ND2Reader(MicroscopeDataReaderBase): + """Nikon ND2 data reader""" + + SDK_LIBRARY_FILES = { + "Windows": { + "main": "/nikon/windows/nd2readsdk-shared.dll", + }, + "Linux": { + "main": "/nikon/linux/libnd2readsdk-shared.so", + "dependencies": ("libjpeg.so.8", "libtiff.so.5"), + }, + } + + @staticmethod + def get_library_path() -> str: + """Returns the path of the library (dll) file""" + platform_name = platform.system() + + if __class__.LIBRARY_DIR_KEY not in os.environ: + return None + + if platform_name not in __class__.SDK_LIBRARY_FILES: + return None + + return ( + os.environ.get(__class__.LIBRARY_DIR_KEY) + + __class__.SDK_LIBRARY_FILES[platform_name]["main"] + ) + + @staticmethod + def is_available() -> bool: + """Determine if library is available""" + return (__class__.LIBRARY_DIR_KEY in os.environ) and os.path.isfile( + __class__.get_library_path() + ) + + def _init_library(self): + # load sdk libraries (dependencies) + if "dependencies" in __class__.SDK_LIBRARY_FILES[platform.system()]: + platform_library_dir = os.path.dirname(__class__.get_library_path()) + dependencies = __class__.SDK_LIBRARY_FILES[platform.system()][ + "dependencies" + ] + + for dependency in dependencies: + dependency_path = f"{platform_library_dir}/{dependency}" + ctypes.cdll.LoadLibrary(dependency_path) + + # load sdk library + self.__dll = ctypes.cdll.LoadLibrary(__class__.get_library_path()) + + # define ctypes interfaces + self.__dll.Lim_FileOpenForReadUtf8.argtypes = (ctypes.c_char_p,) + self.__dll.Lim_FileOpenForReadUtf8.restype = ctypes.c_void_p + self.__dll.Lim_FileGetMetadata.argtypes = (ctypes.c_void_p,) + self.__dll.Lim_FileGetMetadata.restype = ctypes.c_char_p + self.__dll.Lim_FileGetAttributes.argtypes = (ctypes.c_void_p,) + self.__dll.Lim_FileGetAttributes.restype = ctypes.c_char_p + self.__dll.Lim_FileGetTextinfo.argtypes = (ctypes.c_void_p,) + self.__dll.Lim_FileGetTextinfo.restype = ctypes.c_char_p + self.__dll.Lim_FileGetExperiment.argtypes = (ctypes.c_void_p,) + self.__dll.Lim_FileGetExperiment.restype = ctypes.c_char_p + self.__dll.Lim_FileGetFrameMetadata.argtypes = (ctypes.c_void_p, ctypes.c_uint) + self.__dll.Lim_FileGetFrameMetadata.restype = ctypes.c_char_p + self.__dll.Lim_FileGetSeqCount.argtypes = (ctypes.c_void_p,) + self.__dll.Lim_FileGetSeqCount.restype = ctypes.c_uint + self.__dll.Lim_FileGetCoordSize.argtypes = (ctypes.c_void_p,) + self.__dll.Lim_FileGetCoordSize.restype = ctypes.c_size_t + self.__dll.Lim_FileGetCoordsFromSeqIndex.argtypes = ( + ctypes.c_void_p, + ctypes.c_uint, + ) + self.__dll.Lim_FileGetCoordsFromSeqIndex.restype = ctypes.c_size_t + self.__dll.Lim_FileGetImageData.argtypes = ( + ctypes.c_void_p, + ctypes.c_uint, + ctypes.c_void_p, + ) + self.__dll.Lim_FileGetImageData.restype = ctypes.c_int + + self.__dll.Lim_FileClose.argtypes = (ctypes.c_void_p,) + + def _load_file(self, data_file_path: str) -> object: + handle = self.__dll.Lim_FileOpenForReadUtf8(data_file_path.encode("utf-8")) + + if handle is None: + raise FileNotFoundError(f"Open Error: {data_file_path}") + + return (handle,) + + def _build_original_metadata(self, data_name: str) -> dict: + (handle,) = self.resource_handles + + attributes = self.__dll.Lim_FileGetAttributes(handle) + metadata = self.__dll.Lim_FileGetMetadata(handle) + textinfo = self.__dll.Lim_FileGetTextinfo(handle) + experiments = self.__dll.Lim_FileGetExperiment(handle) + frame_metadata = self.__dll.Lim_FileGetFrameMetadata( + handle, 0 + ) # get 1st frame info + + attributes = json.loads(attributes) + metadata = json.loads(metadata) + textinfo = json.loads(textinfo) if textinfo is not None else None + experiments = json.loads(experiments) if experiments is not None else None + frame_metadata = json.loads(frame_metadata) + + # frame_metadata は、必要な項目のみに絞る + frame_metadata_single = frame_metadata["channels"][0] + + original_metadata = { + "data_name": data_name, + "attributes": attributes, + "metadata": metadata, + "textinfo": textinfo, + "experiments": experiments, + "frame_metadata": frame_metadata_single, + } + + return original_metadata + + def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: + """ + @link OME/NativeND2Reader + """ + + attributes = original_metadata["attributes"] + metadata = original_metadata["metadata"] + textinfo = original_metadata["textinfo"] + experiments = original_metadata["experiments"] + first_experiment_parameters = experiments[0]["parameters"] + metadata_ch0_microscope = metadata["channels"][0]["microscope"] + + # experiment, periods, の参照は先頭データの内容から取得 + if "periods" in first_experiment_parameters: + try: + fps = ( + 1000 + / first_experiment_parameters["periods"][0]["periodDiff"]["avg"] + ) + except: # noqa: E722 + fps = 1000 / first_experiment_parameters["periodDiff"]["avg"] + else: + fps = 0 + + omeData = OMEDataModel( + image_name=original_metadata["data_name"], + size_x=attributes["widthPx"], + size_y=attributes["heightPx"], + size_t=attributes["sequenceCount"], + size_z=0, # size_z は後続処理で計算・設定する + size_c=len(metadata["channels"]), + acquisition_date=re.sub(" +", " ", textinfo["date"]), + objective_model=metadata_ch0_microscope.get("objectiveName", None), + fps=fps, + ) + + return omeData + + def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: + """Build metadata in lab-specific format""" + + attributes = original_metadata["attributes"] + metadata = original_metadata["metadata"] + textinfo = original_metadata["textinfo"] + experiments = original_metadata["experiments"] + frame_metadata = original_metadata["frame_metadata"] + + # ---------------------------------------- + # データの次元タイプ別でのパラメータ構築 + # ---------------------------------------- + + experiments_count = len(experiments) if experiments is not None else 0 + dimension_type = None + loop_count = 0 + z_slicenum = 0 + z_interval = 0 + + if experiments_count == 0: + dimension_type = DimensionType.D_2D_SINGLE.value + elif experiments_count == 2: + dimension_type = DimensionType.D_3D_MULTI.value + loop_count = experiments[0]["count"] + z_slicenum = experiments[1]["count"] + z_interval = experiments[1]["parameters"]["stepUm"] + else: + if experiments[0]["type"] == ExperimentLoopType.NETimeLoop.value: + dimension_type = DimensionType.D_2D_MULTI.value + loop_count = experiments[0]["count"] + elif experiments[0]["type"] == ExperimentLoopType.ZStackLoop.value: + dimension_type = DimensionType.D_3D_SINGLE.value + z_slicenum = experiments[0]["count"] + z_interval = experiments[0]["parameters"]["stepUm"] + + # ※ome_metadata の一部項目をアップデート + self.ome_metadata.size_z = z_slicenum + + # ---------------------------------------- + # Lab固有metadata変数構築 + # ---------------------------------------- + + # ※一部のデータ項目は ch0 より取得 + metadata_ch0_microscope = metadata["channels"][0]["microscope"] + metadata_ch0_volume = metadata["channels"][0]["volume"] + axes_calibration_x = metadata_ch0_volume["axesCalibration"][0] + axes_calibration_y = metadata_ch0_volume["axesCalibration"][1] + + lab_specific_metadata = { + # /* 11 cinoibebts (From Lab MEX) */ + "uiWidth": attributes["widthPx"], + "uiHeight": attributes["heightPx"], + "uiWidthBytes": attributes["widthBytes"], + "uiColor": attributes["componentCount"], + "uiBpcInMemory": attributes["bitsPerComponentInMemory"], + "uiBpcSignificant": attributes["bitsPerComponentSignificant"], + "uiSequenceCount": attributes["sequenceCount"], + # TODO: ライブラリバージョンにより、取得値が異なっている可能性がある? (uiTileWidth) + "uiTileWidth": attributes.get("tileWidthPx", None), # Optional + # TODO: ライブラリバージョンにより、取得値が異なっている可能性がある? (uiTileHeight) + "uiTileHeight": attributes.get("tileHeightPx", None), # Optional + # TODO: 旧ライブラリでは、数値が設定されているが、旧ライブラリでは文字列 (uiCompression) + "uiCompression": attributes.get("compressionType", None), # Optional + # TODO: 旧ライブラリでは、適切ではない値が格納されている可能性がある? (uiQuality) + "uiQuality": attributes.get("compressionLevel", None), # Optional + # /* 4 components (From Lab MEX) */ + "Type": dimension_type, + "Loops": loop_count, + "ZSlicenum": z_slicenum, + "ZInterval": z_interval, + # /* 7 components (From Lab MEX) */ + "dTimeStart": frame_metadata["time"]["absoluteJulianDayNumber"], + # TODO: ライブラリバージョンにより、取得値が異なっている可能性がある? (MicroPerPixel) + "MicroPerPixel": axes_calibration_x, + "PixelAspect": axes_calibration_x / axes_calibration_y, + "ObjectiveName": metadata_ch0_microscope.get( + "objectiveName", None + ), # Optional, channels[0] からの取得 + # TODO: ライブラリバージョンにより、取得値が異なっている可能性がある? (dObjectiveMag) + "dObjectiveMag": metadata_ch0_microscope.get( + "objectiveMagnification", None + ), # Optional, channels[0] からの取得 + "dObjectiveNA": metadata_ch0_microscope.get( + "objectiveNumericalAperture", None + ), # Optional, channels[0] からの取得 + "dZoom": metadata_ch0_microscope.get( + "zoomMagnification", None + ), # Optional, channels[0] からの取得 + # /* 3 components (From Lab MEX) */ + # Note: All elements of textinfo are optional. + "wszDescription": textinfo.get("description", None), + "wszCapturing": textinfo.get("capturing", None), + "Date": textinfo.get("date", None), + } + + return lab_specific_metadata + + def _release_resources(self) -> None: + (handle,) = self.resource_handles + + self.__dll.Lim_FileClose(handle) + + def _get_image_stacks(self) -> list: + """Return microscope image stacks""" + + (handle,) = self.resource_handles + + # initialization + pic: LIMPICTURE = LIMPICTURE() + seq_count = self.__dll.Lim_FileGetSeqCount(handle) + + # read image attributes + attributes = self.__dll.Lim_FileGetAttributes(handle) + attributes = json.loads(attributes) + image_component_count = int(attributes["componentCount"]) + + # initialize return value (each channel's stack) + result_channels_stacks = [[] for i in range(image_component_count)] + + # loop for each sequence + for seq_idx in range(seq_count): + # read image attributes + if LimCode.LIM_OK != self.__dll.Lim_FileGetImageData( + handle, seq_idx, ctypes.byref(pic) + ): + break + + # calculate pixel byte size + # Note: pixcel_bytes は [1/2/4/6/8] を取りうる想定 + pixcel_bytes = int(pic.uiComponents * int((pic.uiBitsPerComp + 7) / 8)) + if pixcel_bytes not in (1, 2, 4, 6, 8): + raise AttributeError(f"Invalid pixcel_bytes: {pixcel_bytes}") + + # # debug print + # print( + # f"Picture info: #{seq_idx + 1}", + # pic.uiWidth, pic.uiHeight, bytes, pic.uiComponents, + # pic.uiBitsPerComp, pixcel_bytes, + # ) + + # scan image lines + lines_buffer = [] + for line_idx in range(pic.uiHeight): + # Calculate the number of bytes per line (ctypes._CData format) + # * Probably the same value as pic.uiWidthBytes, + # but ported based on the logic of the SDK sample code. + # * It appears that the unit should be ctypes.c_ushort. + line_bytes = ctypes.c_ushort * int(pixcel_bytes * pic.uiWidth / 2) + + # Data acquisition for line and conversion to np.ndarray format + line_buffer = line_bytes.from_address( + pic.pImageData + (line_idx * pic.uiWidthBytes) + ) + line_buffer_array = np.ctypeslib.as_array(line_buffer) + + # stored in line buffer stack + lines_buffer.append(line_buffer_array) + + # allocate planar image buffer (np.ndarray) + raw_plane_buffer = np.concatenate([[v] for v in lines_buffer]) + + # extract image data for each channel(component) + for component_idx in range(pic.uiComponents): + # In nikon nd2 format, "component" in effect indicates "channel". + channel_idx = component_idx + + # A frame image is cut out from a raw frame image + # in units of channel(component). + # Note: The pixel values of each component are adjacent to each other, + # one pixel at a time. + # Image: [px1: [c1][c2]..[cN]]..[pxN: [c1][c2]..[cN]] + component_pixel_indexes = [ + (i * image_component_count) + component_idx + for i in range(pic.uiWidth) + ] + channel_plane_buffer = raw_plane_buffer[:, component_pixel_indexes] + + # construct return value (each channel's stack) + result_channels_stacks[channel_idx].append(channel_plane_buffer) + + return result_channels_stacks diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py new file mode 100644 index 000000000..429f5dcba --- /dev/null +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -0,0 +1,420 @@ +import ctypes +import os +import platform + +import numpy as np +from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel + +import studio.app.optinist.microscopes.modules.olympus.lib as lib +from studio.app.optinist.microscopes.modules.olympus.area_image_size import ( + AreaImageSize, +) +from studio.app.optinist.microscopes.modules.olympus.axis_info import AxisInfo +from studio.app.optinist.microscopes.modules.olympus.channel_info import ChannelInfo +from studio.app.optinist.microscopes.modules.olympus.file_creation_time import ( + FileCreationTime, +) +from studio.app.optinist.microscopes.modules.olympus.frame_manager import FrameManager +from studio.app.optinist.microscopes.modules.olympus.h_ida import ( + CMN_RECT, + IDA_AXIS_INFO, + IDA_OpenMode, + IDA_Result, +) +from studio.app.optinist.microscopes.modules.olympus.objective_lens_info import ( + ObjectiveLensInfo, +) +from studio.app.optinist.microscopes.modules.olympus.pixel_length import PixelLength +from studio.app.optinist.microscopes.modules.olympus.roi_collection import RoiCollection +from studio.app.optinist.microscopes.modules.olympus.system_info import SystemInfo +from studio.app.optinist.microscopes.modules.olympus.user_comment import UserComment + + +class OIRReader(MicroscopeDataReaderBase): + """Olympus OIR data reader + + * IDAL SDK usage method is based on IDA_Sample/IDA_Sample.cpp + """ + + SDK_LIBRARY_FILES = { + "Windows": { + "main": "/olympus/windows/Idaldll.dll", + }, + "Linux": { + "main": "/olympus/linux/libIdaldll.so", + "dependencies": (), + }, + } + + @staticmethod + def get_library_path() -> str: + """Returns the path of the library (dll) file""" + platform_name = platform.system() + + if __class__.LIBRARY_DIR_KEY not in os.environ: + return None + + if platform_name not in __class__.SDK_LIBRARY_FILES: + return None + + return ( + os.environ.get(__class__.LIBRARY_DIR_KEY) + + __class__.SDK_LIBRARY_FILES[platform_name]["main"] + ) + + @staticmethod + def is_available() -> bool: + """Determine if library is available""" + return (__class__.LIBRARY_DIR_KEY in os.environ) and os.path.isfile( + __class__.get_library_path() + ) + + def _init_library(self): + # load sdk libraries (dependencies) + if "dependencies" in __class__.SDK_LIBRARY_FILES[platform.system()]: + platform_library_dir = os.path.dirname(__class__.get_library_path()) + dependencies = __class__.SDK_LIBRARY_FILES[platform.system()][ + "dependencies" + ] + + for dependency in dependencies: + dependency_path = f"{platform_library_dir}/{dependency}" + ctypes.cdll.LoadLibrary(dependency_path) + + # load sdk library + ida = self.__dll = lib.load_library(__class__.get_library_path()) + + # initialize sdk library + ida_result = ida.Initialize() + if ida_result != IDA_Result.IDA_RESULT_SUCCESS: + raise Exception("IDA Initialize Error") + + def _load_file(self, data_file_path: str) -> object: + ida = self.__dll + + # Get Accessor + hAccessor = self.__hAccessor = ctypes.c_void_p() + ida_result = ida.GetAccessor(data_file_path, ctypes.byref(hAccessor)) + if ida_result != IDA_Result.IDA_RESULT_SUCCESS or not hAccessor: + raise FileNotFoundError(f"IDA GetAccessor Error: {data_file_path}") + + # Connect + ida.Connect(hAccessor) + + # Open file + hFile = ctypes.c_void_p() + ida_result = ida.Open( + hAccessor, + data_file_path, + IDA_OpenMode.IDA_OM_READ, + ctypes.byref(hFile), + ) + if ida_result != IDA_Result.IDA_RESULT_SUCCESS or not hFile: + raise FileNotFoundError(f"IDA Open Error: {data_file_path}") + + # Get Group Handle + hGroup = ctypes.c_void_p() + specify_group = ( + 0 # OIR Data has only 1 group, omp2info file may have more groups + ) + ida.GetGroup(hAccessor, hFile, specify_group, ctypes.byref(hGroup)) + + # GetArea + hArea = ctypes.c_void_p() + specify_layer = 0 # OIR and omp2info file has only 1 layer + specify_area = ctypes.c_int() + ida.GetArea(hAccessor, hGroup, specify_layer, specify_area, ctypes.byref(hArea)) + + return (hAccessor, hFile, hGroup, hArea) + + def _build_original_metadata(self, data_name: str) -> dict: + ida = self.__dll + + (hAccessor, hFile, hGroup, hArea) = self.resource_handles + + # -------------------------------------------------- + # Get data from API + # -------------------------------------------------- + + # GetNumberOfGroup + num_of_groups = ctypes.c_int() + ida.GetNumOfGroups(hAccessor, hFile, ctypes.byref(num_of_groups)) + + # GetNumberOfLevels + num_of_levels = ctypes.c_int() + ida.GetNumOfLevels(hAccessor, hGroup, ctypes.byref(num_of_levels)) + + # GetLevelImageSize + rect = CMN_RECT() + specify_layer = 0 # OIR and omp2info file has only 1 layer + ida.GetLevelImageSize(hAccessor, hGroup, specify_layer, ctypes.byref(rect)) + + # GetNumOfArea + num_of_area = ctypes.c_int() + ida.GetNumOfArea(hAccessor, hGroup, specify_layer, ctypes.byref(num_of_area)) + + # Channel Information + channel_info = ChannelInfo(hAccessor, hArea) + + # Axes Information + axis_info = AxisInfo(hAccessor, hArea) + + # Pixel Length + pixel_length = PixelLength(hAccessor, hArea) + + # Objective Lens Info + objective_lens_info = ObjectiveLensInfo(hAccessor, hArea) + + # File Creation Time + file_creation_time = FileCreationTime(hAccessor, hArea) + + # System Information + system_info = SystemInfo(hAccessor, hArea) + + # User Comment + user_comment = UserComment(hAccessor, hArea) + + # -------------------------------------------------- + # Construct metadata + # -------------------------------------------------- + + original_metadata = { + "data_name": data_name, + "num_of_groups": num_of_groups.value, + "num_of_levels": num_of_levels.value, + "num_of_area": num_of_area.value, + "rect": { + "x": rect.x, + "y": rect.y, + "width": rect.width, + "height": rect.height, + }, + "axis_info": axis_info.get_values(), + "pixel_length": pixel_length.get_values(), + "channel_info": channel_info.get_values(), + "objective_lens_info": objective_lens_info.get_values(), + "file_creation_time": file_creation_time.get_values(), + "system_info": system_info.get_values(), + "user_comment": user_comment.get_values(), + } + + return original_metadata + + def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: + """ + @link OME/NativeND2Reader + """ + + rect = original_metadata["rect"] + axis_info = original_metadata["axis_info"] + channel_info = original_metadata["channel_info"] + objective_lens_info = original_metadata["objective_lens_info"] + file_creation_time = original_metadata["file_creation_time"] + + # get sequence counts + # Note: Use the largest sequence count for each axis. + axes_sequence_counts = [] + for axis_name, axis in axis_info.items(): + axes_sequence_counts.append(axis["max"]) + sequence_count = max(axes_sequence_counts) + + # get axis z frame count + nZLoop = axis_info["ZSTACK"]["max"] if "ZSTACK" in axis_info else 1 + + # get fps + # TODO: fps の取得には、FrameManager経由 + # (FrameManager.m_vecAxisPosition[].GetPosition) での + # アクセスが必要となる模様. 今後計算対象予定. + fps = 0 + + omeData = OMEDataModel( + image_name=original_metadata["data_name"], + size_x=rect["width"], + size_y=rect["height"], + size_t=sequence_count, + size_z=nZLoop, + size_c=len(channel_info), + acquisition_date=file_creation_time["creation_time"], + objective_model=objective_lens_info["name"], + fps=fps, + ) + + return omeData + + def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: + # -------------------------------------------------- + # Get parameters from original_metadata + # -------------------------------------------------- + + num_of_groups = original_metadata["num_of_groups"] + num_of_levels = original_metadata["num_of_levels"] + num_of_area = original_metadata["num_of_area"] + rect = original_metadata["rect"] + pixel_length = original_metadata["pixel_length"] + channel_info = original_metadata["channel_info"] + objective_lens_info = original_metadata["objective_lens_info"] + file_creation_time = original_metadata["file_creation_time"] + system_info = original_metadata["system_info"] + user_comment = original_metadata["user_comment"] + + # -------------------------------------------------- + # Make parameters + # -------------------------------------------------- + + (hAccessor, hFile, hGroup, hArea) = self.resource_handles + del hFile, hGroup + + axis_info = AxisInfo(hAccessor, hArea) + + nLLoop = nTLoop = nZLoop = 0 + Zstep = Zstart = Zend = Tstep = 0.0 + + if axis_info.exist("LAMBDA"): + axis = axis_info.get_axis("LAMBDA") + nLLoop = axis.get_max() + nLLoop = nLLoop or 1 + + if axis_info.exist("ZSTACK"): + axis = axis_info.get_axis("ZSTACK") + nZLoop = axis.get_max() + Zstep = axis.get_step() + Zstart = axis.get_start() + Zend = axis.get_end() + nZLoop = nZLoop or 1 + + if axis_info.exist("TIMELAPSE"): + axis = axis_info.get_axis("TIMELAPSE") + nTLoop = axis.get_max() + Tstep = axis.get_step() + nTLoop = nTLoop or 1 + + # -------------------------------------------------- + # Construct metadata + # -------------------------------------------------- + + lab_specific_metadata = { + "uiWidth": rect["width"], + "uiHeight": rect["height"], + "Loops": nTLoop, + "ZSlicenum": nZLoop, + "nChannel": len(channel_info), + "PixelLengthX": pixel_length["x"], + "PixelLengthY": pixel_length["y"], + "ZInterval": Zstep, + "TInterval": Tstep, + "ZStart": Zstart, + "ZEnd": Zend, + "ObjectiveName": objective_lens_info["name"], + "ObjectiveMag": objective_lens_info["magnification"], + "ObjectiveNA": objective_lens_info["na"], + "ReflectiveIndex": objective_lens_info["reflective_index"], + "Immersion": objective_lens_info["immersion"], + "Date": file_creation_time["creation_time"], + "NumberOfGroup": num_of_groups, + "NumberOfLevel": num_of_levels, + "NumberOfArea": num_of_area, + "ByteDepthCh0": channel_info[0]["depth"] if len(channel_info) > 0 else None, + "SystemName": system_info["system_name"], + "SystemVersion": system_info["system_version"], + "DeviceName": system_info["device_name"], + "UserName": system_info["user_name"], + "CommentByUser": user_comment["comment"], + } + + return lab_specific_metadata + + def _get_image_stacks(self) -> list: + """Return microscope image stacks""" + + (hAccessor, hFile, hGroup, hArea) = self.resource_handles + + rect = CMN_RECT() + + # Imaging ROI Information + imaging_roi = RoiCollection( + hAccessor, hArea, "ImagingROIList", "ImagingROIInfo" + ) + + # Channel Information + channel_info = ChannelInfo(hAccessor, hArea) + + # Image Size + area_image_size = AreaImageSize(hAccessor, hArea) + + # Axes Information + axis_info = AxisInfo(hAccessor, hArea) + + pAxes = (IDA_AXIS_INFO * 3)() + + nLLoop = nTLoop = nZLoop = 0 + + # For Max Loop Values for lambda, z, t + if axis_info.exist("LAMBDA"): + nLLoop = axis_info.get_axis("LAMBDA").get_max() + if axis_info.exist("ZSTACK"): + nZLoop = axis_info.get_axis("ZSTACK").get_max() + if axis_info.exist("TIMELAPSE"): + nTLoop = axis_info.get_axis("TIMELAPSE").get_max() + + nLLoop = nLLoop or 1 + nTLoop = nTLoop or 1 + nZLoop = nZLoop or 1 + + # Retrieve all imaged area + rect.width = area_image_size.get_x() + rect.height = area_image_size.get_y() + + # initialize return value (each channel's stack) + result_channels_stacks = [] + + # Retrieve Image data and TimeStamp frame-by-frame + for channel_no in range(channel_info.get_num_of_channel()): + # Variable for storing results (image stack) + result_stack = [] + + for i in range(nLLoop): + for j in range(nZLoop): + for k in range(nTLoop): + nAxisCount = lib.set_frame_axis_index( + i, j, k, imaging_roi, axis_info, pAxes, 0 + ) + + # Create Frame Manager + frame_manager = FrameManager( + hAccessor, + hArea, + channel_info.get_channel_id(channel_no), + pAxes, + nAxisCount, + ) + + # Get Image Body + buffer_pointer = frame_manager.get_image_body(rect) + ctypes_buffer_ptr = buffer_pointer[1] + + # Obtain image data in ndarray format + pucBuffer_ndarray = np.ctypeslib.as_array(ctypes_buffer_ptr) + result_stack.append(pucBuffer_ndarray) + + frame_manager.release_image_body() + + # construct return value (each channel's stack) + result_channels_stacks.append(result_stack) + + return result_channels_stacks + + def _release_resources(self) -> None: + # ---------------------------------------- + # Release each resource + # ---------------------------------------- + + ida = self.__dll + + (hAccessor, hFile, hGroup, hArea) = self.resource_handles + + ida.ReleaseArea(hAccessor, hArea) + ida.ReleaseGroup(hAccessor, hGroup) + ida.Close(hAccessor, hFile) + ida.Disconnect(hAccessor) + ida.ReleaseAccessor(ctypes.byref(hAccessor)) + ida.Terminate() diff --git a/studio/app/optinist/microscopes/modules/olympus/area_image_size.py b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py new file mode 100644 index 000000000..d7cedc215 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py @@ -0,0 +1,35 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/AreaImageSize.h,cpp + +""" +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class AreaImageSize: + def __init__(self, hAccessor, hArea): + result, hProp = lib.get_area_property(hAccessor, hArea, "ImageSize") + result, pImageSize = lib.get_property_value(hAccessor, hProp, "size") + self.m_nX = pImageSize[0].value.point.x + self.m_nY = pImageSize[0].value.point.y + if pImageSize: + del pImageSize + if hProp: + lib.ida.ReleaseProperty(hAccessor, hProp) + + def get_x(self): + return self.m_nX + + def get_y(self): + return self.m_nY + + def print(self): + print("Image Size") + print(f"\tx={self.m_nX}") + print(f"\ty={self.m_nY}") + + def get_values(self): + return { + "x": self.m_nX, + "y": self.m_nY, + } diff --git a/studio/app/optinist/microscopes/modules/olympus/axis_info.py b/studio/app/optinist/microscopes/modules/olympus/axis_info.py new file mode 100644 index 000000000..88d49fb77 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/axis_info.py @@ -0,0 +1,161 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/AxisInfo.h,cpp + +""" +import ctypes as ct + +import studio.app.optinist.microscopes.modules.olympus.h_ida as h_ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class AxisIndex: + def __init__(self): + self.m_bIsExit = False + self.m_nIndex = 0 + self.m_nType = h_ida.IDA_AxisType.IDA_AT_TIME + + def set_exit(self, bIsExit): + self.m_bIsExit = bIsExit + + def set_index(self, nIndex): + self.m_nIndex = nIndex + + def set_type(self, nType): + self.m_nType = nType + + def get_exit(self): + return self.m_bIsExit + + def get_index(self): + return self.m_nIndex + + def get_type(self): + return self.m_nType + + +class AxisPosition: + def __init__(self): + self.m_bIsExit = False + self.m_dPos = 0.0 + self.m_nType = h_ida.IDA_AxisType.IDA_AT_TIME + + def set_exit(self, bIsExit): + self.m_bIsExit = bIsExit + + def set_position(self, dPos): + self.m_dPos = dPos + + def set_type(self, nType): + self.m_nType = nType + + def get_exit(self): + return self.m_bIsExit + + def get_position(self): + return self.m_dPos + + def get_type(self): + return self.m_nType + + +class Axis: + def __init__(self): + self.m_dStart = 0.0 + self.m_dEnd = 0.0 + self.m_dStep = 0.0 + self.m_nMax = 0 + + def set_start(self, val): + self.m_dStart = val + + def set_end(self, val): + self.m_dEnd = val + + def set_step(self, val): + self.m_dStep = val + + def set_max(self, val): + self.m_nMax = val + + def get_start(self): + return self.m_dStart + + def get_end(self): + return self.m_dEnd + + def get_step(self): + return self.m_dStep + + def get_max(self): + return self.m_nMax + + +class AxisInfo: + def __init__(self, hAccessor, hArea): + self.m_axes = {} + + result, hPropAxes = lib.get_area_property(hAccessor, hArea, "Axes") + result, pAxes = lib.get_property_value(hAccessor, hPropAxes, "axisName") + + for c_axis in pAxes: + axis = Axis() + result, hPropAxis = lib.get_area_property( + hAccessor, + hArea, + "AxisInfo", + [ + "axisName", + ct.cast(ct.c_wchar_p(c_axis.value.pszString), ct.c_void_p), + ], + ) + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: + result, pStart = lib.get_property_value(hAccessor, hPropAxis, "start") + axis.set_start(pStart[0].value.dDouble) + del pStart + + result, pEnd = lib.get_property_value(hAccessor, hPropAxis, "end") + axis.set_end(pEnd[0].value.dDouble) + del pEnd + + result, pStep = lib.get_property_value(hAccessor, hPropAxis, "step") + axis.set_step(pStep[0].value.dDouble) + del pStep + + result, pMax = lib.get_property_value(hAccessor, hPropAxis, "maxSize") + axis.set_max(pMax[0].value.nInteger) + del pMax + self.m_axes[c_axis.value.pszString] = axis + if hPropAxis: + lib.ida.ReleaseProperty(hAccessor, hPropAxis) + if pAxes: + del pAxes + + if hPropAxes: + lib.ida.ReleaseProperty(hAccessor, hPropAxes) + + def get_axis(self, name): + return self.m_axes.get(name, None) + + def exist(self, name): + return name in self.m_axes + + def print(self): + print("Axis Information") + for name, axis in self.m_axes.items(): + print(f"\taxisName = {name}") + print(f"\t\tstart = {axis.get_start()}") + print(f"\t\tstep = {axis.get_step()}") + print(f"\t\tend = {axis.get_end()}") + print(f"\t\tmax = {axis.get_max()}") + + def get_values(self): + result = {} + for name, axis in self.m_axes.items(): + result[name] = { + "start": axis.get_start(), + "step": axis.get_step(), + "end": axis.get_end(), + "max": axis.get_max(), + } + return result diff --git a/studio/app/optinist/microscopes/modules/olympus/channel_info.py b/studio/app/optinist/microscopes/modules/olympus/channel_info.py new file mode 100644 index 000000000..efdb5cc02 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/channel_info.py @@ -0,0 +1,122 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/ChannelInfo.h,cpp + +""" +import ctypes as ct + +import studio.app.optinist.microscopes.modules.olympus.h_ida as h_ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class ChannelInfo: + def __init__(self, hAccessor, hArea): + # Variables + self.m_vecpszChannelIdList = [] + self.m_vecpszChannelName = [] + self.m_vecnChannelDepth = [] + self.m_vecnChannelBitCount = [] + self.m_vecpnChannelLUTR = [] + self.m_vecpnChannelLUTG = [] + self.m_vecpnChannelLUTB = [] + + # Init + result, hPropEnabled = lib.get_area_property( + hAccessor, hArea, "EnabledChannelIdList" + ) + result, pChIDs = lib.get_property_value(hAccessor, hPropEnabled, "id") + # num_ch = len(pChIDs) + + # Get Channel Info + for ch in pChIDs: + self.m_vecpszChannelIdList.append(ch.value.pszString) + + result, hPropChannel = lib.get_area_property( + hAccessor, + hArea, + "ChannelInfo", + ["channelId", ct.cast(ct.c_wchar_p(ch.value.pszString), ct.c_void_p)], + ) + + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: + # Get Ch Names + result, pChNames = lib.get_property_value( + hAccessor, hPropChannel, "name" + ) + self.m_vecpszChannelName.append(pChNames[0].value.pszString) + del pChNames + + # Get Ch depth (bit depth) + result, pChDepth = lib.get_property_value( + hAccessor, hPropChannel, "depth" + ) + self.m_vecnChannelDepth.append(pChDepth[0].value.nInteger) + del pChDepth + + # Get Ch depth (available bit depth) + result, pChAvaiDepth = lib.get_property_value( + hAccessor, hPropChannel, "bitCount" + ) + self.m_vecnChannelBitCount.append(pChAvaiDepth[0].value.nInteger) + del pChAvaiDepth + + # LUT + result, lut_r, lut_g, lut_b = lib.get_lut( + hAccessor, hArea, ch.value.pszString + ) + self.m_vecpnChannelLUTR.append(lut_r) + self.m_vecpnChannelLUTG.append(lut_g) + self.m_vecpnChannelLUTB.append(lut_b) + + if hPropChannel: + lib.ida.ReleaseProperty(hAccessor, hPropChannel) + + if hPropEnabled: + lib.ida.ReleaseProperty(hAccessor, hPropEnabled) + if pChIDs: + del pChIDs + + def get_num_of_channel(self): + return len(self.m_vecpszChannelIdList) + + def get_channel_id(self, idx): + try: + return self.m_vecpszChannelIdList[idx] + except Exception: + return None + + def get_channel_name(self, idx): + return None + + def get_channel_depth(self, idx): + return None + + def get_channel_bit_count(self, idx): + return None + + def get_channel_LUT(self, chIdx, lutIdx): + return None + + def print(self): + print("Channel Information") + for cnt in range(len(self.m_vecpszChannelIdList)): + print(f"\tID = {self.m_vecpszChannelIdList[cnt]}") + print(f"\tName = {self.m_vecpszChannelName[cnt]}") + print(f"\tDepth[byte] = {self.m_vecnChannelDepth[cnt]}") + print(f"\tAvai Bit[bit] = {self.m_vecnChannelBitCount[cnt]}") + + def get_values(self): + results = [] + for cnt in range(len(self.m_vecpszChannelIdList)): + result = { + "id": self.m_vecpszChannelIdList[cnt], + "name": self.m_vecpszChannelName[cnt], + "depth": self.m_vecnChannelDepth[cnt], + "bit_count": self.m_vecnChannelBitCount[cnt], + } + results.append(result) + return results + + @property + def depth_of_ch0(self): + return self.m_vecnChannelDepth[0] diff --git a/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py b/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py new file mode 100644 index 000000000..45d3cfd14 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py @@ -0,0 +1,33 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/FileCreationTime.h,cpp + +""" +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class FileCreationTime: + def __init__(self, hAccessor, hArea): + # Variables + self.m_szCreationTime = None + + result, hProp = lib.get_area_property(hAccessor, hArea, "CreationDateTime") + result, pCreationDateTime = lib.get_property_value(hAccessor, hProp, "dateTime") + self.m_szCreationTime = pCreationDateTime[0].value.pszString + del pCreationDateTime + + if hProp: + lib.ida.ReleaseProperty(hAccessor, hProp) + + def print(self): + print("File Creation Time") + print(f"\tTime={self.m_szCreationTime}") + + def get_values(self): + return { + "creation_time": self.m_szCreationTime, + } + + @property + def creation_time(self): + return self.m_szCreationTime diff --git a/studio/app/optinist/microscopes/modules/olympus/frame_manager.py b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py new file mode 100644 index 000000000..d132f826c --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py @@ -0,0 +1,241 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/FrameManager.h,cpp + +""" +import ctypes as ct + +import studio.app.optinist.microscopes.modules.olympus.h_ida as h_ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib +from studio.app.optinist.microscopes.modules.olympus.axis_info import ( + AxisIndex, + AxisPosition, +) +from studio.app.optinist.microscopes.modules.olympus.roi_collection import Roi + + +class FrameManager: + def __init__(self, hAccessor, hArea, pszChannelId, pAxes, nNumOfAxes): + self.m_hAccessor = hAccessor + self.m_hArea = hArea + self.m_pszChannelId = pszChannelId + self.m_nAxisCount = nNumOfAxes + self.m_pAxes = [] + for axis in pAxes: + ainfo = h_ida.IDA_AXIS_INFO() + ainfo.nNumber = axis.nNumber + ainfo.nType = axis.nType + self.m_pAxes.append(ainfo) + self.m_hImage = ct.c_void_p() + self.m_rect = None + self.m_pucImageBuffer = None + self.m_vecAxisIndex = [] + self.m_vecAxisPosition = [] + self.m_vecRois = [] + lib.ida.GetImage( + hAccessor, hArea, pszChannelId, pAxes, nNumOfAxes, ct.byref(self.m_hImage) + ) + + def get_image_body(self, rect): + # ---------------------------------------- + # Get image buffer pointer + # ---------------------------------------- + + self.m_rect = rect + self.m_pucImageBuffer = lib.get_image_body( + self.m_hAccessor, self.m_hImage, ct.byref(self.m_rect) + ) + + # ---------------------------------------- + # Get ctypes type image buffer pointer + # ---------------------------------------- + + # Note: specify c_uint16 + buffer_size = ct.c_uint16 * rect.width * rect.height + + ctypes_buffer_ptr = buffer_size.from_buffer(self.m_pucImageBuffer) + + return (self.m_pucImageBuffer, ctypes_buffer_ptr) + + def release_image_body(self): + if self.m_pucImageBuffer: + self.m_pucImagebuffer = None + + def write_image_body(self, filename): + # Note: Not impletented. + pass + + def write_image_body_binary(self, filename): + # Note: Not impletented. + pass + + def get_frame_index(self): + pFrameAxes = lib.get_image_axis(self.m_hAccessor, self.m_hImage) + for p in pFrameAxes: + axis_index = AxisIndex() + axis_index.set_exit(True) + axis_index.set_type(p.nType) + axis_index.set_index(p.nNumber) + self.m_vecAxisIndex.append(axis_index) + + del pFrameAxes + return self.m_vecAxisIndex + + def write_frame_index(self): + for ai in self.m_vecAxisIndex: + if ai.get_exit(): + print(f"\tType={ai.get_type()}, Number={ai.get_index()}") + + def get_frame_position(self): + result, hProp = lib.get_frame_property( + self.m_hAccessor, self.m_hImage, "AxisPosition", "axisName", "TIMELAPSE" + ) + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: + result, pAxisPosition = lib.get_property_value( + self.m_hAccessor, hProp, "position" + ) + axis_pos = AxisPosition() + axis_pos.set_type(h_ida.IDA_AxisType.IDA_AT_TIME) + axis_pos.set_exit(True) + axis_pos.set_position(pAxisPosition[0].value.dDouble) + self.m_vecAxisPosition.append(axis_pos) + del pAxisPosition + if hProp: + lib.ida.ReleaseProperty(self.m_hAccessor, hProp) + + result, hProp = lib.get_frame_property( + self.m_hAccessor, self.m_hImage, "AxisPosition", "axisName", "ZSTACK" + ) + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: + result, pAxisPosition = lib.get_property_value( + self.m_hAccessor, hProp, "position" + ) + axis_pos = AxisPosition() + axis_pos.set_type(h_ida.IDA_AxisType.IDA_AT_Z) + axis_pos.set_exit(True) + axis_pos.set_position(pAxisPosition[0].value.dDouble) + self.m_vecAxisPosition.append(axis_pos) + del pAxisPosition + if hProp: + lib.ida.ReleaseProperty(self.m_hAccessor, hProp) + + result, hProp = lib.get_frame_property( + self.m_hAccessor, self.m_hImage, "AxisPosition", "axisName", "LAMBDA" + ) + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: + result, pAxisPosition = lib.get_property_value( + self.m_hAccessor, hProp, "position" + ) + axis_pos = AxisPosition() + axis_pos.set_type(h_ida.IDA_AxisType.IDA_AT_LAMBDA) + axis_pos.set_exit(True) + axis_pos.set_position(pAxisPosition[0].value.dDouble) + self.m_vecAxisPosition.append(axis_pos) + del pAxisPosition + if hProp: + lib.ida.ReleaseProperty(hProp) + + return self.m_vecAxisPosition + + def write_frame_position(self): + for ap in self.m_vecAxisPosition: + if ap.get_exit(): + if ap.get_type() == h_ida.IDA_AxisType.IDA_AT_LAMBDA: + print(f"\tLAMBDA={ap.get_position()}") + elif ap.get_type() == h_ida.IDA_AxisType.IDA_AT_Z: + print(f"\tZSTACK={ap.get_position()}") + elif ap.get_type() == h_ida.IDA_AxisType.IDA_AT_TIME: + print(f"\tTIMELAPSE={ap.get_position()}") + + def get_frame_roi(self): + result, hProp = lib.get_frame_property( + self.m_hAccessor, self.m_hImage, "StimulationROIList" + ) + result, pAnalysisROIDs = lib.get_property_value(self.m_hAccessor, hProp, "id") + for aroi in pAnalysisROIDs: + roi = Roi() + # Get Image ROI Info from ID + result, hPropInfo = lib.get_frame_property( + self.m_hAccessor, + self.m_hImage, + "StimulationROIInfo", + "roiId", + ct.c_wchar_p(aroi.value.pszString), + ) + # Get Analysis ROI Name + result, pAnalysisROIName = lib.get_property_value( + self.m_hAccessor, hPropInfo, "name" + ) + roi.set_id(aroi.value.pszString) + roi.set_name(pAnalysisROIName[0].value.pszString) + del pAnalysisROIName + + # Get Analysis ROI Type + result, pAnalysisROIType = lib.get_property_value( + self.m_hAccessor, hPropInfo, "type" + ) + roi.set_type(pAnalysisROIType[0].value.pszString) + del pAnalysisROIType + + # Get Analysis ROI Shape + result, pAnalysisROIShape = lib.get_property_value( + self.m_hAccessor, hPropInfo, "shape" + ) + roi.set_shape(pAnalysisROIShape[0].value.pszString) + del pAnalysisROIShape + + # Get Analysis ROI Rotation + result, pAnalysisROIRotation = lib.get_property_value( + self.m_hAccessor, hPropInfo, "rotation" + ) + roi.set_rotation(pAnalysisROIRotation[0].value.dDouble) + del pAnalysisROIRotation + + # Get Analysis ROI Data + result, pAnalysisROIData = lib.get_property_value( + self.m_hAccessor, hPropInfo, "data" + ) + roi.set_points(pAnalysisROIData, -1) + del pAnalysisROIData + + if roi.get_type() == "MULTI_POINT": + # PanX + result, pPanX = lib.get_property_value( + self.m_hAccessor, hPropInfo, "panX" + ) + roi.set_pan_x(pPanX[0].value.dDouble) + del pPanX + + # PanY + result, pPanY = lib.get_property_value( + self.m_hAccessor, hPropInfo, "panY" + ) + roi.set_pan_y(pPanY[0].value.dDouble) + del pPanY + + # Zoom + result, pZoom = lib.get_property_value( + self.m_hAccessor, hPropInfo, "zoom" + ) + roi.set_zoom(pZoom[0].value.dDouble) + del pZoom + + # Z + result, pZ = lib.get_property_value( + self.m_hAccessor, hPropInfo, "zPosition" + ) + roi.set_z(pZ[0].value.dDouble) + del pZ + + self.m_vecRois.append(roi) + if hPropInfo: + lib.ida.ReleaseProperty(self.m_hAccessor, hPropInfo) + del pAnalysisROIDs + if hProp: + lib.ida.ReleaseProperty(self.m_hAccessor, hProp) + + return self.m_vecRois + + def write_frame_roi(self): + for r in self.m_vecRois: + r.write_roi() diff --git a/studio/app/optinist/microscopes/modules/olympus/h_ida.py b/studio/app/optinist/microscopes/modules/olympus/h_ida.py new file mode 100644 index 000000000..274dbe115 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/h_ida.py @@ -0,0 +1,98 @@ +"""Olympus IDA wrapper module + +* Porting of IDA/include/Idaldll.h + +""" +import ctypes as ct +from enum import IntEnum, auto + + +class IDA_Result(IntEnum): + IDA_RESULT_SUCCESS = 0 + IDA_RESULT_INVALID_PARAM = auto() + IDA_RESULT_ILLEGAL_STATE = auto() + IDA_RESULT_ACCESSOR_NOT_FOUND = auto() + IDA_RESULT_FILE_NOT_FOUND = auto() + IDA_RESULT_PROPERTY_NOT_FOUND = auto() + IDA_RESULT_CHANNEL_NOT_FOUND = auto() + IDA_RESULT_AMBIGUOUS_FRAME = auto() + IDA_RESULT_NO_LUT = auto() + IDA_RESULT_UNSUPPORTED_OPERATION = auto() + IDA_RESULT_NO_IMAGE = auto() + IDA_RESULT_ALLOCATION_FAILED = auto() + IDA_RESULT_FILE_CORRUPT = auto() + IDA_RESULT_FAILURE = auto() + IDA_INVALID_IMAGE = auto() + IDA_RESULT_INSUFFICIENT_BUFFER = auto() + IDA_RESULT_IMAGE_NOT_ACQUIRED = auto() + IDA_RESULT_NO_MORE_IMAGE = auto() + + +class IDA_OpenMode(IntEnum): + IDA_OM_READ = 0x00 + IDA_OM_WRITE = 0x01 + IDA_OM_NORMAL = 0x00 + IDA_OM_FORCE = 0x02 + IDA_OM_STREAMING = 0x04 + IDA_OM_READ_WRITE = 0x08 + IDA_OM_APPEND = 0x10 + + +class CMN_RECT(ct.Structure): + _fields_ = [ + ("x", ct.c_uint64), + ("y", ct.c_uint64), + ("width", ct.c_uint64), + ("height", ct.c_uint64), + ] + + +class IDA_POINT(ct.Structure): + _fields_ = [ + ("x", ct.c_int), + ("y", ct.c_int), + ] + + +class IDA_VALUE_UN(ct.Union): + _fields_ = [ + ("nInteger", ct.c_int), + ("dDouble", ct.c_double), + ("pszString", ct.c_wchar_p), + ("point", IDA_POINT), + ("rRect", CMN_RECT), + ] + + +class IDA_VALUE(ct.Structure): + _fields_ = [ + ("nType", ct.c_int), + ("value", IDA_VALUE_UN), + ] + + +class IDA_PARAM_ELEMENT(ct.Structure): + _fields_ = [ + ("pszKey", ct.c_wchar_p), + ("pValue", ct.c_void_p), + ] + + +class IDA_PARAM(ct.Structure): + _fields_ = [ + ("nSize", ct.c_int), + ("pElements", ct.POINTER(IDA_PARAM_ELEMENT)), + ] + + +class IDA_AXIS_INFO(ct.Structure): + _fields_ = [ + ("nType", ct.c_int), + ("nNumber", ct.c_int64), + ] + + +class IDA_AxisType(IntEnum): + IDA_AT_TIME = 0 + IDA_AT_Z = 1 + IDA_AT_LAMBDA = 2 diff --git a/studio/app/optinist/microscopes/modules/olympus/lib.py b/studio/app/optinist/microscopes/modules/olympus/lib.py new file mode 100644 index 000000000..b468d14bd --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/lib.py @@ -0,0 +1,226 @@ +"""Olympus IDA wrapper module + +* Utilities for IDA API + +""" +import ctypes as ct + +from studio.app.optinist.microscopes.modules.olympus.h_ida import ( + IDA_AXIS_INFO, + IDA_PARAM, + IDA_PARAM_ELEMENT, + IDA_VALUE, + IDA_AxisType, + IDA_Result, +) + +ida = None + + +def load_library(library_path: str): + global ida + ida = ct.cdll.LoadLibrary(library_path) + return ida + + +def get_property_value(hAccessor, hProp, propName): + size = ct.c_int() + result = ida.GetPropertyValue(hAccessor, hProp, propName, None, 0, ct.byref(size)) + ret = None + if result == IDA_Result.IDA_RESULT_SUCCESS: + ret = (IDA_VALUE * size.value)() + ida.GetPropertyValue(hAccessor, hProp, propName, ret, size, ct.byref(size)) + else: + raise Exception(result) + return result, ret + + +def get_frame_property(hAccessor, hImage, key, axisName=None, axisValue=None): + hProp = ct.c_void_p() + Param_AxisPosition = IDA_PARAM() + if axisName: + Param_AxisPosition.nSize = 1 + element = IDA_PARAM_ELEMENT() + element.pszKey = axisName + element.pValue = ct.cast(axisValue, ct.c_void_p) + Param_AxisPosition.pElements.contents = element + result = ida.GetFrameProperty( + hAccessor, hImage, key, ct.byref(Param_AxisPosition), ct.byref(hProp) + ) + return result, hProp + + +def get_area_property(hAccessor, hArea, key, element_data=None): + param = IDA_PARAM() + if element_data: + element = IDA_PARAM_ELEMENT() + element.pszKey = element_data[0] + element.pValue = element_data[1] + + param.pElements.contents = element + param.nSize = 1 + else: + param.nSize = 0 + hProp = ct.c_void_p() + result = ida.GetAreaProperty( + hAccessor, hArea, key, ct.byref(param), ct.byref(hProp) + ) + return result, hProp + + +def get_lut(hAccessor, hArea, channel_id): + size = ct.c_int() + ida.GetLUT(hAccessor, hArea, channel_id, None, None, None, 0, ct.byref(size)) + pLUTR = (ct.c_int * size.value)() + pLUTG = (ct.c_int * size.value)() + pLUTB = (ct.c_int * size.value)() + result = ida.GetLUT( + hAccessor, hArea, channel_id, pLUTR, pLUTG, pLUTB, size, ct.byref(size) + ) + return result, pLUTR, pLUTG, pLUTB + + +def get_image_body(hAccessor, hImage, rect): + image_size = ct.c_uint64() + image_buffer = None + result = ida.GetImageBody(hAccessor, hImage, rect, None, None, ct.byref(image_size)) + if result != IDA_Result.IDA_RESULT_SUCCESS: + raise Exception("Error: GetImageBody") + + if image_size.value != 0: + image_buffer = (ct.c_uint8 * image_size.value)() + result = ida.GetImageBody( + hAccessor, hImage, rect, image_buffer, image_size, ct.byref(image_size) + ) + return image_buffer + + +def get_image_axis(hAccessor, hImage): + num_of_frame_axis = ct.c_int() + result = ida.GetImageAxis( + hAccessor, hImage, None, None, ct.byref(num_of_frame_axis) + ) + if result != IDA_Result.IDA_RESULT_SUCCESS: + raise Exception("Error: GetImageAxis") + + pFrameAxes = (IDA_AXIS_INFO * num_of_frame_axis.value)() + + result = ida.GetImageAxis( + hAccessor, hImage.pFrameAxes, num_of_frame_axis, ct.byref(num_of_frame_axis) + ) + if result != IDA_Result.IDA_RESULT_SUCCESS: + raise Exception("Error: GetImageAxis") + + return pFrameAxes + + +def set_frame_axis_index( + nLIndex, nZIndex, nTIndex, pRoiCollection, pAxisInfo, pAxes, pnAxisCount +): + """ + // ***************************************************************************** + // @brief SetFrameAxisIndex + // @param[in] nLIndex -- LAMBDA axis index if LAMBDA axis doesn't exist, set 0 + // @param[in] nZIndex -- ZSTACK axis index if ZSTACK axis doesn't exist, set 0 + // @param[in] nTIndex -- TIMELASE axis index if TIMELASE axis doesn't exist, set 0 + // @param[in] pAxisInfo- Axis Information getting from CAxisInfo class + // @param[out] pAes + // -- This output is used by GetImage Function( CFrameManager::CFrameManager ) + // @retval void + // @note + // ***************************************************************************** + """ + + # 0: LAxis + # 1: ZAxis + # 2: TAxis + KEY = ["LAMBDA", "ZSTACK", "TIMELAPSE"] + nSize = [0] * 3 + bHasAxis = [pAxisInfo.exist(key) for key in KEY] + pAxis = [pAxisInfo.get_axis(key) for key in KEY] + pnAxisCount = 0 + for i in range(3): + if bHasAxis[i]: + nSize[i] = pAxis[i].get_max() + if pRoiCollection.has_point_roi(): # Point + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + elif pRoiCollection.has_multi_point_roi(): # Multipoint + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + elif pRoiCollection.has_mapping_roi(): # Mapping + if not bHasAxis[1]: + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + else: + pAxes[0].nNumber = nZIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_Z + pAxes[1].nNumber = nTIndex + pAxes[1].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 2 + elif pRoiCollection.has_line_roi(): # Line + if not bHasAxis[0] and not bHasAxis[1] and bHasAxis[2]: # XT + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + elif not bHasAxis[0] and bHasAxis[1] and not bHasAxis[2]: # XZ + pAxes[0].nNumber = nZIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_Z + pnAxisCount = 1 + elif not bHasAxis[0] and bHasAxis[1] and bHasAxis[2]: # XZT + pAxes[0].nNumber = nZIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_Z + pAxes[1].nNumber = nTIndex + pAxes[1].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 2 + else: + # Note: Not implemented. + pass + else: # XY + if nSize[0] != 0 and nSize[1] != 0 and nSize[2] != 0: # XYLZT + pAxes[0].nNumber = nLIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_LAMBDA + pAxes[1].nNumber = nZIndex + pAxes[1].nType = IDA_AxisType.IDA_AT_Z + pAxes[2].nNumber = nTIndex + pAxes[2].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 3 + elif nSize[0] != 0 and nSize[1] != 0 and nSize[2] == 0: # XYLZ + pAxes[0].nNumber = nLIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_LAMBDA + pAxes[1].nNumber = nZIndex + pAxes[1].nType = IDA_AxisType.IDA_AT_Z + pnAxisCount = 2 + elif nSize[0] != 0 and nSize[1] == 0 and nSize[2] != 0: # XYLT + pAxes[0].nNumber = nLIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_LAMBDA + pAxes[1].nNumber = nTIndex + pAxes[1].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 2 + elif nSize[0] == 0 and nSize[1] != 0 and nSize[2] != 0: # XYZT + pAxes[0].nNumber = nZIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_Z + pAxes[1].nNumber = nTIndex + pAxes[1].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 2 + elif nSize[0] != 0 and nSize[1] == 0 and nSize[2] == 0: # XYL + pAxes[0].nNumber = nLIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_LAMBDA + pnAxisCount = 1 + elif nSize[0] == 0 and nSize[1] != 0 and nSize[2] == 0: # XYZ + pAxes[0].nNumber = nZIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_Z + pnAxisCount = 1 + elif nSize[0] == 0 and nSize[1] == 0 and nSize[2] != 0: # XYT + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + elif nSize[0] == 0 and nSize[1] == 0 and nSize[2] == 0: # XY + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + del pAxis + return pnAxisCount diff --git a/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py new file mode 100644 index 000000000..b3afacbf3 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py @@ -0,0 +1,100 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/ObjectiveLensInfo.h,cpp + +""" +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class ObjectiveLensInfo: + def __init__(self, hAccessor, hArea): + # variables + self.m_szName = None + self.m_dMagnification = None + self.m_szImmersion = None + self.m_dNA = None + self.m_dWD = None + self.m_dReflectiveIndex = None + + # Get Objective Lens Name + result, hProp = lib.get_area_property(hAccessor, hArea, "ObjectiveLensInfo") + result, pName = lib.get_property_value(hAccessor, hProp, "name") + if len(pName) > 0: + self.m_szName = pName[0].value.pszString + del pName + + # Objective Lens Magnification + result, pMag = lib.get_property_value(hAccessor, hProp, "magnification") + if len(pMag) > 0: + self.m_dMagnification = pMag[0].value.dDouble + del pMag + + # Objective Lens Immersion + result, pImmersion = lib.get_property_value(hAccessor, hProp, "immersion") + if len(pImmersion) > 0: + self.m_szImmersion = pImmersion[0].value.pszString + del pImmersion + + # Objective Lens NA + result, pNA = lib.get_property_value(hAccessor, hProp, "na") + if len(pNA) > 0: + self.m_dNA = pNA[0].value.dDouble + del pNA + + # Objective Lens WD + result, pWD = lib.get_property_value(hAccessor, hProp, "wd") + if len(pWD) > 0: + self.m_dWD = pWD[0].value.dDouble + del pWD + + # Objective Lens reflective index + result, pRefraction = lib.get_property_value(hAccessor, hProp, "refraction") + if len(pRefraction) > 0: + self.m_dReflectiveIndex = pRefraction[0].value.dDouble + del pRefraction + + if hProp: + lib.ida.ReleaseProperty(hAccessor, hProp) + + def print(self): + print("Objective Lens Information") + print(f"\tName = {self.m_szName}") + print(f"\tMagnification = {self.m_dMagnification}") + print(f"\tImmersion = {self.m_szImmersion}") + print(f"\tNA = {self.m_dNA}") + print(f"\tWD = {self.m_dWD}") + print(f"\tReflective Index = {self.m_dReflectiveIndex}") + + def get_values(self): + return { + "name": self.m_szName, + "magnification": self.m_dMagnification, + "immersion": self.m_szImmersion, + "na": self.m_dNA, + "wd": self.m_dWD, + "reflective_index": self.m_dReflectiveIndex, + } + + @property + def name(self): + return self.m_szName + + @property + def immersion(self): + return self.m_szImmersion + + @property + def magnification(self): + return self.m_dMagnification + + @property + def na(self): + return self.m_dNA + + @property + def wd(self): + return self.m_dWD + + @property + def reflective_index(self): + return self.m_dReflectiveIndex diff --git a/studio/app/optinist/microscopes/modules/olympus/pixel_length.py b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py new file mode 100644 index 000000000..66a437e55 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py @@ -0,0 +1,42 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/PixelLength.h,cpp + +""" +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class PixelLength: + def __init__(self, hAccessor, hArea): + self.m_dX = 0 + self.m_dY = 0 + + result, hProp = lib.get_area_property(hAccessor, hArea, "PixelLength") + + result, pPixelLength = lib.get_property_value(hAccessor, hProp, "xLength") + self.m_dX = pPixelLength[0].value.dDouble + del pPixelLength + + result, pPixelLength = lib.get_property_value(hAccessor, hProp, "yLength") + self.m_dY = pPixelLength[0].value.dDouble + del pPixelLength + + if hProp: + lib.ida.ReleaseProperty(hAccessor, hProp) + + def print(self): + print("Pixel length[um]") + print(f"\tx = {self.m_dX}") + print(f"\ty = {self.m_dY}") + + def get_values(self): + return { + "x": self.m_dX, + "y": self.m_dY, + } + + def get_pixel_length_x(self): + return self.m_dX + + def get_pixel_length_y(self): + return self.m_dY diff --git a/studio/app/optinist/microscopes/modules/olympus/roi_collection.py b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py new file mode 100644 index 000000000..652b4f769 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py @@ -0,0 +1,221 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/RoiCollection.h,cpp + +""" +import ctypes as ct + +import studio.app.optinist.microscopes.modules.olympus.h_ida as h_ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class Roi: + def __init__(self): + self.m_szId = None + self.m_szName = None + self.m_szType = None + self.m_szShape = None + + self.m_dPanX = 0.0 + self.m_dPanY = 0.0 + self.m_dZoom = 0.0 + self.m_dZPosition = 0.0 + self.m_dRotation = 0.0 + self.m_vecPoints = [] + + def set_id(self, pSrc): + self.m_szId = pSrc + return True + + def set_name(self, pSrc): + self.m_szName = pSrc + return True + + def set_type(self, pSrc): + self.m_szType = pSrc + return True + + def st_shape(self, pSrc): + self.m_szShape = pSrc + return True + + def set_rotation(self, pSrc): + self.m_dRotation = pSrc + return True + + def set_points(self, pSrc, nNumOfSrc): + for p in pSrc: + buf = h_ida.IDA_VALUE.IDA_POINT() + buf.x = p.value.point.x + buf.y = p.value.point.y + self.m_vecPoints.append(buf) + + return True + + def set_pan_x(self, dPanX): + self.m_dPanX = dPanX + + def set_pan_y(self, dPanY): + self.m_dPanY = dPanY + + def set_zoom(self, dZoom): + self.m_dZoom = dZoom + + def set_z(self, dZ): + self.m_dZPosition = dZ + + def get_id(self): + return self.m_szId + + def get_name(self): + return self.m_szName + + def get_type(self): + return self.m_szType + + def get_shape(self): + return self.m_szShape + + def get_rotation(self): + return self.m_dRotation + + def get_points(self): + return self.m_vecPoints + + def get_pan_x(self): + return self.m_dPanX + + def get_pan_y(self): + return self.m_dPanY + + def get_zoom(self): + return self.m_dZoom + + def get_z(self): + return self.dZ + + def write_roi(self): + print(f"\tId = {self.get_id()}") + print(f"\t\tName = {self.get_name()}") + print(f"\t\tShape = {self.get_shape()}") + print(f"\t\tType = {self.get_type()}") + print(f"\t\tRotation = {self.get_rotation()}") + + for p in self.get_points(): + print(f"\t\t\tx={p.x}, y={p.y}") + + print(f"\t\tPanX = {self.get_pan_x()}") + print(f"\t\tPanY = {self.get_pan_y()}") + print(f"\t\tZoom = {self.get_zoom()}") + print(f"\t\tZ = {self.get_z()}") + + +class RoiCollection: + def __init__(self, hAccessor, hArea, pKey_AnalysisROIList, pKey_AnalysisROIInfo): + self.m_vecRois = [] + result, hPropList = lib.get_area_property( + hAccessor, hArea, pKey_AnalysisROIList + ) + result, pAnalysisROIDs = lib.get_property_value(hAccessor, hPropList, "id") + for a_roi in pAnalysisROIDs: + roi = Roi() + + # Get Image ROI Info from ID + result, hPropInfo = lib.get_area_property( + hAccessor, + hArea, + pKey_AnalysisROIInfo, + ["roiId", ct.c_wchar_p(a_roi.value.pszString)], + ) + # Get Analysis ROI Name + result, pAnalysisROIName = lib.get_property_value( + hAccessor, hPropInfo, "name" + ) + roi.set_id(ct.p.value.pszString) + roi.set_name(pAnalysisROIName[0].value.pszString) + del pAnalysisROIName + + # Get Analysis ROI Type + result, pAnalysisROIType = lib.get_property_value( + hAccessor, hPropInfo, "type" + ) + roi.set_type(pAnalysisROIType[0].value.pszString) + del pAnalysisROIType + + # Get Analysis ROI Shape + result, pAnalysisROIShape = lib.get_property_value( + hAccessor, hPropInfo, "shape" + ) + roi.set_shape(pAnalysisROIShape[0].value.pszString) + del pAnalysisROIShape + + # Get Analysis ROI Rotation + result, pAnalysisROIRotation = lib.get_property_value( + hAccessor, hPropInfo, "rotation" + ) + roi.set_rotation(pAnalysisROIRotation[0].value.dDouble) + del pAnalysisROIRotation + + # Get Analysis ROI Data + result, pAnalysisROIData = lib.get_property_value( + hAccessor, hPropInfo, "data" + ) + roi.set_points(pAnalysisROIData, -1) + del pAnalysisROIData + + # Multipoint Data + if roi.get_type() == "MULTI_POINT": + # PanX + result, pPanX = lib.get_property_value(hAccessor, hPropInfo, "panX") + roi.set_pan_x(pPanX[0].value.dDouble) + del pPanX + + # PanY + result, pPanY = lib.get_property_value(hAccessor, hPropInfo, "panY") + roi.set_pan_y(pPanY[0].value.dDouble) + del pPanY + + # Zoom + result, pZoom = lib.get_proiperty_value(hAccessor, hPropInfo, "zoom") + roi.set_zoom(pZoom[0].value.dDouble) + del pZoom + + # Z + result, pZ = lib.get_property_value(hAccessor, hPropInfo, "zPosition") + roi.set_z(pZ[0].value.dDouble) + del pZ + + self.m_vecRois.append(roi) + if hPropInfo: + lib.ida.ReleaseProperty(hAccessor, hPropInfo) + del pAnalysisROIDs + if hPropList: + lib.ida.ReleaseProperty(hAccessor, hPropList) + + def print(self): + for r in self.m_vecRois: + r.write_roi() + + def has_line_roi(self): + for p in self.m_vecRois: + if p.get_type() == "LineROI" or p.get_type() == "FreeLineROI": + return True + return False + + def has_point_roi(self): + for r in self.m_vecRois: + if r.get_type() == "PointROI": + return True + return False + + def has_multi_point_roi(self): + for r in self.m_vecRois: + if r.get_type() == "MultiPointROI": + return True + return False + + def has_mapping_roi(self): + for r in self.m_vecRois: + if r.get_type() == "MappingROI": + return True + return False diff --git a/studio/app/optinist/microscopes/modules/olympus/system_info.py b/studio/app/optinist/microscopes/modules/olympus/system_info.py new file mode 100644 index 000000000..9694df65c --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/system_info.py @@ -0,0 +1,66 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/SystemInfo.h,cpp + +""" +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class SystemInfo: + def __init__(self, hAccessor, hArea): + # variables + self.m_szSystemName = None + self.m_szSystemVersion = None + self.m_szDeviceName = None + self.m_szUserName = None + + result, hProp = lib.get_area_property(hAccessor, hArea, "SystemInfo") + result, pSysName = lib.get_property_value(hAccessor, hProp, "name") + self.m_szSystemName = pSysName[0].value.pszString + del pSysName + + result, pSysVer = lib.get_property_value(hAccessor, hProp, "version") + self.m_szSystemVersion = pSysVer[0].value.pszString + del pSysVer + + result, pDeviceName = lib.get_property_value(hAccessor, hProp, "deviceName") + self.m_szDeviceName = pDeviceName[0].value.pszString + del pDeviceName + + result, pUserName = lib.get_property_value(hAccessor, hProp, "userName") + self.m_szUserName = pUserName[0].value.pszString + del pUserName + + if hProp: + lib.ida.ReleaseProperty(hAccessor, hProp) + + def print(self): + print("System Information") + print(f"\tname = {self.m_szSystemName}") + print(f"\tversion = {self.m_szSystemVersion}") + print(f"\tdeviceName = {self.m_szDeviceName}") + print(f"\tuserName = {self.m_szUserName}") + + def get_values(self): + return { + "system_name": self.m_szSystemName, + "system_version": self.m_szSystemVersion, + "device_name": self.m_szDeviceName, + "user_name": self.m_szUserName, + } + + @property + def system_name(self): + return self.m_szSystemName + + @property + def system_version(self): + return self.m_szSystemVersion + + @property + def device_name(self): + return self.m_szDeviceName + + @property + def user_name(self): + return self.m_szUserName diff --git a/studio/app/optinist/microscopes/modules/olympus/user_comment.py b/studio/app/optinist/microscopes/modules/olympus/user_comment.py new file mode 100644 index 000000000..48c9eee4d --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/user_comment.py @@ -0,0 +1,33 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/UserComment.h,cpp + +""" +import studio.app.optinist.microscopes.modules.olympus.lib as lib + + +class UserComment: + def __init__(self, hAccessor, hArea): + # variables + + result, hProp = lib.get_area_property(hAccessor, hArea, "UserComment") + + result, pUserComment = lib.get_property_value(hAccessor, hProp, "comment") + self.m_szComment = pUserComment[0].value.pszString + del pUserComment + + if hProp: + lib.ida.ReleaseProperty(hAccessor, hProp) + + def print(self): + print("User Comment") + print(f"\tComment = {self.m_szComment}") + + def get_values(self): + return { + "comment": self.m_szComment, + } + + @property + def comment(self): + return self.m_szComment diff --git a/studio/app/optinist/microscopes/test_IsxdReader.py b/studio/app/optinist/microscopes/test_IsxdReader.py new file mode 100644 index 000000000..4aebe5912 --- /dev/null +++ b/studio/app/optinist/microscopes/test_IsxdReader.py @@ -0,0 +1,58 @@ +import json +import logging +import os +from pprint import pprint + +from IsxdReader import IsxdReader + +CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) +TEST_DATA_PATH = ( + CURRENT_DIR_PATH + "/testdata/inscopix/oist_short_example_preprocessed.isxd" +) + + +def test_isxd_reader(): + if not IsxdReader.is_available(): + # Note: To output the logging contents to the console, + # specify the following options to pytest + # > pytest --log-cli-level=DEBUG + logging.warning("IsxdReader is not available.") + return + + # initialize + data_reader = IsxdReader() + data_reader.load(TEST_DATA_PATH) + + # dump attributes + print("[original_metadata]", json.dumps(data_reader.original_metadata, indent=2)) + pprint(data_reader.ome_metadata) + print( + "[lab_specific_metadata]", + json.dumps(data_reader.lab_specific_metadata, indent=2), + ) + + # get image stacks + image_stack = data_reader.get_image_stacks() + + # save tiff image (multi page) test + if len(image_stack) > 0: + from PIL import Image + + save_stack = [Image.fromarray(frame) for frame in image_stack] + save_path = os.path.basename(TEST_DATA_PATH) + ".out.tiff" + print(f"save image: {save_path}") + + save_stack[0].save( + save_path, + compression="tiff_deflate", + save_all=True, + append_images=save_stack[1:], + ) + + # asserts + assert data_reader.original_metadata["spacing"]["width"] > 0 + assert data_reader.ome_metadata.size_x > 0 + + +if __name__ == "__main__": + test_isxd_reader() diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py new file mode 100644 index 000000000..0cb100c23 --- /dev/null +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -0,0 +1,64 @@ +import json +import logging +import os +from pprint import pprint + +from ND2Reader import ND2Reader + +CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) +LIBRARY_DIR = CURRENT_DIR_PATH + "/dll" +TEST_DATA_PATH = CURRENT_DIR_PATH + "/testdata/nikon/pia_volume_area1.nd2" + +os.environ[ND2Reader.LIBRARY_DIR_KEY] = LIBRARY_DIR + + +def test_nd2_reader(): + if not ND2Reader.is_available(): + # Note: To output the logging contents to the console, + # specify the following options to pytest + # > pytest --log-cli-level=DEBUG + logging.warning("ND2Reader is not available.") + return + + # initialize + data_reader = ND2Reader() + data_reader.load(TEST_DATA_PATH) + + # dump attributes + print("[original_metadata]", json.dumps(data_reader.original_metadata, indent=2)) + pprint(data_reader.ome_metadata) + print( + "[lab_specific_metadata]", + json.dumps(data_reader.lab_specific_metadata, indent=2), + ) + + # get image stacks (for all channels) + channels_stacks = data_reader.get_image_stacks() + + # save tiff image (multi page) test + if (len(channels_stacks) > 0) and (len(channels_stacks[0]) > 0): + from PIL import Image + + # save stacks for all channels + for channel_idx, image_stack in enumerate(channels_stacks): + save_stack = [Image.fromarray(frame) for frame in image_stack] + save_path = ( + os.path.basename(TEST_DATA_PATH) + f".out.ch{channel_idx+1}.tiff" + ) + print(f"save image: {save_path}") + + save_stack[0].save( + save_path, + compression="tiff_deflate", + save_all=True, + append_images=save_stack[1:], + ) + + # asserts + assert data_reader.original_metadata["attributes"]["widthPx"] > 0 + assert data_reader.ome_metadata.size_x > 0 + assert data_reader.lab_specific_metadata["uiWidth"] > 0 + + +if __name__ == "__main__": + test_nd2_reader() diff --git a/studio/app/optinist/microscopes/test_OIRReader.py b/studio/app/optinist/microscopes/test_OIRReader.py new file mode 100644 index 000000000..121e13b43 --- /dev/null +++ b/studio/app/optinist/microscopes/test_OIRReader.py @@ -0,0 +1,64 @@ +import json +import logging +import os +from pprint import pprint + +from OIRReader import OIRReader + +CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) +LIBRARY_DIR = CURRENT_DIR_PATH + "/dll" +TEST_DATA_PATH = CURRENT_DIR_PATH + "/testdata/olympus/olympus-xyt005_0001.oir" + +os.environ[OIRReader.LIBRARY_DIR_KEY] = LIBRARY_DIR + + +def test_oir_reader(): + if not OIRReader.is_available(): + # Note: To output the logging contents to the console, + # specify the following options to pytest + # > pytest --log-cli-level=DEBUG + logging.warning("OIRReader is not available.") + return + + # initialize + data_reader = OIRReader() + data_reader.load(TEST_DATA_PATH) + + # dump attributes + print("[original_metadata]", json.dumps(data_reader.original_metadata, indent=2)) + pprint(data_reader.ome_metadata) + print( + "[lab_specific_metadata]", + json.dumps(data_reader.lab_specific_metadata, indent=2), + ) + + # get image stacks (for all channels) + channels_stacks = data_reader.get_image_stacks() + + # save tiff image (multi page) test + if (len(channels_stacks) > 0) and (len(channels_stacks[0]) > 0): + from PIL import Image + + # save stacks for all channels + for channel_idx, image_stack in enumerate(channels_stacks): + save_stack = [Image.fromarray(frame) for frame in image_stack] + save_path = ( + os.path.basename(TEST_DATA_PATH) + f".out.ch{channel_idx+1}.tiff" + ) + print(f"save image: {save_path}") + + save_stack[0].save( + save_path, + compression="tiff_deflate", + save_all=True, + append_images=save_stack[1:], + ) + + # asserts + assert data_reader.original_metadata["rect"]["width"] > 0 + assert data_reader.ome_metadata.size_x > 0 + assert data_reader.lab_specific_metadata["uiWidth"] > 0 + + +if __name__ == "__main__": + test_oir_reader()