Skip to content

Commit

Permalink
Merge pull request #12 from OmooLab/feature/dev
Browse files Browse the repository at this point in the history
Feature/dev
  • Loading branch information
icrdr authored May 14, 2024
2 parents 8b45de4 + c1886f9 commit 8e8b176
Show file tree
Hide file tree
Showing 40 changed files with 3,029 additions and 337 deletions.
11 changes: 11 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"overrides": [
{
"files": "*.md",
"options": {
"tabWidth": 4
}
}
]
}
117 changes: 26 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,96 +15,31 @@ Before us, there have been many tutorials and add-ons for importing volumetric d

Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetric data can be rendered with great detail:

![gallery](docs/images/gallery.png)
![gallery](assets/gallery.png)

The "Bioxel" in "BioxelNodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data. The volumetric data made up of Bioxel is called Bioxels. We are developing a toolkit around Bioxels for better biological data visualization. but before its release, we made this Blender version of bioxels toolkit first, in order to let more people to have fun with volumetric data. [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started)

# Supported Format

| Format | EXT | Test |
| ------ | ---------------------------------------- | ------- |
| DICOM | .dcm, .DICOM | ✅ pass |
| BMP | .bmp, .BMP | ✅ pass |
| JPEG | .jpg, .JPG, .jpeg, .JPEG | ✅ pass |
| PNG | .png, .PNG | ✅ pass |
| TIFF | .tif, .TIF, .tiff, .TIFF | ✅ pass |
| Nifti | .nia, .nii, .nii.gz, .hdr, .img, .img.gz | ✅ pass |
| Nrrd | .nrrd, .nhdr | ✅ pass |
| Meta | .mha, .mhd | yet |
| HDF5 | .hdf, .h4, .hdf4, .he2, .h5, .hdf5, .he5 | ✅ pass |
| VTK | .vtk | yet |
| BioRad | .PIC, .pic | yet |
| Gipl | .gipl, .gipl.gz | yet |
| LSM | .lsm, .LSM | yet |
| MINC | .mnc, .MNC | yet |
| MRC | .mrc, .rec | yet |

# Known limitations

### Known limitations

- Cannot read DICOM with multiple series.
- Sections cannot be generated
- Only sequence files are supported, packed volume data formats are not supported yet (will be supported soon)

# Getting Started

Currently only support Blender 4.1 or above, make sure you have the correct version of Blender.

### Add-on Installation

Download the latest version https://github.com/OmooLab/BioxelNodes/releases/latest
In Blender, `Edit - Preferences - Add-ons - Install`, select the `BioxelNodes_{version}.zip` you just downloaded.

### Dependency Installation

The add-on requires a third-party python dependency called SimpleITK, click the Install SimpleITK button below to install the dependency. After clicking, blender may get stuck, it is downloading and installing, just wait for a moment. After the Installation, **restart blender once**.

![dependency](docs/images/dependency.png)

This step may have failed due to network factors, just click "Set PyPI Mirror" to change the mirror.

### Basic Usage

