From b83d67b62eff7a7800af607e07de3cbcb414564b Mon Sep 17 00:00:00 2001 From: itutu-tienday <> Date: Wed, 20 Dec 2023 10:33:32 +0900 Subject: [PATCH 01/26] add microscope data reader modules (initial) add nikon nd2 reader modules --- .../microscopes/MicroscopeDataReaderBase.py | 117 +++++++++++ studio/app/optinist/microscopes/ND2Reader.py | 195 ++++++++++++++++++ .../optinist/microscopes/test_ND2Reader.py | 35 ++++ 3 files changed, 347 insertions(+) create mode 100644 studio/app/optinist/microscopes/MicroscopeDataReaderBase.py create mode 100644 studio/app/optinist/microscopes/ND2Reader.py create mode 100644 studio/app/optinist/microscopes/test_ND2Reader.py diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py new file mode 100644 index 000000000..aeca26b0a --- /dev/null +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -0,0 +1,117 @@ +import os +from abc import ABCMeta, abstractmethod +from pydantic import BaseModel + + +class OMEDataModel(BaseModel): + """OME(Open Microscopy Environment) aware metadata + @link https://ome-model.readthedocs.io/en/stable/ome-xml/ + """ + image_name: str + size_x: int + size_y: int + size_t: int + size_c: int + + # TODO: 以下今後追加想定 + # SizeZ: int + # AcquisitionDate: date + # Instrument/(Laser|Detector): str + # FrameRate: float # 正確にはOMEの項目ではない様だが、追加想定 + + +class MicroscopeDataReaderBase(metaclass=ABCMeta): + """Microscope data reader base class + """ + + LIBRARY_DIR_KEY = "MICROSCOPES_LIBRARY_DIR" + + def __init__(self): + """ + Initialization + """ + self._init_library() + + def load(self, data_file_path: str): + """ + Reset data + """ + self.__original_metadata = None + self.__ome_metadata = None + self.__lab_specific_metadata = None + + """ + Load data + """ + handle = self._load_data_file(data_file_path) + self.__data_file_path = data_file_path + data_name = os.path.basename(data_file_path) + + """ + Read metadata + """ + self.__original_metadata = self._build_original_metadata( + handle, data_name) + self.__ome_metadata = self._build_ome_metadata( + self.__original_metadata) + self.__lab_specific_metadata = self._build_lab_specific_metadata( + self.__original_metadata) + + """ + Release resources + """ + self._release_resources(handle) + + @abstractmethod + def _init_library(self) -> dict: + """Initialize microscope library + """ + pass + + @abstractmethod + def _load_data_file(self, data_file_path: str) -> object: + """Return metadata specific to microscope instruments + """ + pass + + @abstractmethod + def _build_original_metadata(self, handle: object, data_name: str) -> dict: + """Build metadata specific to microscope instruments + """ + pass + + @abstractmethod + def _build_ome_metadata(self, all_metadata: dict) -> OMEDataModel: + """Build OME(Open Microscopy Environment) aware metadata + """ + pass + + @abstractmethod + def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: + """Build metadata in lab-specific format + """ + pass + + @abstractmethod + def _release_resources(self, handle: object) -> None: + """Release microscope library resources + """ + pass + + @abstractmethod + def _get_images_stack(self) -> list: + """Return microscope image stacks + """ + pass + + @property + def original_metadata(self): + return self.__original_metadata + + @property + def ome_metadata(self): + return self.__ome_metadata + + @property + def lab_specific_metadata(self): + 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..73e82479a --- /dev/null +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -0,0 +1,195 @@ +import os +import ctypes +import json +import platform +from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel + + +class ND2Reader(MicroscopeDataReaderBase): + """Nikon ND2 data reader + """ + + WINDOWS_DLL_FILE = "/nikon/windows/nd2readsdk-shared.dll" + LINUX_DLL_FILE = "/nikon/linux/libnd2readsdk-shared.so" + + @staticmethod + def get_library_path() -> str: + DLL_FILE = __class__.WINDOWS_DLL_FILE \ + if (platform.system() == "Windows") else __class__.LINUX_DLL_FILE + return os.environ.get(__class__.LIBRARY_DIR_KEY, "") + DLL_FILE + + @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) -> dict: + self.__dll = ctypes.cdll.LoadLibrary(__class__.get_library_path()) + + 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_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_FileClose.argtypes = (ctypes.c_void_p, ) + + def _load_data_file(self, data_file_path: str) -> object: + handle = self.__dll.Lim_FileOpenForReadUtf8( + data_file_path.encode("utf-8")) + + if handle is None: + raise FileNotFoundError(data_file_path) + + return handle + + def _build_original_metadata(self, handle: object, data_name: str) -> dict: + attributes = self.__dll.Lim_FileGetAttributes(handle) + metadata = self.__dll.Lim_FileGetMetadata(handle) + textinfo = self.__dll.Lim_FileGetTextinfo(handle) + experiment = self.__dll.Lim_FileGetExperiment(handle) + + attributes = json.loads(attributes) + metadata = json.loads(metadata) + textinfo = json.loads(textinfo) + experiment = json.loads(experiment) + + all_metadata = { + "data_name": data_name, + "attributes": attributes, + "metadata": metadata, + "textinfo": textinfo, + "experiment": experiment, + } + + return all_metadata + + def _build_ome_metadata(self, all_metadata: dict) -> OMEDataModel: + """ + @link OME/NativeND2Reader + """ + + attributes = all_metadata["attributes"] + # metadata = all_metadata["metadata"] + # textinfo = all_metadata["textinfo"] + + omeData = OMEDataModel( + image_name=all_metadata["data_name"], + size_x=attributes["widthPx"], + size_y=attributes["heightPx"], + size_t=attributes["sequenceCount"], + size_c=attributes["componentCount"] # TODO: この内容が正しいか要確認 + ) + + return omeData + + def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: + + # ---------------------------------------- + # Lab固有仕様のmetadata作成 + # ---------------------------------------- + + # TODO: 既存のMEXコードより、以下のコード移植が必要だが、最新版ライブラリでの各値の取得方法が不明となっている。 + # - m_Experiment.uiLevelCount + # - m_Experiment.pAllocatedLevels + # - m_Experiment.pAllocatedLevels[n].uiLoopSize + # - m_Experiment.pAllocatedLevels[n].dInterval + # + # if(m_Experiment.uiLevelCount==0){ + # *type=(char *)"2Ds"; + # } else if (m_Experiment.uiLevelCount==2){ + # *type=(char *)"3Dm"; + # *mxGetPr(Loops)= (double)m_Experiment.pAllocatedLevels[0]. + # uiLoopSize; + # *mxGetPr(ZSlicenum)=(double)m_Experiment.pAllocatedLevels[1].uiLoopSize; + # *mxGetPr(ZInterval)=(double)m_Experiment.pAllocatedLevels[1].dInterval; + # } else { + # if (m_Experiment.pAllocatedLevels[0].uiExpType==2){ + # *type=(char *)"3Ds"; + # *mxGetPr(ZSlicenum)=(double)m_Experiment.pAllocatedLevels[0].uiLoopSize; + # *mxGetPr(ZInterval)=(double)m_Experiment.pAllocatedLevels[0].dInterval; + # } + # if (m_Experiment.pAllocatedLevels[0].uiExpType==0){ + # *type=(char *)"2Dm"; + # *mxGetPr(Loops)=(double)m_Experiment.pAllocatedLevels[0].uiLoopSize; + # } + # } + + attributes = all_metadata["attributes"] + metadata = all_metadata["metadata"] + textinfo = all_metadata["textinfo"] + # experiment = all_metadata["experiment"] + + # ※一部のデータ項目は ch0 より取得 + # TODO: 上記の仕様で適切であるか?(要レビュー) + metadata_ch0_microscope = metadata["channels"][0]["microscope"] + + 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"], + "uiTileWidth": attributes.get("tileWidthPx", None), # Optional + "uiTileHeight": attributes.get("tileHeightPx", None), # Optional + "uiCompression": None, # TODO: Optional ? + # TODO: 最新版ライブラリでは該当するパラメータがない? + # (compressionLevel or compressionType) + "uiQuality": None, # TODO: 最新版ライブラリでは該当するパラメータがない? + + # /* 4 components (From Lab MEX) */ + "Type": None, # TODO: 要設定 + "Loops": None, # TODO: 要設定 + "ZSlicenum": None, # TODO: 要設定 + "ZInterval": None, # TODO: 要設定 + + # /* 7 components (From Lab MEX) */ + "dTimeStart": None, # TODO: 最新版ライブラリでは該当するパラメータがない? + "MicroPerPixel": None, # TODO: 最新版ライブラリでは該当するパラメータがない? + # metadata.volume.axesCalibrated が該当? + "PixelAspect": None, # TODO: 最新版ライブラリでは該当するパラメータがない? + "ObjectiveName": metadata_ch0_microscope.get( + "objectiveName", None), # Optional + # Note: この値 は channels[0] からの取得と想定 + "dObjectiveMag": metadata_ch0_microscope.get( + "objectiveMagnification", + None), # Optional + # Note: この値 は channels[0] からの取得と想定 + # TODO: ライブラリバージョンにより、取得値が異なる可能性がある? + "dObjectiveNA": metadata_ch0_microscope.get( + "objectiveNumericalAperture", + None), # Optional + # Note: この値 は channels[0] からの取得と想定 + "dZoom": metadata_ch0_microscope.get( + "zoomMagnification", None), # Optional + # Note: この値 は 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, handle: object) -> None: + self.__dll.Lim_FileClose(handle) + + def _get_images_stack(self) -> list: + # TODO: under construction + return [] diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py new file mode 100644 index 000000000..0bde397f3 --- /dev/null +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -0,0 +1,35 @@ +import os +from ND2Reader import ND2Reader +import logging + +CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) +LIBRARY_DIR = CURRENT_DIR_PATH + "/dll" +TEST_DATA_PATH = CURRENT_DIR_PATH + "/testdata/nikon-oriOD001.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.exe --log-cli-level=DEBUG + logging.warning("ND2Reader is not available.") + return + + data_reader = ND2Reader() + data_reader.load(TEST_DATA_PATH) + + # # debug print. + import json + print(json.dumps(data_reader.original_metadata)) + # print(data_reader.ome_metadata) + # print(json.dumps(data_reader.lab_specific_metadata)) + + 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() From 1353fc74ed2ed2c558d289c30879f85929e00aac Mon Sep 17 00:00:00 2001 From: itutu-tienday <> Date: Wed, 20 Dec 2023 11:01:28 +0900 Subject: [PATCH 02/26] apply formatter --- .../microscopes/MicroscopeDataReaderBase.py | 35 ++++---- studio/app/optinist/microscopes/ND2Reader.py | 79 +++++++++---------- .../optinist/microscopes/test_ND2Reader.py | 4 +- 3 files changed, 56 insertions(+), 62 deletions(-) diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py index aeca26b0a..02771380b 100644 --- a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -1,5 +1,6 @@ import os from abc import ABCMeta, abstractmethod + from pydantic import BaseModel @@ -7,6 +8,7 @@ class OMEDataModel(BaseModel): """OME(Open Microscopy Environment) aware metadata @link https://ome-model.readthedocs.io/en/stable/ome-xml/ """ + image_name: str size_x: int size_y: int @@ -21,8 +23,7 @@ class OMEDataModel(BaseModel): class MicroscopeDataReaderBase(metaclass=ABCMeta): - """Microscope data reader base class - """ + """Microscope data reader base class""" LIBRARY_DIR_KEY = "MICROSCOPES_LIBRARY_DIR" @@ -50,12 +51,11 @@ def load(self, data_file_path: str): """ Read metadata """ - self.__original_metadata = self._build_original_metadata( - handle, data_name) - self.__ome_metadata = self._build_ome_metadata( - self.__original_metadata) + self.__original_metadata = self._build_original_metadata(handle, data_name) + self.__ome_metadata = self._build_ome_metadata(self.__original_metadata) self.__lab_specific_metadata = self._build_lab_specific_metadata( - self.__original_metadata) + self.__original_metadata + ) """ Release resources @@ -64,44 +64,37 @@ def load(self, data_file_path: str): @abstractmethod def _init_library(self) -> dict: - """Initialize microscope library - """ + """Initialize microscope library""" pass @abstractmethod def _load_data_file(self, data_file_path: str) -> object: - """Return metadata specific to microscope instruments - """ + """Return metadata specific to microscope instruments""" pass @abstractmethod def _build_original_metadata(self, handle: object, data_name: str) -> dict: - """Build metadata specific to microscope instruments - """ + """Build metadata specific to microscope instruments""" pass @abstractmethod def _build_ome_metadata(self, all_metadata: dict) -> OMEDataModel: - """Build OME(Open Microscopy Environment) aware metadata - """ + """Build OME(Open Microscopy Environment) aware metadata""" pass @abstractmethod def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: - """Build metadata in lab-specific format - """ + """Build metadata in lab-specific format""" pass @abstractmethod def _release_resources(self, handle: object) -> None: - """Release microscope library resources - """ + """Release microscope library resources""" pass @abstractmethod def _get_images_stack(self) -> list: - """Return microscope image stacks - """ + """Return microscope image stacks""" pass @property diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index 73e82479a..e4c7aa68b 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -1,53 +1,57 @@ -import os import ctypes import json +import os import platform + from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel class ND2Reader(MicroscopeDataReaderBase): - """Nikon ND2 data reader - """ + """Nikon ND2 data reader""" WINDOWS_DLL_FILE = "/nikon/windows/nd2readsdk-shared.dll" LINUX_DLL_FILE = "/nikon/linux/libnd2readsdk-shared.so" @staticmethod def get_library_path() -> str: - DLL_FILE = __class__.WINDOWS_DLL_FILE \ - if (platform.system() == "Windows") else __class__.LINUX_DLL_FILE + DLL_FILE = ( + __class__.WINDOWS_DLL_FILE + if (platform.system() == "Windows") + else __class__.LINUX_DLL_FILE + ) return os.environ.get(__class__.LIBRARY_DIR_KEY, "") + DLL_FILE @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()) + """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) -> dict: self.__dll = ctypes.cdll.LoadLibrary(__class__.get_library_path()) - self.__dll.Lim_FileOpenForReadUtf8.argtypes = (ctypes.c_char_p, ) + 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.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.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.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.argtypes = (ctypes.c_void_p,) self.__dll.Lim_FileGetExperiment.restype = ctypes.c_char_p - self.__dll.Lim_FileGetCoordSize.argtypes = (ctypes.c_void_p, ) + 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, ) + ctypes.c_void_p, + ctypes.c_uint, + ) self.__dll.Lim_FileGetCoordsFromSeqIndex.restype = ctypes.c_size_t - self.__dll.Lim_FileClose.argtypes = (ctypes.c_void_p, ) + self.__dll.Lim_FileClose.argtypes = (ctypes.c_void_p,) def _load_data_file(self, data_file_path: str) -> object: - handle = self.__dll.Lim_FileOpenForReadUtf8( - data_file_path.encode("utf-8")) + handle = self.__dll.Lim_FileOpenForReadUtf8(data_file_path.encode("utf-8")) if handle is None: raise FileNotFoundError(data_file_path) @@ -89,13 +93,12 @@ def _build_ome_metadata(self, all_metadata: dict) -> OMEDataModel: size_x=attributes["widthPx"], size_y=attributes["heightPx"], size_t=attributes["sequenceCount"], - size_c=attributes["componentCount"] # TODO: この内容が正しいか要確認 + size_c=attributes["componentCount"], # TODO: この内容が正しいか要確認 ) return omeData def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: - # ---------------------------------------- # Lab固有仕様のmetadata作成 # ---------------------------------------- @@ -146,43 +149,39 @@ def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: "uiSequenceCount": attributes["sequenceCount"], "uiTileWidth": attributes.get("tileWidthPx", None), # Optional "uiTileHeight": attributes.get("tileHeightPx", None), # Optional + # TODO: 最新版ライブラリでは該当するパラメータがない? + # (compressionLevel or compressionType) "uiCompression": None, # TODO: Optional ? - # TODO: 最新版ライブラリでは該当するパラメータがない? - # (compressionLevel or compressionType) - "uiQuality": None, # TODO: 最新版ライブラリでは該当するパラメータがない? - + # TODO: 最新版ライブラリでは該当するパラメータがない? + "uiQuality": None, # /* 4 components (From Lab MEX) */ "Type": None, # TODO: 要設定 "Loops": None, # TODO: 要設定 "ZSlicenum": None, # TODO: 要設定 "ZInterval": None, # TODO: 要設定 - # /* 7 components (From Lab MEX) */ "dTimeStart": None, # TODO: 最新版ライブラリでは該当するパラメータがない? "MicroPerPixel": None, # TODO: 最新版ライブラリでは該当するパラメータがない? - # metadata.volume.axesCalibrated が該当? + # metadata.volume.axesCalibrated が該当? "PixelAspect": None, # TODO: 最新版ライブラリでは該当するパラメータがない? "ObjectiveName": metadata_ch0_microscope.get( - "objectiveName", None), # Optional - # Note: この値 は channels[0] からの取得と想定 + "objectiveName", None + ), # Optional, channels[0] からの取得 + # TODO: ライブラリバージョンにより、取得値が異なる可能性がある? "dObjectiveMag": metadata_ch0_microscope.get( - "objectiveMagnification", - None), # Optional - # Note: この値 は channels[0] からの取得と想定 - # TODO: ライブラリバージョンにより、取得値が異なる可能性がある? + "objectiveMagnification", None + ), # Optional, channels[0] からの取得 "dObjectiveNA": metadata_ch0_microscope.get( - "objectiveNumericalAperture", - None), # Optional - # Note: この値 は channels[0] からの取得と想定 + "objectiveNumericalAperture", None + ), # Optional, channels[0] からの取得 "dZoom": metadata_ch0_microscope.get( - "zoomMagnification", None), # Optional - # Note: この値 は channels[0] からの取得と想定 - + "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) + "Date": textinfo.get("date", None), } return lab_specific_metadata diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py index 0bde397f3..9e5975f61 100644 --- a/studio/app/optinist/microscopes/test_ND2Reader.py +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -1,6 +1,7 @@ +import logging import os + from ND2Reader import ND2Reader -import logging CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) LIBRARY_DIR = CURRENT_DIR_PATH + "/dll" @@ -22,6 +23,7 @@ def test_nd2_reader(): # # debug print. import json + print(json.dumps(data_reader.original_metadata)) # print(data_reader.ome_metadata) # print(json.dumps(data_reader.lab_specific_metadata)) From 39c5c1305d4f4bb232a722d621ff9535692f7e92 Mon Sep 17 00:00:00 2001 From: itutu-tienday <> Date: Wed, 20 Dec 2023 11:06:33 +0900 Subject: [PATCH 03/26] add init member variables --- studio/app/optinist/microscopes/MicroscopeDataReaderBase.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py index 02771380b..c229ec32a 100644 --- a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -33,6 +33,11 @@ def __init__(self): """ self._init_library() + # init members + self.__original_metadata = None + self.__ome_metadata = None + self.__lab_specific_metadata = None + def load(self, data_file_path: str): """ Reset data From e02b4235355dc7aaed60da0c3224f70dbbb5d940 Mon Sep 17 00:00:00 2001 From: tienday Date: Thu, 21 Dec 2023 11:59:01 +0900 Subject: [PATCH 04/26] docker image verup - python:3.8.16-slim -> python:3.8.18-slim --- studio/config/docker/Dockerfile | 2 +- studio/config/docker/Dockerfile.dev | 2 +- studio/config/docker/Dockerfile.test | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/studio/config/docker/Dockerfile b/studio/config/docker/Dockerfile index 4a07a2602..3bb50eff9 100644 --- a/studio/config/docker/Dockerfile +++ b/studio/config/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 python:3.8.16-slim +FROM --platform=linux/amd64 python:3.8.18-slim WORKDIR /app diff --git a/studio/config/docker/Dockerfile.dev b/studio/config/docker/Dockerfile.dev index bd81f9145..2a1bc1a84 100644 --- a/studio/config/docker/Dockerfile.dev +++ b/studio/config/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 python:3.8.16-slim +FROM --platform=linux/amd64 python:3.8.18-slim WORKDIR /app diff --git a/studio/config/docker/Dockerfile.test b/studio/config/docker/Dockerfile.test index a60738799..c04459d19 100644 --- a/studio/config/docker/Dockerfile.test +++ b/studio/config/docker/Dockerfile.test @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 python:3.8.16-slim +FROM --platform=linux/amd64 python:3.8.18-slim WORKDIR /app From e56c439ff26856202579745605e757c41dbc077a Mon Sep 17 00:00:00 2001 From: tienday Date: Thu, 21 Dec 2023 12:03:53 +0900 Subject: [PATCH 05/26] Added explicit loading of other libraries on which the microscopy library depends --- studio/app/optinist/microscopes/ND2Reader.py | 40 ++++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index e4c7aa68b..72c40b172 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -9,17 +9,30 @@ class ND2Reader(MicroscopeDataReaderBase): """Nikon ND2 data reader""" - WINDOWS_DLL_FILE = "/nikon/windows/nd2readsdk-shared.dll" - LINUX_DLL_FILE = "/nikon/linux/libnd2readsdk-shared.so" + 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: - DLL_FILE = ( - __class__.WINDOWS_DLL_FILE - if (platform.system() == "Windows") - else __class__.LINUX_DLL_FILE + platform_naem = platform.system() + + if __class__.LIBRARY_DIR_KEY not in os.environ: + return None + + if platform_naem not in __class__.SDK_LIBRARY_FILES: + return None + + return ( + os.environ.get(__class__.LIBRARY_DIR_KEY) + + __class__.SDK_LIBRARY_FILES[platform_naem]["main"] ) - return os.environ.get(__class__.LIBRARY_DIR_KEY, "") + DLL_FILE @staticmethod def is_available() -> bool: @@ -29,8 +42,21 @@ def is_available() -> bool: ) def _init_library(self) -> dict: + # 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}" + self.__dll = 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,) From 79b848a07eaadea0f75c468cbda032212cf26c48 Mon Sep 17 00:00:00 2001 From: tienday Date: Thu, 21 Dec 2023 13:09:03 +0900 Subject: [PATCH 06/26] fix typo --- studio/app/optinist/microscopes/ND2Reader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index 72c40b172..7a5d69571 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -21,17 +21,17 @@ class ND2Reader(MicroscopeDataReaderBase): @staticmethod def get_library_path() -> str: - platform_naem = platform.system() + platform_name = platform.system() if __class__.LIBRARY_DIR_KEY not in os.environ: return None - if platform_naem not in __class__.SDK_LIBRARY_FILES: + if platform_name not in __class__.SDK_LIBRARY_FILES: return None return ( os.environ.get(__class__.LIBRARY_DIR_KEY) - + __class__.SDK_LIBRARY_FILES[platform_naem]["main"] + + __class__.SDK_LIBRARY_FILES[platform_name]["main"] ) @staticmethod From 0922c2ba650a4d01c4195b03f264a92a1f59b518 Mon Sep 17 00:00:00 2001 From: tienday Date: Tue, 26 Dec 2023 14:19:26 +0900 Subject: [PATCH 07/26] change testdata path --- studio/app/optinist/microscopes/test_ND2Reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py index 9e5975f61..71c0cb920 100644 --- a/studio/app/optinist/microscopes/test_ND2Reader.py +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -5,7 +5,7 @@ CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) LIBRARY_DIR = CURRENT_DIR_PATH + "/dll" -TEST_DATA_PATH = CURRENT_DIR_PATH + "/testdata/nikon-oriOD001.nd2" +TEST_DATA_PATH = CURRENT_DIR_PATH + "/testdata/nikon/nikon-oriOD001.nd2" os.environ[ND2Reader.LIBRARY_DIR_KEY] = LIBRARY_DIR From e136dd6f7d0da697fbda09a7330aad75401addba Mon Sep 17 00:00:00 2001 From: tienday Date: Tue, 26 Dec 2023 17:45:59 +0900 Subject: [PATCH 08/26] create Inscopix isxd reader module --- studio/app/optinist/microscopes/IsxdReader.py | 88 +++++++++++++++++++ .../microscopes/MicroscopeDataReaderBase.py | 33 ++++--- studio/app/optinist/microscopes/ND2Reader.py | 30 ++++--- .../optinist/microscopes/test_IsxdReader.py | 43 +++++++++ .../optinist/microscopes/test_ND2Reader.py | 18 ++-- 5 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 studio/app/optinist/microscopes/IsxdReader.py create mode 100644 studio/app/optinist/microscopes/test_IsxdReader.py diff --git a/studio/app/optinist/microscopes/IsxdReader.py b/studio/app/optinist/microscopes/IsxdReader.py new file mode 100644 index 000000000..2f4d08cec --- /dev/null +++ b/studio/app/optinist/microscopes/IsxdReader.py @@ -0,0 +1,88 @@ +import sys + +import isx +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_data_file(self, data_file_path: str) -> object: + return isx.Movie.read(data_file_path) + + def _build_original_metadata(self, handle: object, data_name: str) -> dict: + movie: isx.Movie = handle + 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_c=0, + 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, handle: object) -> None: + # Note: in inscopix sdk, there is no library (ddl) release process. + pass # do nothing. + + def get_images_stack(self) -> list: + movie: isx.Movie = self.data_handle + image_frames = [] + + for i in range(movie.timing.num_samples): + image_frames.append(movie.get_frame_data(i)) + + return image_frames diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py index c229ec32a..ee0ac9c22 100644 --- a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -1,10 +1,10 @@ import os from abc import ABCMeta, abstractmethod +from dataclasses import dataclass -from pydantic import BaseModel - -class OMEDataModel(BaseModel): +@dataclass +class OMEDataModel: """OME(Open Microscopy Environment) aware metadata @link https://ome-model.readthedocs.io/en/stable/ome-xml/ """ @@ -14,12 +14,12 @@ class OMEDataModel(BaseModel): size_y: int size_t: int size_c: int + fps: int # frames_per_second # TODO: OME標準の類似項目に合わせる # TODO: 以下今後追加想定 # SizeZ: int # AcquisitionDate: date # Instrument/(Laser|Detector): str - # FrameRate: float # 正確にはOMEの項目ではない様だが、追加想定 class MicroscopeDataReaderBase(metaclass=ABCMeta): @@ -34,6 +34,7 @@ def __init__(self): self._init_library() # init members + self.__data_handle = None self.__original_metadata = None self.__ome_metadata = None self.__lab_specific_metadata = None @@ -49,14 +50,16 @@ def load(self, data_file_path: str): """ Load data """ - handle = self._load_data_file(data_file_path) + self.__data_handle = self._load_data_file(data_file_path) self.__data_file_path = data_file_path data_name = os.path.basename(data_file_path) """ Read metadata """ - self.__original_metadata = self._build_original_metadata(handle, data_name) + self.__original_metadata = self._build_original_metadata( + self.__data_handle, data_name + ) self.__ome_metadata = self._build_ome_metadata(self.__original_metadata) self.__lab_specific_metadata = self._build_lab_specific_metadata( self.__original_metadata @@ -65,7 +68,7 @@ def load(self, data_file_path: str): """ Release resources """ - self._release_resources(handle) + self._release_resources(self.__data_handle) @abstractmethod def _init_library(self) -> dict: @@ -83,12 +86,12 @@ def _build_original_metadata(self, handle: object, data_name: str) -> dict: pass @abstractmethod - def _build_ome_metadata(self, all_metadata: dict) -> OMEDataModel: + def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: """Build OME(Open Microscopy Environment) aware metadata""" pass @abstractmethod - def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: + def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: """Build metadata in lab-specific format""" pass @@ -98,18 +101,22 @@ def _release_resources(self, handle: object) -> None: pass @abstractmethod - def _get_images_stack(self) -> list: + def get_images_stack(self) -> list: """Return microscope image stacks""" pass @property - def original_metadata(self): + def data_handle(self) -> object: + return self.__data_handle + + @property + def original_metadata(self) -> dict: return self.__original_metadata @property - def ome_metadata(self): + def ome_metadata(self) -> dict: return self.__ome_metadata @property - def lab_specific_metadata(self): + 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 index 7a5d69571..e26a19f8c 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -21,6 +21,7 @@ class ND2Reader(MicroscopeDataReaderBase): @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: @@ -41,7 +42,7 @@ def is_available() -> bool: __class__.get_library_path() ) - def _init_library(self) -> dict: + 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()) @@ -95,7 +96,7 @@ def _build_original_metadata(self, handle: object, data_name: str) -> dict: textinfo = json.loads(textinfo) experiment = json.loads(experiment) - all_metadata = { + original_metadata = { "data_name": data_name, "attributes": attributes, "metadata": metadata, @@ -103,28 +104,29 @@ def _build_original_metadata(self, handle: object, data_name: str) -> dict: "experiment": experiment, } - return all_metadata + return original_metadata - def _build_ome_metadata(self, all_metadata: dict) -> OMEDataModel: + def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: """ @link OME/NativeND2Reader """ - attributes = all_metadata["attributes"] - # metadata = all_metadata["metadata"] - # textinfo = all_metadata["textinfo"] + attributes = original_metadata["attributes"] + # metadata = original_metadata["metadata"] + # textinfo = original_metadata["textinfo"] omeData = OMEDataModel( - image_name=all_metadata["data_name"], + image_name=original_metadata["data_name"], size_x=attributes["widthPx"], size_y=attributes["heightPx"], size_t=attributes["sequenceCount"], size_c=attributes["componentCount"], # TODO: この内容が正しいか要確認 + fps=0, # TODO: 要設定 ) return omeData - def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: + def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: # ---------------------------------------- # Lab固有仕様のmetadata作成 # ---------------------------------------- @@ -155,10 +157,10 @@ def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: # } # } - attributes = all_metadata["attributes"] - metadata = all_metadata["metadata"] - textinfo = all_metadata["textinfo"] - # experiment = all_metadata["experiment"] + attributes = original_metadata["attributes"] + metadata = original_metadata["metadata"] + textinfo = original_metadata["textinfo"] + # experiment = original_metadata["experiment"] # ※一部のデータ項目は ch0 より取得 # TODO: 上記の仕様で適切であるか?(要レビュー) @@ -215,6 +217,6 @@ def _build_lab_specific_metadata(self, all_metadata: dict) -> dict: def _release_resources(self, handle: object) -> None: self.__dll.Lim_FileClose(handle) - def _get_images_stack(self) -> list: + def get_images_stack(self) -> list: # TODO: under construction return [] diff --git a/studio/app/optinist/microscopes/test_IsxdReader.py b/studio/app/optinist/microscopes/test_IsxdReader.py new file mode 100644 index 000000000..a733b5f99 --- /dev/null +++ b/studio/app/optinist/microscopes/test_IsxdReader.py @@ -0,0 +1,43 @@ +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/fixed-oist-sample_data_short.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) + + # debug print. + import json + + # dump attributes + pprint(json.dumps(data_reader.original_metadata)) + pprint(data_reader.ome_metadata) + pprint(json.dumps(data_reader.lab_specific_metadata)) + + # dump image stack + # images_stack = data_reader.get_images_stack() + # pprint(len(images_stack)) + + # 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 index 71c0cb920..b5e89738a 100644 --- a/studio/app/optinist/microscopes/test_ND2Reader.py +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -1,5 +1,6 @@ import logging import os +from pprint import pprint from ND2Reader import ND2Reader @@ -14,20 +15,27 @@ 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.exe --log-cli-level=DEBUG + # > pytest --log-cli-level=DEBUG logging.warning("ND2Reader is not available.") return + # initialize data_reader = ND2Reader() data_reader.load(TEST_DATA_PATH) - # # debug print. + # debug print. import json - print(json.dumps(data_reader.original_metadata)) - # print(data_reader.ome_metadata) - # print(json.dumps(data_reader.lab_specific_metadata)) + # dump attributes + pprint(json.dumps(data_reader.original_metadata)) + pprint(data_reader.ome_metadata) + pprint(json.dumps(data_reader.lab_specific_metadata)) + # dump image stack + # images_stack = data_reader.get_images_stack() + # pprint(len(images_stack)) + + # 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 From 42f7f207f4109e8e4dcbc9fd351e7d869457b92e Mon Sep 17 00:00:00 2001 From: tienday Date: Tue, 26 Dec 2023 18:45:20 +0900 Subject: [PATCH 09/26] create olympus oir reader mock --- studio/app/optinist/microscopes/OIRReader.py | 89 +++++++++++++++++++ .../optinist/microscopes/test_OIRReader.py | 31 +++++++ 2 files changed, 120 insertions(+) create mode 100644 studio/app/optinist/microscopes/OIRReader.py create mode 100644 studio/app/optinist/microscopes/test_OIRReader.py diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py new file mode 100644 index 000000000..df5d0bf1d --- /dev/null +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -0,0 +1,89 @@ +import ctypes + +# import json +import os +import platform + +from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel + + +class OIRReader(MicroscopeDataReaderBase): + """Olympus OIR data reader""" + + 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}" + self.__dll = ctypes.cdll.LoadLibrary(dependency_path) + + self.__dll = ctypes.cdll.LoadLibrary(__class__.get_library_path()) + + # load sdk library + self.__dll.Initialize() + + def _load_data_file(self, data_file_path: str) -> object: + # TODO: Under construction + return None + + def _build_original_metadata(self, handle: object, data_name: str) -> dict: + # TODO: Under construction + return None + + def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: + """ + @link OME/NativeND2Reader + """ + + # TODO: Under construction + return None + + def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: + # TODO: Under construction + return None + + def _release_resources(self, handle: object) -> None: + # TODO: Under construction + pass + + def get_images_stack(self) -> list: + # TODO: under construction + return None diff --git a/studio/app/optinist/microscopes/test_OIRReader.py b/studio/app/optinist/microscopes/test_OIRReader.py new file mode 100644 index 000000000..1efc309be --- /dev/null +++ b/studio/app/optinist/microscopes/test_OIRReader.py @@ -0,0 +1,31 @@ +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() + + pprint(data_reader) + + # TODO: Under construction + + +if __name__ == "__main__": + test_oir_reader() From 05be76a277cdbb4b00dae84a061bb95697c3cac1 Mon Sep 17 00:00:00 2001 From: itutu-tienday <> Date: Thu, 28 Dec 2023 16:05:27 +0900 Subject: [PATCH 10/26] refactoring (use list comprehension, etc) --- studio/app/optinist/microscopes/IsxdReader.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/studio/app/optinist/microscopes/IsxdReader.py b/studio/app/optinist/microscopes/IsxdReader.py index 2f4d08cec..d09dab8a6 100644 --- a/studio/app/optinist/microscopes/IsxdReader.py +++ b/studio/app/optinist/microscopes/IsxdReader.py @@ -1,6 +1,10 @@ import sys -import isx +try: + import isx +except ModuleNotFoundError: + pass + from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel @@ -80,9 +84,6 @@ def _release_resources(self, handle: object) -> None: def get_images_stack(self) -> list: movie: isx.Movie = self.data_handle - image_frames = [] - - for i in range(movie.timing.num_samples): - image_frames.append(movie.get_frame_data(i)) + image_frames = [movie.get_frame_data(i) for i in range(movie.timing.num_samples)] return image_frames From 205445f6ff0288865c1d19a15990c56b0ad4709b Mon Sep 17 00:00:00 2001 From: tienday Date: Thu, 28 Dec 2023 17:30:16 +0900 Subject: [PATCH 11/26] fix linter --- studio/app/optinist/microscopes/.gitignore | 2 ++ studio/app/optinist/microscopes/IsxdReader.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 studio/app/optinist/microscopes/.gitignore diff --git a/studio/app/optinist/microscopes/.gitignore b/studio/app/optinist/microscopes/.gitignore new file mode 100644 index 000000000..dd66836af --- /dev/null +++ b/studio/app/optinist/microscopes/.gitignore @@ -0,0 +1,2 @@ +dll/ +testdata/ diff --git a/studio/app/optinist/microscopes/IsxdReader.py b/studio/app/optinist/microscopes/IsxdReader.py index d09dab8a6..f0ed50baa 100644 --- a/studio/app/optinist/microscopes/IsxdReader.py +++ b/studio/app/optinist/microscopes/IsxdReader.py @@ -84,6 +84,8 @@ def _release_resources(self, handle: object) -> None: def get_images_stack(self) -> list: movie: isx.Movie = self.data_handle - image_frames = [movie.get_frame_data(i) for i in range(movie.timing.num_samples)] + image_frames = [ + movie.get_frame_data(i) for i in range(movie.timing.num_samples) + ] return image_frames From a52d56db1a4af2da10adfa81663c4555366e3b68 Mon Sep 17 00:00:00 2001 From: tienday Date: Thu, 28 Dec 2023 23:59:53 +0900 Subject: [PATCH 12/26] update nikon metadata retrieval code --- studio/app/optinist/microscopes/ND2Reader.py | 9 +++++---- studio/app/optinist/microscopes/test_IsxdReader.py | 7 +++++-- studio/app/optinist/microscopes/test_ND2Reader.py | 7 +++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index e26a19f8c..7494be179 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -112,8 +112,10 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: """ attributes = original_metadata["attributes"] - # metadata = original_metadata["metadata"] - # textinfo = original_metadata["textinfo"] + experiment = original_metadata["experiment"] + + # TODO: experiment, periods, の参照は先頭データに固定でOK? + period_ms = float(experiment[0]["parameters"]["periods"][0]["periodMs"]) omeData = OMEDataModel( image_name=original_metadata["data_name"], @@ -121,7 +123,7 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: size_y=attributes["heightPx"], size_t=attributes["sequenceCount"], size_c=attributes["componentCount"], # TODO: この内容が正しいか要確認 - fps=0, # TODO: 要設定 + fps=(1000 / period_ms), ) return omeData @@ -163,7 +165,6 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: # experiment = original_metadata["experiment"] # ※一部のデータ項目は ch0 より取得 - # TODO: 上記の仕様で適切であるか?(要レビュー) metadata_ch0_microscope = metadata["channels"][0]["microscope"] lab_specific_metadata = { diff --git a/studio/app/optinist/microscopes/test_IsxdReader.py b/studio/app/optinist/microscopes/test_IsxdReader.py index a733b5f99..2127f99e0 100644 --- a/studio/app/optinist/microscopes/test_IsxdReader.py +++ b/studio/app/optinist/microscopes/test_IsxdReader.py @@ -26,9 +26,12 @@ def test_isxd_reader(): import json # dump attributes - pprint(json.dumps(data_reader.original_metadata)) + print("[original_metadata]", json.dumps(data_reader.original_metadata, indent=2)) pprint(data_reader.ome_metadata) - pprint(json.dumps(data_reader.lab_specific_metadata)) + print( + "[lab_specific_metadata]", + json.dumps(data_reader.lab_specific_metadata, indent=2), + ) # dump image stack # images_stack = data_reader.get_images_stack() diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py index b5e89738a..50906c4d3 100644 --- a/studio/app/optinist/microscopes/test_ND2Reader.py +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -27,9 +27,12 @@ def test_nd2_reader(): import json # dump attributes - pprint(json.dumps(data_reader.original_metadata)) + print("[original_metadata]", json.dumps(data_reader.original_metadata, indent=2)) pprint(data_reader.ome_metadata) - pprint(json.dumps(data_reader.lab_specific_metadata)) + print( + "[lab_specific_metadata]", + json.dumps(data_reader.lab_specific_metadata, indent=2), + ) # dump image stack # images_stack = data_reader.get_images_stack() From 3b557cf02c563c4cb78f843fc50485760df48dbb Mon Sep 17 00:00:00 2001 From: tienday Date: Fri, 29 Dec 2023 12:38:54 +0900 Subject: [PATCH 13/26] Added nikon image stack loading process --- studio/app/optinist/microscopes/IsxdReader.py | 4 +- .../microscopes/MicroscopeDataReaderBase.py | 22 ++-- studio/app/optinist/microscopes/ND2Reader.py | 110 ++++++++++++++++-- .../optinist/microscopes/test_ND2Reader.py | 28 +++-- 4 files changed, 136 insertions(+), 28 deletions(-) diff --git a/studio/app/optinist/microscopes/IsxdReader.py b/studio/app/optinist/microscopes/IsxdReader.py index f0ed50baa..c260fa7ae 100644 --- a/studio/app/optinist/microscopes/IsxdReader.py +++ b/studio/app/optinist/microscopes/IsxdReader.py @@ -29,8 +29,8 @@ def _init_library(self): # initialization process. (using pip module) pass # do nothing. - def _load_data_file(self, data_file_path: str) -> object: - return isx.Movie.read(data_file_path) + def _load_data_file(self, data_path: str) -> object: + return isx.Movie.read(data_path) def _build_original_metadata(self, handle: object, data_name: str) -> dict: movie: isx.Movie = handle diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py index ee0ac9c22..50a163303 100644 --- a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -34,12 +34,13 @@ def __init__(self): self._init_library() # init members + self.__data_path = None self.__data_handle = None self.__original_metadata = None self.__ome_metadata = None self.__lab_specific_metadata = None - def load(self, data_file_path: str): + def load(self, data_path: str): """ Reset data """ @@ -50,16 +51,15 @@ def load(self, data_file_path: str): """ Load data """ - self.__data_handle = self._load_data_file(data_file_path) - self.__data_file_path = data_file_path - data_name = os.path.basename(data_file_path) + handle = self._load_data_file(data_path) + self.__data_handle = handle + self.__data_path = data_path + data_name = os.path.basename(data_path) """ Read metadata """ - self.__original_metadata = self._build_original_metadata( - self.__data_handle, data_name - ) + self.__original_metadata = self._build_original_metadata(handle, data_name) self.__ome_metadata = self._build_ome_metadata(self.__original_metadata) self.__lab_specific_metadata = self._build_lab_specific_metadata( self.__original_metadata @@ -68,7 +68,7 @@ def load(self, data_file_path: str): """ Release resources """ - self._release_resources(self.__data_handle) + self._release_resources(handle) @abstractmethod def _init_library(self) -> dict: @@ -76,7 +76,7 @@ def _init_library(self) -> dict: pass @abstractmethod - def _load_data_file(self, data_file_path: str) -> object: + def _load_data_file(self, data_path: str) -> object: """Return metadata specific to microscope instruments""" pass @@ -105,6 +105,10 @@ def get_images_stack(self) -> list: """Return microscope image stacks""" pass + @property + def data_path(self) -> str: + return self.__data_path + @property def data_handle(self) -> object: return self.__data_handle diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index 7494be179..a5d8a91f6 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -2,10 +2,29 @@ import json import os import platform +from enum import IntEnum +import numpy as np from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel +class LimCode(IntEnum): + LIM_OK = 0 + LIM_ERR_UNEXPECTED = -1 + + +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""" @@ -68,6 +87,8 @@ def _init_library(self): 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_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 = ( @@ -75,13 +96,20 @@ def _init_library(self): 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_data_file(self, data_file_path: str) -> object: - handle = self.__dll.Lim_FileOpenForReadUtf8(data_file_path.encode("utf-8")) + def _load_data_file(self, data_path: str) -> object: + handle = self.__dll.Lim_FileOpenForReadUtf8(data_path.encode("utf-8")) if handle is None: - raise FileNotFoundError(data_file_path) + raise FileNotFoundError(data_path) return handle @@ -93,8 +121,8 @@ def _build_original_metadata(self, handle: object, data_name: str) -> dict: attributes = json.loads(attributes) metadata = json.loads(metadata) - textinfo = json.loads(textinfo) - experiment = json.loads(experiment) + textinfo = json.loads(textinfo) if textinfo is not None else None + experiment = json.loads(experiment) if experiment is not None else None original_metadata = { "data_name": data_name, @@ -115,7 +143,12 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: experiment = original_metadata["experiment"] # TODO: experiment, periods, の参照は先頭データに固定でOK? - period_ms = float(experiment[0]["parameters"]["periods"][0]["periodMs"]) + if (experiment is not None) and ("periods" in experiment[0]["parameters"]): + period_ms = float(experiment[0]["parameters"]["periods"][0]["periodMs"]) + fps = 1000 / period_ms + else: + period_ms = 0 + fps = 0 omeData = OMEDataModel( image_name=original_metadata["data_name"], @@ -123,7 +156,7 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: size_y=attributes["heightPx"], size_t=attributes["sequenceCount"], size_c=attributes["componentCount"], # TODO: この内容が正しいか要確認 - fps=(1000 / period_ms), + fps=fps, ) return omeData @@ -219,5 +252,64 @@ def _release_resources(self, handle: object) -> None: self.__dll.Lim_FileClose(handle) def get_images_stack(self) -> list: - # TODO: under construction - return [] + """Return microscope image stacks""" + + # initialization + handle = self._load_data_file(self.data_path) + pic: LIMPICTURE = LIMPICTURE() + seq_count = self.__dll.Lim_FileGetSeqCount(handle) + result_stack = [] + + # 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 + # TODO: 以下の計算式で適切であるか画像フォーマットの正確な仕様を確認必要 + # ※一旦、pixcel_bytes=2 (uiComponents=1, pic.uiBitsPerComp) の動作OKは確認 + # ・pixcel_bytes は最大9バイト(3*3)が想定される? + # ・移植元コードでは、uiComponents へのアクセスに、ビットシフトを利用している模様? + # TODO: またその他のパラメーターの考慮も必要? + # ・"Channel" を考慮した画像取得(アドレス計算)が、要件への対応に必要 + # ・3D("ZSlicenum" ?)画像を考慮した画像取得(アドレス計算)が必要? + pixcel_bytes = int(pic.uiComponents * ((pic.uiBitsPerComp + 7) / 8)) + if pixcel_bytes == 1: + pixcel_data_type = ctypes.c_byte + elif pixcel_bytes > 1 and pixcel_bytes <= 2: + pixcel_data_type = ctypes.c_ushort + elif pixcel_bytes > 2: + pixcel_data_type = ctypes.c_uint + else: + 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, pixcel_data_type, + # ) + + # scan each lines + lines_buffer = [] + for line_idx in range(pic.uiHeight): + # Data acquisition for line and conversion to np.ndarray format + line_buffer = (pixcel_data_type * pic.uiWidth).from_address( + pic.pImageData + (line_idx * pic.uiWidthBytes) + ) + line_buffer_array = np.ctypeslib.as_array(line_buffer) + + # stored in line data stack + lines_buffer.append(line_buffer_array) + + # allocate planar image buffer (nb.ndarray format) + plane_buffer = np.concatenate([[v] for v in lines_buffer]) + result_stack.append(plane_buffer) + + # release resources + self._release_resources(handle) + + return result_stack diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py index 50906c4d3..4f7bff658 100644 --- a/studio/app/optinist/microscopes/test_ND2Reader.py +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -6,7 +6,7 @@ CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) LIBRARY_DIR = CURRENT_DIR_PATH + "/dll" -TEST_DATA_PATH = CURRENT_DIR_PATH + "/testdata/nikon/nikon-oriOD001.nd2" +TEST_DATA_PATH = CURRENT_DIR_PATH + "/testdata/nikon/pia_volume_area1.nd2" os.environ[ND2Reader.LIBRARY_DIR_KEY] = LIBRARY_DIR @@ -23,20 +23,32 @@ def test_nd2_reader(): data_reader = ND2Reader() data_reader.load(TEST_DATA_PATH) - # debug print. import json # 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), - ) + # print("[lab_specific_metadata]", + # json.dumps(data_reader.lab_specific_metadata, indent=2)) # dump image stack - # images_stack = data_reader.get_images_stack() - # pprint(len(images_stack)) + images_stack = [] + images_stack = data_reader.get_images_stack() + + # save tiff image (multi page) test + if len(images_stack) > 0: + from PIL import Image + + save_stack = [Image.fromarray(frame) for frame in images_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["attributes"]["widthPx"] > 0 From df4b395a5ac9d07aa641b502f7fc90d09d4a2e54 Mon Sep 17 00:00:00 2001 From: tienday Date: Sat, 20 Jan 2024 10:40:42 +0900 Subject: [PATCH 14/26] update nd2reader - supports multi-channel images - refactoring --- .../microscopes/MicroscopeDataReaderBase.py | 8 +- studio/app/optinist/microscopes/ND2Reader.py | 77 ++++++++++++------- .../optinist/microscopes/test_ND2Reader.py | 31 ++++---- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py index 50a163303..180964c06 100644 --- a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -10,10 +10,10 @@ class OMEDataModel: """ image_name: str - size_x: int - size_y: int - size_t: int - size_c: int + size_x: int # width + size_y: int # height + size_t: int # time + size_c: int # TODO: 要確認 fps: int # frames_per_second # TODO: OME標準の類似項目に合わせる # TODO: 以下今後追加想定 diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index a5d8a91f6..febb7dc0d 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -142,10 +142,10 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: attributes = original_metadata["attributes"] experiment = original_metadata["experiment"] - # TODO: experiment, periods, の参照は先頭データに固定でOK? + # experiment, periods, の参照は先頭データの内容から取得 if (experiment is not None) and ("periods" in experiment[0]["parameters"]): period_ms = float(experiment[0]["parameters"]["periods"][0]["periodMs"]) - fps = 1000 / period_ms + fps = (1000 / period_ms) if period_ms > 0 else 0 else: period_ms = 0 fps = 0 @@ -155,7 +155,7 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: size_x=attributes["widthPx"], size_y=attributes["heightPx"], size_t=attributes["sequenceCount"], - size_c=attributes["componentCount"], # TODO: この内容が正しいか要確認 + size_c=0, fps=fps, ) @@ -258,7 +258,14 @@ def get_images_stack(self) -> list: handle = self._load_data_file(self.data_path) pic: LIMPICTURE = LIMPICTURE() seq_count = self.__dll.Lim_FileGetSeqCount(handle) - result_stack = [] + + # 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): @@ -269,47 +276,59 @@ def get_images_stack(self) -> list: break # calculate pixel byte size - # TODO: 以下の計算式で適切であるか画像フォーマットの正確な仕様を確認必要 - # ※一旦、pixcel_bytes=2 (uiComponents=1, pic.uiBitsPerComp) の動作OKは確認 - # ・pixcel_bytes は最大9バイト(3*3)が想定される? - # ・移植元コードでは、uiComponents へのアクセスに、ビットシフトを利用している模様? - # TODO: またその他のパラメーターの考慮も必要? - # ・"Channel" を考慮した画像取得(アドレス計算)が、要件への対応に必要 - # ・3D("ZSlicenum" ?)画像を考慮した画像取得(アドレス計算)が必要? - pixcel_bytes = int(pic.uiComponents * ((pic.uiBitsPerComp + 7) / 8)) - if pixcel_bytes == 1: - pixcel_data_type = ctypes.c_byte - elif pixcel_bytes > 1 and pixcel_bytes <= 2: - pixcel_data_type = ctypes.c_ushort - elif pixcel_bytes > 2: - pixcel_data_type = ctypes.c_uint - else: + # 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, pixcel_data_type, + # pic.uiBitsPerComp, pixcel_bytes, # ) - # scan each lines + # 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 = (pixcel_data_type * pic.uiWidth).from_address( + line_buffer = line_bytes.from_address( pic.pImageData + (line_idx * pic.uiWidthBytes) ) line_buffer_array = np.ctypeslib.as_array(line_buffer) - # stored in line data stack + # stored in line buffer stack lines_buffer.append(line_buffer_array) - # allocate planar image buffer (nb.ndarray format) - plane_buffer = np.concatenate([[v] for v in lines_buffer]) - result_stack.append(plane_buffer) - - # release resources + # 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) + + # do release resources self._release_resources(handle) - return result_stack + return result_channels_stacks diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py index 4f7bff658..14ba8fce0 100644 --- a/studio/app/optinist/microscopes/test_ND2Reader.py +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -31,24 +31,27 @@ def test_nd2_reader(): # print("[lab_specific_metadata]", # json.dumps(data_reader.lab_specific_metadata, indent=2)) - # dump image stack - images_stack = [] - images_stack = data_reader.get_images_stack() + # get image stacks (for all channels) + channels_stacks = data_reader.get_images_stack() # save tiff image (multi page) test - if len(images_stack) > 0: + if (len(channels_stacks) > 0) and (len(channels_stacks[0]) > 0): from PIL import Image - save_stack = [Image.fromarray(frame) for frame in images_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:], - ) + # save stacks for all channels + for channel_idx, images_stack in enumerate(channels_stacks): + save_stack = [Image.fromarray(frame) for frame in images_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 From e843ad91ab23aa301e0d1324cb8cb1659d5fa938 Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Tue, 23 Jan 2024 16:43:52 +0900 Subject: [PATCH 15/26] update nd2 lab_specific_metadata --- studio/app/optinist/microscopes/ND2Reader.py | 134 ++++++++++++------- 1 file changed, 83 insertions(+), 51 deletions(-) diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index febb7dc0d..0616428e7 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -2,7 +2,7 @@ import json import os import platform -from enum import IntEnum +from enum import Enum, IntEnum import numpy as np from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel @@ -13,6 +13,21 @@ class LimCode(IntEnum): 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), @@ -87,6 +102,8 @@ def _init_library(self): 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,) @@ -117,19 +134,27 @@ def _build_original_metadata(self, handle: object, data_name: str) -> dict: attributes = self.__dll.Lim_FileGetAttributes(handle) metadata = self.__dll.Lim_FileGetMetadata(handle) textinfo = self.__dll.Lim_FileGetTextinfo(handle) - experiment = self.__dll.Lim_FileGetExperiment(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 - experiment = json.loads(experiment) if experiment 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, - "experiment": experiment, + "experiments": experiments, + "frame_metadata": frame_metadata_single, } return original_metadata @@ -140,11 +165,11 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: """ attributes = original_metadata["attributes"] - experiment = original_metadata["experiment"] + experiments = original_metadata["experiments"] # experiment, periods, の参照は先頭データの内容から取得 - if (experiment is not None) and ("periods" in experiment[0]["parameters"]): - period_ms = float(experiment[0]["parameters"]["periods"][0]["periodMs"]) + if (experiments is not None) and ("periods" in experiments[0]["parameters"]): + period_ms = float(experiments[0]["parameters"]["periods"][0]["periodMs"]) fps = (1000 / period_ms) if period_ms > 0 else 0 else: period_ms = 0 @@ -162,43 +187,49 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: return omeData def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: - # ---------------------------------------- - # Lab固有仕様のmetadata作成 - # ---------------------------------------- - - # TODO: 既存のMEXコードより、以下のコード移植が必要だが、最新版ライブラリでの各値の取得方法が不明となっている。 - # - m_Experiment.uiLevelCount - # - m_Experiment.pAllocatedLevels - # - m_Experiment.pAllocatedLevels[n].uiLoopSize - # - m_Experiment.pAllocatedLevels[n].dInterval - # - # if(m_Experiment.uiLevelCount==0){ - # *type=(char *)"2Ds"; - # } else if (m_Experiment.uiLevelCount==2){ - # *type=(char *)"3Dm"; - # *mxGetPr(Loops)= (double)m_Experiment.pAllocatedLevels[0]. - # uiLoopSize; - # *mxGetPr(ZSlicenum)=(double)m_Experiment.pAllocatedLevels[1].uiLoopSize; - # *mxGetPr(ZInterval)=(double)m_Experiment.pAllocatedLevels[1].dInterval; - # } else { - # if (m_Experiment.pAllocatedLevels[0].uiExpType==2){ - # *type=(char *)"3Ds"; - # *mxGetPr(ZSlicenum)=(double)m_Experiment.pAllocatedLevels[0].uiLoopSize; - # *mxGetPr(ZInterval)=(double)m_Experiment.pAllocatedLevels[0].dInterval; - # } - # if (m_Experiment.pAllocatedLevels[0].uiExpType==0){ - # *type=(char *)"2Dm"; - # *mxGetPr(Loops)=(double)m_Experiment.pAllocatedLevels[0].uiLoopSize; - # } - # } + """Build metadata in lab-specific format""" attributes = original_metadata["attributes"] metadata = original_metadata["metadata"] textinfo = original_metadata["textinfo"] - # experiment = original_metadata["experiment"] + 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"] + + # ---------------------------------------- + # 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) */ @@ -209,27 +240,28 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: "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: 最新版ライブラリでは該当するパラメータがない? - # (compressionLevel or compressionType) - "uiCompression": None, # TODO: Optional ? - # TODO: 最新版ライブラリでは該当するパラメータがない? - "uiQuality": None, + # TODO: 旧ライブラリでは、数値が設定されているが、旧ライブラリでは文字列 (uiCompression) + "uiCompression": attributes.get("compressionType", None), # Optional + # TODO: 旧ライブラリでは、適切ではない値が格納されている可能性がある? (uiQuality) + "uiQuality": attributes.get("compressionLevel", None), # Optional # /* 4 components (From Lab MEX) */ - "Type": None, # TODO: 要設定 - "Loops": None, # TODO: 要設定 - "ZSlicenum": None, # TODO: 要設定 - "ZInterval": None, # TODO: 要設定 + "Type": dimension_type, + "Loops": loop_count, + "ZSlicenum": z_slicenum, + "ZInterval": z_interval, # /* 7 components (From Lab MEX) */ - "dTimeStart": None, # TODO: 最新版ライブラリでは該当するパラメータがない? - "MicroPerPixel": None, # TODO: 最新版ライブラリでは該当するパラメータがない? - # metadata.volume.axesCalibrated が該当? - "PixelAspect": None, # TODO: 最新版ライブラリでは該当するパラメータがない? + "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: ライブラリバージョンにより、取得値が異なる可能性がある? + # TODO: ライブラリバージョンにより、取得値が異なっている可能性がある? (dObjectiveMag) "dObjectiveMag": metadata_ch0_microscope.get( "objectiveMagnification", None ), # Optional, channels[0] からの取得 From a42b5f35ad30ef780669e574e995e56700501e79 Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Mon, 29 Jan 2024 19:33:42 +0900 Subject: [PATCH 16/26] update .codespellignore --- .codespellignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.codespellignore b/.codespellignore index 0672eda0b..b550cf700 100644 --- a/.codespellignore +++ b/.codespellignore @@ -3,3 +3,4 @@ afterAll xdescribe optinist Optinist +nNumber From 5cb62368c64d9bfa143fffbe036a7552df742a55 Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Mon, 29 Jan 2024 19:49:08 +0900 Subject: [PATCH 17/26] add .codespellignore setting --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) 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 From f2170850d1cd98289264f3d95c8498e75a8a907a Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Mon, 29 Jan 2024 20:04:19 +0900 Subject: [PATCH 18/26] oir reader module initial commit --- .codespellignore | 2 + studio/app/optinist/microscopes/ND2Reader.py | 2 +- studio/app/optinist/microscopes/OIRReader.py | 53 +++- .../modules/olympus/area_image_size.py | 26 ++ .../microscopes/modules/olympus/axis_info.py | 147 ++++++++++ .../modules/olympus/channel_info.py | 107 ++++++++ .../modules/olympus/file_creation_time.py | 24 ++ .../modules/olympus/frame_manager.py | 251 ++++++++++++++++++ .../microscopes/modules/olympus/h_ida.py | 91 +++++++ .../microscopes/modules/olympus/lib.py | 103 +++++++ .../modules/olympus/objective_lens_info.py | 83 ++++++ .../modules/olympus/pixel_length.py | 33 +++ .../modules/olympus/roi_collection.py | 216 +++++++++++++++ .../modules/olympus/system_info.py | 37 +++ .../modules/olympus/user_comment.py | 19 ++ .../optinist/microscopes/test_OIRReader.py | 4 +- 16 files changed, 1189 insertions(+), 9 deletions(-) create mode 100644 studio/app/optinist/microscopes/modules/olympus/area_image_size.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/axis_info.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/channel_info.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/file_creation_time.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/frame_manager.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/h_ida.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/lib.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/pixel_length.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/roi_collection.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/system_info.py create mode 100644 studio/app/optinist/microscopes/modules/olympus/user_comment.py diff --git a/.codespellignore b/.codespellignore index b550cf700..7e3714bf4 100644 --- a/.codespellignore +++ b/.codespellignore @@ -3,4 +3,6 @@ afterAll xdescribe optinist Optinist +nnumber nNumber +NNUMBER diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index 0616428e7..49914fb85 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -86,7 +86,7 @@ def _init_library(self): for dependency in dependencies: dependency_path = f"{platform_library_dir}/{dependency}" - self.__dll = ctypes.cdll.LoadLibrary(dependency_path) + ctypes.cdll.LoadLibrary(dependency_path) # load sdk library self.__dll = ctypes.cdll.LoadLibrary(__class__.get_library_path()) diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index df5d0bf1d..de6b4d6b5 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -6,6 +6,9 @@ from MicroscopeDataReaderBase import MicroscopeDataReaderBase, OMEDataModel +import studio.app.optinist.microscopes.modules.olympus.lib as lib +from studio.app.optinist.microscopes.modules.olympus.h_ida import IDA_OpenMode + class OIRReader(MicroscopeDataReaderBase): """Olympus OIR data reader""" @@ -53,16 +56,56 @@ def _init_library(self): for dependency in dependencies: dependency_path = f"{platform_library_dir}/{dependency}" - self.__dll = ctypes.cdll.LoadLibrary(dependency_path) - - self.__dll = ctypes.cdll.LoadLibrary(__class__.get_library_path()) + ctypes.cdll.LoadLibrary(dependency_path) # load sdk library + self.__dll = lib.load_library(__class__.get_library_path()) + + # initialize sdk library self.__dll.Initialize() def _load_data_file(self, data_file_path: str) -> object: - # TODO: Under construction - return None + ida = self.__dll + hAccessor = self.__hAccessor = ctypes.c_void_p() + hFile = self.__hFile = ctypes.c_void_p() + + # Get Accessor + ida.GetAccessor(data_file_path, ctypes.byref(hAccessor)) + if not hAccessor: + # TODO: raise exception + print("Please check the File path") + return + + # Connect + ida.Connect(hAccessor) + + # Open file + # TODO: process exception + ida.Open( + # result = ida.Open( + hAccessor, + data_file_path, + IDA_OpenMode.IDA_OM_READ, + ctypes.byref(hFile), + ) + + # GetNumberOfGroup + num_of_group = ctypes.c_int() + ida.GetNumOfGroups(hAccessor, hFile, ctypes.byref(num_of_group)) + + # Get Group Handle + hGroup = self.__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)) + + # GetNumberOfLevels + num_of_layer = ctypes.c_int() + ida.GetNumOfLevels(hAccessor, hGroup, ctypes.byref(num_of_layer)) + print("-- GetNumOfLevels - num_of_layer:", num_of_layer) + + return (hAccessor, hFile, hGroup) def _build_original_metadata(self, handle: object, data_name: str) -> dict: # TODO: Under construction 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..48404d751 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py @@ -0,0 +1,26 @@ +import lib + +ida = lib.ida + + +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: + 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}") 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..eb0c13d62 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/axis_info.py @@ -0,0 +1,147 @@ +import ctypes as ct + +import h_ida +import lib + +ida = lib.ida + + +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 == 0: + 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: + ida.ReleaseProperty(hAccessor, hPropAxis) + if pAxes: + del pAxes + + if hPropAxes: + ida.ReleaseProperty(hAccessor, hPropAxes) + + def get_axis(self, name): + return self.m_axes.get(name, None) + + 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 exist(self, name): + return name in self.m_axes 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..e34cd3635 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/channel_info.py @@ -0,0 +1,107 @@ +import ctypes as ct + +import h_ida +import lib + +ida = lib.ida + + +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) + print(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: + ida.ReleaseProperty(hAccessor, hPropChannel) + + if hPropEnabled: + 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_depth_of_ch0_tm(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..4759318f3 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py @@ -0,0 +1,24 @@ +import 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_file_creation_time_tm(self, hAccessor, hArea): + result, hProp = lib.get_area_property(hAccessor, hArea, "CreationDateTime") + result, pCreationDateTime = lib.get_property_value(hAccessor, hProp, "dateTime") + return pCreationDateTime[0].value.pszString 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..ac0d21068 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py @@ -0,0 +1,251 @@ +import ctypes as ct + +import h_ida +import lib +from axis_info import AxisIndex, AxisPosition +from 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_pucImageBuffer_asWORD = 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): + self.m_rect = rect + self.m_pucImageBuffer = lib.get_image_body( + self.m_hAccessor, self.m_hImage, ct.byref(self.m_rect) + ) + return self.m_pucImageBuffer + + def release_image_body(self): + if self.m_pucImageBuffer: + self.m_pucImagebuffer = None + self.m_pucImageBuffer_asWORD = ct.cast( + self.m_pucImageBuffer, ct.POINTER(ct.c_uint16) + ) + + def write_image_body(self, filename): + # TODO: Do something here + pass + + def write_image_body_binary(self, filename): + # TODO: Do something here + pass + + def get_pixel_value_tm(self, myDataCnt): + return self.m_pucImageBuffer_asWORD[myDataCnt] + + def pucBuffer_to_WORD_TM(self, width, height): + # TODO: The width and height should be obtained externally as appropriate. + # (If it is difficult to obtain them here, they should be obtained elsewhere.) + buffer_size = ct.c_uint16 * width * height + + self.m_pucImageBuffer_asWORD = buffer_size.from_buffer(self.m_pucImageBuffer) + + return self.m_pucImageBuffer_asWORD + + def get_frame_index(self): + pFrameAxes = lib.get_image_axis(self.m_hAccessor, self.m_hImage) + for f in pFrameAxes: + axis_index = AxisIndex() + axis_index.set_exit(True) + # TODO: Is the following code correct? (p.nType, p.nNumber) + axis_index.set_type(ct.p.nType) + axis_index.set_index(ct.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 == 0: + 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 == 0: + 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 == 0: + 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_timestamp_channel_tm(self): + my_timestamp_channel = 0 + for idx, ap in enumerate(self.m_vecAxisPosition): + if ap.get_exit(): + if ap.get_type() == h_ida.IDA_AxisType.IDA_AT_TIME: + my_timestamp_channel = idx + return my_timestamp_channel + + def get_timestamp_tm(self, mychannel): + mytimestamp = self.m_vecAxisPosition[mychannel].get_position() + return mytimestamp + + 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, pAnalysisData = lib.get_property_value( + self.m_hAccessor, hPropInfo, "data" + ) + + # TODO: linterで警告が生じているため、コメントアウト(別途修正) + # roi.set_points(pAnalysisROIData, -1) + + del pAnalysisData + + 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..a5d8e5452 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/h_ida.py @@ -0,0 +1,91 @@ +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 + + +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..a4a034320 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/lib.py @@ -0,0 +1,103 @@ +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_Result, +) + +ida = None + + +def load_library(library_path: str): + 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 != 0: + print("Error: GetImageBody") + return None + + 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() + # TODO: do check result + # result = ida.GetImageAxis( + ida.GetImageAxis(hAccessor, hImage, None, None, ct.byref(num_of_frame_axis)) + pFrameAxes = (IDA_AXIS_INFO * num_of_frame_axis.value)() + # TODO: do check result + # result = ida.GetImageAxis( + ida.GetImageAxis( + hAccessor, hImage.pFrameAxes, num_of_frame_axis, ct.byref(num_of_frame_axis) + ) + return pFrameAxes 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..d984183a4 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py @@ -0,0 +1,83 @@ +import 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_name_tm(self, hAccessor, hArea): + result, hProp = lib.get_area_property(hAccessor, hArea, "ObjectiveLensInfo") + result, pName = lib.get_property_value(hAccessor, hProp, "name") + return pName[0].value.pszString + + def get_immersion_tm(self, hAccessor, hArea): + result, hProp = lib.get_area_property(hAccessor, hArea, "ObjectiveLensInfo") + result, pImmersion = lib.get_property_value(hAccessor, hProp, "immersion") + return pImmersion[0].value.pszString + + def get_magnification_tm(self): + return self.m_dMagnification + + def get_na_tm(self): + return self.m_dNA + + def get_wd_tm(self): + return self.m_dWD + + def get_reflective_index_tm(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..357ee9331 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py @@ -0,0 +1,33 @@ +import lib + +ida = lib.ida + + +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: + 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_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..304eadebf --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py @@ -0,0 +1,216 @@ +import ctypes as ct + +import lib +from h_ida import IDA_VALUE + + +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 = 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..c6965e6bd --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/system_info.py @@ -0,0 +1,37 @@ +import 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}") 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..2abb16159 --- /dev/null +++ b/studio/app/optinist/microscopes/modules/olympus/user_comment.py @@ -0,0 +1,19 @@ +import 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}") diff --git a/studio/app/optinist/microscopes/test_OIRReader.py b/studio/app/optinist/microscopes/test_OIRReader.py index 1efc309be..1a1ce94c7 100644 --- a/studio/app/optinist/microscopes/test_OIRReader.py +++ b/studio/app/optinist/microscopes/test_OIRReader.py @@ -1,6 +1,5 @@ import logging import os -from pprint import pprint from OIRReader import OIRReader @@ -21,8 +20,7 @@ def test_oir_reader(): # initialize data_reader = OIRReader() - - pprint(data_reader) + data_reader.load(TEST_DATA_PATH) # TODO: Under construction From 9103391b508a2ee257ca3b57d9939bc0a7d715ff Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Tue, 30 Jan 2024 13:12:12 +0900 Subject: [PATCH 19/26] Implement required functionality (needs refactoring) --- studio/app/optinist/microscopes/OIRReader.py | 293 ++++++++++++++++-- .../modules/olympus/area_image_size.py | 6 +- .../microscopes/modules/olympus/axis_info.py | 10 +- .../modules/olympus/channel_info.py | 10 +- .../modules/olympus/file_creation_time.py | 2 +- .../modules/olympus/frame_manager.py | 11 +- .../microscopes/modules/olympus/lib.py | 102 ++++++ .../modules/olympus/objective_lens_info.py | 2 +- .../modules/olympus/pixel_length.py | 6 +- .../modules/olympus/roi_collection.py | 4 +- .../modules/olympus/system_info.py | 2 +- .../modules/olympus/user_comment.py | 2 +- .../optinist/microscopes/test_OIRReader.py | 38 ++- 13 files changed, 434 insertions(+), 54 deletions(-) diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index de6b4d6b5..8294eec7d 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -1,13 +1,32 @@ import ctypes - -# import json 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.h_ida import IDA_OpenMode +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, +) +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): @@ -61,14 +80,16 @@ def _init_library(self): # load sdk library self.__dll = lib.load_library(__class__.get_library_path()) - # initialize sdk library - self.__dll.Initialize() - def _load_data_file(self, data_file_path: str) -> object: ida = self.__dll + hAccessor = self.__hAccessor = ctypes.c_void_p() hFile = self.__hFile = ctypes.c_void_p() + # initialize sdk library + # TODO: 要リファクタリング:_load_data_file の外部でのコールとする + ida.Initialize() + # Get Accessor ida.GetAccessor(data_file_path, ctypes.byref(hAccessor)) if not hAccessor: @@ -89,10 +110,6 @@ def _load_data_file(self, data_file_path: str) -> object: ctypes.byref(hFile), ) - # GetNumberOfGroup - num_of_group = ctypes.c_int() - ida.GetNumOfGroups(hAccessor, hFile, ctypes.byref(num_of_group)) - # Get Group Handle hGroup = self.__hGroup = ctypes.c_void_p() specify_group = ( @@ -100,16 +117,19 @@ def _load_data_file(self, data_file_path: str) -> object: ) ida.GetGroup(hAccessor, hFile, specify_group, ctypes.byref(hGroup)) - # GetNumberOfLevels - num_of_layer = ctypes.c_int() - ida.GetNumOfLevels(hAccessor, hGroup, ctypes.byref(num_of_layer)) - print("-- GetNumOfLevels - num_of_layer:", num_of_layer) + # GetArea + hArea = self.__hArea = ctypes.c_void_p() + # TODO: `specify_layer = 0` is valid? + 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) + # TODO: return type を要リファクタリング(他のReaderのI/Fとも要整合) + return (hAccessor, hFile, hGroup, hArea) def _build_original_metadata(self, handle: object, data_name: str) -> dict: - # TODO: Under construction - return None + # TODO: 各データ取得クラス(ChannelInfo, etc)に get_metadata を定義、情報を取得・構築する + return {} def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: """ @@ -120,13 +140,240 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: return None def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: - # TODO: Under construction - return None + # TODO: 以下の各値は original_metadata より取得する形式とする + + ida = self.__dll + + hAccessor = self.__hAccessor + hFile = self.__hFile + hGroup = self.__hGroup + hArea = self.__hArea + + # GetNumberOfGroup + num_of_group = ctypes.c_int() + ida.GetNumOfGroups(hAccessor, hFile, ctypes.byref(num_of_group)) + + # GetNumberOfLevels + num_of_layer = ctypes.c_int() + ida.GetNumOfLevels(hAccessor, hGroup, ctypes.byref(num_of_layer)) + + # 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) + channel_info.print() + + # Image Size + area_image_size = AreaImageSize(hAccessor, hArea) + area_image_size.print() + + # Axes Information + axis_info = AxisInfo(hAccessor, hArea) + axis_info.print() + + # Pixel Length + pixel_length = PixelLength(hAccessor, hArea) + pixel_length.print() + + # Objective Lens Info + objective_lens_info = ObjectiveLensInfo(hAccessor, hArea) + objective_lens_info.print() + + # File Creation Time + file_creation_time = FileCreationTime(hAccessor, hArea) + file_creation_time.print() + + # System Information + system_info = SystemInfo(hAccessor, hArea) + system_info.print() + + # User Comment + user_comment = UserComment(hAccessor, hArea) + user_comment.print() + + # ==================== + # Something here + # ==================== + + 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 + + # ==================== + # Output + # ==================== + + result_data = { + "uiWidth": rect.width, + "uiHeight": rect.height, + "Loops": nTLoop, + "ZSlicenum": nZLoop, + "nChannel": channel_info.get_num_of_channel(), + "PixelLengthX": pixel_length.get_pixel_length_x(), + "PixelLengthY": pixel_length.get_pixel_length_y(), + "ZInterval": Zstep, + "TInterval": Tstep, + "ZStart": Zstart, + "ZEnd": Zend, + "ObjectiveName": objective_lens_info.get_name_tm(hAccessor, hArea), + "ObjectiveMag": objective_lens_info.get_magnification_tm(), + "ObjectiveNA": objective_lens_info.get_na_tm(), + "ReflectiveIndex": objective_lens_info.get_reflective_index_tm(), + "Immersion": objective_lens_info.get_immersion_tm(hAccessor, hArea), + "Date": file_creation_time.get_file_creation_time_tm(hAccessor, hArea), + "NumberOfGroup": num_of_group.value, + "NumberOfLevel": num_of_layer.value, + "NumberOfArea": num_of_area.value, + "ByteDepthCh0": channel_info.get_depth_of_ch0_tm(), + "SystemName": system_info.m_szSystemName, + "SystemVersion": system_info.m_szSystemVersion, + "DeviceName": system_info.m_szDeviceName, + "UserName": system_info.m_szUserName, + "CommentByUser": user_comment.m_szComment, + } + + return result_data def _release_resources(self, handle: object) -> None: - # TODO: Under construction - pass + # ---------------------------------------- + # Release each resource + # ---------------------------------------- + + ida = self.__dll + + hAccessor = self.__hAccessor + hFile = self.__hFile + hGroup = self.__hGroup + hArea = self.__hArea + + ida.ReleaseArea(hAccessor, hArea) + ida.ReleaseGroup(hAccessor, hGroup) + ida.Close(hAccessor, hFile) + ida.Disconnect(hAccessor) + ida.ReleaseAccessor(ctypes.byref(hAccessor)) + ida.Terminate() def get_images_stack(self) -> list: - # TODO: under construction - return None + """Return microscope image stacks""" + + # initialization + (hAccessor, hFile, hGroup, hArea) = self._load_data_file(self.data_path) + + 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 + # m_pucImageBuffer = frame_manager.get_image_body(rect) + frame_manager.get_image_body(rect) + + # NOTE: + # Since there are concerns about the efficiency + # of this process (acquiring pixel data one dot at a time), + # another process (using ndarray) is used. + # # Store Image Data Pixel by Pixel + # frame_manager.pucBuffer_to_WORD_TM() + # for nDataCnt in range(rect.width * rect.height): + # result = frame_manager.get_pixel_value_tm(nDataCnt) + # result += 1 + + # Obtain image data in ndarray format + pucBuffer_to_WORD_TM = frame_manager.pucBuffer_to_WORD_TM( + area_image_size.get_x(), + area_image_size.get_y(), + ) + pucBuffer_ndarray = np.ctypeslib.as_array(pucBuffer_to_WORD_TM) + result_stack.append(pucBuffer_ndarray) + + frame_manager.release_image_body() + + # TODO: 要否確認 + # frame_manager.get_frame_position() + # frame_manager.write_frame_position() + + # construct return value (each channel's stack) + result_channels_stacks.append(result_stack) + + # do release resources + # TODO: 要リファクタリング:引数仕様整理 + self._release_resources((hAccessor, hFile, hGroup, hArea)) + + return result_channels_stacks diff --git a/studio/app/optinist/microscopes/modules/olympus/area_image_size.py b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py index 48404d751..6bedbfd48 100644 --- a/studio/app/optinist/microscopes/modules/olympus/area_image_size.py +++ b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py @@ -1,6 +1,4 @@ -import lib - -ida = lib.ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib class AreaImageSize: @@ -12,7 +10,7 @@ def __init__(self, hAccessor, hArea): if pImageSize: del pImageSize if hProp: - ida.ReleaseProperty(hAccessor, hProp) + lib.ida.ReleaseProperty(hAccessor, hProp) def get_x(self): return self.m_nX diff --git a/studio/app/optinist/microscopes/modules/olympus/axis_info.py b/studio/app/optinist/microscopes/modules/olympus/axis_info.py index eb0c13d62..2f344ef76 100644 --- a/studio/app/optinist/microscopes/modules/olympus/axis_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/axis_info.py @@ -1,9 +1,7 @@ import ctypes as ct -import h_ida -import lib - -ida = lib.ida +import studio.app.optinist.microscopes.modules.olympus.h_ida as h_ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib class AxisIndex: @@ -124,12 +122,12 @@ def __init__(self, hAccessor, hArea): del pMax self.m_axes[c_axis.value.pszString] = axis if hPropAxis: - ida.ReleaseProperty(hAccessor, hPropAxis) + lib.ida.ReleaseProperty(hAccessor, hPropAxis) if pAxes: del pAxes if hPropAxes: - ida.ReleaseProperty(hAccessor, hPropAxes) + lib.ida.ReleaseProperty(hAccessor, hPropAxes) def get_axis(self, name): return self.m_axes.get(name, None) diff --git a/studio/app/optinist/microscopes/modules/olympus/channel_info.py b/studio/app/optinist/microscopes/modules/olympus/channel_info.py index e34cd3635..46b0eefc5 100644 --- a/studio/app/optinist/microscopes/modules/olympus/channel_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/channel_info.py @@ -1,9 +1,7 @@ import ctypes as ct -import h_ida -import lib - -ida = lib.ida +import studio.app.optinist.microscopes.modules.olympus.h_ida as h_ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib class ChannelInfo: @@ -67,10 +65,10 @@ def __init__(self, hAccessor, hArea): self.m_vecpnChannelLUTB.append(lut_b) if hPropChannel: - ida.ReleaseProperty(hAccessor, hPropChannel) + lib.ida.ReleaseProperty(hAccessor, hPropChannel) if hPropEnabled: - ida.ReleaseProperty(hAccessor, hPropEnabled) + lib.ida.ReleaseProperty(hAccessor, hPropEnabled) if pChIDs: del pChIDs diff --git a/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py b/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py index 4759318f3..3bcc96244 100644 --- a/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py +++ b/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py @@ -1,4 +1,4 @@ -import lib +import studio.app.optinist.microscopes.modules.olympus.lib as lib class FileCreationTime: diff --git a/studio/app/optinist/microscopes/modules/olympus/frame_manager.py b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py index ac0d21068..dab79844f 100644 --- a/studio/app/optinist/microscopes/modules/olympus/frame_manager.py +++ b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py @@ -1,9 +1,12 @@ import ctypes as ct -import h_ida -import lib -from axis_info import AxisIndex, AxisPosition -from roi_collection import Roi +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: diff --git a/studio/app/optinist/microscopes/modules/olympus/lib.py b/studio/app/optinist/microscopes/modules/olympus/lib.py index a4a034320..0637d1b6b 100644 --- a/studio/app/optinist/microscopes/modules/olympus/lib.py +++ b/studio/app/optinist/microscopes/modules/olympus/lib.py @@ -5,6 +5,7 @@ IDA_PARAM, IDA_PARAM_ELEMENT, IDA_VALUE, + IDA_AxisType, IDA_Result, ) @@ -12,6 +13,7 @@ def load_library(library_path: str): + global ida ida = ct.cdll.LoadLibrary(library_path) return ida @@ -101,3 +103,103 @@ def get_image_axis(hAccessor, hImage): hAccessor, hImage.pFrameAxes, num_of_frame_axis, ct.byref(num_of_frame_axis) ) return pFrameAxes + + +def set_frame_axis_index( + nLIndex, nZIndex, nTIndex, pRoiCollection, pAxisInfo, pAxes, pnAxisCount +): + # TODO: + + # 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(): + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + elif pRoiCollection.has_multi_point_roi(): + pAxes[0].nNumber = nTIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_TIME + pnAxisCount = 1 + elif pRoiCollection.has_mapping_roi(): + 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(): + if not bHasAxis[0] and not bHasAxis[1] and bHasAxis[2]: + 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]: + pAxes[0].nNumber = nZIndex + pAxes[0].nType = IDA_AxisType.IDA_AT_Z + pnAxisCount = 1 + elif not bHasAxis[0] and bHasAxis[1] and bHasAxis[2]: + 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: + # TODO: What? + pass + else: + if nSize[0] != 0 and nSize[1] != 0 and nSize[2] != 0: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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 index d984183a4..1c859321d 100644 --- a/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py @@ -1,4 +1,4 @@ -import lib +import studio.app.optinist.microscopes.modules.olympus.lib as lib class ObjectiveLensInfo: diff --git a/studio/app/optinist/microscopes/modules/olympus/pixel_length.py b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py index 357ee9331..48f1687bc 100644 --- a/studio/app/optinist/microscopes/modules/olympus/pixel_length.py +++ b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py @@ -1,6 +1,4 @@ -import lib - -ida = lib.ida +import studio.app.optinist.microscopes.modules.olympus.lib as lib class PixelLength: @@ -19,7 +17,7 @@ def __init__(self, hAccessor, hArea): del pPixelLength if hProp: - ida.ReleaseProperty(hAccessor, hProp) + lib.ida.ReleaseProperty(hAccessor, hProp) def print(self): print("Pixel length[um]") diff --git a/studio/app/optinist/microscopes/modules/olympus/roi_collection.py b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py index 304eadebf..820ec8319 100644 --- a/studio/app/optinist/microscopes/modules/olympus/roi_collection.py +++ b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py @@ -1,7 +1,7 @@ import ctypes as ct -import lib -from h_ida import IDA_VALUE +import studio.app.optinist.microscopes.modules.olympus.lib as lib +from studio.app.optinist.microscopes.modules.olympus.h_ida import IDA_VALUE class Roi: diff --git a/studio/app/optinist/microscopes/modules/olympus/system_info.py b/studio/app/optinist/microscopes/modules/olympus/system_info.py index c6965e6bd..21407e67b 100644 --- a/studio/app/optinist/microscopes/modules/olympus/system_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/system_info.py @@ -1,4 +1,4 @@ -import lib +import studio.app.optinist.microscopes.modules.olympus.lib as lib class SystemInfo: diff --git a/studio/app/optinist/microscopes/modules/olympus/user_comment.py b/studio/app/optinist/microscopes/modules/olympus/user_comment.py index 2abb16159..d154c86d8 100644 --- a/studio/app/optinist/microscopes/modules/olympus/user_comment.py +++ b/studio/app/optinist/microscopes/modules/olympus/user_comment.py @@ -1,4 +1,4 @@ -import lib +import studio.app.optinist.microscopes.modules.olympus.lib as lib class UserComment: diff --git a/studio/app/optinist/microscopes/test_OIRReader.py b/studio/app/optinist/microscopes/test_OIRReader.py index 1a1ce94c7..b4a18c14c 100644 --- a/studio/app/optinist/microscopes/test_OIRReader.py +++ b/studio/app/optinist/microscopes/test_OIRReader.py @@ -1,5 +1,7 @@ +import json import logging import os +from pprint import pprint from OIRReader import OIRReader @@ -22,7 +24,41 @@ def test_oir_reader(): data_reader = OIRReader() data_reader.load(TEST_DATA_PATH) - # TODO: Under construction + # 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_images_stack() + + # 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, images_stack in enumerate(channels_stacks): + save_stack = [Image.fromarray(frame) for frame in images_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 + # TODO: 要実装 + # 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__": From 78ffc1afb81c234c477f209202294bb59a7dc87a Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Tue, 30 Jan 2024 19:10:00 +0900 Subject: [PATCH 20/26] refactor olympus modules --- studio/app/optinist/microscopes/OIRReader.py | 24 +++++++------- .../modules/olympus/area_image_size.py | 5 +++ .../microscopes/modules/olympus/axis_info.py | 7 ++++- .../modules/olympus/channel_info.py | 9 ++++-- .../modules/olympus/file_creation_time.py | 12 ++++--- .../modules/olympus/frame_manager.py | 30 +++++++++++------- .../microscopes/modules/olympus/h_ida.py | 7 +++++ .../microscopes/modules/olympus/lib.py | 9 ++++-- .../modules/olympus/objective_lens_info.py | 31 ++++++++++++------- .../modules/olympus/pixel_length.py | 5 +++ .../modules/olympus/roi_collection.py | 5 +++ .../modules/olympus/system_info.py | 21 +++++++++++++ .../modules/olympus/user_comment.py | 9 ++++++ 13 files changed, 129 insertions(+), 45 deletions(-) diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index 8294eec7d..edb5b7510 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -238,21 +238,21 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: "TInterval": Tstep, "ZStart": Zstart, "ZEnd": Zend, - "ObjectiveName": objective_lens_info.get_name_tm(hAccessor, hArea), - "ObjectiveMag": objective_lens_info.get_magnification_tm(), - "ObjectiveNA": objective_lens_info.get_na_tm(), - "ReflectiveIndex": objective_lens_info.get_reflective_index_tm(), - "Immersion": objective_lens_info.get_immersion_tm(hAccessor, hArea), - "Date": file_creation_time.get_file_creation_time_tm(hAccessor, hArea), + "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_group.value, "NumberOfLevel": num_of_layer.value, "NumberOfArea": num_of_area.value, - "ByteDepthCh0": channel_info.get_depth_of_ch0_tm(), - "SystemName": system_info.m_szSystemName, - "SystemVersion": system_info.m_szSystemVersion, - "DeviceName": system_info.m_szDeviceName, - "UserName": system_info.m_szUserName, - "CommentByUser": user_comment.m_szComment, + "ByteDepthCh0": channel_info.depth_of_ch0, + "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 result_data diff --git a/studio/app/optinist/microscopes/modules/olympus/area_image_size.py b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py index 6bedbfd48..604faa083 100644 --- a/studio/app/optinist/microscopes/modules/olympus/area_image_size.py +++ b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/AreaImageSize.h,cpp + +""" import studio.app.optinist.microscopes.modules.olympus.lib as lib diff --git a/studio/app/optinist/microscopes/modules/olympus/axis_info.py b/studio/app/optinist/microscopes/modules/olympus/axis_info.py index 2f344ef76..20b51bb7a 100644 --- a/studio/app/optinist/microscopes/modules/olympus/axis_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/axis_info.py @@ -1,3 +1,8 @@ +"""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 @@ -104,7 +109,7 @@ def __init__(self, hAccessor, hArea): ct.cast(ct.c_wchar_p(c_axis.value.pszString), ct.c_void_p), ], ) - if result == 0: + 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 diff --git a/studio/app/optinist/microscopes/modules/olympus/channel_info.py b/studio/app/optinist/microscopes/modules/olympus/channel_info.py index 46b0eefc5..9830d38a6 100644 --- a/studio/app/optinist/microscopes/modules/olympus/channel_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/channel_info.py @@ -1,3 +1,8 @@ +"""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 @@ -25,7 +30,6 @@ def __init__(self, hAccessor, hArea): # Get Channel Info for ch in pChIDs: self.m_vecpszChannelIdList.append(ch.value.pszString) - print(ch.value.pszString) result, hPropChannel = lib.get_area_property( hAccessor, @@ -101,5 +105,6 @@ def print(self): print(f"\tDepth[byte] = {self.m_vecnChannelDepth[cnt]}") print(f"\tAvai Bit[bit] = {self.m_vecnChannelBitCount[cnt]}") - def get_depth_of_ch0_tm(self): + @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 index 3bcc96244..5e297a70d 100644 --- a/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py +++ b/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/FileCreationTime.h,cpp + +""" import studio.app.optinist.microscopes.modules.olympus.lib as lib @@ -18,7 +23,6 @@ def print(self): print("File Creation Time") print(f"\tTime={self.m_szCreationTime}") - def get_file_creation_time_tm(self, hAccessor, hArea): - result, hProp = lib.get_area_property(hAccessor, hArea, "CreationDateTime") - result, pCreationDateTime = lib.get_property_value(hAccessor, hProp, "dateTime") - return pCreationDateTime[0].value.pszString + @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 index dab79844f..f034049a4 100644 --- a/studio/app/optinist/microscopes/modules/olympus/frame_manager.py +++ b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py @@ -1,3 +1,8 @@ +"""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 @@ -42,21 +47,24 @@ def get_image_body(self, rect): def release_image_body(self): if self.m_pucImageBuffer: self.m_pucImagebuffer = None + # TODO: 以下の変数代入処理はリファクタリング対象 self.m_pucImageBuffer_asWORD = ct.cast( self.m_pucImageBuffer, ct.POINTER(ct.c_uint16) ) def write_image_body(self, filename): - # TODO: Do something here + # Note: Not impletented. pass def write_image_body_binary(self, filename): - # TODO: Do something here + # Note: Not impletented. pass + # TODO: needs refactoring def get_pixel_value_tm(self, myDataCnt): return self.m_pucImageBuffer_asWORD[myDataCnt] + # TODO: needs refactoring def pucBuffer_to_WORD_TM(self, width, height): # TODO: The width and height should be obtained externally as appropriate. # (If it is difficult to obtain them here, they should be obtained elsewhere.) @@ -68,12 +76,11 @@ def pucBuffer_to_WORD_TM(self, width, height): def get_frame_index(self): pFrameAxes = lib.get_image_axis(self.m_hAccessor, self.m_hImage) - for f in pFrameAxes: + for p in pFrameAxes: axis_index = AxisIndex() axis_index.set_exit(True) - # TODO: Is the following code correct? (p.nType, p.nNumber) - axis_index.set_type(ct.p.nType) - axis_index.set_index(ct.p.nNumber) + axis_index.set_type(p.nType) + axis_index.set_index(p.nNumber) self.m_vecAxisIndex.append(axis_index) del pFrameAxes @@ -145,6 +152,7 @@ def write_frame_position(self): elif ap.get_type() == h_ida.IDA_AxisType.IDA_AT_TIME: print(f"\tTIMELAPSE={ap.get_position()}") + # TODO: needs refactoring def get_timestamp_channel_tm(self): my_timestamp_channel = 0 for idx, ap in enumerate(self.m_vecAxisPosition): @@ -153,6 +161,7 @@ def get_timestamp_channel_tm(self): my_timestamp_channel = idx return my_timestamp_channel + # TODO: needs refactoring def get_timestamp_tm(self, mychannel): mytimestamp = self.m_vecAxisPosition[mychannel].get_position() return mytimestamp @@ -202,14 +211,11 @@ def get_frame_roi(self): del pAnalysisROIRotation # Get Analysis ROI Data - result, pAnalysisData = lib.get_property_value( + result, pAnalysisROIData = lib.get_property_value( self.m_hAccessor, hPropInfo, "data" ) - - # TODO: linterで警告が生じているため、コメントアウト(別途修正) - # roi.set_points(pAnalysisROIData, -1) - - del pAnalysisData + roi.set_points(pAnalysisROIData, -1) + del pAnalysisROIData if roi.get_type() == "MULTI_POINT": # PanX diff --git a/studio/app/optinist/microscopes/modules/olympus/h_ida.py b/studio/app/optinist/microscopes/modules/olympus/h_ida.py index a5d8e5452..274dbe115 100644 --- a/studio/app/optinist/microscopes/modules/olympus/h_ida.py +++ b/studio/app/optinist/microscopes/modules/olympus/h_ida.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA/include/Idaldll.h + +""" import ctypes as ct from enum import IntEnum, auto @@ -29,6 +34,8 @@ class IDA_OpenMode(IntEnum): 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): diff --git a/studio/app/optinist/microscopes/modules/olympus/lib.py b/studio/app/optinist/microscopes/modules/olympus/lib.py index 0637d1b6b..776f80542 100644 --- a/studio/app/optinist/microscopes/modules/olympus/lib.py +++ b/studio/app/optinist/microscopes/modules/olympus/lib.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Utilities for IDA API + +""" import ctypes as ct from studio.app.optinist.microscopes.modules.olympus.h_ida import ( @@ -75,11 +80,13 @@ def get_lut(hAccessor, hArea, channel_id): return result, pLUTR, pLUTG, pLUTB +# TODO: どこで利用されている? 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 != 0: + # TODO: 例外処理 print("Error: GetImageBody") return None @@ -108,8 +115,6 @@ def get_image_axis(hAccessor, hImage): def set_frame_axis_index( nLIndex, nZIndex, nTIndex, pRoiCollection, pAxisInfo, pAxes, pnAxisCount ): - # TODO: - # 0: LAxis # 1: ZAxis # 2: TAxis diff --git a/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py index 1c859321d..e15922fe9 100644 --- a/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/ObjectiveLensInfo.h,cpp + +""" import studio.app.optinist.microscopes.modules.olympus.lib as lib @@ -60,24 +65,26 @@ def print(self): print(f"\tWD = {self.m_dWD}") print(f"\tReflective Index = {self.m_dReflectiveIndex}") - def get_name_tm(self, hAccessor, hArea): - result, hProp = lib.get_area_property(hAccessor, hArea, "ObjectiveLensInfo") - result, pName = lib.get_property_value(hAccessor, hProp, "name") - return pName[0].value.pszString + @property + def name(self): + return self.m_szName - def get_immersion_tm(self, hAccessor, hArea): - result, hProp = lib.get_area_property(hAccessor, hArea, "ObjectiveLensInfo") - result, pImmersion = lib.get_property_value(hAccessor, hProp, "immersion") - return pImmersion[0].value.pszString + @property + def immersion(self): + return self.m_szImmersion - def get_magnification_tm(self): + @property + def magnification(self): return self.m_dMagnification - def get_na_tm(self): + @property + def na(self): return self.m_dNA - def get_wd_tm(self): + @property + def wd(self): return self.m_dWD - def get_reflective_index_tm(self): + @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 index 48f1687bc..8074f5715 100644 --- a/studio/app/optinist/microscopes/modules/olympus/pixel_length.py +++ b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/PixelLength.h,cpp + +""" import studio.app.optinist.microscopes.modules.olympus.lib as lib diff --git a/studio/app/optinist/microscopes/modules/olympus/roi_collection.py b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py index 820ec8319..f6d57684f 100644 --- a/studio/app/optinist/microscopes/modules/olympus/roi_collection.py +++ b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/RoiCollection.h,cpp + +""" import ctypes as ct import studio.app.optinist.microscopes.modules.olympus.lib as lib diff --git a/studio/app/optinist/microscopes/modules/olympus/system_info.py b/studio/app/optinist/microscopes/modules/olympus/system_info.py index 21407e67b..e6f78cd4e 100644 --- a/studio/app/optinist/microscopes/modules/olympus/system_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/system_info.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/SystemInfo.h,cpp + +""" import studio.app.optinist.microscopes.modules.olympus.lib as lib @@ -35,3 +40,19 @@ def print(self): print(f"\tversion = {self.m_szSystemVersion}") print(f"\tdeviceName = {self.m_szDeviceName}") print(f"\tuserName = {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 index d154c86d8..78d098a6e 100644 --- a/studio/app/optinist/microscopes/modules/olympus/user_comment.py +++ b/studio/app/optinist/microscopes/modules/olympus/user_comment.py @@ -1,3 +1,8 @@ +"""Olympus IDA wrapper module + +* Porting of IDA_Sample/UserComment.h,cpp + +""" import studio.app.optinist.microscopes.modules.olympus.lib as lib @@ -17,3 +22,7 @@ def __init__(self, hAccessor, hArea): def print(self): print("User Comment") print(f"\tComment = {self.m_szComment}") + + @property + def comment(self): + return self.m_szComment From 6c64ee0ffb28a8b79c78b3a8f2b1a02d775c17a0 Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Tue, 30 Jan 2024 21:14:22 +0900 Subject: [PATCH 21/26] refactor olympus modules --- .../microscopes/modules/olympus/lib.py | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/studio/app/optinist/microscopes/modules/olympus/lib.py b/studio/app/optinist/microscopes/modules/olympus/lib.py index 776f80542..b468d14bd 100644 --- a/studio/app/optinist/microscopes/modules/olympus/lib.py +++ b/studio/app/optinist/microscopes/modules/olympus/lib.py @@ -80,15 +80,12 @@ def get_lut(hAccessor, hArea, channel_id): return result, pLUTR, pLUTG, pLUTB -# TODO: どこで利用されている? 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 != 0: - # TODO: 例外処理 - print("Error: GetImageBody") - return None + if result != IDA_Result.IDA_RESULT_SUCCESS: + raise Exception("Error: GetImageBody") if image_size.value != 0: image_buffer = (ct.c_uint8 * image_size.value)() @@ -100,21 +97,40 @@ def get_image_body(hAccessor, hImage, rect): def get_image_axis(hAccessor, hImage): num_of_frame_axis = ct.c_int() - # TODO: do check result - # result = ida.GetImageAxis( - ida.GetImageAxis(hAccessor, hImage, None, None, ct.byref(num_of_frame_axis)) + 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)() - # TODO: do check result - # result = ida.GetImageAxis( - ida.GetImageAxis( + + 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 @@ -126,15 +142,15 @@ def set_frame_axis_index( for i in range(3): if bHasAxis[i]: nSize[i] = pAxis[i].get_max() - if pRoiCollection.has_point_roi(): + 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(): + 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(): + elif pRoiCollection.has_mapping_roi(): # Mapping if not bHasAxis[1]: pAxes[0].nNumber = nTIndex pAxes[0].nType = IDA_AxisType.IDA_AT_TIME @@ -145,26 +161,26 @@ def set_frame_axis_index( pAxes[1].nNumber = nTIndex pAxes[1].nType = IDA_AxisType.IDA_AT_TIME pnAxisCount = 2 - elif pRoiCollection.has_line_roi(): - if not bHasAxis[0] and not bHasAxis[1] and bHasAxis[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]: + 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]: + 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: - # TODO: What? + # Note: Not implemented. pass - else: - if nSize[0] != 0 and nSize[1] != 0 and nSize[2] != 0: + 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 @@ -172,37 +188,37 @@ def set_frame_axis_index( 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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 From 544c94c2e09ea7b6016f5650e999151cfc4a9e5b Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Tue, 30 Jan 2024 23:52:14 +0900 Subject: [PATCH 22/26] refactor olympus metadata reader --- studio/app/optinist/microscopes/OIRReader.py | 180 +++++++++++------- .../modules/olympus/area_image_size.py | 6 + .../microscopes/modules/olympus/axis_info.py | 15 +- .../modules/olympus/channel_info.py | 12 ++ .../modules/olympus/file_creation_time.py | 5 + .../modules/olympus/objective_lens_info.py | 10 + .../modules/olympus/pixel_length.py | 6 + .../modules/olympus/system_info.py | 8 + .../modules/olympus/user_comment.py | 5 + 9 files changed, 181 insertions(+), 66 deletions(-) diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index edb5b7510..28dcd43ab 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -19,6 +19,7 @@ CMN_RECT, IDA_AXIS_INFO, IDA_OpenMode, + IDA_Result, ) from studio.app.optinist.microscopes.modules.olympus.objective_lens_info import ( ObjectiveLensInfo, @@ -93,22 +94,20 @@ def _load_data_file(self, data_file_path: str) -> object: # Get Accessor ida.GetAccessor(data_file_path, ctypes.byref(hAccessor)) if not hAccessor: - # TODO: raise exception - print("Please check the File path") - return + raise Exception("GetAccessor Error: Please check the File path.") # Connect ida.Connect(hAccessor) # Open file - # TODO: process exception - ida.Open( - # result = ida.Open( + result = ida.Open( hAccessor, data_file_path, IDA_OpenMode.IDA_OM_READ, ctypes.byref(hFile), ) + if result != IDA_Result.IDA_RESULT_SUCCESS: + raise Exception("Open Error") # Get Group Handle hGroup = self.__hGroup = ctypes.c_void_p() @@ -119,7 +118,6 @@ def _load_data_file(self, data_file_path: str) -> object: # GetArea hArea = self.__hArea = ctypes.c_void_p() - # TODO: `specify_layer = 0` is valid? 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)) @@ -128,20 +126,6 @@ def _load_data_file(self, data_file_path: str) -> object: return (hAccessor, hFile, hGroup, hArea) def _build_original_metadata(self, handle: object, data_name: str) -> dict: - # TODO: 各データ取得クラス(ChannelInfo, etc)に get_metadata を定義、情報を取得・構築する - return {} - - def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: - """ - @link OME/NativeND2Reader - """ - - # TODO: Under construction - return None - - def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: - # TODO: 以下の各値は original_metadata より取得する形式とする - ida = self.__dll hAccessor = self.__hAccessor @@ -149,13 +133,17 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: hGroup = self.__hGroup hArea = self.__hArea + # -------------------------------------------------- + # Get data from API + # -------------------------------------------------- + # GetNumberOfGroup - num_of_group = ctypes.c_int() - ida.GetNumOfGroups(hAccessor, hFile, ctypes.byref(num_of_group)) + num_of_groups = ctypes.c_int() + ida.GetNumOfGroups(hAccessor, hFile, ctypes.byref(num_of_groups)) # GetNumberOfLevels - num_of_layer = ctypes.c_int() - ida.GetNumOfLevels(hAccessor, hGroup, ctypes.byref(num_of_layer)) + num_of_levels = ctypes.c_int() + ida.GetNumOfLevels(hAccessor, hGroup, ctypes.byref(num_of_levels)) # GetLevelImageSize rect = CMN_RECT() @@ -168,39 +156,103 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: # Channel Information channel_info = ChannelInfo(hAccessor, hArea) - channel_info.print() - - # Image Size - area_image_size = AreaImageSize(hAccessor, hArea) - area_image_size.print() # Axes Information axis_info = AxisInfo(hAccessor, hArea) - axis_info.print() # Pixel Length pixel_length = PixelLength(hAccessor, hArea) - pixel_length.print() # Objective Lens Info objective_lens_info = ObjectiveLensInfo(hAccessor, hArea) - objective_lens_info.print() # File Creation Time file_creation_time = FileCreationTime(hAccessor, hArea) - file_creation_time.print() # System Information system_info = SystemInfo(hAccessor, hArea) - system_info.print() # User Comment user_comment = UserComment(hAccessor, hArea) - user_comment.print() - # ==================== - # Something here - # ==================== + # -------------------------------------------------- + # 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"] + + # 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) + + fps = 0 # TODO: 計算対象予定 + + omeData = OMEDataModel( + image_name=original_metadata["data_name"], + size_x=rect["width"], + size_y=rect["height"], + size_t=sequence_count, + size_c=0, + 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 = self.__hAccessor + hArea = self.__hArea + + axis_info = AxisInfo(hAccessor, hArea) nLLoop = nTLoop = nZLoop = 0 Zstep = Zstart = Zend = Tstep = 0.0 @@ -222,40 +274,40 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: Tstep = axis.get_step() nTLoop = nTLoop or 1 - # ==================== - # Output - # ==================== + # -------------------------------------------------- + # Construct metadata + # -------------------------------------------------- - result_data = { - "uiWidth": rect.width, - "uiHeight": rect.height, + lab_specific_metadata = { + "uiWidth": rect["width"], + "uiHeight": rect["height"], "Loops": nTLoop, "ZSlicenum": nZLoop, - "nChannel": channel_info.get_num_of_channel(), - "PixelLengthX": pixel_length.get_pixel_length_x(), - "PixelLengthY": pixel_length.get_pixel_length_y(), + "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_group.value, - "NumberOfLevel": num_of_layer.value, - "NumberOfArea": num_of_area.value, - "ByteDepthCh0": channel_info.depth_of_ch0, - "SystemName": system_info.system_name, - "SystemVersion": system_info.system_version, - "DeviceName": system_info.device_name, - "UserName": system_info.user_name, - "CommentByUser": user_comment.comment, + "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 result_data + return lab_specific_metadata def _release_resources(self, handle: object) -> None: # ---------------------------------------- diff --git a/studio/app/optinist/microscopes/modules/olympus/area_image_size.py b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py index 604faa083..d7cedc215 100644 --- a/studio/app/optinist/microscopes/modules/olympus/area_image_size.py +++ b/studio/app/optinist/microscopes/modules/olympus/area_image_size.py @@ -27,3 +27,9 @@ 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 index 20b51bb7a..88d49fb77 100644 --- a/studio/app/optinist/microscopes/modules/olympus/axis_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/axis_info.py @@ -137,6 +137,9 @@ def __init__(self, hAccessor, hArea): 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(): @@ -146,5 +149,13 @@ def print(self): print(f"\t\tend = {axis.get_end()}") print(f"\t\tmax = {axis.get_max()}") - def exist(self, name): - return name in self.m_axes + 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 index 9830d38a6..efdb5cc02 100644 --- a/studio/app/optinist/microscopes/modules/olympus/channel_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/channel_info.py @@ -105,6 +105,18 @@ def print(self): 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 index 5e297a70d..45d3cfd14 100644 --- a/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py +++ b/studio/app/optinist/microscopes/modules/olympus/file_creation_time.py @@ -23,6 +23,11 @@ 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/objective_lens_info.py b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py index e15922fe9..b3afacbf3 100644 --- a/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/objective_lens_info.py @@ -65,6 +65,16 @@ def print(self): 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 diff --git a/studio/app/optinist/microscopes/modules/olympus/pixel_length.py b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py index 8074f5715..66a437e55 100644 --- a/studio/app/optinist/microscopes/modules/olympus/pixel_length.py +++ b/studio/app/optinist/microscopes/modules/olympus/pixel_length.py @@ -29,6 +29,12 @@ def print(self): 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 diff --git a/studio/app/optinist/microscopes/modules/olympus/system_info.py b/studio/app/optinist/microscopes/modules/olympus/system_info.py index e6f78cd4e..9694df65c 100644 --- a/studio/app/optinist/microscopes/modules/olympus/system_info.py +++ b/studio/app/optinist/microscopes/modules/olympus/system_info.py @@ -41,6 +41,14 @@ def print(self): 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 diff --git a/studio/app/optinist/microscopes/modules/olympus/user_comment.py b/studio/app/optinist/microscopes/modules/olympus/user_comment.py index 78d098a6e..48c9eee4d 100644 --- a/studio/app/optinist/microscopes/modules/olympus/user_comment.py +++ b/studio/app/optinist/microscopes/modules/olympus/user_comment.py @@ -23,6 +23,11 @@ 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 From 099d0b6924f0835d67fb27ea274d72e1264ac130 Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Wed, 31 Jan 2024 00:36:57 +0900 Subject: [PATCH 23/26] refactor olympus image stack reader --- studio/app/optinist/microscopes/OIRReader.py | 25 ++------- .../modules/olympus/frame_manager.py | 55 ++++++------------- .../modules/olympus/roi_collection.py | 4 +- .../optinist/microscopes/test_OIRReader.py | 7 +-- 4 files changed, 27 insertions(+), 64 deletions(-) diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index 28dcd43ab..ab8952d3c 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -393,34 +393,17 @@ def get_images_stack(self) -> list: pAxes, nAxisCount, ) + # Get Image Body - # m_pucImageBuffer = frame_manager.get_image_body(rect) - frame_manager.get_image_body(rect) - - # NOTE: - # Since there are concerns about the efficiency - # of this process (acquiring pixel data one dot at a time), - # another process (using ndarray) is used. - # # Store Image Data Pixel by Pixel - # frame_manager.pucBuffer_to_WORD_TM() - # for nDataCnt in range(rect.width * rect.height): - # result = frame_manager.get_pixel_value_tm(nDataCnt) - # result += 1 + buffer_pointer = frame_manager.get_image_body(rect) + ctypes_buffer_ptr = buffer_pointer[1] # Obtain image data in ndarray format - pucBuffer_to_WORD_TM = frame_manager.pucBuffer_to_WORD_TM( - area_image_size.get_x(), - area_image_size.get_y(), - ) - pucBuffer_ndarray = np.ctypeslib.as_array(pucBuffer_to_WORD_TM) + pucBuffer_ndarray = np.ctypeslib.as_array(ctypes_buffer_ptr) result_stack.append(pucBuffer_ndarray) frame_manager.release_image_body() - # TODO: 要否確認 - # frame_manager.get_frame_position() - # frame_manager.write_frame_position() - # construct return value (each channel's stack) result_channels_stacks.append(result_stack) diff --git a/studio/app/optinist/microscopes/modules/olympus/frame_manager.py b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py index f034049a4..d132f826c 100644 --- a/studio/app/optinist/microscopes/modules/olympus/frame_manager.py +++ b/studio/app/optinist/microscopes/modules/olympus/frame_manager.py @@ -29,7 +29,6 @@ def __init__(self, hAccessor, hArea, pszChannelId, pAxes, nNumOfAxes): self.m_hImage = ct.c_void_p() self.m_rect = None self.m_pucImageBuffer = None - self.m_pucImageBuffer_asWORD = None self.m_vecAxisIndex = [] self.m_vecAxisPosition = [] self.m_vecRois = [] @@ -38,19 +37,29 @@ def __init__(self, hAccessor, hArea, pszChannelId, pAxes, nNumOfAxes): ) 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) ) - return self.m_pucImageBuffer + + # ---------------------------------------- + # 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 - # TODO: 以下の変数代入処理はリファクタリング対象 - self.m_pucImageBuffer_asWORD = ct.cast( - self.m_pucImageBuffer, ct.POINTER(ct.c_uint16) - ) def write_image_body(self, filename): # Note: Not impletented. @@ -60,20 +69,6 @@ def write_image_body_binary(self, filename): # Note: Not impletented. pass - # TODO: needs refactoring - def get_pixel_value_tm(self, myDataCnt): - return self.m_pucImageBuffer_asWORD[myDataCnt] - - # TODO: needs refactoring - def pucBuffer_to_WORD_TM(self, width, height): - # TODO: The width and height should be obtained externally as appropriate. - # (If it is difficult to obtain them here, they should be obtained elsewhere.) - buffer_size = ct.c_uint16 * width * height - - self.m_pucImageBuffer_asWORD = buffer_size.from_buffer(self.m_pucImageBuffer) - - return self.m_pucImageBuffer_asWORD - def get_frame_index(self): pFrameAxes = lib.get_image_axis(self.m_hAccessor, self.m_hImage) for p in pFrameAxes: @@ -95,7 +90,7 @@ def get_frame_position(self): result, hProp = lib.get_frame_property( self.m_hAccessor, self.m_hImage, "AxisPosition", "axisName", "TIMELAPSE" ) - if result == 0: + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: result, pAxisPosition = lib.get_property_value( self.m_hAccessor, hProp, "position" ) @@ -111,7 +106,7 @@ def get_frame_position(self): result, hProp = lib.get_frame_property( self.m_hAccessor, self.m_hImage, "AxisPosition", "axisName", "ZSTACK" ) - if result == 0: + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: result, pAxisPosition = lib.get_property_value( self.m_hAccessor, hProp, "position" ) @@ -127,7 +122,7 @@ def get_frame_position(self): result, hProp = lib.get_frame_property( self.m_hAccessor, self.m_hImage, "AxisPosition", "axisName", "LAMBDA" ) - if result == 0: + if result == h_ida.IDA_Result.IDA_RESULT_SUCCESS: result, pAxisPosition = lib.get_property_value( self.m_hAccessor, hProp, "position" ) @@ -152,20 +147,6 @@ def write_frame_position(self): elif ap.get_type() == h_ida.IDA_AxisType.IDA_AT_TIME: print(f"\tTIMELAPSE={ap.get_position()}") - # TODO: needs refactoring - def get_timestamp_channel_tm(self): - my_timestamp_channel = 0 - for idx, ap in enumerate(self.m_vecAxisPosition): - if ap.get_exit(): - if ap.get_type() == h_ida.IDA_AxisType.IDA_AT_TIME: - my_timestamp_channel = idx - return my_timestamp_channel - - # TODO: needs refactoring - def get_timestamp_tm(self, mychannel): - mytimestamp = self.m_vecAxisPosition[mychannel].get_position() - return mytimestamp - def get_frame_roi(self): result, hProp = lib.get_frame_property( self.m_hAccessor, self.m_hImage, "StimulationROIList" diff --git a/studio/app/optinist/microscopes/modules/olympus/roi_collection.py b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py index f6d57684f..652b4f769 100644 --- a/studio/app/optinist/microscopes/modules/olympus/roi_collection.py +++ b/studio/app/optinist/microscopes/modules/olympus/roi_collection.py @@ -5,8 +5,8 @@ """ 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.h_ida import IDA_VALUE class Roi: @@ -45,7 +45,7 @@ def set_rotation(self, pSrc): def set_points(self, pSrc, nNumOfSrc): for p in pSrc: - buf = IDA_VALUE.IDA_POINT() + buf = h_ida.IDA_VALUE.IDA_POINT() buf.x = p.value.point.x buf.y = p.value.point.y self.m_vecPoints.append(buf) diff --git a/studio/app/optinist/microscopes/test_OIRReader.py b/studio/app/optinist/microscopes/test_OIRReader.py index b4a18c14c..35f0c3e8e 100644 --- a/studio/app/optinist/microscopes/test_OIRReader.py +++ b/studio/app/optinist/microscopes/test_OIRReader.py @@ -55,10 +55,9 @@ def test_oir_reader(): ) # asserts - # TODO: 要実装 - # assert data_reader.original_metadata["attributes"]["widthPx"] > 0 - # assert data_reader.ome_metadata.size_x > 0 - # assert data_reader.lab_specific_metadata["uiWidth"] > 0 + 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__": From c05f2091829ee761ae9787396fc240be87290539 Mon Sep 17 00:00:00 2001 From: itutu-tienday Date: Wed, 31 Jan 2024 11:00:30 +0900 Subject: [PATCH 24/26] refactor microscopes modules throughout - Organize processing sequence (initialization to termination) - Organize naming conventions --- studio/app/optinist/microscopes/.gitignore | 5 ++ studio/app/optinist/microscopes/IsxdReader.py | 19 +++-- .../microscopes/MicroscopeDataReaderBase.py | 55 +++++++----- studio/app/optinist/microscopes/ND2Reader.py | 24 +++--- studio/app/optinist/microscopes/OIRReader.py | 83 +++++++++---------- .../optinist/microscopes/test_IsxdReader.py | 26 ++++-- .../optinist/microscopes/test_ND2Reader.py | 15 ++-- .../optinist/microscopes/test_OIRReader.py | 6 +- 8 files changed, 131 insertions(+), 102 deletions(-) diff --git a/studio/app/optinist/microscopes/.gitignore b/studio/app/optinist/microscopes/.gitignore index dd66836af..1c54ff51d 100644 --- a/studio/app/optinist/microscopes/.gitignore +++ b/studio/app/optinist/microscopes/.gitignore @@ -1,2 +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 index c260fa7ae..1e62e6b93 100644 --- a/studio/app/optinist/microscopes/IsxdReader.py +++ b/studio/app/optinist/microscopes/IsxdReader.py @@ -29,11 +29,14 @@ def _init_library(self): # initialization process. (using pip module) pass # do nothing. - def _load_data_file(self, data_path: str) -> object: - return isx.Movie.read(data_path) + 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 - def _build_original_metadata(self, handle: object, data_name: str) -> dict: - movie: isx.Movie = handle spacing: isx.Spacing = movie.spacing timing: isx.Timing = movie.timing @@ -78,12 +81,14 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: # Note: Not currently supported return None - def _release_resources(self, handle: object) -> None: + def _release_resources(self) -> None: # Note: in inscopix sdk, there is no library (ddl) release process. pass # do nothing. - def get_images_stack(self) -> list: - movie: isx.Movie = self.data_handle + 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) ] diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py index 180964c06..ff4dead87 100644 --- a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -34,13 +34,27 @@ def __init__(self): self._init_library() # init members - self.__data_path = None - self.__data_handle = None + self.__data_file_path = None + self.__resource_handles = None self.__original_metadata = None self.__ome_metadata = None self.__lab_specific_metadata = None - def load(self, data_path: str): + 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 """ @@ -49,26 +63,25 @@ def load(self, data_path: str): self.__lab_specific_metadata = None """ - Load data + Load data file """ - handle = self._load_data_file(data_path) - self.__data_handle = handle - self.__data_path = data_path - data_name = os.path.basename(data_path) + 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(handle, data_name) + 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 ) - """ - Release resources - """ - self._release_resources(handle) + def get_image_stacks(self) -> list: + """Return microscope image stacks""" + return self._get_image_stacks() @abstractmethod def _init_library(self) -> dict: @@ -76,12 +89,12 @@ def _init_library(self) -> dict: pass @abstractmethod - def _load_data_file(self, data_path: str) -> object: + def _load_file(self, data_file_path: str) -> object: """Return metadata specific to microscope instruments""" pass @abstractmethod - def _build_original_metadata(self, handle: object, data_name: str) -> dict: + def _build_original_metadata(self, data_name: str) -> dict: """Build metadata specific to microscope instruments""" pass @@ -96,22 +109,22 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: pass @abstractmethod - def _release_resources(self, handle: object) -> None: + def _release_resources() -> None: """Release microscope library resources""" pass @abstractmethod - def get_images_stack(self) -> list: + def _get_image_stacks(self) -> list: """Return microscope image stacks""" pass @property - def data_path(self) -> str: - return self.__data_path + def data_file_path(self) -> str: + return self.__data_file_path @property - def data_handle(self) -> object: - return self.__data_handle + def resource_handles(self) -> list: + return self.__resource_handles @property def original_metadata(self) -> dict: diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index 49914fb85..0f1f1a559 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -122,15 +122,17 @@ def _init_library(self): self.__dll.Lim_FileClose.argtypes = (ctypes.c_void_p,) - def _load_data_file(self, data_path: str) -> object: - handle = self.__dll.Lim_FileOpenForReadUtf8(data_path.encode("utf-8")) + 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(data_path) + raise FileNotFoundError(data_file_path) - return handle + return (handle,) + + def _build_original_metadata(self, data_name: str) -> dict: + (handle,) = self.resource_handles - def _build_original_metadata(self, handle: object, data_name: str) -> dict: attributes = self.__dll.Lim_FileGetAttributes(handle) metadata = self.__dll.Lim_FileGetMetadata(handle) textinfo = self.__dll.Lim_FileGetTextinfo(handle) @@ -280,14 +282,17 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: return lab_specific_metadata - def _release_resources(self, handle: object) -> None: + def _release_resources(self) -> None: + (handle,) = self.resource_handles + self.__dll.Lim_FileClose(handle) - def get_images_stack(self) -> list: + def _get_image_stacks(self) -> list: """Return microscope image stacks""" + (handle,) = self.resource_handles + # initialization - handle = self._load_data_file(self.data_path) pic: LIMPICTURE = LIMPICTURE() seq_count = self.__dll.Lim_FileGetSeqCount(handle) @@ -360,7 +365,4 @@ def get_images_stack(self) -> list: # construct return value (each channel's stack) result_channels_stacks[channel_idx].append(channel_plane_buffer) - # do release resources - self._release_resources(handle) - return result_channels_stacks diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index ab8952d3c..4b096f335 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -31,7 +31,10 @@ class OIRReader(MicroscopeDataReaderBase): - """Olympus OIR data reader""" + """Olympus OIR data reader + + * IDAL SDK usage method is based on IDA_Sample/IDA_Sample.cpp + """ SDK_LIBRARY_FILES = { "Windows": { @@ -79,19 +82,16 @@ def _init_library(self): ctypes.cdll.LoadLibrary(dependency_path) # load sdk library - self.__dll = lib.load_library(__class__.get_library_path()) - - def _load_data_file(self, data_file_path: str) -> object: - ida = self.__dll - - hAccessor = self.__hAccessor = ctypes.c_void_p() - hFile = self.__hFile = ctypes.c_void_p() + ida = self.__dll = lib.load_library(__class__.get_library_path()) # initialize sdk library - # TODO: 要リファクタリング:_load_data_file の外部でのコールとする ida.Initialize() + def _load_file(self, data_file_path: str) -> object: + ida = self.__dll + # Get Accessor + hAccessor = self.__hAccessor = ctypes.c_void_p() ida.GetAccessor(data_file_path, ctypes.byref(hAccessor)) if not hAccessor: raise Exception("GetAccessor Error: Please check the File path.") @@ -100,6 +100,7 @@ def _load_data_file(self, data_file_path: str) -> object: ida.Connect(hAccessor) # Open file + hFile = ctypes.c_void_p() result = ida.Open( hAccessor, data_file_path, @@ -110,28 +111,24 @@ def _load_data_file(self, data_file_path: str) -> object: raise Exception("Open Error") # Get Group Handle - hGroup = self.__hGroup = ctypes.c_void_p() + 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 = self.__hArea = ctypes.c_void_p() + 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)) - # TODO: return type を要リファクタリング(他のReaderのI/Fとも要整合) return (hAccessor, hFile, hGroup, hArea) - def _build_original_metadata(self, handle: object, data_name: str) -> dict: + def _build_original_metadata(self, data_name: str) -> dict: ida = self.__dll - hAccessor = self.__hAccessor - hFile = self.__hFile - hGroup = self.__hGroup - hArea = self.__hArea + (hAccessor, hFile, hGroup, hArea) = self.resource_handles # -------------------------------------------------- # Get data from API @@ -216,7 +213,7 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: axes_sequence_counts.append(axis["max"]) sequence_count = max(axes_sequence_counts) - fps = 0 # TODO: 計算対象予定 + fps = 0 # TODO: 今後計算対象予定 omeData = OMEDataModel( image_name=original_metadata["data_name"], @@ -249,8 +246,8 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: # Make parameters # -------------------------------------------------- - hAccessor = self.__hAccessor - hArea = self.__hArea + (hAccessor, hFile, hGroup, hArea) = self.resource_handles + del hFile, hGroup axis_info = AxisInfo(hAccessor, hArea) @@ -261,6 +258,7 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: 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() @@ -268,6 +266,7 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: 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() @@ -309,30 +308,10 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: return lab_specific_metadata - def _release_resources(self, handle: object) -> None: - # ---------------------------------------- - # Release each resource - # ---------------------------------------- - - ida = self.__dll - - hAccessor = self.__hAccessor - hFile = self.__hFile - hGroup = self.__hGroup - hArea = self.__hArea - - ida.ReleaseArea(hAccessor, hArea) - ida.ReleaseGroup(hAccessor, hGroup) - ida.Close(hAccessor, hFile) - ida.Disconnect(hAccessor) - ida.ReleaseAccessor(ctypes.byref(hAccessor)) - ida.Terminate() - - def get_images_stack(self) -> list: + def _get_image_stacks(self) -> list: """Return microscope image stacks""" - # initialization - (hAccessor, hFile, hGroup, hArea) = self._load_data_file(self.data_path) + (hAccessor, hFile, hGroup, hArea) = self.resource_handles rect = CMN_RECT() @@ -407,8 +386,20 @@ def get_images_stack(self) -> list: # construct return value (each channel's stack) result_channels_stacks.append(result_stack) - # do release resources - # TODO: 要リファクタリング:引数仕様整理 - self._release_resources((hAccessor, hFile, hGroup, hArea)) - 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/test_IsxdReader.py b/studio/app/optinist/microscopes/test_IsxdReader.py index 2127f99e0..4aebe5912 100644 --- a/studio/app/optinist/microscopes/test_IsxdReader.py +++ b/studio/app/optinist/microscopes/test_IsxdReader.py @@ -1,3 +1,4 @@ +import json import logging import os from pprint import pprint @@ -6,7 +7,7 @@ CURRENT_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) TEST_DATA_PATH = ( - CURRENT_DIR_PATH + "/testdata/inscopix/fixed-oist-sample_data_short.isxd" + CURRENT_DIR_PATH + "/testdata/inscopix/oist_short_example_preprocessed.isxd" ) @@ -22,9 +23,6 @@ def test_isxd_reader(): data_reader = IsxdReader() data_reader.load(TEST_DATA_PATH) - # debug print. - import json - # dump attributes print("[original_metadata]", json.dumps(data_reader.original_metadata, indent=2)) pprint(data_reader.ome_metadata) @@ -33,9 +31,23 @@ def test_isxd_reader(): json.dumps(data_reader.lab_specific_metadata, indent=2), ) - # dump image stack - # images_stack = data_reader.get_images_stack() - # pprint(len(images_stack)) + # 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 diff --git a/studio/app/optinist/microscopes/test_ND2Reader.py b/studio/app/optinist/microscopes/test_ND2Reader.py index 14ba8fce0..0cb100c23 100644 --- a/studio/app/optinist/microscopes/test_ND2Reader.py +++ b/studio/app/optinist/microscopes/test_ND2Reader.py @@ -1,3 +1,4 @@ +import json import logging import os from pprint import pprint @@ -23,24 +24,24 @@ def test_nd2_reader(): data_reader = ND2Reader() data_reader.load(TEST_DATA_PATH) - import json - # 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)) + print( + "[lab_specific_metadata]", + json.dumps(data_reader.lab_specific_metadata, indent=2), + ) # get image stacks (for all channels) - channels_stacks = data_reader.get_images_stack() + 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, images_stack in enumerate(channels_stacks): - save_stack = [Image.fromarray(frame) for frame in images_stack] + 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" ) diff --git a/studio/app/optinist/microscopes/test_OIRReader.py b/studio/app/optinist/microscopes/test_OIRReader.py index 35f0c3e8e..121e13b43 100644 --- a/studio/app/optinist/microscopes/test_OIRReader.py +++ b/studio/app/optinist/microscopes/test_OIRReader.py @@ -33,15 +33,15 @@ def test_oir_reader(): ) # get image stacks (for all channels) - channels_stacks = data_reader.get_images_stack() + 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, images_stack in enumerate(channels_stacks): - save_stack = [Image.fromarray(frame) for frame in images_stack] + 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" ) From 2533781eec239877c452d1ee52c55b5cbfa3236c Mon Sep 17 00:00:00 2001 From: tienday Date: Thu, 1 Feb 2024 14:37:34 +0900 Subject: [PATCH 25/26] enhance exception handling --- studio/app/optinist/microscopes/ND2Reader.py | 2 +- studio/app/optinist/microscopes/OIRReader.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index 0f1f1a559..76e4f09c0 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -126,7 +126,7 @@ 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(data_file_path) + raise FileNotFoundError(f"Open Error: {data_file_path}") return (handle,) diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index 4b096f335..5fda8cbaa 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -85,30 +85,32 @@ def _init_library(self): ida = self.__dll = lib.load_library(__class__.get_library_path()) # initialize sdk library - ida.Initialize() + 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.GetAccessor(data_file_path, ctypes.byref(hAccessor)) - if not hAccessor: - raise Exception("GetAccessor Error: Please check the File path.") + 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() - result = ida.Open( + ida_result = ida.Open( hAccessor, data_file_path, IDA_OpenMode.IDA_OM_READ, ctypes.byref(hFile), ) - if result != IDA_Result.IDA_RESULT_SUCCESS: - raise Exception("Open Error") + 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() From 8a61d52693b245808bf1da24e35e1c0bdfc24e66 Mon Sep 17 00:00:00 2001 From: tienday Date: Thu, 1 Feb 2024 20:08:14 +0900 Subject: [PATCH 26/26] expand OME metadata --- studio/app/optinist/microscopes/IsxdReader.py | 5 +++- .../microscopes/MicroscopeDataReaderBase.py | 14 +++++------ studio/app/optinist/microscopes/ND2Reader.py | 25 +++++++++++++++---- studio/app/optinist/microscopes/OIRReader.py | 17 +++++++++++-- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/studio/app/optinist/microscopes/IsxdReader.py b/studio/app/optinist/microscopes/IsxdReader.py index 1e62e6b93..c0bc36415 100644 --- a/studio/app/optinist/microscopes/IsxdReader.py +++ b/studio/app/optinist/microscopes/IsxdReader.py @@ -71,7 +71,10 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: size_x=spacing["width"], size_y=spacing["height"], size_t=timing["num_samples"], - size_c=0, + 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"]), ) diff --git a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py index ff4dead87..2006126ec 100644 --- a/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py +++ b/studio/app/optinist/microscopes/MicroscopeDataReaderBase.py @@ -12,14 +12,12 @@ class OMEDataModel: image_name: str size_x: int # width size_y: int # height - size_t: int # time - size_c: int # TODO: 要確認 - fps: int # frames_per_second # TODO: OME標準の類似項目に合わせる - - # TODO: 以下今後追加想定 - # SizeZ: int - # AcquisitionDate: date - # Instrument/(Laser|Detector): str + 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): diff --git a/studio/app/optinist/microscopes/ND2Reader.py b/studio/app/optinist/microscopes/ND2Reader.py index 76e4f09c0..0a12ccef1 100644 --- a/studio/app/optinist/microscopes/ND2Reader.py +++ b/studio/app/optinist/microscopes/ND2Reader.py @@ -2,6 +2,7 @@ import json import os import platform +import re from enum import Enum, IntEnum import numpy as np @@ -167,14 +168,22 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: """ 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 (experiments is not None) and ("periods" in experiments[0]["parameters"]): - period_ms = float(experiments[0]["parameters"]["periods"][0]["periodMs"]) - fps = (1000 / period_ms) if period_ms > 0 else 0 + 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: - period_ms = 0 fps = 0 omeData = OMEDataModel( @@ -182,7 +191,10 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: size_x=attributes["widthPx"], size_y=attributes["heightPx"], size_t=attributes["sequenceCount"], - size_c=0, + 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, ) @@ -223,6 +235,9 @@ def _build_lab_specific_metadata(self, original_metadata: dict) -> dict: z_slicenum = experiments[0]["count"] z_interval = experiments[0]["parameters"]["stepUm"] + # ※ome_metadata の一部項目をアップデート + self.ome_metadata.size_z = z_slicenum + # ---------------------------------------- # Lab固有metadata変数構築 # ---------------------------------------- diff --git a/studio/app/optinist/microscopes/OIRReader.py b/studio/app/optinist/microscopes/OIRReader.py index 5fda8cbaa..429f5dcba 100644 --- a/studio/app/optinist/microscopes/OIRReader.py +++ b/studio/app/optinist/microscopes/OIRReader.py @@ -207,6 +207,9 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: 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. @@ -215,14 +218,24 @@ def _build_ome_metadata(self, original_metadata: dict) -> OMEDataModel: axes_sequence_counts.append(axis["max"]) sequence_count = max(axes_sequence_counts) - fps = 0 # TODO: 今後計算対象予定 + # 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_c=0, + size_z=nZLoop, + size_c=len(channel_info), + acquisition_date=file_creation_time["creation_time"], + objective_model=objective_lens_info["name"], fps=fps, )