feat: add more format support
This commit is contained in:
56
.github/workflows/upload-assets.yml
vendored
56
.github/workflows/upload-assets.yml
vendored
@@ -6,31 +6,8 @@ on:
|
||||
- "release/*"
|
||||
|
||||
jobs:
|
||||
# draft_release:
|
||||
# name: Draft Release
|
||||
# runs-on: ubuntu-latest
|
||||
# outputs:
|
||||
# upload_url: ${{ steps.draft_release.outputs.upload_url }}
|
||||
# version: ${{ steps.set_env.outputs.version }}
|
||||
# steps:
|
||||
# - name: Set Version Env
|
||||
# id: set_env
|
||||
# run: |
|
||||
# ref_name=${{ github.ref_name }}
|
||||
# echo "version=${ref_name#release/}" >> "$GITHUB_OUTPUT"
|
||||
# - name: Get release
|
||||
# id: draft_release
|
||||
# uses: cardinalby/git-get-release-action@1.2.4
|
||||
# with:
|
||||
# releaseName: Draft
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
draft_release:
|
||||
name: Draft Release
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.draft_release.outputs.upload_url }}
|
||||
@@ -41,14 +18,37 @@ jobs:
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}
|
||||
echo "version=${ref_name#release/}" >> "$GITHUB_OUTPUT"
|
||||
- name: Draft Release ${{ steps.set_env.outputs.version }}
|
||||
uses: release-drafter/release-drafter@v5
|
||||
- name: Get release
|
||||
id: draft_release
|
||||
uses: cardinalby/git-get-release-action@1.2.4
|
||||
with:
|
||||
name: ${{ steps.set_env.outputs.version }}
|
||||
tag: ${{ steps.set_env.outputs.version }}
|
||||
releaseName: Draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
# draft_release:
|
||||
# name: Draft Release
|
||||
# permissions:
|
||||
# contents: write
|
||||
# pull-requests: write
|
||||
# runs-on: ubuntu-latest
|
||||
# outputs:
|
||||
# upload_url: ${{ steps.draft_release.outputs.upload_url }}
|
||||
# version: ${{ steps.set_env.outputs.version }}
|
||||
# steps:
|
||||
# - name: Set Version Env
|
||||
# id: set_env
|
||||
# run: |
|
||||
# ref_name=${{ github.ref_name }}
|
||||
# echo "version=${ref_name#release/}" >> "$GITHUB_OUTPUT"
|
||||
# - name: Draft Release ${{ steps.set_env.outputs.version }}
|
||||
# uses: release-drafter/release-drafter@v5
|
||||
# id: draft_release
|
||||
# with:
|
||||
# name: ${{ steps.set_env.outputs.version }}
|
||||
# tag: ${{ steps.set_env.outputs.version }}
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload_blender_addon:
|
||||
name: Upload Blender Add-on
|
||||
|
||||
@@ -2,14 +2,13 @@ import bpy
|
||||
|
||||
from .nodes import custom_nodes
|
||||
from . import auto_load
|
||||
from . import menu
|
||||
|
||||
|
||||
bl_info = {
|
||||
"name": "Bioxel Nodes",
|
||||
"author": "Ma Nan",
|
||||
"description": "",
|
||||
"blender": (4, 1, 0),
|
||||
"blender": (4, 0, 0),
|
||||
"version": (0, 1, 0),
|
||||
"location": "File -> Import",
|
||||
"warning": "",
|
||||
@@ -18,23 +17,18 @@ bl_info = {
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
BIN
bioxelnodes/assets/Nodes/BioxelNodes_4.0.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_4.0.blend
LFS
Normal file
Binary file not shown.
@@ -134,7 +134,7 @@ def get_register_base_types():
|
||||
"Node", "NodeSocket", "NodeTree",
|
||||
"UIList", "RenderEngine",
|
||||
"Gizmo", "GizmoGroup",
|
||||
])
|
||||
] if hasattr(bpy.types, name))
|
||||
|
||||
|
||||
# Find order to register to solve dependencies
|
||||
|
||||
@@ -5,7 +5,7 @@ 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_description = "Convert Bioxels To Mesh"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1 +1 @@
|
||||
from .menu import CustomNodes
|
||||
from .menus import CustomNodes
|
||||
@@ -3,12 +3,6 @@ import bpy
|
||||
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"
|
||||
@@ -182,12 +176,17 @@ class CustomNodes():
|
||||
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
|
||||
@@ -1 +1,2 @@
|
||||
from .preferences import ExternalPackagePreferences
|
||||
from .package import PackageInstaller
|
||||
|
||||
@@ -42,12 +42,13 @@ class InstallationError(Exception):
|
||||
class PackageInstaller():
|
||||
def __init__(
|
||||
self,
|
||||
log_dir: str,
|
||||
requirements_dir: str,
|
||||
log_dir: str = None,
|
||||
pypi_mirror_provider='Default',
|
||||
) -> None:
|
||||
self.pypi_mirror_provider = pypi_mirror_provider
|
||||
self.log_path: Path = Path(log_dir)
|
||||
self.log_path: Path = Path(log_dir) if log_dir \
|
||||
else Path.home() / '.externalpackage' / 'logs'
|
||||
self.requirements_path: Path = Path(requirements_dir)
|
||||
|
||||
def start_logging(self, logfile_name: str = 'side-packages-install') -> logging.Logger:
|
||||
@@ -250,7 +251,7 @@ class PackageInstaller():
|
||||
if result.returncode != 0:
|
||||
log.error('Command failed: %s', cmd_list)
|
||||
log.error('stdout: %s', result.stdout.decode())
|
||||
log.error('stderr: %s', result.stderr.decode())
|
||||
log.error('stderr: %s', result.stderr.decode(errors='ignore'))
|
||||
else:
|
||||
log.info('Command succeeded: %s', cmd_list)
|
||||
log.info('stdout: %s', result.stdout.decode())
|
||||
@@ -350,6 +351,7 @@ class EXTERNALPACKAGE_OT_Install_Package(bpy.types.Operator):
|
||||
name='Python Package',
|
||||
description='Python Package to Install'
|
||||
) # type: ignore
|
||||
|
||||
version: bpy.props.StringProperty(
|
||||
name='Python Package',
|
||||
description='Python Package to Install'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import bpy
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from .package import PackageInstaller, PYPI_MIRROR
|
||||
|
||||
@@ -10,6 +11,25 @@ def get_pypi_mirrors(self, context, edit_text):
|
||||
return PYPI_MIRROR.keys()
|
||||
|
||||
|
||||
class RebootBlender(bpy.types.Operator):
|
||||
bl_idname = "externalpackage.reboot_blender"
|
||||
bl_label = "Reboot Blender"
|
||||
bl_description = "Reboot Blender"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self, event)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
blender_launcher = Path(
|
||||
bpy.app.binary_path).parent / "blender-launcher.exe"
|
||||
subprocess.run([str(blender_launcher), "-con", "--python-expr",
|
||||
"import bpy; bpy.ops.wm.recover_last_session()"])
|
||||
bpy.ops.wm.quit_blender()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ExternalPackagePreferences():
|
||||
requirements_dir: bpy.props.StringProperty(
|
||||
name="requirements.txt Directory",
|
||||
@@ -34,11 +54,12 @@ class ExternalPackagePreferences():
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Install the required packages.")
|
||||
layout.label(
|
||||
text='**Install the required packages, then hit "Reboot Blender"**')
|
||||
|
||||
col_main = layout.column(heading='', align=False)
|
||||
row_import = col_main.row()
|
||||
row_import.prop(self, 'pypi_mirror_provider', text='Set PyPI Mirror')
|
||||
row_import.prop(self, 'pypi_mirror_provider', text='PyPI Mirror')
|
||||
|
||||
installer = PackageInstaller(
|
||||
pypi_mirror_provider=self.pypi_mirror_provider,
|
||||
@@ -70,3 +91,5 @@ class ExternalPackagePreferences():
|
||||
op.package = name
|
||||
op.version = version
|
||||
op.description = f'Install required python package: {name}'
|
||||
|
||||
layout.operator(RebootBlender.bl_idname)
|
||||
|
||||
@@ -6,71 +6,114 @@ import numpy as np
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
import mathutils
|
||||
from .utils import calc_bbox_verts, extract_last_number, get_bioxels_obj, get_node_by_type, show_message
|
||||
import math
|
||||
from .utils import calc_bbox_verts, get_text_index, get_bioxels_obj, get_node_by_type, show_message
|
||||
from .nodes import custom_nodes
|
||||
from .props import BIOXELNODES_Series
|
||||
try:
|
||||
import SimpleITK as sitk
|
||||
except:
|
||||
...
|
||||
|
||||
SUPPORT_EXTS = ['.dcm', '.tif', '.tiff', '.png', '.bmp', '', '.jpg']
|
||||
SUPPORT_EXTS = ['.dcm', '.DICOM',
|
||||
'.bmp', '.BMP',
|
||||
'.PIC', '.pic',
|
||||
'.gipl', '.gipl.gz',
|
||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||
'.lsm', '.LSM',
|
||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||
'.mnc', '.MNC',
|
||||
'.mrc', '.rec',
|
||||
'.mha', '.mhd',
|
||||
'.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5',
|
||||
'.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz',
|
||||
'.nrrd', '.nhdr',
|
||||
'.png', '.PNG',
|
||||
'.vtk']
|
||||
|
||||
SEQUENCE_EXTS = ['.dcm', '.DICOM',
|
||||
'.bmp', '.BMP',
|
||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||
'.png', '.PNG']
|
||||
|
||||
DICOM_EXTS = ['.dcm', '.DICOM']
|
||||
|
||||
FH_EXTS = ['.dcm', '.DICOM',
|
||||
'.gipl', '.gipl.gz',
|
||||
'.mnc', '.MNC',
|
||||
'.mrc', '.rec',
|
||||
'.mha', '.mhd',
|
||||
'.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz',
|
||||
'.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5',
|
||||
'.nrrd', '.nhdr',
|
||||
'.vtk',
|
||||
'.gz']
|
||||
|
||||
|
||||
def get_data_files(filepath: str):
|
||||
def get_bioxels_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple):
|
||||
return (
|
||||
math.ceil(orig_shape[0] / bioxel_size * orig_spacing[0]),
|
||||
math.ceil(orig_shape[1] / bioxel_size * orig_spacing[1]),
|
||||
math.ceil(orig_shape[2] / bioxel_size * orig_spacing[2]),
|
||||
)
|
||||
|
||||
|
||||
def collect_image_sequence(filepath: str):
|
||||
file_path = Path(filepath).resolve()
|
||||
suffix = file_path.suffix
|
||||
|
||||
files = list(file_path.parent.iterdir())
|
||||
files = [f for f in files if f.is_file() and f.suffix == suffix]
|
||||
files = [f for f in files if f.is_file()
|
||||
and get_ext(file_path) == get_ext(f)
|
||||
and get_text_index(f.stem)]
|
||||
|
||||
def last_number(file: Path):
|
||||
return extract_last_number(file.stem)
|
||||
def get_index(f: Path):
|
||||
return get_text_index(f.stem)
|
||||
|
||||
files.sort(key=last_number)
|
||||
files = [str(f) for f in files]
|
||||
return files
|
||||
files.sort(key=get_index)
|
||||
sequence = [str(f) for f in files]
|
||||
return sequence
|
||||
|
||||
|
||||
def on_orig_spacing_changed(self, context):
|
||||
if self.auto:
|
||||
self.auto = False
|
||||
new_orig_spacing = tuple(self.orig_spacing)
|
||||
|
||||
orig_shape = tuple(self.orig_shape)
|
||||
bioxel_size = float(self.bioxel_size)
|
||||
|
||||
self.bioxels_shape = (
|
||||
int(orig_shape[0] / bioxel_size * new_orig_spacing[0]),
|
||||
int(orig_shape[1] / bioxel_size * new_orig_spacing[1]),
|
||||
int(orig_shape[2] / bioxel_size * new_orig_spacing[2]),
|
||||
)
|
||||
def get_ext(filepath: str):
|
||||
file_path = Path(filepath)
|
||||
if file_path.name.endswith(".nii.gz"):
|
||||
return ".nii.gz"
|
||||
elif file_path.name.endswith(".img.gz"):
|
||||
return ".img.gz"
|
||||
elif file_path.name.endswith(".gipl.gz"):
|
||||
return ".gipl.gz"
|
||||
else:
|
||||
self.auto = True
|
||||
return file_path.suffix
|
||||
|
||||
|
||||
def on_bioxel_size_changed(self, context):
|
||||
if self.auto:
|
||||
self.auto = False
|
||||
new_bioxel_size = float(self.bioxel_size)
|
||||
|
||||
orig_shape = tuple(self.orig_shape)
|
||||
orig_spacing = tuple(self.orig_spacing)
|
||||
|
||||
self.bioxels_shape = (
|
||||
int(orig_shape[0] / new_bioxel_size * orig_spacing[0]),
|
||||
int(orig_shape[1] / new_bioxel_size * orig_spacing[1]),
|
||||
int(orig_shape[2] / new_bioxel_size * orig_spacing[2]),
|
||||
)
|
||||
def read_image(filepath: str, series_id=""):
|
||||
ext = get_ext(filepath)
|
||||
dir_path = Path(filepath).resolve().parent
|
||||
filename = Path(filepath).resolve().stem
|
||||
if ext in DICOM_EXTS:
|
||||
reader = sitk.ImageSeriesReader()
|
||||
reader.MetaDataDictionaryArrayUpdateOn()
|
||||
reader.LoadPrivateTagsOn()
|
||||
series_files = reader.GetGDCMSeriesFileNames(
|
||||
str(dir_path), series_id)
|
||||
reader.SetFileNames(series_files)
|
||||
image = reader.Execute()
|
||||
elif ext in SEQUENCE_EXTS and get_text_index(filename):
|
||||
sequence = collect_image_sequence(filepath)
|
||||
image = sitk.ReadImage(sequence)
|
||||
else:
|
||||
self.auto = True
|
||||
image = sitk.ReadImage(filepath)
|
||||
if image.GetDimension() == 2:
|
||||
image = sitk.ReadImage([filepath])
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def on_info_changed(self, context):
|
||||
for region in context.area.regions:
|
||||
if region.type == "UI":
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
class ImportDICOMDialog(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.import_dicom_dialog"
|
||||
class ImportImageDialog(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.import_image_dialog"
|
||||
bl_label = "Volume Data as Bioxels"
|
||||
bl_description = "Import Volume Data as Bioxels (VDB)."
|
||||
bl_description = "Import Volume Data as Bioxels (VDB)"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
@@ -78,64 +121,62 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
options={'HIDDEN'}
|
||||
) # type: ignore
|
||||
|
||||
bioxels_shape: bpy.props.IntVectorProperty(
|
||||
name="Bioxels Shape (ReadOnly)",
|
||||
min=0,
|
||||
default=(100, 100, 100)
|
||||
series_id: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
read_as: bpy.props.EnumProperty(
|
||||
name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""), ("labels", "Labels", "")]
|
||||
) # type: ignore
|
||||
|
||||
label_index: bpy.props.IntProperty(
|
||||
name="Label Index",
|
||||
default=1
|
||||
) # type: ignore
|
||||
|
||||
bioxel_size: bpy.props.FloatProperty(
|
||||
name="Bioxel Size",
|
||||
name="Bioxel Size (Larger size means small resolution)",
|
||||
soft_min=0.1, soft_max=10.0,
|
||||
min=1e-2, max=1e2,
|
||||
default=1,
|
||||
update=on_bioxel_size_changed
|
||||
) # type: ignore
|
||||
|
||||
orig_spacing: bpy.props.FloatVectorProperty(
|
||||
name="Original Spacing",
|
||||
default=(1, 1, 1),
|
||||
update=on_orig_spacing_changed
|
||||
) # type: ignore
|
||||
|
||||
orig_shape: bpy.props.IntVectorProperty(
|
||||
name="Original Shape",
|
||||
default=(100, 100, 100),
|
||||
options={'HIDDEN'}
|
||||
) # type: ignore
|
||||
|
||||
auto: bpy.props.BoolProperty(
|
||||
name="Auto Setting",
|
||||
default=False,
|
||||
options={'HIDDEN'}
|
||||
default=(100, 100, 100)
|
||||
) # type: ignore
|
||||
|
||||
scene_scale: bpy.props.FloatProperty(
|
||||
name="Scene Scale",
|
||||
name="Scene Scale (Bioxels Unit pre Blender Unit)",
|
||||
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",
|
||||
do_orient: bpy.props.BoolProperty(
|
||||
name="Orient to RAS",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
do_change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting",
|
||||
default=True,
|
||||
invert_scalar: bpy.props.BoolProperty(
|
||||
name="Invert Scalar (Background value maybe higher than object)",
|
||||
default=False,
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
files = get_data_files(self.filepath)
|
||||
name = Path(self.filepath).parent.name
|
||||
bioxels_name = Path(self.filepath).parent.name \
|
||||
if get_ext(self.filepath) in SEQUENCE_EXTS \
|
||||
else Path(self.filepath).name.removesuffix(get_ext(self.filepath))
|
||||
|
||||
import SimpleITK as sitk
|
||||
image = sitk.ReadImage(files)
|
||||
image = read_image(self.filepath, self.series_id)
|
||||
|
||||
bioxel_size = float(self.bioxel_size)
|
||||
orig_spacing = tuple(self.orig_spacing)
|
||||
bioxel_size = self.bioxel_size
|
||||
orig_spacing = self.orig_spacing
|
||||
image_spacing = image.GetSpacing()
|
||||
image_shape = image.GetSize()
|
||||
|
||||
@@ -145,11 +186,8 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
image_spacing[2] / orig_spacing[2] * bioxel_size
|
||||
)
|
||||
|
||||
bioxels_shape = (
|
||||
int(image_shape[0] / bioxel_size * orig_spacing[0]),
|
||||
int(image_shape[1] / bioxel_size * orig_spacing[1]),
|
||||
int(image_shape[2] / bioxel_size * orig_spacing[2]),
|
||||
)
|
||||
bioxels_shape = get_bioxels_shape(
|
||||
bioxel_size, image_shape, orig_spacing)
|
||||
|
||||
print("Resampling...")
|
||||
image = sitk.Resample(
|
||||
@@ -164,13 +202,14 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
outputPixelType=image.GetPixelID(),
|
||||
)
|
||||
|
||||
if self.do_orient:
|
||||
print("Orienting to RAS...")
|
||||
image = sitk.DICOMOrient(image, 'RAS')
|
||||
|
||||
array = sitk.GetArrayFromImage(image)
|
||||
orig_dtype = str(array.dtype)
|
||||
print(f"Coverting Dtype from {orig_dtype} to float...")
|
||||
array = array.astype(float)
|
||||
print("Oriented Origin:", image.GetOrigin())
|
||||
print("Oriented Direction:", image.GetDirection())
|
||||
|
||||
# return {'FINISHED'}
|
||||
|
||||
# ITK indices, by convention, are [i,j,k] while NumPy indices are [k,j,i]
|
||||
# https://www.slicer.org/wiki/Coordinate_systems
|
||||
@@ -179,27 +218,62 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
# R (ight) i -> k -> x
|
||||
# A (nterior) j -> j -> y
|
||||
# S (uperior) k -> i -> z
|
||||
|
||||
array = sitk.GetArrayFromImage(image)
|
||||
array = np.transpose(array)
|
||||
bioxels_max = float(np.max(array))
|
||||
bioxels_min = float(np.min(array))
|
||||
|
||||
orig_dtype = str(array.dtype)
|
||||
# print(f"Coverting Dtype from {orig_dtype} to float...")
|
||||
# array = array.astype(float)
|
||||
if array.ndim == 4:
|
||||
if array.shape[3] == 3 and self.read_as == "scalar":
|
||||
# RGB -> Grayscale
|
||||
array = np.dot(array[..., :3], [0.2989, 0.5870, 0.1140])
|
||||
else:
|
||||
array = array[:, :, :, 0]
|
||||
|
||||
bioxels_shape = array.shape
|
||||
|
||||
print("Bioxel Size:", bioxel_size)
|
||||
print("Bioxels Shape:", bioxels_shape)
|
||||
|
||||
bioxels_offset = 0.0
|
||||
if bioxels_min < 0 and orig_dtype[0] != "u":
|
||||
bioxels_offset = -bioxels_min
|
||||
|
||||
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))
|
||||
print("Offseted Max:", bioxels_max)
|
||||
print("Offseted Min:", bioxels_min)
|
||||
|
||||
# Build VDB
|
||||
grid = vdb.FloatGrid()
|
||||
grid.copyFromArray(array.copy())
|
||||
stat_table = [("Max", orig_max),
|
||||
("Min", orig_min),
|
||||
("Median", orig_median),
|
||||
("Percentile 80%", orig_percentile80),
|
||||
("Max (Offset)", bioxels_max),
|
||||
("Min (Offset)", bioxels_min)]
|
||||
|
||||
print("Bioxels Value Stat: Max:")
|
||||
for stat in stat_table:
|
||||
print("| {: >20} | {: >20} |".format(*stat))
|
||||
|
||||
# # 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
|
||||
# so we need to convert them into RAS
|
||||
@@ -210,22 +284,26 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
to_up='Y'
|
||||
).to_4x4()
|
||||
|
||||
origin = image.GetOrigin()
|
||||
direction = image.GetDirection()
|
||||
|
||||
mat_location = mathutils.Matrix.Translation(
|
||||
mathutils.Vector(image.GetOrigin())
|
||||
mathutils.Vector(origin)
|
||||
)
|
||||
|
||||
mat_rotation = mathutils.Matrix(
|
||||
np.array(image.GetDirection()).reshape((3, 3))
|
||||
np.array(direction).reshape((3, 3))
|
||||
).to_4x4()
|
||||
|
||||
mat_scale = mathutils.Matrix.Scale(
|
||||
bioxel_size, 4
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
grid.transform = vdb.createLinearTransform(transfrom.transposed())
|
||||
grid.name = "value"
|
||||
grid.name = "scalar"
|
||||
|
||||
preferences = context.preferences.addons[__package__].preferences
|
||||
vdb_dirpath = Path(preferences.cache_dir, 'VDBs')
|
||||
@@ -243,8 +321,8 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
bioxels_obj = bpy.context.active_object
|
||||
|
||||
# Set props to VDB object
|
||||
bioxels_obj.name = f"{name}_Bioxels"
|
||||
bioxels_obj.data.name = f"{name}_Bioxels"
|
||||
bioxels_obj.name = f"{bioxels_name}_Bioxels"
|
||||
bioxels_obj.data.name = f"{bioxels_name}_Bioxels"
|
||||
|
||||
# Make transformation
|
||||
scene_scale = float(self.scene_scale)
|
||||
@@ -289,8 +367,8 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index])
|
||||
|
||||
bioxels_obj.parent = container_obj
|
||||
container_obj.name = name
|
||||
container_obj.data.name = name
|
||||
container_obj.name = bioxels_name
|
||||
container_obj.data.name = bioxels_name
|
||||
container_obj.visible_camera = False
|
||||
container_obj.visible_diffuse = False
|
||||
container_obj.visible_glossy = False
|
||||
@@ -315,7 +393,7 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
# Create BioxelNodes to VDB object
|
||||
print(f"Creating BioxelNodes to the VDB...")
|
||||
|
||||
if self.do_add_segmentnode:
|
||||
if preferences.do_add_segmentnode:
|
||||
nodes = node_tree.nodes
|
||||
links = node_tree.links
|
||||
|
||||
@@ -324,11 +402,14 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
|
||||
segment_node = custom_nodes.add_node(
|
||||
nodes, 'BioxelNodes_Segment')
|
||||
|
||||
segment_node.inputs['Threshold'].default_value = bioxels_default_threshold
|
||||
|
||||
links.new(input_node.outputs[0], segment_node.inputs[0])
|
||||
links.new(segment_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
# Change render setting for better result
|
||||
if self.do_change_render_setting:
|
||||
if preferences.do_change_render_setting:
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
bpy.context.scene.cycles.volume_bounces = 12
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 64
|
||||
@@ -338,37 +419,78 @@ class ImportDICOMDialog(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# print(tuple(self.orig_shape))
|
||||
self.auto = True
|
||||
self.bioxel_size = 1
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
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)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
bioxels_shape = get_bioxels_shape(
|
||||
self.bioxel_size, self.orig_shape, self.orig_spacing)
|
||||
bioxel_count = bioxels_shape[0] * bioxels_shape[1] * bioxels_shape[2]
|
||||
text = f"Shape will be: {str(bioxels_shape)} {bioxel_count:,} "
|
||||
if bioxel_count > 100000000:
|
||||
text += "**TOO LARGE!**"
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "bioxel_size")
|
||||
row = panel.row()
|
||||
row.prop(self, "orig_spacing")
|
||||
panel.label(text=text)
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "read_as")
|
||||
if self.read_as == "labels":
|
||||
panel.prop(self, "label_index")
|
||||
else:
|
||||
panel.prop(self, "invert_scalar")
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "scene_scale")
|
||||
panel.prop(self, "do_orient")
|
||||
|
||||
|
||||
def get_series_ids(self, context):
|
||||
items = []
|
||||
for index, series_id in enumerate(self.series_ids):
|
||||
items.append((
|
||||
series_id.id,
|
||||
series_id.label,
|
||||
"",
|
||||
index
|
||||
))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
class ReadDICOM(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.read_dicom"
|
||||
bl_label = "Volume Data as Bioxels"
|
||||
bl_description = "Import Volume Data as Bioxels (VDB)."
|
||||
bl_description = "Import Volume Data as Bioxels (VDB)"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
subtype="FILE_PATH"
|
||||
) # type: ignore
|
||||
|
||||
ok: bpy.props.BoolProperty(
|
||||
default=True
|
||||
series_id: bpy.props.EnumProperty(
|
||||
name="Select Series",
|
||||
items=get_series_ids
|
||||
) # type: ignore
|
||||
|
||||
series_ids: bpy.props.CollectionProperty(
|
||||
type=BIOXELNODES_Series) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if Path(self.filepath).suffix not in SUPPORT_EXTS:
|
||||
self.report({"WARNING"}, "Not Supported extension.")
|
||||
ext = get_ext(self.filepath)
|
||||
if ext not in SUPPORT_EXTS:
|
||||
self.report({"WARNING"}, "Not supported extension.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
files = get_data_files(self.filepath)
|
||||
|
||||
import SimpleITK as sitk
|
||||
|
||||
image = sitk.ReadImage(files)
|
||||
image = read_image(self.filepath, self.series_id)
|
||||
|
||||
print("Collecting Meta Data...")
|
||||
print("Original Shape:", image.GetSize())
|
||||
@@ -376,11 +498,12 @@ class ReadDICOM(bpy.types.Operator):
|
||||
print("Original Origin:", image.GetOrigin())
|
||||
print("Original Direction:", image.GetDirection())
|
||||
|
||||
bpy.ops.bioxelnodes.import_dicom_dialog(
|
||||
bpy.ops.bioxelnodes.import_image_dialog(
|
||||
'INVOKE_DEFAULT',
|
||||
filepath=self.filepath,
|
||||
orig_shape=image.GetSize(),
|
||||
orig_spacing=image.GetSpacing()
|
||||
orig_spacing=image.GetSpacing(),
|
||||
series_id=self.series_id or ""
|
||||
)
|
||||
|
||||
self.report({"INFO"}, "Successfully Readed.")
|
||||
@@ -395,16 +518,43 @@ class ReadDICOM(bpy.types.Operator):
|
||||
if not self.filepath:
|
||||
return {'CANCELLED'}
|
||||
|
||||
show_message('Reading image data, it may take a while',
|
||||
show_message('Reading image data, it may take a while...',
|
||||
'Please be patient...')
|
||||
|
||||
if get_ext(self.filepath) == '.dcm':
|
||||
dir_path = Path(self.filepath).parent
|
||||
reader = sitk.ImageSeriesReader()
|
||||
reader.MetaDataDictionaryArrayUpdateOn()
|
||||
reader.LoadPrivateTagsOn()
|
||||
series_ids = reader.GetGDCMSeriesIDs(str(dir_path))
|
||||
|
||||
for _id in series_ids:
|
||||
series_id = self.series_ids.add()
|
||||
series_id.id = _id
|
||||
series_id.label = _id
|
||||
|
||||
if len(series_ids) > 1:
|
||||
context.window_manager.invoke_props_dialog(self, width=400)
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
self.series_id = series_ids[0]
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "series_id")
|
||||
layout.label(
|
||||
text="Reading image data, it may take a while...")
|
||||
layout.label(
|
||||
text="Please be patient...")
|
||||
|
||||
class ImportDICOM(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.import_dicom"
|
||||
|
||||
class ImportImage(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.import_image"
|
||||
bl_label = "Volume Data as Bioxels"
|
||||
bl_description = "Import Volume Data as Bioxels (VDB)."
|
||||
bl_description = "Import Volume Data as Bioxels (VDB)"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
@@ -423,21 +573,24 @@ class ImportDICOM(bpy.types.Operator):
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class BIOXELNODES_FH_ImportDicom(bpy.types.FileHandler):
|
||||
bl_idname = "BIOXELNODES_FH_ImportDicom"
|
||||
try:
|
||||
class BIOXELNODES_FH_ImportImage(bpy.types.FileHandler):
|
||||
bl_idname = "BIOXELNODES_FH_ImportImage"
|
||||
bl_label = "File handler for dicom import"
|
||||
bl_import_operator = "bioxelnodes.read_dicom"
|
||||
bl_file_extensions = ".dcm"
|
||||
bl_file_extensions = ";".join(FH_EXTS)
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
return (context.area and context.area.type == 'VIEW_3D')
|
||||
except:
|
||||
...
|
||||
|
||||
|
||||
class ExportVDB(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.export_vdb"
|
||||
bl_label = "Bioxels as VDB"
|
||||
bl_description = "Export Bioxels original VDB data."
|
||||
bl_description = "Export Bioxels original VDB data"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
|
||||
221
bioxelnodes/io_points.py
Normal file
221
bioxelnodes/io_points.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# TODO: WIP
|
||||
import bpy
|
||||
import bmesh
|
||||
|
||||
|
||||
def create_points_obj(points, name="points"):
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
|
||||
)
|
||||
obj = bpy.context.active_object
|
||||
mesh = obj.data
|
||||
|
||||
bm = bmesh.new()
|
||||
value_layer = bm.verts.layers.float.new('value')
|
||||
|
||||
for point in points:
|
||||
pos = point['pos']
|
||||
value = point['value']
|
||||
vert = bm.verts.new(pos) # add a new vert
|
||||
vert[value_layer] = value
|
||||
|
||||
# make the bmesh the object's mesh
|
||||
bm.to_mesh(mesh)
|
||||
bm.free() # always do this when finished
|
||||
return obj
|
||||
|
||||
|
||||
# class ImportDICOMPointsDialog(bpy.types.Operator):
|
||||
# bl_idname = "bioxelnodes.import_dicom_points_dialog"
|
||||
# bl_label = "Volume Data as Bioxels"
|
||||
# bl_description = "Import Volume Data as Bioxels (VDB)"
|
||||
# bl_options = {'UNDO'}
|
||||
|
||||
# filepath: bpy.props.StringProperty(
|
||||
# subtype="FILE_PATH",
|
||||
# options={'HIDDEN'}
|
||||
# ) # type: ignore
|
||||
|
||||
# bioxels_shape: bpy.props.IntVectorProperty(
|
||||
# name="Bioxels Shape (ReadOnly)",
|
||||
# min=0,
|
||||
# default=(100, 100, 100)
|
||||
# ) # type: ignore
|
||||
|
||||
# bioxel_size: bpy.props.FloatProperty(
|
||||
# name="Bioxel Size",
|
||||
# soft_min=0.1, soft_max=10.0,
|
||||
# min=1e-2, max=1e2,
|
||||
# default=1,
|
||||
# update=on_bioxel_size_changed
|
||||
# ) # type: ignore
|
||||
|
||||
# orig_spacing: bpy.props.FloatVectorProperty(
|
||||
# name="Original Spacing",
|
||||
# default=(1, 1, 1),
|
||||
# update=on_orig_spacing_changed
|
||||
# ) # type: ignore
|
||||
|
||||
# orig_shape: bpy.props.IntVectorProperty(
|
||||
# name="Original Shape",
|
||||
# default=(100, 100, 100),
|
||||
# options={'HIDDEN'}
|
||||
# ) # type: ignore
|
||||
|
||||
# auto: bpy.props.BoolProperty(
|
||||
# name="Auto Setting",
|
||||
# default=False,
|
||||
# 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,
|
||||
# ) # type: ignore
|
||||
|
||||
# do_change_render_setting: bpy.props.BoolProperty(
|
||||
# name="Change Render Setting",
|
||||
# default=True,
|
||||
# ) # type: ignore
|
||||
|
||||
# def execute(self, context):
|
||||
# files = get_data_files(self.filepath)
|
||||
# name = Path(self.filepath).parent.name
|
||||
|
||||
# image = sitk.ReadImage(files)
|
||||
|
||||
# bioxel_size = float(self.bioxel_size)
|
||||
# orig_spacing = tuple(self.orig_spacing)
|
||||
# image_spacing = image.GetSpacing()
|
||||
# image_shape = image.GetSize()
|
||||
|
||||
# bioxels_spacing = (
|
||||
# image_spacing[0] / orig_spacing[0] * bioxel_size,
|
||||
# image_spacing[1] / orig_spacing[1] * bioxel_size,
|
||||
# image_spacing[2] / orig_spacing[2] * bioxel_size
|
||||
# )
|
||||
|
||||
# bioxels_shape = (
|
||||
# int(image_shape[0] / bioxel_size * orig_spacing[0]),
|
||||
# int(image_shape[1] / bioxel_size * orig_spacing[1]),
|
||||
# int(image_shape[2] / bioxel_size * orig_spacing[2]),
|
||||
# )
|
||||
|
||||
# print("Resampling...")
|
||||
# image = sitk.Resample(
|
||||
# image1=image,
|
||||
# size=bioxels_shape,
|
||||
# transform=sitk.Transform(),
|
||||
# interpolator=sitk.sitkLinear,
|
||||
# outputOrigin=image.GetOrigin(),
|
||||
# outputSpacing=bioxels_spacing,
|
||||
# outputDirection=image.GetDirection(),
|
||||
# defaultPixelValue=0,
|
||||
# outputPixelType=image.GetPixelID(),
|
||||
# )
|
||||
|
||||
# print("Orienting to RAS...")
|
||||
# image = sitk.DICOMOrient(image, 'RAS')
|
||||
|
||||
# array = sitk.GetArrayFromImage(image)
|
||||
# orig_dtype = str(array.dtype)
|
||||
# print(f"Coverting Dtype from {orig_dtype} to float...")
|
||||
# array = array.astype(float)
|
||||
|
||||
# # ITK indices, by convention, are [i,j,k] while NumPy indices are [k,j,i]
|
||||
# # https://www.slicer.org/wiki/Coordinate_systems
|
||||
|
||||
# # ITK Numpy 3D
|
||||
# # R (ight) i -> k -> x
|
||||
# # A (nterior) j -> j -> y
|
||||
# # S (uperior) k -> i -> z
|
||||
|
||||
# array = np.transpose(array)
|
||||
# bioxels_max = float(np.max(array))
|
||||
# bioxels_min = float(np.min(array))
|
||||
# bioxels_shape = array.shape
|
||||
|
||||
# print("Bioxel Size:", bioxel_size)
|
||||
# print("Bioxels Shape:", bioxels_shape)
|
||||
|
||||
# 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)
|
||||
|
||||
# points = []
|
||||
# for x in range(array.shape[0]):
|
||||
# for y in range(array.shape[1]):
|
||||
# for z in range(array.shape[2]):
|
||||
# points.append({
|
||||
# "pos": (x, y, z),
|
||||
# "value": array[x, y, z],
|
||||
# })
|
||||
|
||||
# # # 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
|
||||
|
||||
# obj = create_points_obj(points)
|
||||
|
||||
# # 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
|
||||
# )
|
||||
|
||||
# obj.matrix_world = mat_ras2blender @ mat_scene_scale
|
||||
|
||||
# self.report({"INFO"}, "Successfully Imported")
|
||||
|
||||
# return {'FINISHED'}
|
||||
|
||||
# def invoke(self, context, event):
|
||||
# # print(tuple(self.orig_shape))
|
||||
# self.auto = True
|
||||
# self.bioxel_size = 1
|
||||
# context.window_manager.invoke_props_dialog(self)
|
||||
# return {'RUNNING_MODAL'}
|
||||
@@ -1,6 +1,6 @@
|
||||
import bpy
|
||||
from .convert import ConvertToMesh
|
||||
from .io import ExportVDB, ImportDICOM
|
||||
from .io import ExportVDB, ImportImage
|
||||
from .misc import SaveBioxels
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ class ObjectMenu(bpy.types.Menu):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(SaveBioxels.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(ConvertToMesh.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(SaveBioxels.bl_idname)
|
||||
|
||||
|
||||
def TOPBAR_FILE_IMPORT(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator(ImportDICOM.bl_idname)
|
||||
layout.operator(ImportImage.bl_idname)
|
||||
|
||||
|
||||
def TOPBAR_FILE_EXPORT(self, context):
|
||||
@@ -30,17 +30,16 @@ def TOPBAR_FILE_EXPORT(self, context):
|
||||
def VIEW3D_OBJECT(self, context):
|
||||
layout = self.layout
|
||||
layout.menu(ObjectMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
|
||||
|
||||
def create_menu():
|
||||
def register():
|
||||
bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
|
||||
bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.prepend(VIEW3D_OBJECT)
|
||||
|
||||
|
||||
def remove_menu():
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT)
|
||||
@@ -40,7 +40,7 @@ else:
|
||||
|
||||
MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Present',
|
||||
'label': 'Presets',
|
||||
'icon': 'MESH_MONKEY',
|
||||
'items': [
|
||||
{
|
||||
@@ -52,17 +52,17 @@ MENU_ITEMS = [
|
||||
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': '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"),
|
||||
# ])
|
||||
# }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -71,7 +71,7 @@ MENU_ITEMS = [
|
||||
'items': [
|
||||
{
|
||||
'label': 'Segment by Level',
|
||||
'icon': 'AREA_JOIN',
|
||||
'icon': 'EMPTY_SINGLE_ARROW',
|
||||
'node_type': 'BioxelNodes_SegmentByLevel',
|
||||
'node_description': '',
|
||||
'node_callback': "\n".join([
|
||||
@@ -81,24 +81,24 @@ MENU_ITEMS = [
|
||||
},
|
||||
{
|
||||
'label': 'Segment by Range',
|
||||
'icon': 'AREA_SWAP',
|
||||
'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': 'OUTLINER_OB_FORCE_FIELD',
|
||||
'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': '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"),
|
||||
# ])
|
||||
# }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -112,8 +112,13 @@ MENU_ITEMS = [
|
||||
'node_description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
}
|
||||
]
|
||||
|
||||
NODE_FILE = "BioxelNodes_4.0"
|
||||
|
||||
if bpy.app.version >= (4, 1, 0):
|
||||
MENU_ITEMS.append({
|
||||
'label': 'Slicer',
|
||||
'icon': 'CLIPUV_DEHLT',
|
||||
'items': [
|
||||
@@ -136,12 +141,21 @@ MENU_ITEMS = [
|
||||
'node_description': '',
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
})
|
||||
NODE_FILE = "BioxelNodes_4.1"
|
||||
|
||||
|
||||
custom_nodes = CustomNodes(
|
||||
menu_items=MENU_ITEMS,
|
||||
nodes_file=Path(__file__).parent/"assets/Nodes/BioxelNodes.blend",
|
||||
nodes_file=Path(__file__).parent / f"assets/Nodes/{NODE_FILE}.blend",
|
||||
root_label='Bioxel Nodes',
|
||||
root_icon="FILE_VOLUME",
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
custom_nodes.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
custom_nodes.unregister()
|
||||
|
||||
@@ -7,16 +7,30 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
|
||||
bl_idname = __package__
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="VDB Cache Directory",
|
||||
name="Set Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
||||
) # type: ignore
|
||||
|
||||
do_add_segmentnode: bpy.props.BoolProperty(
|
||||
name="Add Segment Node",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
do_change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Configuration")
|
||||
layout.prop(self, 'cache_dir', text='Set Cache Directory')
|
||||
|
||||
# ExternalPackagePreferences Config
|
||||
self.requirements_dir = str(Path(__file__).parent)
|
||||
super().draw(context)
|
||||
|
||||
layout.label(text="Configuration")
|
||||
layout.prop(self, 'cache_dir')
|
||||
|
||||
layout.prop(self, "do_change_render_setting")
|
||||
layout.prop(self, "do_add_segmentnode")
|
||||
|
||||
5
bioxelnodes/props.py
Normal file
5
bioxelnodes/props.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import bpy
|
||||
|
||||
class BIOXELNODES_Series(bpy.types.PropertyGroup):
|
||||
id: bpy.props.StringProperty() # type: ignore
|
||||
label: bpy.props.StringProperty() # type: ignore
|
||||
@@ -2,10 +2,13 @@ import bpy
|
||||
import mathutils
|
||||
|
||||
|
||||
def get_node_by_type(nodes, type_name: str):
|
||||
return [node for node in nodes if type(node).__name__ == type_name]
|
||||
def get_type(cls):
|
||||
return type(cls).__name__
|
||||
|
||||
|
||||
def get_node_by_type(nodes, type_name: str):
|
||||
return [node for node in nodes if get_type(node) == type_name]
|
||||
|
||||
def show_message(message="", title="Message Box", icon='INFO'):
|
||||
|
||||
def draw(self, context):
|
||||
@@ -81,6 +84,7 @@ def get_bioxels_obj(current_obj):
|
||||
|
||||
return bioxels_obj
|
||||
|
||||
|
||||
def get_all_bioxels_objs():
|
||||
bioxels_objs = []
|
||||
|
||||
@@ -91,7 +95,8 @@ def get_all_bioxels_objs():
|
||||
|
||||
return list(set(bioxels_objs))
|
||||
|
||||
def extract_last_number(text):
|
||||
|
||||
def get_text_index(text):
|
||||
# Initialize an empty string to store the digits
|
||||
digits = ""
|
||||
|
||||
@@ -110,7 +115,7 @@ def extract_last_number(text):
|
||||
# Reverse the digits string to get the correct order
|
||||
last_number = digits[::-1]
|
||||
|
||||
return int(last_number)
|
||||
return int(last_number) if last_number else None
|
||||
|
||||
|
||||
def add_driver(target_prop, var_sources, expression):
|
||||
|
||||
Reference in New Issue
Block a user