First you need to have your volumetric data ready. If not, you can access open research data from [Dryad](https://datadryad.org) (Dryad publishes data exclusively under a [Creative Commons Public Domain License](https://creativecommons.org/public-domain/cc0/))

Note: Make sure one folder contains only one sequence, multiple sequences need to be manually split into different folders first.

In Blender, `File - Import - Volume Data as Bioxels`, select **one** of the .dcm files and click on "Volume Data as Bioxels" (you can also drag one of the .dcm files directly into the 3D viewport to trigger the import, but this is limited to .dcm format files)

![importing](docs/images/importing.png)

It may take a while to read data. After finishing reading, it will pop up a dialog box

![import dialog](docs/images/import_dialog.png)

Ignore the options, just click OK!

After importing, the necessary nodes are automatically added, the reconstruction and shader are created, and then turned on the cycles rendering to directly see the result.

![result](docs/images/result.png)

Click select Bioxels Object, and open the Geometry Nodes panel to see the following node graph:

![segment node](docs/images/segment_node.png)

You can change the "Threshold" to modify the reconstruction model, or change the "Color", "Density" to modify the shader effect. All the parameters are straightforward, you can understand them by changing the values.

The node, that named "Segment", is a preset node that combines the steps of 3D reconstruction process and shading process. In general, you need to add one of the segmentation node to reconstruction first, and followed by one of the shader node to set the shader. As you can see in the following figure:

![general graph](docs/images/general_graph.png)

Currently there are 3 types of nodes

- Segmentation nodes, responsible for splitting the volume into a reconstructed model (we call them "Segment").
- Shader nodes, responsible for giving shader to the Segment.
- Slicer nodes, responsible for partially cutting the Segment.

For example:

![general graph example](docs/images/general_graph_example.png)

You can add a "Bake" node between segmentation node and shader node, if the reconstruction process consumes too much computing time. But be sure, you are selecting the volume object, not the container object.

![bake node](docs/images/bake_node.png)

# Future Features

- Support more volumetric data formats (.nii, .map, .txm...)
- Generate sections
- More segmentation methods, e.g. AI segmentation.
- Even better shader for volumetric rendering

# Technical Notes

The "Bioxel" in "BioxelNodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data, so maybe its chinese name should be "生素" 😂? The volumetric data made up of Bioxel is called Bioxels. We are developing a toolkit around Bioxels for better biological data visualization. but before its release, we made this Blender version of bioxels toolkit first, in order to let more people to have fun with volumetric data.

### Bioxels

Bioxels is based on the RAS coordinate system, Right Aanterior Superior, which was chosen over LPS because it is more compatible with most 3D CG software coordinate systems, and is in line with the 3D artist's understanding of space.

All distances within Bioxels are in Units, and are specified in Meter pre unit. However, when Bioxels is imported into 3D CG software, its size in the software is not scaled by reading the Meter pre unit directly. The reason for this is that many 3D operations in software require that the primtives not be too large or too small.

### Based on OpenVDB

Bioxels is based entirely on OpenVDB for storage and rendering. The main reason for choosing OpenVDB is that as a volumetric data format, it is the fastest way to work with most CG renderers.

### Based on Geometry Nodes

Bioxel Nodes relies on Blender Geometry Nodes to reconstruct and render volumetric data. Node-based operations ensure that the original data is not permanently altered during reconstruction and rendering operations. The fact that the processing is based on Geometry Nodes without any additional dependencies also ensures that Blender can open files without this plugin installed. Look for more support for OpenVDB in GeometryNodes so that Bioxel Nodes can do more in the future.
- Time sequence volume not supported (will be supported soon)
33 changes: 13 additions & 20 deletions bioxelnodes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
from .nodes import custom_nodes
from . import auto_load
from . import menu

import bpy

from . import auto_load
from . import menus


bl_info = {
"name": "Bioxel Nodes",
"author": "Ma Nan",
"description": "",
"blender": (4, 1, 0),
"version": (0, 1, 0),
"blender": (4, 0, 0),
"version": (0, 1, 1),
"location": "File -> Import",
"warning": "",
"category": "Node"
}

auto_load.init()

bpy.types.Scene.bioxels_dir = bpy.props.StringProperty(
name="Bioxels Directory",
subtype='DIR_PATH',
default="//"
)


def register():
auto_load.register()
custom_nodes.register()
menu.create_menu()
menus.add()
bpy.types.Scene.bioxels_dir = bpy.props.StringProperty(
name="Bioxels Directory",
subtype='DIR_PATH',
default="//"
)


def unregister():
try:
menu.remove_menu()
custom_nodes.unregister()
auto_load.unregister()
except RuntimeError:
pass
menus.remove()
auto_load.unregister()
3 changes: 3 additions & 0 deletions bioxelnodes/assets/Nodes/BioxelNodes_4.0.blend
Git LFS file not shown
File renamed without changes.
41 changes: 32 additions & 9 deletions bioxelnodes/auto_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
modules = None
ordered_classes = None


def init():
global modules
global ordered_classes

modules = get_all_submodules(Path(__file__).parent)
ordered_classes = get_ordered_classes_to_register(modules)


def register():
for cls in ordered_classes:
bpy.utils.register_class(cls)
Expand All @@ -33,15 +35,22 @@ def register():
if hasattr(module, "register"):
module.register()


def unregister():
for cls in reversed(ordered_classes):
bpy.utils.unregister_class(cls)
try:
bpy.utils.unregister_class(cls)
except Exception as e:
print(f"Fail to unregister {cls}, skiped")

for module in modules:
if module.__name__ == __name__:
continue
if hasattr(module, "unregister"):
module.unregister()
try:
if module.__name__ == __name__:
continue
if hasattr(module, "unregister"):
module.unregister()
except Exception as e:
print(f"Fail to unregister {module}, skiped")


# Import modules
Expand All @@ -50,10 +59,12 @@ def unregister():
def get_all_submodules(directory):
return list(iter_submodules(directory, directory.name))


def iter_submodules(path, package_name):
for name in sorted(iter_submodule_names(path)):
yield importlib.import_module("." + name, package_name)


def iter_submodule_names(path, root=""):
for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
if is_package:
Expand All @@ -70,26 +81,32 @@ def iter_submodule_names(path, root=""):
def get_ordered_classes_to_register(modules):
return toposort(get_register_deps_dict(modules))


def get_register_deps_dict(modules):
my_classes = set(iter_my_classes(modules))
my_classes_by_idname = {cls.bl_idname : cls for cls in my_classes if hasattr(cls, "bl_idname")}
my_classes_by_idname = {
cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}

deps_dict = {}
for cls in my_classes:
deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname))
deps_dict[cls] = set(iter_my_register_deps(
cls, my_classes, my_classes_by_idname))
return deps_dict


