Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/dev #31

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@

# Bioxel Nodes

![Static Badge](https://img.shields.io/badge/Blender-orange?style=for-the-badge&logo=blender&logoColor=white)
![GitHub License](https://img.shields.io/github/license/OmooLab/BioxelNodes?style=for-the-badge)
![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge)
![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge)
![Static Badge](https://img.shields.io/badge/Blender-orange?style=for-the-badge&logo=blender&logoColor=white&color=black)
![GitHub License](https://img.shields.io/github/license/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black)
![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black)
![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge&labelColor=black)

![Discord](https://img.shields.io/discord/1265129134397587457?style=for-the-badge&logo=discord&label=Discord&labelColor=white&color=black&link=https%3A%2F%2Fdiscord.gg%2FpYkNyq2TjE)

Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It using Blender's powerful **Geometry Nodes** and **Cycles** to process and render volumetric data.

![cover](https://omoolab.github.io/BioxelNodes/latest/assets/cover.png)

- Fantastic rendering result, also support EEVEE NEXT
- Support multiple formats
- Support 4D volumetric data
- All kinds of cutters
- Simple and powerful nodes
- Fantastic rendering result, also support EEVEE NEXT.
- Support multiple formats.
- Support 4D volumetric data.
- All kinds of cutters.
- Simple and powerful nodes.
- Based on blender natively, can work without addon.

**Click [Getting Started](https://omoolab.github.io/BioxelNodes/latest/installation) to begin your journey into visualizing volumetric data!**
**Read the [getting started](https://omoolab.github.io/BioxelNodes/latest/installation) to begin your journey into visualizing volumetric data!**

Welcome to our [discord server](https://discord.gg/pYkNyq2TjE), if you have any problems with this add-on.

## Support Multiple Formats

Expand Down Expand Up @@ -55,7 +59,7 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It
- Only works with Cycles CPU , Cycles GPU (OptiX), EEVEE
- Section surface cannot be generated when convert to mesh (will be supported soon)

## Compatibile to Newer Version
## Compatible to Newer Version

**Updating this addon may break old files, so read the following carefully before updating**

Expand Down
25 changes: 18 additions & 7 deletions bioxelnodes/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ def progress_callback(factor, text):
progress_callback=progress_callback)
except CancelledByUser:
return
except RuntimeError:
self.has_error = True
return

if cancel():
return
Expand All @@ -183,6 +186,8 @@ def progress_callback(factor, text):

# Init cancel flag
self.is_cancelled = False
self.has_error = False

# Create the thread
self.thread = threading.Thread(target=parse_volumetric_data_func,
args=(self, context, lambda: self.is_cancelled))
Expand Down Expand Up @@ -229,6 +234,11 @@ def modal(self, context, event):
self.report({"WARNING"}, "Canncelled by user.")
return {'CANCELLED'}

# Check if thread is cancelled by user
if self.has_error:
self.report({"ERROR"}, "Fail to parse, something went wrong.")
return {'CANCELLED'}

# If not canncelled...
for key, value in self.meta.items():
print(f"{key}: {value}")
Expand All @@ -239,9 +249,12 @@ def modal(self, context, event):
orig_spacing[1], orig_spacing[2])
bioxel_size = max(min_size, 1.0)

layer_size = get_layer_size(orig_shape,
layer_shape = get_layer_shape(1, orig_shape, orig_spacing)
layer_size = get_layer_size(layer_shape,
bioxel_size)
log10 = math.floor(math.log10(max(*layer_size)))
log10 = max(1,log10)
log10 = min(3,log10)
scene_scale = math.pow(10, -log10)

if self.container:
Expand Down Expand Up @@ -700,6 +713,7 @@ def modal(self, context, event):

container.matrix_world = mat_ras2blender @ mat_scene_scale
container.name = container_name
container.show_in_front = True
container.data.name = container_name

container['bioxel_container'] = True
Expand Down Expand Up @@ -741,8 +755,8 @@ def modal(self, context, event):
layer.hide_select = True
layer.hide_render = True
layer.hide_viewport = True
layer.data.display.use_slice = True
layer.data.display.density = 1e-05
# layer.data.display.use_slice = True
# layer.data.display.density = 1e-05

layer['bioxel_layer'] = True
layer['bioxel_layer_type'] = layer_info['type']
Expand Down Expand Up @@ -811,11 +825,8 @@ def modal(self, context, event):

# Change render setting for better result
preferences = context.preferences.addons[__package__].preferences

if preferences.do_change_render_setting and self.is_first_import:
if bpy.app.version < (4, 2, 0):
bpy.context.scene.render.engine = 'CYCLES'

bpy.context.scene.render.engine = 'CYCLES'
try:
bpy.context.scene.cycles.shading_system = True
bpy.context.scene.cycles.volume_bounces = 12
Expand Down
4 changes: 2 additions & 2 deletions bioxelnodes/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def deep_copy_layer(vdb_path, base_layer, name):
copyed_layer.hide_select = True
copyed_layer.hide_render = True
copyed_layer.hide_viewport = True
copyed_layer.data.display.use_slice = True
copyed_layer.data.display.density = 1e-05
# copyed_layer.data.display.use_slice = True
# copyed_layer.data.display.density = 1e-05

copyed_layer['bioxel_layer'] = True
copyed_layer['bioxel_layer_type'] = base_layer['bioxel_layer_type']
Expand Down
179 changes: 109 additions & 70 deletions bioxelnodes/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
except:
...

"""
Convert any volumetric data to 3D numpy array with order TXYZC
"""

SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
'.bmp', '.BMP',
Expand Down Expand Up @@ -40,7 +43,7 @@
'.tif', '.TIF', '.tiff', '.TIFF',
'.png', '.PNG']

DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM']
DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA']

FH_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
'.gipl', '.gipl.gz',
Expand Down Expand Up @@ -136,7 +139,11 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None):
if len(sequence) > 1:
is_sequence = True

if ext in MRC_EXTS and not is_sequence:
volume = None

# Parsing with mrcfile
if volume is None and ext in MRC_EXTS and not is_sequence:
print("Parsing with mrcfile...")
# TODO: much to do with mrc
with mrcfile.open(filepath) as mrc:
volume = mrc.data
Expand All @@ -159,10 +166,10 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None):

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": "",
Expand All @@ -175,7 +182,9 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None):
"is_oriented": False
}

elif ext in OME_EXTS and not is_sequence:
# Parsing with OMETIFFReader
if volume is None and ext in OME_EXTS and not is_sequence:
print("Parsing with OMETIFFReader...")
reader = OMETIFFReader(fpath=filepath)
ome_volume, metadata, xml_metadata = reader.read()

Expand All @@ -189,69 +198,70 @@ def parse_volumetric_data(filepath: str, series_id="", progress_callback=None):
# 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)
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]

shape = volume.shape[1:4]
try:
spacing = (metadata['PhysicalSizeX'],
metadata['PhysicalSizeY'],
metadata['PhysicalSizeZ'])
except:
spacing = (1, 1, 1)

try:
spacing = (metadata['PhysicalSizeX'],
metadata['PhysicalSizeY'],
metadata['PhysicalSizeZ'])
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
}
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:
# Parsing with SimpleITK
if volume is None:
print("Parsing with SimpleITK...")
if ext in DICOM_EXTS:
dir_path = Path(filepath).resolve().parent
reader = sitk.ImageSeriesReader()
Expand Down Expand Up @@ -294,10 +304,13 @@ def get_meta(key):
name = name.replace(" ", "-")
description = description.replace(" ", "-")

elif ext in SEQUENCE_EXTS:
itk_volume = sitk.ReadImage(sequence)
name = get_sequence_name(filepath).replace(" ", "-")
description = ""
elif ext in SEQUENCE_EXTS and is_sequence:
try:
itk_volume = sitk.ReadImage(sequence)
name = get_sequence_name(filepath).replace(" ", "-")
description = ""
except RuntimeError as e:
raise e
else:
itk_volume = sitk.ReadImage(filepath)
name = Path(filepath).name.removesuffix(ext).replace(" ", "-")
Expand All @@ -311,7 +324,33 @@ def get_meta(key):
if not progressing:
raise CancelledByUser

if itk_volume.GetDimension() == 3:
if itk_volume.GetDimension() == 2:
volume = sitk.GetArrayFromImage(itk_volume)

if volume.ndim == 3:
volume = np.transpose(volume, (1, 0, 2))

volume = np.expand_dims(volume, axis=-2) # expend Z
else:
volume = np.transpose(volume)
volume = np.expand_dims(volume, axis=-1) # expend Z
volume = np.expand_dims(volume, axis=-1) # expend channel

volume = np.expand_dims(volume, axis=0) # expend frame

meta = {
"name": name,
"description": description,
"shape": volume.shape[1:4],
"spacing": (1, 1, 1),
"origin": (0, 0, 0),
"direction": (1, 0, 0, 0, 1, 0, 0, 0, 1),
"frame_count": 1,
"channel_count": volume.shape[-1],
"is_oriented": False
}

elif itk_volume.GetDimension() == 3:
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')

volume = sitk.GetArrayFromImage(itk_volume)
Expand All @@ -332,12 +371,12 @@ def get_meta(key):
"spacing": tuple(itk_volume.GetSpacing()),
"origin": tuple(itk_volume.GetOrigin()),
"direction": tuple(itk_volume.GetDirection()),
"frame_count": volume.shape[0],
"frame_count": 1,
"channel_count": volume.shape[-1],
"is_oriented": True
}

if itk_volume.GetDimension() == 4:
elif itk_volume.GetDimension() == 4:
# FIXME: not sure...
direction = np.array(itk_volume.GetDirection())
direction = direction.reshape(3, 3) if itk_volume.GetDimension() == 3 \
Expand Down
Loading
Loading