feat: support more format
feat: support .mrc .map feat: support .ome.tif feat: improved user experience for importing feat: better contrast algorithm
This commit is contained in:
@@ -9,7 +9,7 @@ bl_info = {
|
||||
"author": "Ma Nan",
|
||||
"description": "",
|
||||
"blender": (4, 1, 0),
|
||||
"version": (0, 2, 7),
|
||||
"version": (0, 2, 8),
|
||||
"location": "File -> Import",
|
||||
"warning": "",
|
||||
"category": "Node"
|
||||
@@ -20,6 +20,8 @@ auto_load.init()
|
||||
|
||||
def register():
|
||||
auto_load.register()
|
||||
bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty()
|
||||
bpy.types.WindowManager.bioxelnodes_progress_text = bpy.props.StringProperty()
|
||||
menus.add()
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
4
bioxelnodes/exceptions.py
Normal file
4
bioxelnodes/exceptions.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class CancelledByUser(Exception):
|
||||
def __init__(self):
|
||||
message = 'Cancelled by user'
|
||||
super().__init__(message)
|
||||
1420
bioxelnodes/io.py
1420
bioxelnodes/io.py
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import bpy
|
||||
from .utils import get_container_from_selection
|
||||
from .operators import (AddPieCutter, AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
|
||||
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange, PickBboxWire, PickMesh, PickVolume)
|
||||
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer
|
||||
from .io import ExportVolumetricData, ImportAsLabelLayer, ImportAsScalarLayer
|
||||
from .save import CleanAllCaches, ReLinkNodes, SaveLayers, SaveStagedData
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
||||
layout.operator(FillByLabel.bl_idname)
|
||||
layout.operator(CombineLabels.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(ExportVolumeData.bl_idname)
|
||||
layout.operator(ExportVolumetricData.bl_idname)
|
||||
|
||||
|
||||
def TOPBAR_FILE_IMPORT(self, context):
|
||||
@@ -131,7 +131,7 @@ def TOPBAR_FILE_IMPORT(self, context):
|
||||
def TOPBAR_FILE_EXPORT(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator(ExportVolumeData.bl_idname,
|
||||
layout.operator(ExportVolumetricData.bl_idname,
|
||||
text="Bioxel Layer (.vdb)")
|
||||
|
||||
|
||||
@@ -171,8 +171,8 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
def TOPBAR(self, context):
|
||||
layout = self.layout
|
||||
layout.menu(BioxelNodesTopbarMenu.bl_idname)
|
||||
|
||||
|
||||
|
||||
|
||||
def add():
|
||||
bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
|
||||
bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
|
||||
|
||||
@@ -366,7 +366,7 @@ class FillByThreshold(bpy.types.Operator):
|
||||
def invoke(self, context, event):
|
||||
base_layer = get_layer(bpy.context.active_object)
|
||||
scalar_min = get_layer_meta(base_layer, "Scalar Min")
|
||||
self.fill_value = scalar_min
|
||||
self.fill_value = min(scalar_min,0)
|
||||
context.window_manager.invoke_props_dialog(self, width=400)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -449,7 +449,7 @@ class FillByRange(bpy.types.Operator):
|
||||
def invoke(self, context, event):
|
||||
base_layer = get_layer(bpy.context.active_object)
|
||||
scalar_min = get_layer_meta(base_layer, "Scalar Min")
|
||||
self.fill_value = scalar_min
|
||||
self.fill_value = min(scalar_min,0)
|
||||
context.window_manager.invoke_props_dialog(self, width=400)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -533,7 +533,7 @@ class FillByLabel(bpy.types.Operator):
|
||||
def invoke(self, context, event):
|
||||
base_layer = get_layer(bpy.context.active_object)
|
||||
scalar_min = get_layer_meta(base_layer, "Scalar Min")
|
||||
self.fill_value = scalar_min
|
||||
self.fill_value = min(scalar_min,0)
|
||||
context.window_manager.invoke_props_dialog(self, width=400)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
369
bioxelnodes/parse.py
Normal file
369
bioxelnodes/parse.py
Normal file
@@ -0,0 +1,369 @@
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
from .exceptions import CancelledByUser
|
||||
from .utils import get_text_index_str
|
||||
|
||||
try:
|
||||
import SimpleITK as sitk
|
||||
from pyometiff import OMETIFFReader
|
||||
import mrcfile
|
||||
except:
|
||||
...
|
||||
|
||||
|
||||
SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
|
||||
'.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',
|
||||
'.ome.tiff', '.ome.tif',
|
||||
'.mrc', '.mrc.gz', '.map', '.map.gz']
|
||||
|
||||
OME_EXTS = ['.ome.tiff', '.ome.tif',
|
||||
'.tif', '.TIF', '.tiff', '.TIFF']
|
||||
|
||||
MRC_EXTS = ['.mrc', '.mrc.gz', '.map', '.map.gz']
|
||||
|
||||
SEQUENCE_EXTS = ['.bmp', '.BMP',
|
||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||
'.png', '.PNG']
|
||||
|
||||
DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM']
|
||||
|
||||
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']
|
||||
|
||||
|
||||
def get_ext(filepath: str) -> 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"
|
||||
elif file_path.name.endswith(".ome.tiff"):
|
||||
return ".ome.tiff"
|
||||
elif file_path.name.endswith(".ome.tif"):
|
||||
return ".ome.tif"
|
||||
elif file_path.name.endswith(".mrc.gz"):
|
||||
return ".mrc.gz"
|
||||
elif file_path.name.endswith(".map.gz"):
|
||||
return ".map.gz"
|
||||
else:
|
||||
return file_path.suffix
|
||||
|
||||
|
||||
def get_sequence_name(filepath: str) -> str:
|
||||
ext = get_ext(filepath)
|
||||
filename = Path(filepath).name.removesuffix(ext)
|
||||
index: str = get_text_index_str(filename)
|
||||
return filename.removesuffix(index)
|
||||
|
||||
|
||||
def get_sequence_index(filepath: str) -> int:
|
||||
ext = get_ext(filepath)
|
||||
filename = Path(filepath).name.removesuffix(ext)
|
||||
index: str = get_text_index_str(filename)
|
||||
return int(index) if index else 0
|
||||
|
||||
|
||||
def collect_sequence(filepath: str):
|
||||
file_path = Path(filepath).resolve()
|
||||
|
||||
files = list(file_path.parent.iterdir())
|
||||
file_dict = {}
|
||||
for file in files:
|
||||
if file.is_file() \
|
||||
and get_ext(file_path) == get_ext(file) \
|
||||
and get_sequence_name(file_path) == get_sequence_name(file):
|
||||
file_dict[get_sequence_index(file)] = file
|
||||
|
||||
for key in file_dict.copy().keys():
|
||||
if not file_dict.get(key+1) \
|
||||
and not file_dict.get(key-1):
|
||||
del file_dict[key]
|
||||
|
||||
file_dict = dict(sorted(file_dict.items()))
|
||||
sequence = [str(f) for f in file_dict.values()]
|
||||
|
||||
if len(sequence) == 0:
|
||||
sequence = [str(file_path)]
|
||||
|
||||
return sequence
|
||||
|
||||
|
||||
def parse_volumetric_data(filepath: str, series_id="", progress_callback=None):
|
||||
"""Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
|
||||
|
||||
Args:
|
||||
filepath (str): file path
|
||||
series_id (str, optional): DICOM series id. Defaults to "".
|
||||
|
||||
Returns:
|
||||
_type_: _description_
|
||||
"""
|
||||
ext = get_ext(filepath)
|
||||
|
||||
if progress_callback:
|
||||
progressing = progress_callback(0, "Reading the Data...")
|
||||
if not progressing:
|
||||
raise CancelledByUser
|
||||
|
||||
is_sequence = False
|
||||
if ext in SEQUENCE_EXTS:
|
||||
sequence = collect_sequence(filepath)
|
||||
if len(sequence) > 1:
|
||||
is_sequence = True
|
||||
|
||||
if ext in MRC_EXTS and not is_sequence:
|
||||
# TODO: much to do with mrc
|
||||
with mrcfile.open(filepath) as mrc:
|
||||
volume = mrc.data
|
||||
# mrc.print_header()
|
||||
# print(volume.shape)
|
||||
# print(mrc.voxel_size)
|
||||
|
||||
if mrc.is_single_image():
|
||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
||||
volume = np.expand_dims(volume, axis=-1) # expend Z
|
||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
||||
|
||||
elif mrc.is_image_stack():
|
||||
volume = np.expand_dims(volume, axis=-1) # expend Z
|
||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
||||
|
||||
elif mrc.is_volume():
|
||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
||||
|
||||
elif mrc.is_volume_stack():
|
||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
||||
|
||||
name = Path(filepath).name.removesuffix(ext).replace(" ", "-")
|
||||
shape = volume.shape[1:4]
|
||||
spacing = (mrc.voxel_size.x, mrc.voxel_size.y, mrc.voxel_size.z)
|
||||
meta = {
|
||||
"name": name,
|
||||
"description": "",
|
||||
"shape": shape,
|
||||
"spacing": spacing,
|
||||
"origin": (0, 0, 0),
|
||||
"direction": (1, 0, 0, 0, 1, 0, 0, 0, 1),
|
||||
"frame_count": volume.shape[0],
|
||||
"channel_count": volume.shape[-1],
|
||||
"is_oriented": False
|
||||
}
|
||||
|
||||
elif ext in OME_EXTS and not is_sequence:
|
||||
reader = OMETIFFReader(fpath=filepath)
|
||||
ome_volume, metadata, xml_metadata = reader.read()
|
||||
|
||||
if progress_callback:
|
||||
progressing = progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||
if not progressing:
|
||||
raise CancelledByUser
|
||||
|
||||
try:
|
||||
# print(ome_volume.shape)
|
||||
# for key in metadata:
|
||||
# print(f"{key},{metadata[key]}")
|
||||
ome_order = metadata['DimOrder BF Array']
|
||||
except:
|
||||
ome_order = "TCZYX"
|
||||
|
||||
if ome_volume.ndim == 2:
|
||||
ome_order = ome_order.replace("T", "")\
|
||||
.replace("C", "").replace("Z", "")
|
||||
bioxel_order = (ome_order.index('X'),
|
||||
ome_order.index('Y'))
|
||||
volume = np.transpose(ome_volume, bioxel_order)
|
||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
||||
volume = np.expand_dims(volume, axis=-1) # expend Z
|
||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
||||
|
||||
elif ome_volume.ndim == 3:
|
||||
# -> XYZC
|
||||
ome_order = ome_order.replace("T", "").replace("C", "")
|
||||
bioxel_order = (ome_order.index('X'),
|
||||
ome_order.index('Y'),
|
||||
ome_order.index('Z'))
|
||||
volume = np.transpose(ome_volume, bioxel_order)
|
||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
||||
elif ome_volume.ndim == 4:
|
||||
# -> XYZC
|
||||
ome_order = ome_order.replace("T", "")
|
||||
bioxel_order = (ome_order.index('X'),
|
||||
ome_order.index('Y'),
|
||||
ome_order.index('Z'),
|
||||
ome_order.index('C'))
|
||||
volume = np.transpose(ome_volume, bioxel_order)
|
||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
||||
elif ome_volume.ndim == 5:
|
||||
# -> TXYZC
|
||||
bioxel_order = (ome_order.index('T'),
|
||||
ome_order.index('X'),
|
||||
ome_order.index('Y'),
|
||||
ome_order.index('Z'),
|
||||
ome_order.index('C'))
|
||||
volume = np.transpose(ome_volume, bioxel_order)
|
||||
|
||||
shape = volume.shape[1:4]
|
||||
|
||||
try:
|
||||
spacing = (metadata['PhysicalSizeX'],
|
||||
metadata['PhysicalSizeY'],
|
||||
metadata['PhysicalSizeZ'])
|
||||
except:
|
||||
spacing = (1, 1, 1)
|
||||
|
||||
name = Path(filepath).name.removesuffix(ext).replace(" ", "-")
|
||||
meta = {
|
||||
"name": name,
|
||||
"description": "",
|
||||
"shape": shape,
|
||||
"spacing": spacing,
|
||||
"origin": (0, 0, 0),
|
||||
"direction": (1, 0, 0, 0, 1, 0, 0, 0, 1),
|
||||
"frame_count": volume.shape[0],
|
||||
"channel_count": volume.shape[-1],
|
||||
"is_oriented": False
|
||||
}
|
||||
|
||||
else:
|
||||
if ext in DICOM_EXTS:
|
||||
dir_path = Path(filepath).resolve().parent
|
||||
reader = sitk.ImageSeriesReader()
|
||||
reader.MetaDataDictionaryArrayUpdateOn()
|
||||
reader.LoadPrivateTagsOn()
|
||||
series_files = reader.GetGDCMSeriesFileNames(
|
||||
str(dir_path), series_id)
|
||||
reader.SetFileNames(series_files)
|
||||
|
||||
itk_volume = reader.Execute()
|
||||
# for k in reader.GetMetaDataKeys(0):
|
||||
# v = reader.GetMetaData(0, k)
|
||||
# print(f'({k}) = = "{v}"')
|
||||
|
||||
def get_meta(key):
|
||||
try:
|
||||
stirng = reader.GetMetaData(0, key).removesuffix(" ")
|
||||
if stirng in ["No study description",
|
||||
"No series description"]:
|
||||
return None
|
||||
else:
|
||||
return stirng
|
||||
except:
|
||||
return None
|
||||
|
||||
study_description = get_meta("0008|1030")
|
||||
series_description = get_meta("0008|103e")
|
||||
series_modality = get_meta("0008|0060")
|
||||
|
||||
name = study_description or dir_path.name
|
||||
if series_description and series_modality:
|
||||
description = f"{series_description}-{series_modality}"
|
||||
elif series_description:
|
||||
description = series_description
|
||||
elif series_modality:
|
||||
description = series_modality
|
||||
else:
|
||||
description = ""
|
||||
|
||||
name = name.replace(" ", "-")
|
||||
description = description.replace(" ", "-")
|
||||
|
||||
elif ext in SEQUENCE_EXTS:
|
||||
itk_volume = sitk.ReadImage(sequence)
|
||||
name = get_sequence_name(filepath).replace(" ", "-")
|
||||
description = ""
|
||||
else:
|
||||
itk_volume = sitk.ReadImage(filepath)
|
||||
name = Path(filepath).name.removesuffix(ext).replace(" ", "-")
|
||||
description = ""
|
||||
|
||||
# for key in itk_volume.GetMetaDataKeys():
|
||||
# print(f"{key},{itk_volume.GetMetaData(key)}")
|
||||
|
||||
if progress_callback:
|
||||
progressing = progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||
if not progressing:
|
||||
raise CancelledByUser
|
||||
|
||||
if itk_volume.GetDimension() == 3:
|
||||
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')
|
||||
|
||||
volume = sitk.GetArrayFromImage(itk_volume)
|
||||
|
||||
# transpose ijk to kji
|
||||
if volume.ndim == 4:
|
||||
volume = np.transpose(volume, (2, 1, 0, 3))
|
||||
else:
|
||||
volume = np.transpose(volume)
|
||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
||||
|
||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
||||
|
||||
meta = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"shape": tuple(itk_volume.GetSize()),
|
||||
"spacing": tuple(itk_volume.GetSpacing()),
|
||||
"origin": tuple(itk_volume.GetOrigin()),
|
||||
"direction": tuple(itk_volume.GetDirection()),
|
||||
"frame_count": volume.shape[0],
|
||||
"channel_count": volume.shape[-1],
|
||||
"is_oriented": True
|
||||
}
|
||||
|
||||
if itk_volume.GetDimension() == 4:
|
||||
# FIXME: not sure...
|
||||
direction = np.array(itk_volume.GetDirection())
|
||||
direction = direction.reshape(3, 3) if itk_volume.GetDimension() == 3 \
|
||||
else direction.reshape(4, 4)
|
||||
|
||||
direction = direction[1:, 1:]
|
||||
direction = tuple(direction.flatten())
|
||||
|
||||
volume = sitk.GetArrayFromImage(itk_volume)
|
||||
|
||||
if volume.ndim == 5:
|
||||
volume = np.transpose(volume, (0, 3, 2, 1, 4))
|
||||
else:
|
||||
volume = np.transpose(volume, (0, 3, 2, 1))
|
||||
volume = np.expand_dims(volume, axis=-1)
|
||||
|
||||
meta = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"shape": tuple(itk_volume.GetSize()[:3]),
|
||||
"spacing": tuple(itk_volume.GetSpacing()[:3]),
|
||||
"origin": tuple(itk_volume.GetOrigin()[:3]),
|
||||
"direction": direction,
|
||||
"frame_count": volume.shape[0],
|
||||
"channel_count": volume.shape[-1],
|
||||
"is_oriented": False
|
||||
}
|
||||
|
||||
return volume, meta
|
||||
@@ -1 +1,3 @@
|
||||
SimpleITK==2.3.1 # Insight Segmentation and Registration Toolkit.
|
||||
SimpleITK==2.3.1 # Insight Segmentation and Registration Toolkit.
|
||||
pyometiff==1.0.0 # .ome.tiff
|
||||
mrcfile==1.5.1 # .map
|
||||
@@ -72,12 +72,19 @@ def get_nodes_by_type(node_group, type_name: str):
|
||||
return [node for node in node_group.nodes if get_node_type(node) == type_name]
|
||||
|
||||
|
||||
def show_message(message="", title="Message Box", icon='INFO'):
|
||||
def progress_bar(self, context):
|
||||
row = self.layout.row()
|
||||
row.progress(
|
||||
factor=context.window_manager.bioxelnodes_progress_factor,
|
||||
type="BAR",
|
||||
text=context.window_manager.bioxelnodes_progress_text
|
||||
)
|
||||
row.scale_x = 2
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text=message)
|
||||
|
||||
bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
|
||||
def progress_update(context, factor, text=""):
|
||||
context.window_manager.bioxelnodes_progress_factor = factor
|
||||
context.window_manager.bioxelnodes_progress_text = text
|
||||
|
||||
|
||||
def calc_bbox_verts(origin, size):
|
||||
@@ -157,7 +164,7 @@ def save_vdb(grids, context):
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
vdb_path = Path(cache_dir, f"{uuid4()}.vdb")
|
||||
print(f"Storing the VDB file ({str(vdb_path)})...")
|
||||
# print(f"Storing the VDB file ({str(vdb_path)})...")
|
||||
vdb.write(str(vdb_path), grids=grids)
|
||||
|
||||
return vdb_path
|
||||
@@ -174,7 +181,7 @@ def save_vdbs(grids_sequence, context):
|
||||
vdb_paths = []
|
||||
for f, grids in enumerate(grids_sequence):
|
||||
vdb_path = Path(vdb_dir_path, f"{vdb_name}.{str(f+1).zfill(4)}.vdb")
|
||||
print(f"Storing the VDB file ({str(vdb_path)})...")
|
||||
# print(f"Storing the VDB file ({str(vdb_path)})...")
|
||||
vdb.write(str(vdb_path), grids=grids)
|
||||
vdb_paths.append(vdb_path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user