feat: redesign nodes

This commit is contained in:
2024-05-29 11:00:38 +08:00
parent d380cf7036
commit fa031e63f8
16 changed files with 937 additions and 466 deletions

View File

@@ -17,7 +17,7 @@ Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetr
![gallery](docs/assets/gallery.png) ![gallery](docs/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) The "Bioxel" in "Bioxel Nodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data. We are developing a toolkit around Bioxel 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 # Supported Format
@@ -41,5 +41,18 @@ The "Bioxel" in "BioxelNodes", is a combination of the words "Bio-" and "Voxel".
# Known limitations # Known limitations
- Sections cannot be generated - Sections cannot be generated (will be supported soon)
- Time sequence volume not supported (will be supported soon) - Time sequence volume not supported (will be supported soon)
# To Upgrade Add-on
To upgrade from an older version of the add-on to the latest, you need to do the following:
1. Remove the old version of Bioxel Nodes at Preferences > Add-ons
2. Add the new version and restart Blender.
It is not support editing the same blender file across add-on versions. In order to make sure that the previous file works properly. You need to save the staged data before upgrading ( read the last section of [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started/#share-your-file) ).
But even then, there is still no guarantee that the new version of the add-on will work on the old blender file. Therefore, it is highly recommended to open a new blender file to start the creating, not based on the old one.
Alternatively, objects from the old file that have nothing to do with Bioxel Nodes could be append to the new blender file.

View File

@@ -9,7 +9,7 @@ bl_info = {
"author": "Ma Nan", "author": "Ma Nan",
"description": "", "description": "",
"blender": (4, 0, 0), "blender": (4, 0, 0),
"version": (0, 1, 1), "version": (0, 2, 0),
"location": "File -> Import", "location": "File -> Import",
"warning": "", "warning": "",
"category": "Node" "category": "Node"
@@ -21,8 +21,8 @@ auto_load.init()
def register(): def register():
auto_load.register() auto_load.register()
menus.add() menus.add()
bpy.types.Scene.bioxels_dir = bpy.props.StringProperty( bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty(
name="Bioxels Directory", name="Bioxel Layers Directory",
subtype='DIR_PATH', subtype='DIR_PATH',
default="//" default="//"
) )

View File

@@ -1,57 +0,0 @@
import bpy
from .utils import get_bioxels_obj, get_node_by_type
class ConvertToMesh(bpy.types.Operator):
bl_idname = "bioxelnodes.convert_to_mesh"
bl_label = "Bioxels To Mesh"
bl_description = "Convert Bioxels To Mesh"
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
bioxels_obj = None
for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj)
break
return True if bioxels_obj else False
def execute(self, context):
bioxels_objs = []
for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj)
if bioxels_obj:
bioxels_objs.append(bioxels_obj)
if len(bioxels_objs) == 0:
self.report({"WARNING"}, "Cannot find any bioxels.")
return {'FINISHED'}
for bioxels_obj in bioxels_objs:
bpy.ops.mesh.primitive_cube_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
mesh_obj = bpy.context.active_object
mesh_obj.name = f"{bioxels_obj.parent.name}_Mesh"
bpy.ops.node.new_geometry_nodes_modifier()
modifier = mesh_obj.modifiers[0]
nodes = modifier.node_group.nodes
links = modifier.node_group.links
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
object_node = nodes.new("GeometryNodeObjectInfo")
realize_nodes = nodes.new("GeometryNodeRealizeInstances")
object_node.inputs[0].default_value = bioxels_obj
object_node.transform_space = 'RELATIVE'
links.new(object_node.outputs['Geometry'], realize_nodes.inputs[0])
links.new(realize_nodes.outputs[0], output_node.inputs[0])
bpy.ops.object.convert(target='MESH')
bpy.context.object.active_material_index = 1
bpy.ops.object.material_slot_remove()
self.report({"INFO"}, f"Successfully convert to mesh")
return {'FINISHED'}

View File

@@ -146,10 +146,13 @@ class CustomNodes():
def _find_item(self, found_items, menu_items, node_type: str): def _find_item(self, found_items, menu_items, node_type: str):
for item in menu_items: for item in menu_items:
if item == 'separator':
continue
if item.get("node_type") == node_type: if item.get("node_type") == node_type:
found_items.append(item) found_items.append(item)
item_items = item.get('items') if item != 'separator' else None item_items = item.get('items')
if item_items: if item_items:
self._find_item(found_items, item_items, node_type) self._find_item(found_items, item_items, node_type)
@@ -169,7 +172,7 @@ class CustomNodes():
op.node_callback = item.get('node_callback') or "" op.node_callback = item.get('node_callback') or ""
return op.add_node(node_tree) return op.add_node(node_tree)
else: else:
raise RuntimeError("No custom node type found.") raise RuntimeError("Not found in menu.")
def register(self): def register(self):
for cls in self.menu_classes: for cls in self.menu_classes:

View File

@@ -6,16 +6,18 @@ import numpy as np
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
import mathutils import mathutils
import math
from .utils import calc_bbox_verts, get_text_index_str, get_bioxels_obj, get_node_by_type, show_message
from .nodes import custom_nodes from .nodes import custom_nodes
from .props import BIOXELNODES_Series from .props import BIOXELNODES_Series
from .utils import (calc_bbox_verts, get_container, get_layer, get_text_index_str,
get_node_by_type, hide_in_ray, lock_transform, show_message)
try: try:
import SimpleITK as sitk import SimpleITK as sitk
except: except:
... ...
SUPPORT_EXTS = ['.dcm', '.DICOM', SUPPORT_EXTS = ['.dcm', '.DCM', '.DICOM',
'.bmp', '.BMP', '.bmp', '.BMP',
'.PIC', '.pic', '.PIC', '.pic',
'.gipl', '.gipl.gz', '.gipl', '.gipl.gz',
@@ -31,15 +33,15 @@ SUPPORT_EXTS = ['.dcm', '.DICOM',
'.png', '.PNG', '.png', '.PNG',
'.vtk'] '.vtk']
SEQUENCE_EXTS = ['.dcm', '.DICOM', SEQUENCE_EXTS = ['.dcm', '.DCM', '.DICOM',
'.bmp', '.BMP', '.bmp', '.BMP',
'.jpg', '.JPG', '.jpeg', '.JPEG', '.jpg', '.JPG', '.jpeg', '.JPEG',
'.tif', '.TIF', '.tiff', '.TIFF', '.tif', '.TIF', '.tiff', '.TIFF',
'.png', '.PNG'] '.png', '.PNG']
DICOM_EXTS = ['.dcm', '.DICOM'] DICOM_EXTS = ['.dcm', '.DCM', '.DICOM']
FH_EXTS = ['.dcm', '.DICOM', FH_EXTS = ['.dcm', '.DCM', '.DICOM',
'.gipl', '.gipl.gz', '.gipl', '.gipl.gz',
'.mnc', '.MNC', '.mnc', '.MNC',
'.mrc', '.rec', '.mrc', '.rec',
@@ -51,7 +53,7 @@ FH_EXTS = ['.dcm', '.DICOM',
'.gz'] '.gz']
def get_bioxels_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple): def get_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple):
shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]), shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]),
int(orig_shape[1] / bioxel_size * orig_spacing[1]), int(orig_shape[1] / bioxel_size * orig_spacing[1]),
int(orig_shape[2] / bioxel_size * orig_spacing[2])) int(orig_shape[2] / bioxel_size * orig_spacing[2]))
@@ -132,27 +134,31 @@ def read_image(filepath: str, series_id=""):
def rgb2gray(image): def rgb2gray(image):
# Convert sRGB image to gray scale and rescale results to [0,255] # Convert sRGB image to gray scale and rescale results to [0,255]
channels = [sitk.VectorIndexSelectionCast( layers = [sitk.VectorIndexSelectionCast(
image, i, sitk.sitkFloat32) for i in range(image.GetNumberOfComponentsPerPixel())] image, i, sitk.sitkFloat32) for i in range(image.GetNumberOfComponentsPerPixel())]
# linear mapping # linear mapping
I = (0.2126*channels[0] + 0.7152*channels[1] + 0.0722*channels[2]) I = (0.2126*layers[0] + 0.7152*layers[1] + 0.0722*layers[2])
return sitk.Cast(I, sitk.sitkFloat32) return sitk.Cast(I, sitk.sitkFloat32)
def x2gray(image): def x2gray(image):
return sitk.VectorIndexSelectionCast(image, 0, sitk.sitkUInt16) return sitk.VectorIndexSelectionCast(image, 0, sitk.sitkUInt16)
class ImportVolumeDataDialog(bpy.types.Operator): class ImportVolumeDataDialog(bpy.types.Operator):
bl_idname = "bioxelnodes.import_volume_data_dialog" bl_idname = "bioxelnodes.import_volume_data_dialog"
bl_label = "Volume Data as Bioxels" bl_label = "Volume Data as Bioxel Layer"
bl_description = "Import Volume Data as Bioxels (VDB)" bl_description = "Import Volume Data as Bioxel Layer"
bl_options = {'UNDO'} bl_options = {'UNDO'}
filepath: bpy.props.StringProperty( filepath: bpy.props.StringProperty(
subtype="FILE_PATH", subtype="FILE_PATH"
options={'HIDDEN'}
) # type: ignore ) # type: ignore
container_name: bpy.props.StringProperty() # type: ignore
layer_name: bpy.props.StringProperty() # type: ignore
series_id: bpy.props.StringProperty() # type: ignore series_id: bpy.props.StringProperty() # type: ignore
resample_method: bpy.props.EnumProperty( resample_method: bpy.props.EnumProperty(
@@ -163,6 +169,8 @@ class ImportVolumeDataDialog(bpy.types.Operator):
("gaussian", "Gaussian", "")] ("gaussian", "Gaussian", "")]
) # type: ignore ) # type: ignore
container: bpy.props.StringProperty() # type: ignore
read_as: bpy.props.EnumProperty( read_as: bpy.props.EnumProperty(
name="Read as", name="Read as",
default="scalar", default="scalar",
@@ -170,15 +178,10 @@ class ImportVolumeDataDialog(bpy.types.Operator):
("labels", "Labels", "")] ("labels", "Labels", "")]
) # type: ignore ) # type: ignore
label_index: bpy.props.IntProperty(
name="Label Index",
default=1
) # type: ignore
bioxel_size: bpy.props.FloatProperty( bioxel_size: bpy.props.FloatProperty(
name="Bioxel Size (Larger size means small resolution)", name="Bioxel Size (Larger size means small resolution)",
soft_min=0.1, soft_max=10.0, soft_min=0.1, soft_max=10.0,
min=1e-2, max=1e2, min=0.1, max=1e2,
default=1, default=1,
) # type: ignore ) # type: ignore
@@ -193,7 +196,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
) # type: ignore ) # type: ignore
scene_scale: bpy.props.FloatProperty( scene_scale: bpy.props.FloatProperty(
name="Scene Scale (Bioxels Unit pre Blender Unit)", name="Scene Scale (Bioxel Unit pre Blender Unit)",
soft_min=0.001, soft_max=100.0, soft_min=0.001, soft_max=100.0,
min=1e-6, max=1e6, min=1e-6, max=1e6,
default=0.01, default=0.01,
@@ -210,20 +213,20 @@ class ImportVolumeDataDialog(bpy.types.Operator):
) # type: ignore ) # type: ignore
def execute(self, context): def execute(self, context):
image, bioxels_name = read_image(self.filepath, self.series_id) image, name = read_image(self.filepath, self.series_id)
container_name = self.container_name or name
bioxel_size = self.bioxel_size bioxel_size = self.bioxel_size
orig_spacing = self.orig_spacing orig_spacing = self.orig_spacing
image_spacing = image.GetSpacing() image_spacing = image.GetSpacing()
image_shape = image.GetSize() image_shape = image.GetSize()
bioxels_spacing = ( layer_spacing = (
image_spacing[0] / orig_spacing[0] * bioxel_size, image_spacing[0] / orig_spacing[0] * bioxel_size,
image_spacing[1] / orig_spacing[1] * bioxel_size, image_spacing[1] / orig_spacing[1] * bioxel_size,
image_spacing[2] / orig_spacing[2] * bioxel_size image_spacing[2] / orig_spacing[2] * bioxel_size
) )
bioxels_shape = get_bioxels_shape( layer_shape = get_layer_shape(
bioxel_size, image_shape, orig_spacing) bioxel_size, image_shape, orig_spacing)
if self.read_as == "labels": if self.read_as == "labels":
@@ -254,11 +257,11 @@ class ImportVolumeDataDialog(bpy.types.Operator):
print(f"Resampling...") print(f"Resampling...")
image = sitk.Resample( image = sitk.Resample(
image1=image, image1=image,
size=bioxels_shape, size=layer_shape,
transform=sitk.Transform(), transform=sitk.Transform(),
interpolator=interpolator, interpolator=interpolator,
outputOrigin=image.GetOrigin(), outputOrigin=image.GetOrigin(),
outputSpacing=bioxels_spacing, outputSpacing=layer_spacing,
outputDirection=image.GetDirection(), outputDirection=image.GetDirection(),
defaultPixelValue=default_value, defaultPixelValue=default_value,
outputPixelType=image.GetPixelID(), outputPixelType=image.GetPixelID(),
@@ -281,63 +284,15 @@ class ImportVolumeDataDialog(bpy.types.Operator):
# A (nterior) j -> j -> y # A (nterior) j -> j -> y
# S (uperior) k -> i -> z # S (uperior) k -> i -> z
array = sitk.GetArrayFromImage(image) array = sitk.GetArrayFromImage(image)
orig_dtype = str(array.dtype) orig_dtype = str(array.dtype)
# print(f"Coverting Dtype from {orig_dtype} to float...") # print(f"Coverting Dtype from {orig_dtype} to float...")
# array = array.astype(float) # array = array.astype(float)
if array.ndim == 4: if array.ndim == 4:
# if array.shape[0] == 3 and self.read_as == "scalar":
# # RGB -> Grayscale
# array = np.dot(array[..., :3], [0.2989, 0.5870, 0.1140])
# else:
# array = array[:, :, :, 0]
array = array[:, :, :, 0] array = array[:, :, :, 0]
array = np.transpose(array) array = np.transpose(array)
bioxels_shape = array.shape
bioxels_offset = 0.0
if self.read_as == "labels":
array = array == np.full_like(array, self.label_index)
bioxels_default_threshold = 1.0
bioxels_max = 1.0
bioxels_min = 0.0
bioxels_name = f"{bioxels_name}_{self.label_index}"
else:
if self.invert_scalar:
array = -array
orig_max = float(np.max(array))
orig_min = float(np.min(array))
orig_median = float(np.median(array))
orig_percentile80 = float(np.percentile(array, 80)) \
if self.invert_scalar else float(np.percentile(array, 80))
# if (orig_dtype[0] != "u" and orig_min < 0) \
# or (orig_dtype[0] == "u" and self.invert_scalar):
if orig_min < 0:
bioxels_offset = -orig_min
array = array + np.full_like(array, bioxels_offset)
bioxels_default_threshold = orig_percentile80
bioxels_max = float(np.max(array))
bioxels_min = float(np.min(array))
stats_table = [("Max", orig_max),
("Min", orig_min),
("Median", orig_median),
("80%", orig_percentile80)]
print("Bioxels Value Stats:")
for stats in stats_table:
print("| {: >10} | {: >40} |".format(*stats))
# # Build VDB
grid = vdb.BoolGrid() if self.read_as == "labels" else vdb.FloatGrid()
grid.copyFromArray(array.copy().astype(float))
# After sitk.DICOMOrient(), origin and direction will also orient base on LPS # After sitk.DICOMOrient(), origin and direction will also orient base on LPS
# so we need to convert them into RAS # so we need to convert them into RAS
mat_lps2ras = axis_conversion( mat_lps2ras = axis_conversion(
@@ -365,139 +320,237 @@ class ImportVolumeDataDialog(bpy.types.Operator):
transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \ transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \
if self.do_orient else mat_location @ mat_rotation @ mat_scale if self.do_orient else mat_location @ mat_rotation @ mat_scale
grid.transform = vdb.createLinearTransform(transfrom.transposed()) # Wrapper a Container
grid.name = "scalar" if not self.container:
# Make transformation
scene_scale = float(self.scene_scale)
# (S)uperior -Z -> Y
# (A)osterior Y -> Z
mat_ras2blender = axis_conversion(
from_forward='-Z',
from_up='Y',
to_forward='Y',
to_up='Z'
).to_4x4()
mat_scene_scale = mathutils.Matrix.Scale(
scene_scale, 4
)
bpy.ops.mesh.primitive_cube_add(
enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
)
container = bpy.context.active_object
bbox_verts = calc_bbox_verts((0, 0, 0), array.shape)
for index, vert in enumerate(container.data.vertices):
bbox_transform = transfrom
vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index])
container.matrix_world = mat_ras2blender @ mat_scene_scale
container.name = container_name
container.data.name = container_name
container['bioxel_container'] = True
container['scene_scale'] = scene_scale
bpy.ops.node.new_geometry_nodes_modifier()
container_node_tree = container.modifiers[0].node_group
input_node = get_node_by_type(container_node_tree.nodes,
'NodeGroupInput')[0]
container_node_tree.links.remove(input_node.outputs[0].links[0])
# bpy.ops.mesh.primitive_cube_add(
# enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
# )
# frame = bpy.context.active_object
# bbox_verts = calc_bbox_verts((0, 0, 0), array.shape)
# for index, vert in enumerate(frame.data.vertices):
# # bbox_transform = mat_ras2blender @ mat_scene_scale @ transfrom
# bbox_transform = transfrom
# vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index])
# frame.name = f"Frame_{container_name}"
# frame.data.name = f"Frame_{container_name}"
# lock_transform(frame)
# hide_in_ray(frame)
# frame.hide_select = True
# frame.hide_render = True
# frame.display_type = 'WIRE'
# frame.parent = container
else:
container = bpy.data.objects[self.container]
container_node_tree = container.modifiers[0].node_group
preferences = context.preferences.addons[__package__].preferences preferences = context.preferences.addons[__package__].preferences
vdb_dirpath = Path(preferences.cache_dir, 'VDBs') cache_dir = Path(preferences.cache_dir, 'VDBs')
vdb_dirpath.mkdir(parents=True, exist_ok=True) cache_dir.mkdir(parents=True, exist_ok=True)
vdb_path = Path(vdb_dirpath, f"{uuid4()}.vdb")
print(f"Storing the VDB file ({str(vdb_path)})...") loc, rot, sca = transfrom.decompose()
vdb.write(str(vdb_path), grids=[grid])
# Read VDB layer_origin = tuple(loc)
print(f"Importing the VDB file to Blender scene...") layer_rotation = tuple(rot.to_euler())
bpy.ops.object.volume_import( layer_shape = tuple(array.shape)
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
bioxels_obj = bpy.context.active_object def create_layer(array, layer_name, layer_type="scalar"):
grid = vdb.FloatGrid()
array = array.copy().astype(float)
grid.copyFromArray(array)
grid.transform = vdb.createLinearTransform(transfrom.transposed())
grid.name = layer_type
# Set props to VDB object vdb_path = Path(cache_dir, f"{uuid4()}.vdb")
bioxels_obj.name = f"{bioxels_name}_Bioxels" print(f"Storing the cache ({str(vdb_path)})...")
bioxels_obj.data.name = f"{bioxels_name}_Bioxels" vdb.write(str(vdb_path), grids=[grid])
# Make transformation # Read VDB
scene_scale = float(self.scene_scale) print(f"Loading the cache to Blender scene...")
bpy.ops.object.volume_import(
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
# (S)uperior -Z -> Y layer = bpy.context.active_object
# (A)osterior Y -> Z
mat_ras2blender = axis_conversion(
from_forward='-Z',
from_up='Y',
to_forward='Y',
to_up='Z'
).to_4x4()
mat_scene_scale = mathutils.Matrix.Scale( # Set props to VDB object
scene_scale, 4 layer.name = layer_name
) layer.data.name = layer_name
bioxels_obj.matrix_world = mat_ras2blender @ mat_scene_scale lock_transform(layer)
bioxels_obj.lock_location[0] = True hide_in_ray(layer)
bioxels_obj.lock_location[1] = True layer.hide_select = True
bioxels_obj.lock_location[2] = True layer.hide_render = True
bioxels_obj.lock_rotation[0] = True layer.hide_viewport = True
bioxels_obj.lock_rotation[1] = True layer.data.display.use_slice = True
bioxels_obj.lock_rotation[2] = True layer.data.display.density = 1e-05
bioxels_obj.lock_scale[0] = True
bioxels_obj.lock_scale[1] = True
bioxels_obj.lock_scale[2] = True
bioxels_obj.hide_select = True
bioxels_obj['bioxels'] = True
bpy.ops.node.new_geometry_nodes_modifier() layer['bioxel_layer'] = True
node_tree = bioxels_obj.modifiers[0].node_group layer['bioxel_layer_type'] = layer_type
layer.parent = container
# Wrapper a Container for collection in layer.users_collection:
bpy.ops.mesh.primitive_cube_add( collection.objects.unlink(layer)
enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
)
container_obj = bpy.context.active_object
bbox_verts = calc_bbox_verts((0, 0, 0), bioxels_shape)
for index, vert in enumerate(container_obj.data.vertices):
bbox_transform = mat_ras2blender @ mat_scene_scale @ transfrom
vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index])
bioxels_obj.parent = container_obj for collection in container.users_collection:
container_obj.name = bioxels_name collection.objects.link(layer)
container_obj.data.name = bioxels_name
container_obj.visible_camera = False
container_obj.visible_diffuse = False
container_obj.visible_glossy = False
container_obj.visible_transmission = False
container_obj.visible_volume_scatter = False
container_obj.visible_shadow = False
container_obj.display_type = 'WIRE'
container_obj['bioxels_container'] = True
container_obj['scene_scale'] = scene_scale
container_obj['bioxel_size'] = bioxel_size
container_obj['bioxels_max'] = bioxels_max
container_obj['bioxels_min'] = bioxels_min
container_obj['bioxels_offset'] = bioxels_offset
container_obj['bioxels_shape'] = bioxels_shape
bpy.ops.object.modifier_add(type='NODES') print(f"Creating layer ...")
container_obj.modifiers[0].node_group = node_tree
container_obj.modifiers[0].show_in_editmode = False
container_obj.modifiers[0].show_viewport = False
container_obj.modifiers[0].show_render = False
# Create BioxelNodes to VDB object bpy.ops.node.new_geometry_nodes_modifier()
print(f"Creating BioxelNodes to the VDB...") node_tree = layer.modifiers[0].node_group
if preferences.do_add_segmentnode:
nodes = node_tree.nodes nodes = node_tree.nodes
links = node_tree.links links = node_tree.links
input_node = get_node_by_type(nodes, 'NodeGroupInput')[0] input_node = get_node_by_type(nodes, 'NodeGroupInput')[0]
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0] output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
segment_node = custom_nodes.add_node( node_type = 'BioxelNodes_AsLabel' if layer_type == "label" else 'BioxelNodes_AsScalar'
nodes, 'BioxelNodes_Segment') source_node = custom_nodes.add_node(nodes, node_type)
segment_node.inputs['Threshold'].default_value = bioxels_default_threshold links.new(input_node.outputs[0], source_node.inputs[0])
links.new(source_node.outputs[0], output_node.inputs[0])
links.new(input_node.outputs[0], segment_node.inputs[0]) source_node.inputs['Bioxel Size'].default_value = bioxel_size
links.new(segment_node.outputs[0], output_node.inputs[0]) source_node.inputs['Shape'].default_value = layer_shape
source_node.inputs['Origin'].default_value = layer_origin
source_node.inputs['Rotation'].default_value = layer_rotation
return layer
if self.read_as == "labels":
orig_max = int(np.max(array))
orig_min = int(np.min(array))
layer_name = self.layer_name or "Label"
for i in range(orig_max):
label = array == np.full_like(array, i+1)
layer = create_layer(array=label,
layer_name=f"{container_name}_{layer_name}_{i+1}",
layer_type="label")
output_node = get_node_by_type(container_node_tree.nodes,
'NodeGroupOutput')[0]
mask_node = custom_nodes.add_node(container_node_tree.nodes,
'BioxelNodes_MaskByLabel')
mask_node.inputs[0].default_value = layer
if len(output_node.inputs[0].links) == 0:
container_node_tree.links.new(mask_node.outputs[0],
output_node.inputs[0])
else:
if self.invert_scalar:
array = -array
orig_max = float(np.max(array))
orig_min = float(np.min(array))
orig_median = float(np.median(array))
orig_percentile80 = float(np.percentile(array, 80)) \
if self.invert_scalar else float(np.percentile(array, 80))
stats_table = [("Max", orig_max),
("Min", orig_min),
("Median", orig_median),
("80%", orig_percentile80)]
print("Volume Data Stats:")
for stats in stats_table:
print("| {: >10} | {: >40} |".format(*stats))
scalar_offset = 0
if orig_min < 0:
scalar_offset = -orig_min
array = array + np.full_like(array, scalar_offset)
layer_name = self.layer_name or "Scalar"
layer = create_layer(array=array,
layer_name=f"{container_name}_{layer_name}",
layer_type="scalar")
layer_node_tree = layer.modifiers[0].node_group
source_node = layer_node_tree.nodes['BioxelNodes_AsScalar']
source_node.inputs['Offset'].default_value = scalar_offset
source_node.inputs['Max'].default_value = orig_max
source_node.inputs['Min'].default_value = orig_min
output_node = get_node_by_type(container_node_tree.nodes,
'NodeGroupOutput')[0]
mask_node = custom_nodes.add_node(container_node_tree.nodes,
'BioxelNodes_MaskByThreshold')
mask_node.inputs[0].default_value = layer
if len(output_node.inputs[0].links) == 0:
container_node_tree.links.new(mask_node.outputs[0],
output_node.inputs[0])
bpy.context.view_layer.objects.active = container
# Change render setting for better result # Change render setting for better result
if preferences.do_change_render_setting: if preferences.do_change_render_setting:
bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.volume_bounces = 12 bpy.context.scene.cycles.volume_bounces = 12
bpy.context.scene.cycles.transparent_max_bounces = 64 bpy.context.scene.cycles.transparent_max_bounces = 16
bpy.context.scene.cycles.volume_preview_step_rate = 10
self.report({"INFO"}, "Successfully Imported") self.report({"INFO"}, "Successfully Imported")
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): def invoke(self, context, event):
min_size = min(
self.orig_spacing[0], self.orig_spacing[1], self.orig_spacing[2])
default_size = 1.0
self.bioxel_size = min_size if min_size > default_size else default_size
context.window_manager.invoke_props_dialog(self, width=400) context.window_manager.invoke_props_dialog(self, width=400)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def draw(self, context): def draw(self, context):
layout = self.layout layer_shape = get_layer_shape(
bioxels_shape = get_bioxels_shape(
self.bioxel_size, self.orig_shape, self.orig_spacing) self.bioxel_size, self.orig_shape, self.orig_spacing)
bioxel_count = bioxels_shape[0] * bioxels_shape[1] * bioxels_shape[2] bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2]
text = f"Shape will be: {str(bioxels_shape)} {bioxel_count:,} " text = f"Shape will be: {str(layer_shape)} {bioxel_count:,} "
if bioxel_count > 100000000: if bioxel_count > 100000000:
text += "**TOO LARGE!**" text += "**TOO LARGE!**"
layout = self.layout
panel = layout.box()
panel.prop(self, "container_name")
panel.prop(self, "layer_name")
panel = layout.box() panel = layout.box()
panel.prop(self, "resample_method") panel.prop(self, "resample_method")
panel.prop(self, "bioxel_size") panel.prop(self, "bioxel_size")
@@ -505,16 +558,18 @@ class ImportVolumeDataDialog(bpy.types.Operator):
row.prop(self, "orig_spacing") row.prop(self, "orig_spacing")
panel.label(text=text) panel.label(text=text)
panel = layout.box()
panel.prop(self, "do_orient")
panel = layout.box() panel = layout.box()
panel.prop(self, "read_as") panel.prop(self, "read_as")
if self.read_as == "labels": if self.read_as == "labels":
panel.prop(self, "label_index") ...
else: else:
panel.prop(self, "invert_scalar") panel.prop(self, "invert_scalar")
panel = layout.box() panel = layout.box()
panel.prop(self, "scene_scale") panel.prop(self, "scene_scale")
panel.prop(self, "do_orient")
def get_series_ids(self, context): def get_series_ids(self, context):
@@ -530,14 +585,15 @@ def get_series_ids(self, context):
return items return items
class ReadDICOM(bpy.types.Operator): class ParseVolumeData(bpy.types.Operator):
bl_idname = "bioxelnodes.read_dicom" bl_idname = "bioxelnodes.parse_volume_data"
bl_label = "Volume Data as Bioxels" bl_label = "Volume Data as Bioxel Layer"
bl_description = "Import Volume Data as Bioxels (VDB)" bl_description = "Import Volume Data as Bioxel Layer"
bl_options = {'UNDO'} bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
container: bpy.props.StringProperty() # type: ignore
series_id: bpy.props.EnumProperty( series_id: bpy.props.EnumProperty(
name="Select Series", name="Select Series",
@@ -555,25 +611,41 @@ class ReadDICOM(bpy.types.Operator):
print("Collecting Meta Data...") print("Collecting Meta Data...")
image, name = read_image(self.filepath, self.series_id) image, name = read_image(self.filepath, self.series_id)
stats_table = [("Shape", str(image.GetSize())), stats_table = [("Shape", str(image.GetSize())),
("Spacing", str(image.GetSpacing())), ("Spacing", str(image.GetSpacing())),
("Origin", str(image.GetOrigin())), ("Origin", str(image.GetOrigin())),
("Direction", str(image.GetDirection())), ("Direction", str(image.GetDirection())),
("Data Type", image.GetPixelIDTypeAsString())] ("Data Type", image.GetPixelIDTypeAsString())]
for k in image.GetMetaDataKeys():
stats_table.append((k, str(image.GetMetaData(k))))
print("Meta Data:") print("Meta Data:")
for stats in stats_table: for stats in stats_table:
print("| {: >10} | {: >40} |".format(*stats)) print("| {: >20} | {: >100} |".format(*stats))
do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS
orig_shape = image.GetSize()
orig_spacing = image.GetSpacing()
min_size = min(orig_spacing[0], orig_spacing[1], orig_spacing[2])
bioxel_size = max(min_size, 1.0)
if self.container:
container = bpy.data.objects[self.container]
container_name = container.name
else:
container_name = name
bpy.ops.bioxelnodes.import_volume_data_dialog( bpy.ops.bioxelnodes.import_volume_data_dialog(
'INVOKE_DEFAULT', 'INVOKE_DEFAULT',
filepath=self.filepath, filepath=self.filepath,
orig_shape=image.GetSize(), container_name=container_name,
orig_spacing=image.GetSpacing(), orig_shape=orig_shape,
orig_spacing=orig_spacing,
bioxel_size=bioxel_size,
series_id=self.series_id or "", series_id=self.series_id or "",
do_orient=do_orient do_orient=do_orient,
container=self.container
) )
self.report({"INFO"}, "Successfully Readed.") self.report({"INFO"}, "Successfully Readed.")
@@ -623,15 +695,15 @@ class ReadDICOM(bpy.types.Operator):
class ImportVolumeData(bpy.types.Operator): class ImportVolumeData(bpy.types.Operator):
bl_idname = "bioxelnodes.import_volume_data" bl_idname = "bioxelnodes.import_volume_data"
bl_label = "Volume Data as Bioxels" bl_label = "Volume Data as Bioxel Layer"
bl_description = "Import Volume Data as Bioxels (VDB)" bl_description = "Import Volume Data as Bioxel Layer"
bl_options = {'UNDO'} bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
def execute(self, context): def execute(self, context):
bpy.ops.bioxelnodes.read_dicom( bpy.ops.bioxelnodes.parse_volume_data(
'INVOKE_DEFAULT', 'INVOKE_DEFAULT',
filepath=self.filepath, filepath=self.filepath,
directory=self.directory directory=self.directory
@@ -647,7 +719,7 @@ try:
class BIOXELNODES_FH_ImportVolumeData(bpy.types.FileHandler): class BIOXELNODES_FH_ImportVolumeData(bpy.types.FileHandler):
bl_idname = "BIOXELNODES_FH_ImportVolumeData" bl_idname = "BIOXELNODES_FH_ImportVolumeData"
bl_label = "File handler for dicom import" bl_label = "File handler for dicom import"
bl_import_operator = "bioxelnodes.read_dicom" bl_import_operator = "bioxelnodes.parse_volume_data"
bl_file_extensions = ";".join(FH_EXTS) bl_file_extensions = ";".join(FH_EXTS)
@classmethod @classmethod
@@ -657,10 +729,40 @@ except:
... ...
class AddVolumeData(bpy.types.Operator):
bl_idname = "bioxelnodes.add_volume_data"
bl_label = "Add Volume Data to Container"
bl_description = "Add Volume Data to Container"
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
@classmethod
def poll(cls, context):
container = get_container(bpy.context.active_object)
return True if container else False
def execute(self, context):
container = get_container(bpy.context.active_object)
bpy.ops.bioxelnodes.parse_volume_data(
'INVOKE_DEFAULT',
filepath=self.filepath,
directory=self.directory,
container=container.name
)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class ExportVDB(bpy.types.Operator): class ExportVDB(bpy.types.Operator):
bl_idname = "bioxelnodes.export_vdb" bl_idname = "bioxelnodes.export_vdb"
bl_label = "Bioxels as VDB" bl_label = "Bioxel Layer as VDB"
bl_description = "Export Bioxels original VDB data" bl_description = "Export Bioxel Layer as VDB"
bl_options = {'UNDO'} bl_options = {'UNDO'}
filepath: bpy.props.StringProperty( filepath: bpy.props.StringProperty(
@@ -671,26 +773,19 @@ class ExportVDB(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
bioxels_obj = None layer = get_layer(bpy.context.active_object)
for obj in bpy.context.selected_objects: return True if layer else False
bioxels_obj = get_bioxels_obj(obj)
break
return True if bioxels_obj else False
def invoke(self, context, event): def invoke(self, context, event):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def execute(self, context): def execute(self, context):
bioxels_obj = None layer = get_layer(bpy.context.active_object)
for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj)
break
filepath = f"{self.filepath.split('.')[0]}.vdb" filepath = f"{self.filepath.split('.')[0]}.vdb"
# "//" # "//"
source_dir = bpy.path.abspath(bioxels_obj.data.filepath) source_dir = bpy.path.abspath(layer.data.filepath)
output_path: Path = Path(filepath).resolve() output_path: Path = Path(filepath).resolve()
source_path: Path = Path(source_dir).resolve() source_path: Path = Path(source_dir).resolve()

View File

@@ -1,18 +1,32 @@
import bpy import bpy
from .convert import ConvertToMesh from .operators import ConvertToMesh, JoinLayers
from .io import ExportVDB, ImportVolumeData from .io import ExportVDB, ImportVolumeData, AddVolumeData
from .misc import SaveBioxels from .misc import SaveBioxelLayers
class ObjectMenu(bpy.types.Menu): class View3DBioxelsMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_OBJECT" bl_idname = "BIOXELNODES_MT_VIEW3D"
bl_label = "Bioxels" bl_label = "Bioxel Nodes"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator(ConvertToMesh.bl_idname) layout.operator(ConvertToMesh.bl_idname)
layout.operator(AddVolumeData.bl_idname)
layout.separator() layout.separator()
layout.operator(SaveBioxels.bl_idname) layout.operator(SaveBioxelLayers.bl_idname)
class OutlinerBioxelsMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_OUTLINER"
bl_label = "Bioxel Nodes"
def draw(self, context):
layout = self.layout
layout.operator(JoinLayers.bl_idname)
layout.operator(ConvertToMesh.bl_idname)
layout.operator(AddVolumeData.bl_idname)
layout.separator()
layout.operator(SaveBioxelLayers.bl_idname)
def TOPBAR_FILE_IMPORT(self, context): def TOPBAR_FILE_IMPORT(self, context):
@@ -29,17 +43,25 @@ def TOPBAR_FILE_EXPORT(self, context):
def VIEW3D_OBJECT(self, context): def VIEW3D_OBJECT(self, context):
layout = self.layout layout = self.layout
layout.menu(ObjectMenu.bl_idname) layout.menu(View3DBioxelsMenu.bl_idname)
layout.separator()
def OUTLINER_OBJECT(self, context):
layout = self.layout
layout.menu(OutlinerBioxelsMenu.bl_idname)
layout.separator() layout.separator()
def add(): def add():
bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT) bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT) bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
bpy.types.OUTLINER_MT_object.prepend(OUTLINER_OBJECT)
bpy.types.VIEW3D_MT_object_context_menu.prepend(VIEW3D_OBJECT) bpy.types.VIEW3D_MT_object_context_menu.prepend(VIEW3D_OBJECT)
def remove(): def remove():
bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT) bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT)
bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT) bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT)
bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT)
bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT) bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT)

