init
This commit is contained in:
200
mednodes/dicom.py
Normal file
200
mednodes/dicom.py
Normal file
@@ -0,0 +1,200 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user