Files
Bioxel/mednodes/dicom.py
2024-04-09 22:19:38 +08:00

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)