diff --git a/README.md b/README.md index b60b7d8..d296520 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,27 @@ # Bioxel Nodes -![Static Badge](https://img.shields.io/badge/Blender-orange?style=for-the-badge&logo=blender&logoColor=white) -![GitHub License](https://img.shields.io/github/license/OmooLab/BioxelNodes?style=for-the-badge) -![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge) -![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge) +![Static Badge](https://img.shields.io/badge/Blender-orange?style=for-the-badge&logo=blender&logoColor=white&color=black) +![GitHub License](https://img.shields.io/github/license/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black) +![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black) +![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black) + +![Discord](https://img.shields.io/discord/1265129134397587457?style=for-the-badge&logo=discord&label=Discord&labelColor=white&color=black&link=https%3A%2F%2Fdiscord.gg%2FpYkNyq2TjE) Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It using Blender's powerful **Geometry Nodes** and **Cycles** to process and render volumetric data. ![cover](https://omoolab.github.io/BioxelNodes/latest/assets/cover.png) -- Fantastic rendering result, also support EEVEE NEXT -- Support multiple formats -- Support 4D volumetric data -- All kinds of cutters -- Simple and powerful nodes +- Fantastic rendering result, also support EEVEE NEXT. +- Support multiple formats. +- Support 4D volumetric data. +- All kinds of cutters. +- Simple and powerful nodes. - Based on blender natively, can work without addon. -**Click [Getting Started](https://omoolab.github.io/BioxelNodes/latest/installation) to begin your journey into visualizing volumetric data!** +**Read the [getting started](https://omoolab.github.io/BioxelNodes/latest/installation) to begin your journey into visualizing volumetric data!** + +Welcome to our [discord server](https://discord.gg/pYkNyq2TjE), if you have any problems with this add-on. ## Support Multiple Formats @@ -55,7 +59,7 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It - Only works with Cycles CPU , Cycles GPU (OptiX), EEVEE - Section surface cannot be generated when convert to mesh (will be supported soon) -## Compatibile to Newer Version +## Compatible to Newer Version **Updating this addon may break old files, so read the following carefully before updating** diff --git a/bioxelnodes/io.py b/bioxelnodes/io.py index 32451d2..f68b4ba 100644 --- a/bioxelnodes/io.py +++ b/bioxelnodes/io.py @@ -175,6 +175,9 @@ class ParseVolumetricData(bpy.types.Operator): progress_callback=progress_callback) except CancelledByUser: return + except RuntimeError: + self.has_error = True + return if cancel(): return @@ -183,6 +186,8 @@ class ParseVolumetricData(bpy.types.Operator): # Init cancel flag self.is_cancelled = False + self.has_error = False + # Create the thread self.thread = threading.Thread(target=parse_volumetric_data_func, args=(self, context, lambda: self.is_cancelled)) @@ -229,6 +234,11 @@ class ParseVolumetricData(bpy.types.Operator): self.report({"WARNING"}, "Canncelled by user.") return {'CANCELLED'} + # Check if thread is cancelled by user + if self.has_error: + self.report({"ERROR"}, "Fail to parse, something went wrong.") + return {'CANCELLED'} + # If not canncelled... for key, value in self.meta.items(): print(f"{key}: {value}") @@ -239,9 +249,12 @@ class ParseVolumetricData(bpy.types.Operator): orig_spacing[1], orig_spacing[2]) bioxel_size = max(min_size, 1.0) - layer_size = get_layer_size(orig_shape, + layer_shape = get_layer_shape(1, orig_shape, orig_spacing) + layer_size = get_layer_size(layer_shape, bioxel_size) log10 = math.floor(math.log10(max(*layer_size))) + log10 = max(1,log10) + log10 = min(3,log10) scene_scale = math.pow(10, -log10) if self.container: @@ -700,6 +713,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator): container.matrix_world = mat_ras2blender @ mat_scene_scale container.name = container_name + container.show_in_front = True container.data.name = container_name container['bioxel_container'] = True @@ -741,8 +755,8 @@ class ImportVolumetricDataDialog(bpy.types.Operator): layer.hide_select = True layer.hide_render = True layer.hide_viewport = True - layer.data.display.use_slice = True - layer.data.display.density = 1e-05 + # layer.data.display.use_slice = True + # layer.data.display.density = 1e-05 layer['bioxel_layer'] = True layer['bioxel_layer_type'] = layer_info['type'] @@ -811,11 +825,8 @@ class ImportVolumetricDataDialog(bpy.types.Operator): # Change render setting for better result preferences = context.preferences.addons[__package__].preferences - if preferences.do_change_render_setting and self.is_first_import: - if bpy.app.version < (4, 2, 0): - bpy.context.scene.render.engine = 'CYCLES' - + bpy.context.scene.render.engine = 'CYCLES' try: bpy.context.scene.cycles.shading_system = True bpy.context.scene.cycles.volume_bounces = 12 diff --git a/bioxelnodes/operators.py b/bioxelnodes/operators.py index cc78c7b..e945458 100644 --- a/bioxelnodes/operators.py +++ b/bioxelnodes/operators.py @@ -89,8 +89,8 @@ def deep_copy_layer(vdb_path, base_layer, name): copyed_layer.hide_select = True copyed_layer.hide_render = True copyed_layer.hide_viewport = True - copyed_layer.data.display.use_slice = True - copyed_layer.data.display.density = 1e-05 + # copyed_layer.data.display.use_slice = True + # copyed_layer.data.display.density = 1e-05 copyed_layer['bioxel_layer'] = True copyed_layer['bioxel_layer_type'] = base_layer['bioxel_layer_type'] diff --git a/bioxelnodes/parse.py b/bioxelnodes/parse.py index 1fdd732..c35d505 100644 --- a/bioxelnodes/parse.py +++ b/bioxelnodes/parse.py @@ -11,6 +11,9 @@ try: except: ... +""" +Convert any volumetric data to 3D numpy array with order TXYZC +""" SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA', '.bmp', '.BMP', @@ -40,7 +43,7 @@ SEQUENCE_EXTS = ['.bmp', '.BMP', '.tif', '.TIF', '.tiff', '.TIFF', '.png', '.PNG'] -DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM'] +DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA'] FH_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA', '.gipl', '.gipl.gz', @@ -136,7 +139,11 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None): if len(sequence) > 1: is_sequence = True - if ext in MRC_EXTS and not is_sequence: + volume = None + + # Parsing with mrcfile + if volume is None and ext in MRC_EXTS and not is_sequence: + print("Parsing with mrcfile...") # TODO: much to do with mrc with mrcfile.open(filepath) as mrc: volume = mrc.data @@ -159,10 +166,10 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None): elif mrc.is_volume_stack(): volume = np.expand_dims(volume, axis=-1) # expend channel - name = Path(filepath).name.removesuffix(ext).replace(" ", "-") shape = volume.shape[1:4] spacing = (mrc.voxel_size.x, mrc.voxel_size.y, mrc.voxel_size.z) + meta = { "name": name, "description": "", @@ -175,7 +182,9 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None): "is_oriented": False } - elif ext in OME_EXTS and not is_sequence: + # Parsing with OMETIFFReader + if volume is None and ext in OME_EXTS and not is_sequence: + print("Parsing with OMETIFFReader...") reader = OMETIFFReader(fpath=filepath) ome_volume, metadata, xml_metadata = reader.read() @@ -189,69 +198,70 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None): # for key in metadata: # print(f"{key},{metadata[key]}") ome_order = metadata['DimOrder BF Array'] + if ome_volume.ndim == 2: + ome_order = ome_order.replace("T", "")\ + .replace("C", "").replace("Z", "") + bioxel_order = (ome_order.index('X'), + ome_order.index('Y')) + volume = np.transpose(ome_volume, bioxel_order) + volume = np.expand_dims(volume, axis=0) # expend frame + volume = np.expand_dims(volume, axis=-1) # expend Z + volume = np.expand_dims(volume, axis=-1) # expend channel + + elif ome_volume.ndim == 3: + # -> XYZC + ome_order = ome_order.replace("T", "").replace("C", "") + bioxel_order = (ome_order.index('X'), + ome_order.index('Y'), + ome_order.index('Z')) + volume = np.transpose(ome_volume, bioxel_order) + volume = np.expand_dims(volume, axis=0) # expend frame + volume = np.expand_dims(volume, axis=-1) # expend channel + elif ome_volume.ndim == 4: + # -> XYZC + ome_order = ome_order.replace("T", "") + bioxel_order = (ome_order.index('X'), + ome_order.index('Y'), + ome_order.index('Z'), + ome_order.index('C')) + volume = np.transpose(ome_volume, bioxel_order) + volume = np.expand_dims(volume, axis=0) # expend frame + elif ome_volume.ndim == 5: + # -> TXYZC + bioxel_order = (ome_order.index('T'), + ome_order.index('X'), + ome_order.index('Y'), + ome_order.index('Z'), + ome_order.index('C')) + volume = np.transpose(ome_volume, bioxel_order) + + shape = volume.shape[1:4] + + try: + spacing = (metadata['PhysicalSizeX'], + metadata['PhysicalSizeY'], + metadata['PhysicalSizeZ']) + except: + spacing = (1, 1, 1) + + name = Path(filepath).name.removesuffix(ext).replace(" ", "-") + meta = { + "name": name, + "description": "", + "shape": shape, + "spacing": spacing, + "origin": (0, 0, 0), + "direction": (1, 0, 0, 0, 1, 0, 0, 0, 1), + "frame_count": volume.shape[0], + "channel_count": volume.shape[-1], + "is_oriented": False + } except: - ome_order = "TCZYX" + ... - if ome_volume.ndim == 2: - ome_order = ome_order.replace("T", "")\ - .replace("C", "").replace("Z", "") - bioxel_order = (ome_order.index('X'), - ome_order.index('Y')) - volume = np.transpose(ome_volume, bioxel_order) - volume = np.expand_dims(volume, axis=0) # expend frame - volume = np.expand_dims(volume, axis=-1) # expend Z - volume = np.expand_dims(volume, axis=-1) # expend channel - - elif ome_volume.ndim == 3: - # -> XYZC - ome_order = ome_order.replace("T", "").replace("C", "") - bioxel_order = (ome_order.index('X'), - ome_order.index('Y'), - ome_order.index('Z')) - volume = np.transpose(ome_volume, bioxel_order) - volume = np.expand_dims(volume, axis=0) # expend frame - volume = np.expand_dims(volume, axis=-1) # expend channel - elif ome_volume.ndim == 4: - # -> XYZC - ome_order = ome_order.replace("T", "") - bioxel_order = (ome_order.index('X'), - ome_order.index('Y'), - ome_order.index('Z'), - ome_order.index('C')) - volume = np.transpose(ome_volume, bioxel_order) - volume = np.expand_dims(volume, axis=0) # expend frame - elif ome_volume.ndim == 5: - # -> TXYZC - bioxel_order = (ome_order.index('T'), - ome_order.index('X'), - ome_order.index('Y'), - ome_order.index('Z'), - ome_order.index('C')) - volume = np.transpose(ome_volume, bioxel_order) - - shape = volume.shape[1:4] - - try: - spacing = (metadata['PhysicalSizeX'], - metadata['PhysicalSizeY'], - metadata['PhysicalSizeZ']) - except: - spacing = (1, 1, 1) - - name = Path(filepath).name.removesuffix(ext).replace(" ", "-") - meta = { - "name": name, - "description": "", - "shape": shape, - "spacing": spacing, - "origin": (0, 0, 0), - "direction": (1, 0, 0, 0, 1, 0, 0, 0, 1), - "frame_count": volume.shape[0], - "channel_count": volume.shape[-1], - "is_oriented": False - } - - else: + # Parsing with SimpleITK + if volume is None: + print("Parsing with SimpleITK...") if ext in DICOM_EXTS: dir_path = Path(filepath).resolve().parent reader = sitk.ImageSeriesReader() @@ -294,10 +304,13 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None): name = name.replace(" ", "-") description = description.replace(" ", "-") - elif ext in SEQUENCE_EXTS: - itk_volume = sitk.ReadImage(sequence) - name = get_sequence_name(filepath).replace(" ", "-") - description = "" + elif ext in SEQUENCE_EXTS and is_sequence: + try: + itk_volume = sitk.ReadImage(sequence) + name = get_sequence_name(filepath).replace(" ", "-") + description = "" + except RuntimeError as e: + raise e else: itk_volume = sitk.ReadImage(filepath) name = Path(filepath).name.removesuffix(ext).replace(" ", "-") @@ -311,7 +324,33 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None): if not progressing: raise CancelledByUser - if itk_volume.GetDimension() == 3: + if itk_volume.GetDimension() == 2: + volume = sitk.GetArrayFromImage(itk_volume) + + if volume.ndim == 3: + volume = np.transpose(volume, (1, 0, 2)) + + volume = np.expand_dims(volume, axis=-2) # expend Z + else: + volume = np.transpose(volume) + volume = np.expand_dims(volume, axis=-1) # expend Z + volume = np.expand_dims(volume, axis=-1) # expend channel + + volume = np.expand_dims(volume, axis=0) # expend frame + + meta = { + "name": name, + "description": description, + "shape": volume.shape[1:4], + "spacing": (1, 1, 1), + "origin": (0, 0, 0), + "direction": (1, 0, 0, 0, 1, 0, 0, 0, 1), + "frame_count": 1, + "channel_count": volume.shape[-1], + "is_oriented": False + } + + elif itk_volume.GetDimension() == 3: itk_volume = sitk.DICOMOrient(itk_volume, 'RAS') volume = sitk.GetArrayFromImage(itk_volume) @@ -332,12 +371,12 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None): "spacing": tuple(itk_volume.GetSpacing()), "origin": tuple(itk_volume.GetOrigin()), "direction": tuple(itk_volume.GetDirection()), - "frame_count": volume.shape[0], + "frame_count": 1, "channel_count": volume.shape[-1], "is_oriented": True } - if itk_volume.GetDimension() == 4: + elif itk_volume.GetDimension() == 4: # FIXME: not sure... direction = np.array(itk_volume.GetDirection()) direction = direction.reshape(3, 3) if itk_volume.GetDimension() == 3 \ diff --git a/docs/index.md b/docs/index.md index fd4bc0d..e8fc155 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,28 @@ +[中文文档](https://uj6xfhbzp0.feishu.cn/wiki/Qx3VwHuNPimeI8kr6nDcvl1DnHf?from=from_copylink) + # Bioxel Nodes +![Static Badge](https://img.shields.io/badge/Blender-orange?style=for-the-badge&logo=blender&logoColor=white&color=black) +![GitHub License](https://img.shields.io/github/license/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black) +![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black) +![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black) + +![Discord](https://img.shields.io/discord/1265129134397587457?style=for-the-badge&logo=discord&label=Discord&labelColor=white&color=black&link=https%3A%2F%2Fdiscord.gg%2FpYkNyq2TjE) + Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It using Blender's powerful **Geometry Nodes** and **Cycles** to process and render volumetric data. -![cover](assets/cover.png) +![cover](https://omoolab.github.io/BioxelNodes/latest/assets/cover.png) -- Fantastic rendering result, also support EEVEE NEXT -- Support multiple formats -- Support 4D volumetric data -- All kinds of cutters -- Simple and powerful nodes +- Fantastic rendering result, also support EEVEE NEXT. +- Support multiple formats. +- Support 4D volumetric data. +- All kinds of cutters. +- Simple and powerful nodes. - Based on blender natively, can work without addon. -**Click [Getting Started](https://omoolab.github.io/BioxelNodes/latest/installation) to begin your journey into visualizing volumetric data!** +**Read the [getting started](https://omoolab.github.io/BioxelNodes/latest/installation) to begin your journey into visualizing volumetric data!** + +Welcome to our [discord server](https://discord.gg/pYkNyq2TjE), if you have any problems with this add-on. ## Support Multiple Formats @@ -48,7 +59,7 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It - Only works with Cycles CPU , Cycles GPU (OptiX), EEVEE - Section surface cannot be generated when convert to mesh (will be supported soon) -## Compatibile to Newer Version +## Compatible to Newer Version **Updating this addon may break old files, so read the following carefully before updating** @@ -59,6 +70,7 @@ After the addon update, your old project files may not work either, this may be Also, unlike the newer versions, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings. ## Roadmap -- Better multi-format import experience -- One-click bake model with texture -- AI Segmentation to Generate Labels \ No newline at end of file + +- Better multi-format import experience +- One-click bake model with texture +- AI Segmentation to Generate Labels