From c3f88b78464ab7c908e9bbeeea4456ab277033ea Mon Sep 17 00:00:00 2001 From: Ma Nan Date: Mon, 5 Aug 2024 11:27:50 +0800 Subject: [PATCH 1/4] fix: layer cache cannot be saved via save stage data --- bioxelnodes/operators/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bioxelnodes/operators/misc.py b/bioxelnodes/operators/misc.py index f988f12..03747bb 100644 --- a/bioxelnodes/operators/misc.py +++ b/bioxelnodes/operators/misc.py @@ -62,7 +62,7 @@ class SaveStagedData(bpy.types.Operator): bl_description = "Save all staged data in this file for sharing" save_layer: bpy.props.BoolProperty( - name="Save Layer VDB Cache", + name="Save Layer Caches", default=True, ) # type: ignore @@ -116,7 +116,7 @@ class SaveStagedData(bpy.types.Operator): fails = [] for layer in get_all_layer_objs(): try: - save_layer(layer, self.cache_dir) + save_layer_cache(layer, self.cache_dir) except: fails.append(layer) From 65edf401648688d069d3b434e58ce6e7adf84aeb Mon Sep 17 00:00:00 2001 From: Ma Nan Date: Mon, 5 Aug 2024 13:48:24 +0800 Subject: [PATCH 2/4] feat: better drag and drop --- bioxelnodes/bioxel/parse.py | 3 +- bioxelnodes/operators/io.py | 103 +++++++++++++++++----------------- bioxelnodes/operators/misc.py | 8 +-- 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/bioxelnodes/bioxel/parse.py b/bioxelnodes/bioxel/parse.py index decec83..5eeb3e0 100644 --- a/bioxelnodes/bioxel/parse.py +++ b/bioxelnodes/bioxel/parse.py @@ -29,7 +29,8 @@ SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA', '.png', '.PNG', '.vtk', '.ome.tiff', '.ome.tif', - '.mrc', '.mrc.gz', '.map', '.map.gz'] + '.mrc', '.mrc.gz', '.map', '.map.gz', + '.gz'] OME_EXTS = ['.ome.tiff', '.ome.tif', '.tif', '.TIF', '.tiff', '.TIFF'] diff --git a/bioxelnodes/operators/io.py b/bioxelnodes/operators/io.py index d26c967..7f13063 100644 --- a/bioxelnodes/operators/io.py +++ b/bioxelnodes/operators/io.py @@ -24,19 +24,6 @@ from .utils import (change_render_setting, get_cache_dir, get_preferences, import SimpleITK as sitk import transforms3d -FH_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA', - '.gipl', '.gipl.gz', - '.mnc', '.MNC', - '.mrc', '.rec', - '.mha', '.mhd', - '.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz', - '.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5', - '.nrrd', '.nhdr', - '.vtk', - '.gz' - '.ome.tiff', '.ome.tif', - '.mrc', '.mrc.gz', '.map', '.map.gz'] - def get_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple): shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]), @@ -69,24 +56,20 @@ class ImportVolumetricData(): bl_options = {'UNDO'} filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore - directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore read_as = "scalar" def execute(self, context): - container_objs = get_container_objs_from_selection() + data_path = Path(self.filepath).resolve() + ext = get_ext(data_path) + if ext not in SUPPORT_EXTS: + self.report({"WARNING"}, "Not supported format.") + return {'CANCELLED'} - if len(container_objs) > 0: - bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT', - filepath=self.filepath, - directory=self.directory, - container_obj_name=container_objs[0].name, - read_as=self.read_as) - else: - bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT', - filepath=self.filepath, - directory=self.directory, - read_as=self.read_as) + bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT', + filepath=self.filepath, + skip_read_as=True, + read_as=self.read_as) return {'FINISHED'} @@ -113,7 +96,7 @@ class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler): bl_idname = "BIOXELNODES_FH_ImportVolumetricData" bl_label = "File handler for dicom import" bl_import_operator = "bioxelnodes.parse_volumetric_data" - bl_file_extensions = ";".join(FH_EXTS) + bl_file_extensions = ";".join(SUPPORT_EXTS) @classmethod def poll_drop(cls, context): @@ -135,21 +118,26 @@ def get_series_ids(self, context): class ParseVolumetricData(bpy.types.Operator): bl_idname = "bioxelnodes.parse_volumetric_data" - bl_label = "Import Volumetric Data" + bl_label = "Import Volumetric Data (BioxelNodes)" bl_description = "Import Volumetric Data as Layer" bl_options = {'UNDO'} meta = None thread = None _timer = None + container_obj_name = "" progress: bpy.props.FloatProperty(name="Progress", options={"SKIP_SAVE"}, default=1) # type: ignore filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore - directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore - container_obj_name: bpy.props.StringProperty() # type: ignore + + skip_read_as: bpy.props.BoolProperty(name="Skip Read As", + default=False) # type: ignore + + skip_series_select: bpy.props.BoolProperty(name="Skip Sries Select", + default=True) # type: ignore read_as: bpy.props.EnumProperty(name="Read as", default="scalar", @@ -166,7 +154,7 @@ class ParseVolumetricData(bpy.types.Operator): data_path = Path(self.filepath).resolve() ext = get_ext(data_path) if ext not in SUPPORT_EXTS: - self.report({"WARNING"}, "Not supported extension.") + self.report({"WARNING"}, "Not supported format.") return {'CANCELLED'} print("Collecting Meta Data...") @@ -297,12 +285,19 @@ class ParseVolumetricData(bpy.types.Operator): return {'FINISHED'} def invoke(self, context, event): - if not self.filepath and not self.directory: + if not self.filepath: return {'CANCELLED'} data_path = Path(self.filepath).resolve() ext = get_ext(data_path) + container_objs = get_container_objs_from_selection() + if len(container_objs) > 0: + self.container_obj_name = container_objs[0].name + + title = f"Add to **{self.container_obj_name}**" \ + if self.container_obj_name != "" else f"Init a Container" + # Series Selection if ext in DICOM_EXTS: data_dirpath = data_path.parent @@ -358,9 +353,10 @@ class ParseVolumetricData(bpy.types.Operator): series_item.label = value if len(series_items.keys()) > 1: + self.skip_series_select = False context.window_manager.invoke_props_dialog(self, width=400, - title="Which series to import?") + title=title) return {'RUNNING_MODAL'} elif len(series_items.keys()) == 1: self.series_id = list(series_items.keys())[0] @@ -368,27 +364,37 @@ class ParseVolumetricData(bpy.types.Operator): self.report({"ERROR"}, "Get no vaild series.") return {'CANCELLED'} - self.execute(context) - return {'RUNNING_MODAL'} + if self.skip_read_as: + self.execute(context) + return {'RUNNING_MODAL'} + else: + context.window_manager.invoke_props_dialog(self, + width=400, + title=title) + return {'RUNNING_MODAL'} def draw(self, context): layout = self.layout - layout.label( - text='Detect multi-series in DICOM, pick one') - layout.prop(self, "series_id") + if not self.skip_read_as: + layout.label( + text='What kind is the data read as?') + layout.prop(self, "read_as") + if not self.skip_series_select: + layout.label(text='Which series to import?') + layout.prop(self, "series_id") def get_sequence_sources(self, context): - items = [("-1", "None (Static)", "")] + items = [("-1", "None (1 frame)", "")] orig_shape = tuple(self.orig_shape) if self.frame_count > 1: - items.append(("0", f"Frame (Get {self.frame_count} frames)", "")) + items.append(("0", f"Frame ({self.frame_count} frames)", "")) elif self.frame_count == 1 and self.channel_count > 1: - items.append(("4", f"Channel (Get {self.channel_count} frames)", "")) + items.append(("4", f"Channel ({self.channel_count} frames)", "")) elif self.frame_count == 1 and self.channel_count == 1: - items.append(("1", f"X (Get {orig_shape[0]} frames)", "")) - items.append(("2", f"Y (Get {orig_shape[1]} frames)", "")) - items.append(("3", f"Z (Get {orig_shape[2]} frames)", "")) + items.append(("1", f"X ({orig_shape[0]} frames)", "")) + items.append(("2", f"Y ({orig_shape[1]} frames)", "")) + items.append(("3", f"Z ({orig_shape[2]} frames)", "")) return items @@ -681,12 +687,9 @@ class ImportVolumetricDataDialog(bpy.types.Operator): return {'FINISHED'} def invoke(self, context, event): - if self.read_as == "label": - volume_dtype = "Label" - elif self.read_as == "scalar": - volume_dtype = "Scalar" - title = f"As {volume_dtype} Opitons (Add to Container: {self.container_obj_name})" \ - if self.container_obj_name != "" else f"As {volume_dtype} Options (Init a Container)" + layer_kind = self.read_as.capitalize() + title = f"Add to **{self.container_obj_name}**, As {layer_kind}" \ + if self.container_obj_name != "" else f"Init a Container, As {layer_kind}" context.window_manager.invoke_props_dialog(self, width=500, title=title) diff --git a/bioxelnodes/operators/misc.py b/bioxelnodes/operators/misc.py index 03747bb..c2a7a27 100644 --- a/bioxelnodes/operators/misc.py +++ b/bioxelnodes/operators/misc.py @@ -61,7 +61,7 @@ class SaveStagedData(bpy.types.Operator): bl_label = "Save Staged Data" bl_description = "Save all staged data in this file for sharing" - save_layer: bpy.props.BoolProperty( + save_cache: bpy.props.BoolProperty( name="Save Layer Caches", default=True, ) # type: ignore @@ -112,7 +112,7 @@ class SaveStagedData(bpy.types.Operator): self.report({"INFO"}, f"Successfully saved to {output_path}") - if self.save_layer: + if self.save_cache: fails = [] for layer in get_all_layer_objs(): try: @@ -140,7 +140,7 @@ class SaveStagedData(bpy.types.Operator): def draw(self, context): layout = self.layout panel = layout.box() - panel.prop(self, "save_layer") + panel.prop(self, "save_cache") panel.prop(self, "cache_dir") panel = layout.box() panel.prop(self, "save_lib") @@ -148,7 +148,7 @@ class SaveStagedData(bpy.types.Operator): class SaveCaches(bpy.types.Operator): - bl_idname = "bioxelnodes.save_layers" + bl_idname = "bioxelnodes.save_caches" bl_label = "Save Caches" bl_description = "Save Container's caches to directory." From 0cad35e2da61248159d5cf46edee56985051d86a Mon Sep 17 00:00:00 2001 From: Ma Nan Date: Mon, 5 Aug 2024 14:13:48 +0800 Subject: [PATCH 3/4] fix: fail to read all sequence in folder with same name prefix --- bioxelnodes/bioxel/parse.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/bioxelnodes/bioxel/parse.py b/bioxelnodes/bioxel/parse.py index 5eeb3e0..3b9f202 100644 --- a/bioxelnodes/bioxel/parse.py +++ b/bioxelnodes/bioxel/parse.py @@ -14,22 +14,22 @@ Convert any volumetric data to 3D numpy array with order TXYZC """ SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA', + '.ome.tiff', '.ome.tif', + '.tif', '.TIF', '.tiff', '.TIFF', + '.mrc', '.mrc.gz', '.map', '.map.gz', '.bmp', '.BMP', + '.png', '.PNG', + '.jpg', '.JPG', '.jpeg', '.JPEG', '.PIC', '.pic', '.gipl', '.gipl.gz', - '.jpg', '.JPG', '.jpeg', '.JPEG', '.lsm', '.LSM', - '.tif', '.TIF', '.tiff', '.TIFF', '.mnc', '.MNC', '.mrc', '.rec', '.mha', '.mhd', '.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5', '.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz', '.nrrd', '.nhdr', - '.png', '.PNG', '.vtk', - '.ome.tiff', '.ome.tif', - '.mrc', '.mrc.gz', '.map', '.map.gz', '.gz'] OME_EXTS = ['.ome.tiff', '.ome.tif', @@ -69,7 +69,7 @@ def get_file_name(filepath: Path): return filepath.name.removesuffix(ext).replace(" ", "-") -def get_file_index(filepath: Path): +def get_file_number(filepath: Path) -> str: name = get_file_name(filepath) digits = "" @@ -86,15 +86,18 @@ def get_file_index(filepath: Path): break # Reverse the digits string to get the correct order - last_number = digits[::-1] - - return int(last_number) if last_number != "" else 0 + return digits[::-1] def get_sequence_name(filepath: Path) -> str: name = get_file_name(filepath) - index = get_file_index(filepath) - return name.removesuffix(str(index)) + number = get_file_number(filepath) + return name.removesuffix(number) + + +def get_sequence_index(filepath: Path) -> int: + number = get_file_number(filepath) + return int(number) if number != "" else 0 def collect_sequence(filepath: Path): @@ -103,9 +106,10 @@ def collect_sequence(filepath: Path): if f.is_file() \ and get_ext(filepath) == get_ext(f) \ and get_sequence_name(filepath) == get_sequence_name(f): - index = get_file_index(f) + index = get_sequence_index(f) file_dict[index] = f + # reomve isolated seq file for key in file_dict.copy().keys(): if not file_dict.get(key+1) \ and not file_dict.get(key-1): From 2563f398da24a3003cc3213d440e0d8f7943acd8 Mon Sep 17 00:00:00 2001 From: Ma Nan Date: Mon, 5 Aug 2024 15:53:03 +0800 Subject: [PATCH 4/4] chore: update version --- bioxelnodes/blender_manifest.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bioxelnodes/blender_manifest.toml b/bioxelnodes/blender_manifest.toml index f2d729b..ed191ec 100644 --- a/bioxelnodes/blender_manifest.toml +++ b/bioxelnodes/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "bioxelnodes" -version = "0.3.1" +version = "0.3.2" name = "Bioxel Nodes" tagline = "For scientific volumetric data visualization in Blender" maintainer = "Ma Nan " diff --git a/pyproject.toml b/pyproject.toml index a85589c..c6041a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bioxelnodes" -version = "0.3.1" +version = "0.3.2" description = "" authors = ["Ma Nan "] license = "MIT"