diff --git a/bioxelnodes/assets/Nodes.blend b/bioxelnodes/assets/Nodes.blend index f23fc9e..70cc114 100644 --- a/bioxelnodes/assets/Nodes.blend +++ b/bioxelnodes/assets/Nodes.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae5e166bf8adfc1d5387b7ec1365adcb7a3d65d68e74b84232d90791ba9a0c6f -size 1705390 +oid sha256:d6832d20cb556ff904cfd7c5574b048ea54011cb531fe6423c4689186de2c8aa +size 1749214 diff --git a/bioxelnodes/custom_nodes/node_menu.py b/bioxelnodes/custom_nodes/node_menu.py index 28b1645..1bdf719 100644 --- a/bioxelnodes/custom_nodes/node_menu.py +++ b/bioxelnodes/custom_nodes/node_menu.py @@ -84,6 +84,7 @@ class CustomNodes(): op.node_label = item.get('label') or "" op.node_description = item.get( 'node_description') or "Add Custom Node." + op.node_link = item.get('link') or True op.node_callback = item.get('node_callback') or "" menu_classes.append(Menu) @@ -111,6 +112,7 @@ class CustomNodes(): op.nodes_file = self.nodes_file op.node_type = item['node_type'] op.node_label = item.get('label') or "" + op.node_link = item.get('link') or True op.node_callback = item.get('node_callback') or "" return op.add_node(node_tree) else: diff --git a/bioxelnodes/custom_nodes/nodes.py b/bioxelnodes/custom_nodes/nodes.py index 3d2a248..214cb62 100644 --- a/bioxelnodes/custom_nodes/nodes.py +++ b/bioxelnodes/custom_nodes/nodes.py @@ -29,6 +29,11 @@ class AddCustomNode(): subtype="NONE" ) # type: ignore + node_link: bpy.props.BoolProperty( + name='node_link', + default=True + ) # type: ignore + node_callback: bpy.props.StringProperty( name='node_callback', default='' @@ -50,7 +55,7 @@ class AddCustomNode(): return node - def get_node_tree(self, node_type): + def get_node_tree(self, node_type, node_link): # try to get node from current file if exists node_tree = bpy.data.node_groups.get(node_type) # if not exists, get it from asset file. @@ -59,7 +64,7 @@ class AddCustomNode(): 'EXEC_DEFAULT', directory=f"{self.nodes_file}/NodeTree", filename=node_type, - link=False, + link=node_link, use_recursive=True ) @@ -81,10 +86,8 @@ class AddCustomNode(): ... def add_node(self, node_tree): - self.get_node_tree(self.node_type) - + self.get_node_tree(self.node_type, self.node_link) node = node_tree.new("GeometryNodeGroup") - self.assign_node_tree(node) return node @@ -96,7 +99,7 @@ class CUSTOMNODES_OT_Add_Custom_Node(bpy.types.Operator, AddCustomNode): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.get_node_tree(self.node_type) + self.get_node_tree(self.node_type, self.node_link) # intended to be called upon button press in the node tree prev_context = bpy.context.area.type diff --git a/bioxelnodes/io.py b/bioxelnodes/io.py index a376184..b679bef 100644 --- a/bioxelnodes/io.py +++ b/bioxelnodes/io.py @@ -9,7 +9,7 @@ import mathutils from .utils import calc_bbox_verts, extract_last_number, get_bioxels_obj, show_message from .nodes import custom_nodes -SUPPORT_EXTS = ['.dcm', '.tif', '.tiff', '.png', '.bmp'] +SUPPORT_EXTS = ['.dcm', '.tif', '.tiff', '.png', '.bmp', '', '.jpg'] def get_data_files(filepath: str): @@ -84,13 +84,6 @@ class ImportDICOMDialog(bpy.types.Operator): default=(100, 100, 100) ) # type: ignore - bioxels_scale: bpy.props.FloatProperty( - name="Bioxel Size Scale", - soft_min=0.001, soft_max=100.0, - min=1e-6, max=1e6, - default=0.01, - ) # type: ignore - bioxel_size: bpy.props.FloatProperty( name="Bioxel Size", soft_min=0.1, soft_max=10.0, @@ -117,6 +110,13 @@ class ImportDICOMDialog(bpy.types.Operator): options={'HIDDEN'} ) # type: ignore + scene_scale: bpy.props.FloatProperty( + name="Scene Scale", + soft_min=0.001, soft_max=100.0, + min=1e-6, max=1e6, + default=0.01, + ) # type: ignore + do_add_segmentnode: bpy.props.BoolProperty( name="Add Segment Node", default=True, @@ -178,25 +178,10 @@ class ImportDICOMDialog(bpy.types.Operator): ) try: - image = sitk.DICOMOrient(image, 'LPS') + image = sitk.DICOMOrient(image, 'RAS') except: ... - image_origin = image.GetOrigin() - bioxels_scale = float(self.bioxels_scale) - bioxel_size *= bioxels_scale - bioxels_origin = ( - image_origin[0] * bioxels_scale, - image_origin[1] * bioxels_scale, - image_origin[2] * bioxels_scale, - ) - bioxels_size = ( - bioxels_shape[0] * bioxel_size, - bioxels_shape[1] * bioxel_size, - bioxels_shape[2] * bioxel_size, - ) - direction = image.GetDirection() - array = sitk.GetArrayFromImage(image) orig_dtype = str(array.dtype) print(f"Coverting Dtype {orig_dtype} -> float") @@ -206,26 +191,51 @@ class ImportDICOMDialog(bpy.types.Operator): # https://www.slicer.org/wiki/Coordinate_systems # ITK Numpy 3D - # L (eft) i -> k -> x - # P (osterior) j -> j -> y + # R (ight) i -> k -> x + # A (nterior) j -> j -> y # S (uperior) k -> i -> z array = np.transpose(array) - value_max = float(np.max(array)) - value_min = float(np.min(array)) + bioxels_max = float(np.max(array)) + bioxels_min = float(np.min(array)) - value_offset = 0.0 - if value_min < 0 and orig_dtype[0] != "u": - value_offset = -value_min - array = array + np.full_like(array, value_offset) - value_max = float(np.max(array)) - value_min = float(np.min(array)) - print("Offseted Max:", value_max) - print("Offseted Min:", value_min) + bioxels_offset = 0.0 + if bioxels_min < 0 and orig_dtype[0] != "u": + bioxels_offset = -bioxels_min + array = array + np.full_like(array, bioxels_offset) + bioxels_max = float(np.max(array)) + bioxels_min = float(np.min(array)) + print("Offseted Max:", bioxels_max) + print("Offseted Min:", bioxels_min) # Build VDB grid = vdb.FloatGrid() grid.copyFromArray(array.copy()) + + # After sitk.DICOMOrient(), origin and direction will also orient base on LPS + # so we need to convert them into RAS + mat_lps2ras = axis_conversion( + from_forward='-Z', + from_up='-Y', + to_forward='-Z', + to_up='Y' + ).to_4x4() + + mat_location = mathutils.Matrix.Translation( + mathutils.Vector(image.GetOrigin()) + ) + + mat_rotation = mathutils.Matrix( + np.array(image.GetDirection()).reshape((3, 3)) + ).to_4x4() + + mat_scale = mathutils.Matrix.Scale( + bioxel_size, 4 + ) + + transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale + + grid.transform = vdb.createLinearTransform(transfrom.transposed()) grid.name = "value" preferences = context.preferences.addons[__package__].preferences @@ -244,28 +254,26 @@ class ImportDICOMDialog(bpy.types.Operator): bioxels_obj = bpy.context.active_object # Set props to VDB object - bioxels_obj.name = f"{name}_Values" - bioxels_obj.data.name = f"{name}_Values" + bioxels_obj.name = f"{name}_Bioxels" + bioxels_obj.data.name = f"{name}_Bioxels" # Make transformation + scene_scale = float(self.scene_scale) # (S)uperior -Z -> Y - # (P)osterior -Y -> Z - axis_rot = axis_conversion( + # (A)osterior Y -> Z + mat_ras2blender = axis_conversion( from_forward='-Z', - from_up='-Y', + from_up='Y', to_forward='Y', to_up='Z' ).to_4x4() - mat_loc = mathutils.Matrix.Translation( - mathutils.Vector(bioxels_origin)) - mat_sca = mathutils.Matrix.Scale(bioxel_size, 4) - mat_rot = mathutils.Matrix( - np.array(direction).reshape((3, 3)) - ).to_4x4() + mat_scene_scale = mathutils.Matrix.Scale( + scene_scale, 4 + ) - bioxels_obj.matrix_world = axis_rot @ mat_loc @ mat_rot @ mat_sca + bioxels_obj.matrix_world = mat_ras2blender @ mat_scene_scale bioxels_obj.lock_location[0] = True bioxels_obj.lock_location[1] = True @@ -277,16 +285,7 @@ class ImportDICOMDialog(bpy.types.Operator): bioxels_obj.lock_scale[1] = True bioxels_obj.lock_scale[2] = True bioxels_obj.hide_select = True - bioxels_obj['value_max'] = value_max - bioxels_obj['value_min'] = value_min - bioxels_obj['value_offset'] = value_offset - bioxels_obj['image_origin'] = mathutils.Vector( - bioxels_origin) / bioxel_size - bioxels_obj['bioxel_size'] = bioxel_size - bioxels_obj['bioxels_shape'] = bioxels_shape - bioxels_obj['bioxels_size'] = bioxels_size - bioxels_obj['bioxels_origin'] = bioxels_origin - bioxels_obj['bioxels_values'] = True + bioxels_obj['bioxels'] = True bpy.ops.node.new_geometry_nodes_modifier() node_tree = bioxels_obj.modifiers[0].node_group @@ -296,14 +295,16 @@ class ImportDICOMDialog(bpy.types.Operator): enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1) ) container_obj = bpy.context.active_object - - bbox_verts = calc_bbox_verts(bioxels_origin, bioxels_size) + bbox_verts = calc_bbox_verts((0, 0, 0), bioxels_shape) for index, vert in enumerate(container_obj.data.vertices): - vert.co = bbox_verts[index] + co = transfrom @ mathutils.Vector(bbox_verts[index]) + co = mat_ras2blender @ co + co = mat_scene_scale @ co + vert.co = co bioxels_obj.parent = container_obj container_obj.name = name - container_obj.data.name = f"{name}_Container" + container_obj.data.name = name container_obj.visible_camera = False container_obj.visible_diffuse = False container_obj.visible_glossy = False @@ -311,7 +312,13 @@ class ImportDICOMDialog(bpy.types.Operator): container_obj.visible_volume_scatter = False container_obj.visible_shadow = False container_obj.display_type = 'WIRE' - container_obj['bioxels'] = True + 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') container_obj.modifiers[0].node_group = node_tree @@ -440,8 +447,8 @@ class BIOXELNODES_FH_ImportDicom(bpy.types.FileHandler): class ExportVDB(bpy.types.Operator): bl_idname = "bioxelnodes.export_vdb" - bl_label = "Biovels as VDB" - bl_description = "Export Biovels original VDB data." + bl_label = "Bioxels as VDB" + bl_description = "Export Bioxels original VDB data." bl_options = {'UNDO'} filepath: bpy.props.StringProperty( @@ -451,13 +458,12 @@ class ExportVDB(bpy.types.Operator): filename_ext = ".vdb" def invoke(self, context, event): - bioxels_objs = [] + bioxels_obj = None for obj in bpy.context.selected_objects: bioxels_obj = get_bioxels_obj(obj) - if bioxels_obj: - bioxels_objs.append(bioxels_obj) + break - if len(bioxels_objs) > 0: + if bioxels_obj: context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} else: @@ -465,14 +471,13 @@ class ExportVDB(bpy.types.Operator): return {'CANCELLED'} def execute(self, context): - bioxels_objs = [] + bioxels_obj = None for obj in bpy.context.selected_objects: bioxels_obj = get_bioxels_obj(obj) - if bioxels_obj: - bioxels_objs.append(bioxels_obj) + break - bioxels_obj = bioxels_objs[0] - output_path: Path = Path(self.filepath).resolve() + filepath = f"{self.filepath.split('.')[0]}.vdb" + output_path: Path = Path(filepath).resolve() source_path: Path = Path(bioxels_obj.data.filepath).resolve() # print('output_path', output_path) # print('source_path', source_path) diff --git a/bioxelnodes/nodes.py b/bioxelnodes/nodes.py index bae71f8..2207071 100644 --- a/bioxelnodes/nodes.py +++ b/bioxelnodes/nodes.py @@ -9,11 +9,12 @@ def add_driver_to_node_factory(source_prop, target_prop): import bpy from bioxelnodes.utils import add_direct_driver, get_bioxels_obj bioxels_obj = get_bioxels_obj(bpy.context.active_object) +container_obj = bioxels_obj.parent if bioxels_obj: add_direct_driver( target=node, target_prop='{target_prop}', - source=bioxels_obj, + source=container_obj, source_prop='{source_prop}' ) else: @@ -22,6 +23,20 @@ else: return callback_str +def set_prop_to_node_factory(source_prop, target_prop): + callback_str = f""" +import bpy +from bioxelnodes.utils import get_bioxels_obj +bioxels_obj = get_bioxels_obj(bpy.context.active_object) +container_obj = bioxels_obj.parent +if bioxels_obj: + node.inputs.get('{target_prop}').default_value = container_obj.get('{source_prop}') +else: + print('Cannot find any bioxels.') + """ + return callback_str + + MENU_ITEMS = [ { 'label': 'Present', @@ -32,7 +47,20 @@ MENU_ITEMS = [ 'icon': 'OUTLINER_OB_META', 'node_type': 'BioxelNodes_Segment', 'node_description': '', - 'node_callback': add_driver_to_node_factory("value_offset", "Offset") + 'node_callback': "\n".join([ + set_prop_to_node_factory("bioxel_size", "Detail Size"), + set_prop_to_node_factory("bioxels_offset", "Offset"), + ]) + }, + { + 'label': 'Anatomy', + 'icon': 'OUTLINER_OB_META', + '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"), + ]) } ] }, @@ -45,21 +73,30 @@ MENU_ITEMS = [ 'icon': 'AREA_JOIN', 'node_type': 'BioxelNodes_SegmentByLevel', 'node_description': '', - 'node_callback': add_driver_to_node_factory("value_offset", "Offset") + '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': 'AREA_SWAP', 'node_type': 'BioxelNodes_SegmentByRange', 'node_description': '', - 'node_callback': add_driver_to_node_factory("value_offset", "Offset") + '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': 'OUTLINER_OB_FORCE_FIELD', 'node_type': 'BioxelNodes_SegmentsByLayers', 'node_description': '', - 'node_callback': add_driver_to_node_factory("value_offset", "Offset") + 'node_callback': "\n".join([ + set_prop_to_node_factory("bioxel_size", "Detail Size"), + set_prop_to_node_factory("bioxels_offset", "Offset"), + ]) } ] }, @@ -80,11 +117,10 @@ MENU_ITEMS = [ 'icon': 'CLIPUV_DEHLT', 'items': [ { - 'label': 'LPS Slicer', + 'label': 'RAS Slicer', 'icon': 'EMPTY_DATA', - 'node_type': 'BioxelNodes_LPS-Slicer', - 'node_description': '', - 'node_callback': add_driver_to_node_factory("image_origin", "Origin") + 'node_type': 'BioxelNodes_RAS-Slicer', + 'node_description': '' }, { 'label': 'Axis Slicer', diff --git a/bioxelnodes/utils.py b/bioxelnodes/utils.py index 5710fad..acb69e0 100644 --- a/bioxelnodes/utils.py +++ b/bioxelnodes/utils.py @@ -12,9 +12,9 @@ def show_message(message="", title="Message Box", icon='INFO'): def calc_bbox_verts(origin, size): bbox_origin = mathutils.Vector( - (-origin[0], -origin[2], -origin[1])) + (origin[0], origin[1], origin[2])) bbox_size = mathutils.Vector( - (-size[0], -size[2], -size[1])) + (size[0], size[1], size[2])) bbox_verts = [ ( bbox_origin[0] + 0, @@ -62,17 +62,17 @@ def calc_bbox_verts(origin, size): def get_bioxels_obj(current_obj): bioxels_obj = None - if current_obj.get('bioxels'): + if current_obj.get('bioxels_container'): bioxels_values = [] for obj in bpy.data.objects: - if obj.parent == current_obj and obj.get('bioxels_values'): + if obj.parent == current_obj and obj.get('bioxels'): bioxels_values.append(obj) if len(bioxels_values) > 0: bioxels_obj = bioxels_values[0] - elif current_obj.get('bioxels_values') and current_obj.parent: - if current_obj.parent.get('bioxels'): + elif current_obj.get('bioxels') and current_obj.parent: + if current_obj.parent.get('bioxels_container'): bioxels_obj = current_obj return bioxels_obj