201 lines
6.1 KiB
Python
201 lines
6.1 KiB
Python
import bpy
|
|
from bpy_extras.io_utils import axis_conversion, ImportHelper
|
|
import pyopenvdb as vdb
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
import mathutils
|
|
from .nodes import custom_nodes
|
|
|
|
|
|
class ImportDICOM(bpy.types.Operator, ImportHelper):
|
|
bl_idname = "mednodes.import_dicom"
|
|
bl_label = "DICOM as VDB (.dcm)"
|
|
bl_description = "Load DICOM as VDB"
|
|
bl_options = {'UNDO'}
|
|
|
|
filepath: bpy.props.StringProperty(
|
|
subtype="FILE_PATH"
|
|
) # type: ignore
|
|
|
|
global_scale: bpy.props.FloatProperty(
|
|
name="Scale",
|
|
soft_min=0.001, soft_max=100.0,
|
|
min=1e-6, max=1e6,
|
|
default=0.01,
|
|
) # type: ignore
|
|
|
|
do_resample: bpy.props.BoolProperty(
|
|
name="Uniform Spacing (Resample)",
|
|
default=True,
|
|
) # type: ignore
|
|
|
|
do_change_render_setting: bpy.props.BoolProperty(
|
|
name="Change Render Setting",
|
|
default=True,
|
|
) # type: ignore
|
|
|
|
def execute(self, context):
|
|
import SimpleITK as sitk
|
|
|
|
preferences = context.preferences.addons[__package__].preferences
|
|
|
|
file_path: Path = Path(self.filepath).resolve()
|
|
name = file_path.parent.stem
|
|
|
|
reader = sitk.ImageSeriesReader()
|
|
dicom_names = reader.GetGDCMSeriesFileNames(
|
|
file_path.parent.as_posix())
|
|
reader.SetFileNames(dicom_names)
|
|
image = reader.Execute()
|
|
|
|
print("Spacing:", image.GetSpacing())
|
|
print("Origin:", image.GetOrigin())
|
|
print("Direction:", image.GetDirection())
|
|
|
|
image = sitk.DICOMOrient(image, 'LPS')
|
|
|
|
size = image.GetSize()
|
|
spacing = image.GetSpacing()
|
|
origin = image.GetOrigin()
|
|
direction = image.GetDirection()
|
|
|
|
print("Size:", size)
|
|
print("Spacing:", spacing)
|
|
print("Origin:", origin)
|
|
print("Direction:", direction)
|
|
|
|
if self.do_resample:
|
|
resampled_spacing = (1, 1, 1)
|
|
resampled_size = (
|
|
int(size[0]*spacing[0]),
|
|
int(size[1]*spacing[1]),
|
|
int(size[2]*spacing[2]),
|
|
)
|
|
|
|
print("Resampling...")
|
|
image = sitk.Resample(
|
|
image1=image,
|
|
size=resampled_size,
|
|
transform=sitk.Transform(),
|
|
interpolator=sitk.sitkLinear,
|
|
outputOrigin=origin,
|
|
outputSpacing=resampled_spacing,
|
|
outputDirection=direction,
|
|
defaultPixelValue=0,
|
|
outputPixelType=image.GetPixelID(),
|
|
)
|
|
print("Resampled Size:", resampled_size)
|
|
|
|
spacing = resampled_spacing
|
|
size = resampled_size
|
|
|
|
array = sitk.GetArrayFromImage(image)
|
|
|
|
# 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
|
|
# L (eft) i -> k -> x
|
|
# P (osterior) j -> j -> y
|
|
# S (uperior) k -> i -> z
|
|
|
|
array = np.transpose(array)
|
|
value_max = float(np.max(array))
|
|
value_min = float(np.min(array))
|
|
|
|
value_offset = 0.0
|
|
if value_min < 0:
|
|
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("offset max value:", value_max)
|
|
print("offset min value:", value_min)
|
|
|
|
# Build VDB
|
|
grid = vdb.FloatGrid()
|
|
grid.copyFromArray(array.copy())
|
|
grid.name = "density"
|
|
|
|
vdb_dirpath = Path(preferences.cache_dir, 'VDBs')
|
|
vdb_dirpath.mkdir(parents=True, exist_ok=True)
|
|
vdb_path = Path(vdb_dirpath, f"{uuid4()}.vdb")
|
|
vdb.write(vdb_path.as_posix(), grids=[grid])
|
|
|
|
# Read VDB
|
|
bpy.ops.object.volume_import(
|
|
filepath=vdb_path.as_posix(), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
|
|
vdb_obj = bpy.context.active_object
|
|
|
|
# Set props to VDB object
|
|
vdb_obj.name = name
|
|
vdb_obj.data.name = name
|
|
|
|
vdb_obj['value_max'] = value_max
|
|
vdb_obj['value_min'] = value_min
|
|
vdb_obj['value_offset'] = value_offset
|
|
|
|
vdb_obj['size'] = size
|
|
vdb_obj['spacing'] = spacing
|
|
vdb_obj['origin'] = origin
|
|
|
|
# Make transformation
|
|
|
|
# (S)uperior -Z -> Y
|
|
# (P)osterior -Y -> Z
|
|
axis_rot = axis_conversion(
|
|
from_forward='-Z',
|
|
from_up='-Y',
|
|
to_forward='Y',
|
|
to_up='Z'
|
|
).to_4x4()
|
|
|
|
mat_loc = mathutils.Matrix.Translation(
|
|
mathutils.Vector(origin) * self.global_scale)
|
|
mat_sca = mathutils.Matrix.Scale(self.global_scale, 4)
|
|
mat_rot = mathutils.Matrix(
|
|
np.array(direction).reshape((3, 3))
|
|
).to_4x4()
|
|
|
|
vdb_obj.matrix_world = axis_rot @ mat_loc @ mat_rot @ mat_sca
|
|
|
|
# Wrapper a Locator
|
|
bpy.ops.object.empty_add(
|
|
type='PLAIN_AXES',
|
|
align='WORLD',
|
|
location=(0, 0, 0),
|
|
scale=(1, 1, 1)
|
|
)
|
|
loc_obj = bpy.context.active_object
|
|
loc_obj.name = f"LOC_{name}"
|
|
vdb_obj.parent = loc_obj
|
|
|
|
# Create MedNodes to VDB object
|
|
bpy.context.view_layer.objects.active = vdb_obj
|
|
bpy.ops.node.new_geometry_nodes_modifier()
|
|
modifier = vdb_obj.modifiers[0]
|
|
nodes = modifier.node_group.nodes
|
|
links = modifier.node_group.links
|
|
|
|
input_node = nodes.get("Group Input")
|
|
output_node = nodes.get("Group Output")
|
|
segment_node = custom_nodes.add_node(nodes, 'MedNodes_Segment')
|
|
|
|
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:
|
|
bpy.context.scene.render.engine = 'CYCLES'
|
|
bpy.context.scene.cycles.volume_bounces = 12
|
|
bpy.context.scene.cycles.transparent_max_bounces = 64
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
def MEDNODES_add_topbar_menu(self, context):
|
|
layout = self.layout
|
|
layout.operator(ImportDICOM.bl_idname)
|