View File

@@ -1,97 +1,86 @@
import bpy import bpy
from pathlib import Path from pathlib import Path
import shutil import shutil
from bioxelnodes.utils import get_all_bioxels_objs, get_bioxels_obj from bioxelnodes.utils import get_all_layers, get_container, get_container_layers
class SaveBioxels(bpy.types.Operator): def save_layer(layer, output_dir):
bl_idname = "bioxelnodes.save_bioxels" name = layer.name
bl_label = "Save Bioxels"
bl_description = "Save Bioxels to Directory." # "//"
output_dir = bpy.path.abspath(output_dir)
source_dir = bpy.path.abspath(layer.data.filepath)
output_path: Path = Path(output_dir, f"{name}.vdb").resolve()
source_path: Path = Path(source_dir).resolve()
if output_path != source_path:
shutil.copy(source_path, output_path)
blend_path = Path(bpy.path.abspath("//")).resolve()
layer.data.filepath = bpy.path.relpath(
str(output_path), start=str(blend_path))
class SaveBioxelLayers(bpy.types.Operator):
bl_idname = "bioxelnodes.save_bioxel_layers"
bl_label = "Save Bioxel Layers"
bl_description = "Save Bioxel Layers to Directory."
bl_options = {'UNDO'} bl_options = {'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
bioxels_obj = None container = get_container(bpy.context.active_object)
for obj in bpy.context.selected_objects: return True if container else False
bioxels_obj = get_bioxels_obj(obj)
break
return True if bioxels_obj else False
def execute(self, context): def execute(self, context):
bioxels_objs = [] container = get_container(bpy.context.active_object)
for obj in bpy.context.selected_objects: if not container:
bioxels_obj = get_bioxels_obj(obj) self.report({"WARNING"}, "Cannot find any bioxel container.")
if bioxels_obj:
bioxels_objs.append(bioxels_obj)
if len(bioxels_objs) == 0:
self.report({"WARNING"}, "Cannot find any bioxels.")
return {'FINISHED'} return {'FINISHED'}
for bioxels_obj in bioxels_objs: for layer in get_container_layers(container):
name = bioxels_obj.parent.name try:
save_layer(layer, context.scene.bioxel_layer_dir)
except:
self.report(
{"WARNING"}, f"Fail to save {layer.name}, skiped")
# "//" self.report({"INFO"}, f"Successfully saved bioxel layers.")
bioxels_dir = bpy.path.abspath(context.scene.bioxels_dir)
source_dir = bpy.path.abspath(bioxels_obj.data.filepath)
output_path: Path = Path(bioxels_dir, f"{name}.vdb").resolve()
source_path: Path = Path(source_dir).resolve()
if output_path != source_path:
shutil.copy(source_path, output_path)
blend_path = Path(bpy.path.abspath("//")).resolve()
bioxels_obj.data.filepath = bpy.path.relpath(
str(output_path), start=str(blend_path))
self.report({"INFO"}, f"Successfully saved bioxels")
return {'FINISHED'} return {'FINISHED'}
class SaveAllBioxels(bpy.types.Operator): class SaveAllBioxelLayers(bpy.types.Operator):
bl_idname = "bioxelnodes.save_all_bioxels" bl_idname = "bioxelnodes.save_all_bioxel_layers"
bl_label = "Save All Bioxels" bl_label = "Save All Bioxel Layers"
bl_description = "Save All Bioxels to Directory." bl_description = "Save All Bioxel Layers to Directory."
bl_options = {'UNDO'} bl_options = {'UNDO'}
def execute(self, context): def execute(self, context):
bioxels_objs = get_all_bioxels_objs() layers = get_all_layers()
if len(bioxels_objs) == 0: if len(layers) == 0:
self.report({"WARNING"}, "Cannot find any bioxels.") self.report({"WARNING"}, "Cannot find any bioxel layer.")
return {'FINISHED'} return {'FINISHED'}
for bioxels_obj in bioxels_objs: for layer in layers:
name = bioxels_obj.parent.name try:
save_layer(layer, context.scene.bioxel_layer_dir)
except:
self.report(
{"WARNING"}, f"Fail to save {layer.name}, skiped")
# "//" self.report({"INFO"}, f"Successfully saved bioxel layers.")
bioxels_dir = bpy.path.abspath(context.scene.bioxels_dir)
source_dir = bpy.path.abspath(bioxels_obj.data.filepath)
output_path: Path = Path(bioxels_dir, f"{name}.vdb").resolve()
source_path: Path = Path(source_dir).resolve()
if output_path != source_path:
shutil.copy(source_path, output_path)
blend_path = Path(bpy.path.abspath("//")).resolve()
bioxels_obj.data.filepath = bpy.path.relpath(
str(output_path), start=str(blend_path))
self.report({"INFO"}, f"Successfully saved bioxels")
return {'FINISHED'} return {'FINISHED'}
class BIOXELNODES_PT_Bioxels(bpy.types.Panel): class BIOXELNODES_PT_Bioxels(bpy.types.Panel):
bl_idname = "BIOXELNODES_PT_Bioxels" bl_idname = "BIOXELNODES_PT_Bioxels"
bl_label = "Bioxels" bl_label = "Bioxel Nodes"
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
bl_context = "scene" bl_context = "scene"
@@ -99,5 +88,5 @@ class BIOXELNODES_PT_Bioxels(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
scene = context.scene scene = context.scene
layout.prop(scene, 'bioxels_dir') layout.prop(scene, 'bioxel_layer_dir')
layout.operator(SaveAllBioxels.bl_idname) layout.operator(SaveAllBioxelLayers.bl_idname)

View File

@@ -38,112 +38,311 @@ else:
return callback_str return callback_str
MENU_ITEMS = [
{
'label': 'Presets',
'icon': 'MESH_MONKEY',
'items': [
{
'label': 'Segment',
'icon': 'BONE_DATA',
'node_type': 'BioxelNodes_Segment',
'node_description': '',
'node_callback': "\n".join([
set_prop_to_node_factory("bioxel_size", "Detail Size"),
set_prop_to_node_factory("bioxels_offset", "Offset"),
])
}
# {
# 'label': 'Anatomy',
# 'icon': 'CONSTRAINT_BONE',
# 'node_type': 'BioxelNodes_Anatomy',
# 'node_description': '',
# 'node_callback': "\n".join([
# set_prop_to_node_factory("bioxel_size", "Detail Size"),
# set_prop_to_node_factory("bioxels_offset", "Offset"),
# ])
# }
]
},
{
'label': 'Segment',
'icon': 'OUTLINER_DATA_VOLUME',
'items': [
{
'label': 'Segment by Level',
'icon': 'EMPTY_SINGLE_ARROW',
'node_type': 'BioxelNodes_SegmentByLevel',
'node_description': '',
'node_callback': "\n".join([
set_prop_to_node_factory("bioxel_size", "Detail Size"),
set_prop_to_node_factory("bioxels_offset", "Offset"),
])
},
{
'label': 'Segment by Range',
'icon': 'IPO_CONSTANT',
'node_type': 'BioxelNodes_SegmentByRange',
'node_description': '',
'node_callback': "\n".join([
set_prop_to_node_factory("bioxel_size", "Detail Size"),
set_prop_to_node_factory("bioxels_offset", "Offset"),
])
}
# {
# 'label': 'Segment by Layers',
# 'icon': 'ALIGN_JUSTIFY',
# 'node_type': 'BioxelNodes_SegmentsByLayers',
# 'node_description': '',
# 'node_callback': "\n".join([
# set_prop_to_node_factory("bioxel_size", "Detail Size"),
# set_prop_to_node_factory("bioxels_offset", "Offset"),
# ])
# }
]
},
{
'label': 'Shader',
'icon': 'SHADING_RENDERED',
'items': [
{
'label': 'Segment Shader',
'icon': 'OUTLINER_OB_VOLUME',
'node_type': 'BioxelNodes_SegmentShader',
'node_description': ''
}
]
}
]
NODE_FILE = "BioxelNodes_4.0"
if bpy.app.version >= (4, 1, 0): if bpy.app.version >= (4, 1, 0):
MENU_ITEMS.append({
'label': 'Slicer',
'icon': 'CLIPUV_DEHLT',
'items': [
{
'label': 'RAS Slicer',
'icon': 'EMPTY_DATA',
'node_type': 'BioxelNodes_RAS-Slicer',
'node_description': ''
},
{
'label': 'Axis Slicer',
'icon': 'OUTLINER_OB_EMPTY',
'node_type': 'BioxelNodes_AxisSlicer',
'node_description': '',
},
{
'label': 'Box Slicer',
'icon': 'MOD_WIREFRAME',
'node_type': 'BioxelNodes_BoxSlicer',
'node_description': '',
}
]
})
NODE_FILE = "BioxelNodes_4.1" NODE_FILE = "BioxelNodes_4.1"
MENU_ITEMS = [
{
'label': 'Methods',
'icon': 'OUTLINER_DATA_VOLUME',
'items': [
{
'label': 'Mask by Threshold',
'icon': 'EMPTY_SINGLE_ARROW',
'node_type': 'BioxelNodes_MaskByThreshold',
'node_description': ''
},
{
'label': 'Mask by Range',
'icon': 'IPO_CONSTANT',
'node_type': 'BioxelNodes_MaskByRange',
'node_description': ''
},
{
'label': 'Mask by Label',
'icon': 'MESH_CAPSULE',
'node_type': 'BioxelNodes_MaskByLabel',
'node_description': ''
}
]
},
{
'label': 'Shaders',
'icon': 'SHADING_RENDERED',
'items': [
{
'label': 'Solid Shader',
'icon': 'SHADING_SOLID',
'node_type': 'BioxelNodes_AssignSolidShader',
'node_description': ''
},
{
'label': 'Slime Shader',
'icon': 'OUTLINER_OB_META',
'node_type': 'BioxelNodes_AssignSlimeShader',
'node_description': ''
},
{
'label': 'Volume Shader',
'icon': 'OUTLINER_OB_VOLUME',
'node_type': 'BioxelNodes_AssignVolumeShader',
'node_description': ''
},
{
'label': 'Universal Shader',
'icon': 'SHADING_RENDERED',
'node_type': 'BioxelNodes_AssignUniversalShader',
'node_description': ''
}
]
},
{
'label': 'Colors',
'icon': 'COLOR',
'items': [
{
'label': 'Color Presets',
'icon': 'COLOR',
'node_type': 'BioxelNodes_SetColorPresets',
'node_description': ''
},
{
'label': 'Color Ramp 2',
'icon': 'IPO_QUAD',
'node_type': 'BioxelNodes_SetColorRamp2',
'node_description': ''
},
{
'label': 'Color Ramp 3',
'icon': 'IPO_CUBIC',
'node_type': 'BioxelNodes_SetColorRamp3',
'node_description': ''
},
{
'label': 'Color Ramp 4',
'icon': 'IPO_QUART',
'node_type': 'BioxelNodes_SetColorRamp4',
'node_description': ''
},
{
'label': 'Color Ramp 5',
'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorRamp5',
'node_description': ''
}
]
},
{
'label': 'Slicers',
'icon': 'MOD_BEVEL',
'items': [
{
'label': 'Apply Slicers',
'icon': 'MOD_BEVEL',
'node_type': 'BioxelNodes_ApplySlicers',
'node_description': ''
},
"separator",
{
'label': 'Plane Slicer',
'icon': 'MOD_LATTICE',
'node_type': 'BioxelNodes_PlaneSlicer',
'node_description': '',
},
{
'label': 'Object Plane Slicer',
'icon': 'OUTLINER_OB_LATTICE',
'node_type': 'BioxelNodes_ObjectPlaneSlicer',
'node_description': '',
}
]
},
{
'label': 'Utils',
'icon': 'MODIFIER',
'items': [
{
'label': 'Join Component',
'icon': 'CONSTRAINT_BONE',
'node_type': 'BioxelNodes_JoinComponent',
'node_description': ''
},
{
'label': 'Separate Component',
'node_type': 'BioxelNodes_SeparateComponent',
'node_description': ''
}
]
},
{
'label': 'Others',
'icon': 'PACKAGE',
'items': [
{
'label': 'Source As Label',
'icon': 'GP_SELECT_POINTS',
'node_type': 'BioxelNodes_AsLabel',
'node_description': ''
},
{
'label': 'Source As Scalar',
'icon': 'GP_SELECT_STROKES',
'node_type': 'BioxelNodes_AsScalar',
'node_description': ''
}
]
}
]
else:
NODE_FILE = "BioxelNodes_4.0"
MENU_ITEMS = [
{
'label': 'Methods',
'icon': 'OUTLINER_DATA_VOLUME',
'items': [
{
'label': 'Mask by Threshold',
'icon': 'EMPTY_SINGLE_ARROW',
'node_type': 'BioxelNodes_MaskByThreshold',
'node_description': ''
},
{
'label': 'Mask by Range',
'icon': 'IPO_CONSTANT',
'node_type': 'BioxelNodes_MaskByRange',
'node_description': ''
},
{
'label': 'Mask by Label',
'icon': 'MESH_CAPSULE',
'node_type': 'BioxelNodes_MaskByLabel',
'node_description': ''
}
]
},
{
'label': 'Shaders',
'icon': 'SHADING_RENDERED',
'items': [
{
'label': 'Solid Shader',
'icon': 'SHADING_SOLID',
'node_type': 'BioxelNodes_AssignSolidShader',
'node_description': ''
},
{
'label': 'Slime Shader',
'icon': 'OUTLINER_OB_META',
'node_type': 'BioxelNodes_AssignSlimeShader',
'node_description': ''
},
{
'label': 'Volume Shader',
'icon': 'OUTLINER_OB_VOLUME',
'node_type': 'BioxelNodes_AssignVolumeShader',
'node_description': ''
},
{
'label': 'Universal Shader',
'icon': 'SHADING_RENDERED',
'node_type': 'BioxelNodes_AssignUniversalShader',
'node_description': ''
}
]
},
{
'label': 'Colors',
'icon': 'COLOR',
'items': [
{
'label': 'Color Ramp 2',
'icon': 'IPO_QUAD',
'node_type': 'BioxelNodes_SetColorRamp2',
'node_description': ''
},
{
'label': 'Color Ramp 3',
'icon': 'IPO_CUBIC',
'node_type': 'BioxelNodes_SetColorRamp3',
'node_description': ''
},
{
'label': 'Color Ramp 4',
'icon': 'IPO_QUART',
'node_type': 'BioxelNodes_SetColorRamp4',
'node_description': ''
},
{
'label': 'Color Ramp 5',
'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorRamp5',
'node_description': ''
}
]
},
{
'label': 'Slicers',
'icon': 'MOD_BEVEL',
'items': [
{
'label': 'Apply Slicers',
'icon': 'MOD_BEVEL',
'node_type': 'BioxelNodes_ApplySlicers',
'node_description': ''
},
"separator",
{
'label': 'Plane Slicer',
'icon': 'MOD_LATTICE',
'node_type': 'BioxelNodes_PlaneSlicer',
'node_description': '',
},
{
'label': 'Object Plane Slicer',
'icon': 'OUTLINER_OB_LATTICE',
'node_type': 'BioxelNodes_ObjectPlaneSlicer',
'node_description': '',
}
]
},
{
'label': 'Utils',
'icon': 'MODIFIER',
'items': [
{
'label': 'Join Component',
'icon': 'CONSTRAINT_BONE',
'node_type': 'BioxelNodes_JoinComponent',
'node_description': ''
},
{
'label': 'Separate Component',
'node_type': 'BioxelNodes_SeparateComponent',
'node_description': ''
}
]
},
{
'label': 'Others',
'icon': 'PACKAGE',
'items': [
{
'label': 'Source As Label',
'icon': 'GP_SELECT_POINTS',
'node_type': 'BioxelNodes_AsLabel',
'node_description': ''
},
{
'label': 'Source As Scalar',
'icon': 'GP_SELECT_STROKES',
'node_type': 'BioxelNodes_AsScalar',
'node_description': ''
}
]
}
]
custom_nodes = CustomNodes( custom_nodes = CustomNodes(
menu_items=MENU_ITEMS, menu_items=MENU_ITEMS,

188
bioxelnodes/operators.py Normal file
View File

@@ -0,0 +1,188 @@
from pathlib import Path
import bpy
import pyopenvdb as vdb
from uuid import uuid4
from .nodes import custom_nodes
from .utils import (get_container, get_container_layers,
get_layer, get_node_by_type, hide_in_ray, lock_transform)
def get_scalar_layer_selection(self, context):
items = [("None", "None", "")]
base_layer = bpy.data.objects[self.base_layer]
container = base_layer.parent
for layer in get_container_layers(container):
if layer.get("bioxel_layer_type") == "scalar":
items.append((
layer.name,
layer.name,
""
))
return items
def get_label_layer_selection(self, context):
items = [("None", "None", "")]
base_layer = bpy.data.objects[self.base_layer]
container = base_layer.parent
for layer in get_container_layers(container):
if layer.get("bioxel_layer_type") == "label":
items.append((
layer.name,
layer.name,
""
))
return items
class JoinLayers(bpy.types.Operator):
bl_idname = "bioxelnodes.join_layers"
bl_label = "Join Additinal Layers"
bl_description = "Join Layers"
bl_options = {'UNDO'}
base_layer: bpy.props.StringProperty(
options={"HIDDEN"}
) # type: ignore
scalar_layer: bpy.props.EnumProperty(
name="Scaler Layer",
items=get_scalar_layer_selection
) # type: ignore
label_layer: bpy.props.EnumProperty(
name="Label Layer",
items=get_label_layer_selection
) # type: ignore
# color_layer: bpy.props.StringProperty() # type: ignore
@classmethod
def poll(cls, context):
layer = get_layer(bpy.context.active_object)
return True if layer else False
def execute(self, context):
base_layer = bpy.data.objects[self.base_layer]
if not base_layer:
self.report({"WARNING"}, "Cannot find any bioxel layer as base.")
return {'FINISHED'}
base_layer_dir = bpy.path.abspath(base_layer.data.filepath)
base_grids, base_metadata = vdb.readAll(base_layer_dir)
preferences = context.preferences.addons[__package__].preferences
cache_dir = Path(preferences.cache_dir, 'VDBs')
cache_dir.mkdir(parents=True, exist_ok=True)
layers = []
if self.scalar_layer != "None":
layers.append(bpy.data.objects[self.scalar_layer])
if self.label_layer != "None":
layers.append(bpy.data.objects[self.label_layer])
# if self.color_layer:
# layers.append(bpy.data.objects[self.color_layer])
if len(layers) == 0:
self.report({"WARNING"}, "No additinal layers setted.")
return {'FINISHED'}
for layer in layers:
layer_dir = bpy.path.abspath(layer.data.filepath)
grids, metadata = vdb.readAll(layer_dir)
base_grids.extend(grids)
vdb_path = Path(cache_dir, f"{uuid4()}.vdb")
print(f"Storing the VDB file ({str(vdb_path)})...")
vdb.write(str(vdb_path), grids=base_grids)
# Read VDB
print(f"Loading the cache to Blender scene...")
bpy.ops.object.volume_import(
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
joined_layer = bpy.context.active_object
bpy.ops.object.modifier_add(type='NODES')
joined_layer.modifiers[0].node_group = base_layer.modifiers[0].node_group
# Set props to VDB object
joined_layer.name = f"{base_layer.name}_Joined"
joined_layer.data.name = f"{base_layer.name}_Joined"
lock_transform(joined_layer)
hide_in_ray(joined_layer)
joined_layer.hide_select = True
joined_layer.hide_render = True
joined_layer.hide_viewport = True
joined_layer.data.display.use_slice = True
joined_layer.data.display.density = 1e-05
joined_layer['bioxel_layer'] = True
joined_layer.parent = base_layer.parent
return {'FINISHED'}
def invoke(self, context, event):
base_layer = get_layer(bpy.context.active_object)
self.base_layer = base_layer.name
context.window_manager.invoke_props_dialog(self, width=400)
return {'RUNNING_MODAL'}
class ConvertToMesh(bpy.types.Operator):
bl_idname = "bioxelnodes.convert_to_mesh"
bl_label = "Bioxel Components To Mesh"
bl_description = "Convert Bioxel Components To Mesh"
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
container = get_container(bpy.context.active_object)
return True if container else False
def execute(self, context):
container = get_container(bpy.context.active_object)
if not container:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
bpy.ops.mesh.primitive_cube_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
mesh = bpy.context.active_object
mesh.name = f"Mesh_{container.name}"
bpy.ops.object.constraint_add(type='COPY_TRANSFORMS')
mesh.constraints["Copy Transforms"].target = container
bpy.ops.node.new_geometry_nodes_modifier()
modifier = mesh.modifiers[0]
nodes = modifier.node_group.nodes
links = modifier.node_group.links
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
object_node = nodes.new("GeometryNodeObjectInfo")
realize_nodes = nodes.new("GeometryNodeRealizeInstances")
separate_node = custom_nodes.add_node(
nodes, "BioxelNodes_SeparateComponent")
object_node.inputs[0].default_value = container
separate_node.inputs[1].default_value = 1
links.new(object_node.outputs['Geometry'], separate_node.inputs[0])
links.new(separate_node.outputs[0], realize_nodes.inputs[0])
links.new(realize_nodes.outputs[0], output_node.inputs[0])
bpy.ops.object.convert(target='MESH')
bpy.ops.constraint.apply(constraint="Copy Transforms", owner='OBJECT')
bpy.context.object.active_material_index = 1
bpy.ops.object.material_slot_remove()
self.report({"INFO"}, f"Successfully convert to mesh")
return {'FINISHED'}

View File

@@ -12,11 +12,6 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
default=str(Path(Path.home(), '.bioxelnodes')) default=str(Path(Path.home(), '.bioxelnodes'))
) # type: ignore ) # type: ignore
do_add_segmentnode: bpy.props.BoolProperty(
name="Add Segment Node",
default=True,
) # type: ignore
do_change_render_setting: bpy.props.BoolProperty( do_change_render_setting: bpy.props.BoolProperty(
name="Change Render Setting", name="Change Render Setting",
default=True, default=True,
@@ -33,4 +28,3 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
layout.prop(self, 'cache_dir') layout.prop(self, 'cache_dir')
layout.prop(self, "do_change_render_setting") layout.prop(self, "do_change_render_setting")
layout.prop(self, "do_add_segmentnode")

View File

@@ -9,6 +9,7 @@ def get_type(cls):
def get_node_by_type(nodes, type_name: str): def get_node_by_type(nodes, type_name: str):
return [node for node in nodes if get_type(node) == type_name] return [node for node in nodes if get_type(node) == type_name]
def show_message(message="", title="Message Box", icon='INFO'): def show_message(message="", title="Message Box", icon='INFO'):
def draw(self, context): def draw(self, context):
@@ -67,33 +68,57 @@ def calc_bbox_verts(origin, size):
return bbox_verts return bbox_verts
def get_bioxels_obj(current_obj): def lock_transform(obj):
bioxels_obj = None obj.lock_location[0] = True
if current_obj.get('bioxels_container'): obj.lock_location[1] = True
bioxels_values = [] obj.lock_location[2] = True
for obj in bpy.data.objects: obj.lock_rotation[0] = True
if obj.parent == current_obj and obj.get('bioxels'): obj.lock_rotation[1] = True
bioxels_values.append(obj) obj.lock_rotation[2] = True
obj.lock_scale[0] = True
if len(bioxels_values) > 0: obj.lock_scale[1] = True
bioxels_obj = bioxels_values[0] obj.lock_scale[2] = True
elif current_obj.get('bioxels') and current_obj.parent:
if current_obj.parent.get('bioxels_container'):
bioxels_obj = current_obj
return bioxels_obj
def get_all_bioxels_objs(): def hide_in_ray(obj):
bioxels_objs = [] obj.visible_camera = False
obj.visible_diffuse = False
obj.visible_glossy = False
obj.visible_transmission = False
obj.visible_volume_scatter = False
obj.visible_shadow = False
def get_container(current_obj):
if current_obj:
return current_obj if current_obj.get('bioxel_container') else None
else:
return None
def get_layer(current_obj):
if current_obj.get('bioxel_layer') and current_obj.parent:
if current_obj.parent.get('bioxel_container'):
return current_obj
return None
def get_container_layers(container):
layers = []
for obj in bpy.data.objects: for obj in bpy.data.objects:
if obj.parent: if obj.parent == container and get_layer(obj):
if obj.parent.get('bioxels_container') and obj.get('bioxels'): layers.append(obj)
bioxels_objs.append(obj)
return list(set(bioxels_objs)) return layers
def get_all_layers():
layers = []
for obj in bpy.data.objects:
if get_layer(obj):
layers.append(obj)
return layers
def get_text_index_str(text): def get_text_index_str(text):

View File

@@ -60,4 +60,4 @@ The VDB cache and the Bioxel nodes are not reachable to computers if you don't s
![before sharin](assets/before-sharing.png) ![before sharin](assets/before-sharing.png)
After that, both the VDB cache and the Bioxel nodes are stored in relative locations. When you share a file with someone, pack the entire directory so that the resources are not lost. The other person will be able to load correctly, regardless of whether they have BioxelNodes installed. After that, both the VDB cache and the Bioxel nodes are stored in relative locations. When you share a file with someone, pack the entire directory so that the resources are not lost. The other person will be able to load correctly, regardless of whether they have Bioxel Nodes installed.

View File

@@ -17,7 +17,7 @@ Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetr
![gallery](assets/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) The "Bioxel" in "Bioxel Nodes", 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 # Supported Format

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "bioxelnodes" name = "bioxelnodes"
version = "0.1.1" version = "0.2.0"
description = "" description = ""
authors = ["Ma Nan <icrdr2010@outlook.com>"] authors = ["Ma Nan <icrdr2010@outlook.com>"]
license = "MIT" license = "MIT"