def iter_my_register_deps(cls, my_classes, my_classes_by_idname):
yield from iter_my_deps_from_annotations(cls, my_classes)
yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname)


def iter_my_deps_from_annotations(cls, my_classes):
for value in typing.get_type_hints(cls, {}, {}).values():
dependency = get_dependency_from_annotation(value)
if dependency is not None:
if dependency in my_classes:
yield dependency


def get_dependency_from_annotation(value):
if blender_version >= (2, 93):
if isinstance(value, bpy.props._PropertyDeferred):
Expand All @@ -100,6 +117,7 @@ def get_dependency_from_annotation(value):
return value[1]["type"]
return None


def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
if bpy.types.Panel in cls.__bases__:
parent_idname = getattr(cls, "bl_parent_id", None)
Expand All @@ -108,33 +126,37 @@ def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
if parent_cls is not None:
yield parent_cls


def iter_my_classes(modules):
base_types = get_register_base_types()
for cls in get_classes_in_modules(modules):
if any(base in base_types for base in cls.__bases__):
if not getattr(cls, "is_registered", False):
yield cls


def get_classes_in_modules(modules):
classes = set()
for module in modules:
for cls in iter_classes_in_module(module):
classes.add(cls)
return classes


def iter_classes_in_module(module):
for value in module.__dict__.values():
if inspect.isclass(value):
yield value


def get_register_base_types():
return set(getattr(bpy.types, name) for name in [
"Panel", "Operator", "PropertyGroup", "FileHandler",
"AddonPreferences", "Header", "Menu",
"Node", "NodeSocket", "NodeTree",
"UIList", "RenderEngine",
"Gizmo", "GizmoGroup",
])
] if hasattr(bpy.types, name))


# Find order to register to solve dependencies
Expand All @@ -151,5 +173,6 @@ def toposort(deps_dict):
sorted_values.add(value)
else:
unsorted.append(value)
deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted}
deps_dict = {value: deps_dict[value] -
sorted_values for value in unsorted}
return sorted_list
2 changes: 1 addition & 1 deletion bioxelnodes/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
class ConvertToMesh(bpy.types.Operator):
bl_idname = "bioxelnodes.convert_to_mesh"
bl_label = "Bioxels To Mesh"
bl_description = "Convert Bioxels To Mesh."
bl_description = "Convert Bioxels To Mesh"
bl_options = {'UNDO'}

@classmethod
Expand Down
2 changes: 1 addition & 1 deletion bioxelnodes/customnodes/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .menu import CustomNodes
from .menus import CustomNodes
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@
from pathlib import Path
from .nodes import AddCustomNode

bpy.types.Scene.custom_nodes_dir = bpy.props.StringProperty(
name="Nodes Directory",
subtype='DIR_PATH',
default="//"
)


class SaveAllNodes(bpy.types.Operator):
bl_idname = "customnodes.save_all_nodes"
Expand Down Expand Up @@ -182,12 +176,17 @@ def register(self):
bpy.utils.register_class(cls)

bpy.types.NODE_MT_add.append(self.add_node_menu)
bpy.types.Scene.custom_nodes_dir = bpy.props.StringProperty(
name="Nodes Directory",
subtype='DIR_PATH',
default="//"
)

def unregister(self):
bpy.types.NODE_MT_add.remove(self.add_node_menu)
try:
for cls in reversed(self.menu_classes):
bpy.utils.unregister_class(cls)

bpy.types.NODE_MT_add.remove(self.add_node_menu)
except RuntimeError:
pass
3 changes: 2 additions & 1 deletion bioxelnodes/externalpackage/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .preferences import ExternalPackagePreferences
from .preferences import ExternalPackagePreferences
from .package import PackageInstaller
Loading

0 comments on commit 8e8b176

Please sign in to comment.