Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21e0b85662 | ||
| 38fb888f00 | |||
|
|
2429eef5e0 | ||
| 2563f398da | |||
| 0cad35e2da | |||
| 65edf40164 | |||
| c3f88b7846 |
Binary file not shown.
@@ -3,6 +3,7 @@ import numpy as np
|
||||
|
||||
from . import scipy
|
||||
from . import skimage as ski
|
||||
from . import scipy as ndi
|
||||
|
||||
# 3rd-party
|
||||
import transforms3d
|
||||
@@ -97,36 +98,43 @@ class Layer():
|
||||
_mask = np.expand_dims(_mask, axis=-1)
|
||||
self.data = _mask * value + (1-_mask) * self.data
|
||||
|
||||
def resize(self, shape:tuple, progress_callback=None):
|
||||
def resize(self, shape: tuple, progress_callback=None):
|
||||
if len(shape) != 3:
|
||||
raise Exception("Shape must be 3 dim")
|
||||
|
||||
data = self.data
|
||||
order = 0 if self.dtype == bool else 1
|
||||
|
||||
# TXYZC > TXYZ
|
||||
if self.kind in ['label', 'scalar']:
|
||||
data = np.amax(data, -1)
|
||||
|
||||
if self.kind in ['scalar']:
|
||||
dtype = data.dtype
|
||||
data = data.astype(np.float32)
|
||||
# if self.kind in ['scalar']:
|
||||
# dtype = data.dtype
|
||||
# data = data.astype(np.float32)
|
||||
|
||||
data_frames = ()
|
||||
for f in range(self.frame_count):
|
||||
if progress_callback:
|
||||
progress_callback(f, self.frame_count)
|
||||
|
||||
frame = ski.resize(data[f, :, :, :],
|
||||
shape,
|
||||
preserve_range=True,
|
||||
anti_aliasing=data.dtype.kind != "b")
|
||||
# frame = ski.resize(data[f, :, :, :],
|
||||
# shape,
|
||||
# preserve_range=True,
|
||||
# anti_aliasing=data.dtype.kind != "b")
|
||||
|
||||
factors = np.divide(self.shape, shape)
|
||||
zoom_factors = [1 / f for f in factors]
|
||||
frame = ndi.zoom(data[f, :, :, :],
|
||||
zoom_factors,
|
||||
order=order)
|
||||
|
||||
data_frames += (frame,)
|
||||
|
||||
data = np.stack(data_frames)
|
||||
|
||||
if self.kind in ['scalar']:
|
||||
data = data.astype(dtype)
|
||||
# if self.kind in ['scalar']:
|
||||
# data = data.astype(dtype)
|
||||
|
||||
# TXYZ > TXYZC
|
||||
if self.kind in ['label', 'scalar']:
|
||||
|
||||
@@ -14,22 +14,23 @@ Convert any volumetric data to 3D numpy array with order TXYZC
|
||||
"""
|
||||
|
||||
SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
|
||||
'.ome.tiff', '.ome.tif',
|
||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||
'.mrc', '.mrc.gz', '.map', '.map.gz',
|
||||
'.bmp', '.BMP',
|
||||
'.png', '.PNG',
|
||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||
'.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',
|
||||
'.ome.tiff', '.ome.tif',
|
||||
'.mrc', '.mrc.gz', '.map', '.map.gz']
|
||||
'.gz']
|
||||
|
||||
OME_EXTS = ['.ome.tiff', '.ome.tif',
|
||||
'.tif', '.TIF', '.tiff', '.TIFF']
|
||||
@@ -68,7 +69,7 @@ def get_file_name(filepath: Path):
|
||||
return filepath.name.removesuffix(ext).replace(" ", "-")
|
||||
|
||||
|
||||
def get_file_index(filepath: Path):
|
||||
def get_file_number(filepath: Path) -> str:
|
||||
name = get_file_name(filepath)
|
||||
digits = ""
|
||||
|
||||
@@ -85,15 +86,18 @@ def get_file_index(filepath: Path):
|
||||
break
|
||||
|
||||
# Reverse the digits string to get the correct order
|
||||
last_number = digits[::-1]
|
||||
|
||||
return int(last_number) if last_number != "" else 0
|
||||
return digits[::-1]
|
||||
|
||||
|
||||
def get_sequence_name(filepath: Path) -> str:
|
||||
name = get_file_name(filepath)
|
||||
index = get_file_index(filepath)
|
||||
return name.removesuffix(str(index))
|
||||
number = get_file_number(filepath)
|
||||
return name.removesuffix(number)
|
||||
|
||||
|
||||
def get_sequence_index(filepath: Path) -> int:
|
||||
number = get_file_number(filepath)
|
||||
return int(number) if number != "" else 0
|
||||
|
||||
|
||||
def collect_sequence(filepath: Path):
|
||||
@@ -102,9 +106,10 @@ def collect_sequence(filepath: Path):
|
||||
if f.is_file() \
|
||||
and get_ext(filepath) == get_ext(f) \
|
||||
and get_sequence_name(filepath) == get_sequence_name(f):
|
||||
index = get_file_index(f)
|
||||
index = get_sequence_index(f)
|
||||
file_dict[index] = f
|
||||
|
||||
# reomve isolated seq file
|
||||
for key in file_dict.copy().keys():
|
||||
if not file_dict.get(key+1) \
|
||||
and not file_dict.get(key-1):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "bioxelnodes"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
name = "Bioxel Nodes"
|
||||
tagline = "For scientific volumetric data visualization in Blender"
|
||||
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
||||
|
||||
@@ -24,19 +24,6 @@ from .utils import (change_render_setting, get_cache_dir, get_preferences,
|
||||
import SimpleITK as sitk
|
||||
import transforms3d
|
||||
|
||||
FH_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
|
||||
'.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'
|
||||
'.ome.tiff', '.ome.tif',
|
||||
'.mrc', '.mrc.gz', '.map', '.map.gz']
|
||||
|
||||
|
||||
def get_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple):
|
||||
shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]),
|
||||
@@ -69,23 +56,19 @@ class ImportVolumetricData():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
|
||||
|
||||
read_as = "scalar"
|
||||
|
||||
def execute(self, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
data_path = Path(self.filepath).resolve()
|
||||
ext = get_ext(data_path)
|
||||
if ext not in SUPPORT_EXTS:
|
||||
self.report({"WARNING"}, "Not supported format.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if len(container_objs) > 0:
|
||||
bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT',
|
||||
filepath=self.filepath,
|
||||
directory=self.directory,
|
||||
container_obj_name=container_objs[0].name,
|
||||
read_as=self.read_as)
|
||||
else:
|
||||
bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT',
|
||||
filepath=self.filepath,
|
||||
directory=self.directory,
|
||||
skip_read_as=True,
|
||||
read_as=self.read_as)
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -113,7 +96,7 @@ class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
|
||||
bl_idname = "BIOXELNODES_FH_ImportVolumetricData"
|
||||
bl_label = "File handler for dicom import"
|
||||
bl_import_operator = "bioxelnodes.parse_volumetric_data"
|
||||
bl_file_extensions = ";".join(FH_EXTS)
|
||||
bl_file_extensions = ";".join(SUPPORT_EXTS)
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
@@ -135,21 +118,28 @@ def get_series_ids(self, context):
|
||||
|
||||
class ParseVolumetricData(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.parse_volumetric_data"
|
||||
bl_label = "Import Volumetric Data"
|
||||
bl_label = "Import Volumetric Data (BioxelNodes)"
|
||||
bl_description = "Import Volumetric Data as Layer"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
meta = None
|
||||
thread = None
|
||||
_timer = None
|
||||
container_obj_name = ""
|
||||
|
||||
progress: bpy.props.FloatProperty(name="Progress",
|
||||
options={"SKIP_SAVE"},
|
||||
default=1) # type: ignore
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
|
||||
container_obj_name: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
skip_read_as: bpy.props.BoolProperty(name="Skip Read As",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
skip_series_select: bpy.props.BoolProperty(name="Skip Sries Select",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
read_as: bpy.props.EnumProperty(name="Read as",
|
||||
default="scalar",
|
||||
@@ -166,7 +156,7 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
data_path = Path(self.filepath).resolve()
|
||||
ext = get_ext(data_path)
|
||||
if ext not in SUPPORT_EXTS:
|
||||
self.report({"WARNING"}, "Not supported extension.")
|
||||
self.report({"WARNING"}, "Not supported format.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
print("Collecting Meta Data...")
|
||||
@@ -297,12 +287,19 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not self.filepath and not self.directory:
|
||||
if not self.filepath:
|
||||
return {'CANCELLED'}
|
||||
|
||||
data_path = Path(self.filepath).resolve()
|
||||
ext = get_ext(data_path)
|
||||
|
||||
container_objs = get_container_objs_from_selection()
|
||||
if len(container_objs) > 0:
|
||||
self.container_obj_name = container_objs[0].name
|
||||
|
||||
title = f"Add to **{self.container_obj_name}**" \
|
||||
if self.container_obj_name != "" else f"Init a Container"
|
||||
|
||||
# Series Selection
|
||||
if ext in DICOM_EXTS:
|
||||
data_dirpath = data_path.parent
|
||||
@@ -358,9 +355,10 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
series_item.label = value
|
||||
|
||||
if len(series_items.keys()) > 1:
|
||||
self.skip_series_select = False
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=400,
|
||||
title="Which series to import?")
|
||||
title=title)
|
||||
return {'RUNNING_MODAL'}
|
||||
elif len(series_items.keys()) == 1:
|
||||
self.series_id = list(series_items.keys())[0]
|
||||
@@ -368,27 +366,37 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
self.report({"ERROR"}, "Get no vaild series.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.skip_read_as:
|
||||
self.execute(context)
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=400,
|
||||
title=title)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if not self.skip_read_as:
|
||||
layout.label(
|
||||
text='Detect multi-series in DICOM, pick one')
|
||||
text='What kind is the data read as?')
|
||||
layout.prop(self, "read_as")
|
||||
if not self.skip_series_select:
|
||||
layout.label(text='Which series to import?')
|
||||
layout.prop(self, "series_id")
|
||||
|
||||
|
||||
def get_sequence_sources(self, context):
|
||||
items = [("-1", "None (Static)", "")]
|
||||
items = [("-1", "None (1 frame)", "")]
|
||||
orig_shape = tuple(self.orig_shape)
|
||||
if self.frame_count > 1:
|
||||
items.append(("0", f"Frame (Get {self.frame_count} frames)", ""))
|
||||
items.append(("0", f"Frame ({self.frame_count} frames)", ""))
|
||||
elif self.frame_count == 1 and self.channel_count > 1:
|
||||
items.append(("4", f"Channel (Get {self.channel_count} frames)", ""))
|
||||
items.append(("4", f"Channel ({self.channel_count} frames)", ""))
|
||||
elif self.frame_count == 1 and self.channel_count == 1:
|
||||
items.append(("1", f"X (Get {orig_shape[0]} frames)", ""))
|
||||
items.append(("2", f"Y (Get {orig_shape[1]} frames)", ""))
|
||||
items.append(("3", f"Z (Get {orig_shape[2]} frames)", ""))
|
||||
items.append(("1", f"X ({orig_shape[0]} frames)", ""))
|
||||
items.append(("2", f"Y ({orig_shape[1]} frames)", ""))
|
||||
items.append(("3", f"Z ({orig_shape[2]} frames)", ""))
|
||||
|
||||
return items
|
||||
|
||||
@@ -681,12 +689,9 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.read_as == "label":
|
||||
volume_dtype = "Label"
|
||||
elif self.read_as == "scalar":
|
||||
volume_dtype = "Scalar"
|
||||
title = f"As {volume_dtype} Opitons (Add to Container: {self.container_obj_name})" \
|
||||
if self.container_obj_name != "" else f"As {volume_dtype} Options (Init a Container)"
|
||||
layer_kind = self.read_as.capitalize()
|
||||
title = f"Add to **{self.container_obj_name}**, As {layer_kind}" \
|
||||
if self.container_obj_name != "" else f"Init a Container, As {layer_kind}"
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500,
|
||||
title=title)
|
||||
|
||||
@@ -232,7 +232,6 @@ class CombineLabels(bpy.types.Operator, LabelOperator):
|
||||
return {'FINISHED'}
|
||||
base_obj = label_objs[0]
|
||||
label_objs = label_objs[1:]
|
||||
container_obj = base_obj.parent
|
||||
|
||||
base_layer = obj_to_layer(base_obj)
|
||||
modified_layer = base_layer.copy()
|
||||
@@ -241,7 +240,7 @@ class CombineLabels(bpy.types.Operator, LabelOperator):
|
||||
for label_obj in label_objs:
|
||||
label_layer = obj_to_layer(label_obj)
|
||||
label_layer.resize(base_layer.shape)
|
||||
modified_layer.data = np.maximum(base_layer.data, label_layer.data)
|
||||
modified_layer.data = np.maximum(modified_layer.data, label_layer.data)
|
||||
label_names.append(label_layer.name)
|
||||
|
||||
modified_layer.name = f"{'-'.join(label_names)}-Combined"
|
||||
|
||||
@@ -61,8 +61,8 @@ class SaveStagedData(bpy.types.Operator):
|
||||
bl_label = "Save Staged Data"
|
||||
bl_description = "Save all staged data in this file for sharing"
|
||||
|
||||
save_layer: bpy.props.BoolProperty(
|
||||
name="Save Layer VDB Cache",
|
||||
save_cache: bpy.props.BoolProperty(
|
||||
name="Save Layer Caches",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
@@ -112,11 +112,11 @@ class SaveStagedData(bpy.types.Operator):
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
||||
|
||||
if self.save_layer:
|
||||
if self.save_cache:
|
||||
fails = []
|
||||
for layer in get_all_layer_objs():
|
||||
try:
|
||||
save_layer(layer, self.cache_dir)
|
||||
save_layer_cache(layer, self.cache_dir)
|
||||
except:
|
||||
fails.append(layer)
|
||||
|
||||
@@ -140,7 +140,7 @@ class SaveStagedData(bpy.types.Operator):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_layer")
|
||||
panel.prop(self, "save_cache")
|
||||
panel.prop(self, "cache_dir")
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_lib")
|
||||
@@ -148,7 +148,7 @@ class SaveStagedData(bpy.types.Operator):
|
||||
|
||||
|
||||
class SaveCaches(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_layers"
|
||||
bl_idname = "bioxelnodes.save_caches"
|
||||
bl_label = "Save Caches"
|
||||
bl_description = "Save Container's caches to directory."
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "bioxelnodes"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
description = ""
|
||||
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user