Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21e0b85662 | ||
| 38fb888f00 | |||
|
|
2429eef5e0 | ||
| 2563f398da | |||
| 0cad35e2da | |||
| 65edf40164 | |||
| c3f88b7846 | |||
|
|
088d9c1edb | ||
| a6ec769a9b | |||
|
|
7430fbf3ef | ||
| ce397b35aa | |||
|
|
caf184c327 | ||
| 488b05f062 | |||
|
|
2447a47269 | ||
| 9380342b4f | |||
|
|
9b36e9d911 | ||
| c9a961d5e8 | |||
| 77878888ca | |||
|
|
50e8564027 | ||
| f07ac1c70c | |||
|
|
b6e7ae0c8b | ||
| 854af4c3bc |
@@ -50,33 +50,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
upload_blender_addon:
|
build_blender_add-on:
|
||||||
name: Upload Blender Add-on
|
name: Build Blender Extension
|
||||||
needs: draft_release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: "true"
|
|
||||||
- name: Zip Add-on
|
|
||||||
run: |
|
|
||||||
zip -r package.zip bioxelnodes
|
|
||||||
- name: Upload Release Asset
|
|
||||||
id: upload-release-asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
|
||||||
asset_path: ./package.zip
|
|
||||||
asset_name: BioxelNodes_Addon_${{ needs.draft_release.outputs.version }}.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
upload_blender_extension:
|
|
||||||
name: Upload Blender Extension
|
|
||||||
needs: draft_release
|
needs: draft_release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: ["windows-x64", "linux-x64", "macos-arm64", "macos-x64"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -86,13 +66,10 @@ jobs:
|
|||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Zip Extension
|
- name: Build Add-on
|
||||||
run: |
|
run: |
|
||||||
pip download SimpleITK==2.3.1 --dest bioxelnodes/wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64
|
pip install tomlkit
|
||||||
rm -r bioxelnodes/externalpackage
|
python build.py ${{ matrix.platform }}
|
||||||
cp extension/__init__.py bioxelnodes/__init__.py
|
|
||||||
cp extension/preferences.py bioxelnodes/preferences.py
|
|
||||||
cp extension/blender_manifest.toml bioxelnodes/blender_manifest.toml
|
|
||||||
zip -r package.zip bioxelnodes
|
zip -r package.zip bioxelnodes
|
||||||
- name: Upload Release Asset
|
- name: Upload Release Asset
|
||||||
id: upload-release-asset
|
id: upload-release-asset
|
||||||
@@ -102,5 +79,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||||
asset_path: ./package.zip
|
asset_path: ./package.zip
|
||||||
asset_name: BioxelNodes_Extension_${{ needs.draft_release.outputs.version }}.zip
|
asset_name: BioxelNodes_${{ needs.draft_release.outputs.version }}_${{ matrix.platform }}.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -133,4 +133,4 @@ blendcache_*
|
|||||||
|
|
||||||
.secrets
|
.secrets
|
||||||
|
|
||||||
!bioxelnodes/scipy/_nd_image.cp311-win_amd64.pyd
|
!scipy_ndimage/*/**
|
||||||
36
README.md
36
README.md
@@ -2,29 +2,33 @@
|
|||||||
|
|
||||||
# Bioxel Nodes
|
# Bioxel Nodes
|
||||||
|
|
||||||

|
[](https://blender.org/)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
[](https://discord.gg/pYkNyq2TjE)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Fantastic rendering result, also support EEVEE NEXT
|
- Realistic rendering result, also support EEVEE NEXT.
|
||||||
- Support multiple formats
|
- Support multiple formats.
|
||||||
- Support 4D volumetric data
|
- Support 4D volumetric data.
|
||||||
- All kinds of cutters
|
- All kinds of cutters.
|
||||||
- Simple and powerful nodes
|
- Simple and powerful nodes.
|
||||||
- Based on blender natively, can work without addon.
|
- 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
|
## Support Multiple Formats
|
||||||
|
|
||||||
| Format | EXT | Test |
|
| Format | EXT | Test |
|
||||||
| ------ | ---------------------------------------- | ------- |
|
| ------ | ---------------------------------------- | ------- |
|
||||||
| DICOM | .dcm, .DICOM | ✅ pass |
|
| DICOM | .dcm, .DCM, .DICOM, .ima, .IMA | ✅ pass |
|
||||||
| BMP | .bmp, .BMP | ✅ pass |
|
| BMP | .bmp, .BMP | ✅ pass |
|
||||||
| JPEG | .jpg, .JPG, .jpeg, .JPEG | ✅ pass |
|
| JPEG | .jpg, .JPG, .jpeg, .JPEG | ✅ pass |
|
||||||
| PNG | .png, .PNG | ✅ pass |
|
| PNG | .png, .PNG | ✅ pass |
|
||||||
@@ -32,6 +36,8 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It
|
|||||||
| Nifti | .nia, .nii, .nii.gz, .hdr, .img, .img.gz | ✅ pass |
|
| Nifti | .nia, .nii, .nii.gz, .hdr, .img, .img.gz | ✅ pass |
|
||||||
| Nrrd | .nrrd, .nhdr | ✅ pass |
|
| Nrrd | .nrrd, .nhdr | ✅ pass |
|
||||||
| HDF5 | .hdf, .h4, .hdf4, .he2, .h5, .hdf5, .he5 | ✅ pass |
|
| HDF5 | .hdf, .h4, .hdf4, .he2, .h5, .hdf5, .he5 | ✅ pass |
|
||||||
|
| OME | .ome.tiff, .ome.tif | ✅ pass |
|
||||||
|
| MRC | .mrc, .mrc.gz, .map, .map.gz | ✅ pass |
|
||||||
|
|
||||||
## Support 4D volumetric data
|
## Support 4D volumetric data
|
||||||
|
|
||||||
@@ -53,15 +59,15 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It
|
|||||||
- Only works with Cycles CPU , Cycles GPU (OptiX), EEVEE
|
- Only works with Cycles CPU , Cycles GPU (OptiX), EEVEE
|
||||||
- Section surface cannot be generated when convert to mesh (will be supported soon)
|
- 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**
|
**v0.3.x is not compatible to v0.2.x, Updating this addon may break old files. Read the following carefully before upgradation**
|
||||||
|
|
||||||
Before updating this addon, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
Before upgradation, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
||||||
|
|
||||||
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
||||||
|
|
||||||
Also, unlike the newer versions, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
Also, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,13 @@ from . import auto_load
|
|||||||
from . import menus
|
from . import menus
|
||||||
|
|
||||||
|
|
||||||
bl_info = {
|
|
||||||
"name": "Bioxel Nodes",
|
|
||||||
"author": "Ma Nan",
|
|
||||||
"description": "",
|
|
||||||
"blender": (4, 1, 0),
|
|
||||||
"version": (0, 2, 7),
|
|
||||||
"location": "File -> Import",
|
|
||||||
"warning": "",
|
|
||||||
"category": "Node"
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_load.init()
|
auto_load.init()
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
auto_load.register()
|
auto_load.register()
|
||||||
|
bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty()
|
||||||
|
bpy.types.WindowManager.bioxelnodes_progress_text = bpy.props.StringProperty()
|
||||||
menus.add()
|
menus.add()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_4.2.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_4.2.blend
LFS
Normal file
Binary file not shown.
0
bioxelnodes/bioxel/__init__.py
Normal file
0
bioxelnodes/bioxel/__init__.py
Normal file
9
bioxelnodes/bioxel/container.py
Normal file
9
bioxelnodes/bioxel/container.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .layer import Layer
|
||||||
|
|
||||||
|
|
||||||
|
class Container():
|
||||||
|
def __init__(self,
|
||||||
|
name,
|
||||||
|
layers: list[Layer] = []) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.layers = layers
|
||||||
43
bioxelnodes/bioxel/io.py
Normal file
43
bioxelnodes/bioxel/io.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import h5py
|
||||||
|
|
||||||
|
from .container import Container
|
||||||
|
from .layer import Layer
|
||||||
|
|
||||||
|
|
||||||
|
def load_container(load_file: str):
|
||||||
|
load_path = Path(load_file).resolve()
|
||||||
|
with h5py.File(load_path, 'r') as file:
|
||||||
|
layers = []
|
||||||
|
for key, layer_dset in file['layers'].items():
|
||||||
|
layers.append(Layer(data=layer_dset[:],
|
||||||
|
name=layer_dset.attrs['name'],
|
||||||
|
kind=layer_dset.attrs['kind'],
|
||||||
|
affine=layer_dset.attrs['affine']))
|
||||||
|
|
||||||
|
container = Container(name=file.attrs['name'],
|
||||||
|
layers=layers)
|
||||||
|
|
||||||
|
return container
|
||||||
|
|
||||||
|
|
||||||
|
def save_container(container: Container, save_file: str, overwrite=False):
|
||||||
|
save_path = Path(save_file).resolve()
|
||||||
|
if overwrite:
|
||||||
|
if save_path.is_file():
|
||||||
|
save_path.unlink()
|
||||||
|
|
||||||
|
with h5py.File(save_path, "w") as file:
|
||||||
|
file.attrs['name'] = container.name
|
||||||
|
layer_group = file.create_group("layers")
|
||||||
|
for layer in container.layers:
|
||||||
|
layer_key = uuid.uuid4().hex[:8]
|
||||||
|
layer_dset = layer_group.create_dataset(name=layer_key,
|
||||||
|
data=layer.data)
|
||||||
|
layer_dset.attrs['name'] = layer.name
|
||||||
|
layer_dset.attrs['kind'] = layer.kind
|
||||||
|
layer_dset.attrs['affine'] = layer.affine
|
||||||
143
bioxelnodes/bioxel/layer.py
Normal file
143
bioxelnodes/bioxel/layer.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import copy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from . import scipy
|
||||||
|
from . import skimage as ski
|
||||||
|
from . import scipy as ndi
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import transforms3d
|
||||||
|
|
||||||
|
# TODO: turn to dataclasses
|
||||||
|
|
||||||
|
|
||||||
|
class Layer():
|
||||||
|
def __init__(self,
|
||||||
|
data: np.ndarray,
|
||||||
|
name: str,
|
||||||
|
kind="scalar",
|
||||||
|
affine=np.identity(4)) -> None:
|
||||||
|
if data.ndim != 5:
|
||||||
|
raise Exception("Data shape order should be TXYZC")
|
||||||
|
|
||||||
|
affine = np.array(affine)
|
||||||
|
if affine.shape != (4, 4):
|
||||||
|
raise Exception("affine shape should be (4,4)")
|
||||||
|
|
||||||
|
self.data = data
|
||||||
|
self.name = name
|
||||||
|
self.kind = kind
|
||||||
|
self.affine = affine
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bioxel_size(self):
|
||||||
|
t, r, z, s = transforms3d.affines.decompose44(self.affine)
|
||||||
|
return z.tolist()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shape(self):
|
||||||
|
return self.data.shape[1:4]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dtype(self):
|
||||||
|
return self.data.dtype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin(self):
|
||||||
|
t, r, z, s = transforms3d.affines.decompose44(self.affine)
|
||||||
|
return t.tolist()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def euler(self):
|
||||||
|
t, r, z, s = transforms3d.affines.decompose44(self.affine)
|
||||||
|
return list(transforms3d.euler.mat2euler(r))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self):
|
||||||
|
return float(np.min(self.data))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frame_count(self):
|
||||||
|
return self.data.shape[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel_count(self):
|
||||||
|
return self.data.shape[-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self):
|
||||||
|
return float(np.max(self.data))
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
return copy.deepcopy(self)
|
||||||
|
|
||||||
|
def fill(self, value: float, mask: np.ndarray):
|
||||||
|
mask_frames = ()
|
||||||
|
if mask.ndim == 4:
|
||||||
|
if mask.shape[0] != self.frame_count:
|
||||||
|
raise Exception("Mask frame count is not same as ")
|
||||||
|
for f in range(self.frame_count):
|
||||||
|
mask_frame = mask[f, :, :, :]
|
||||||
|
mask_frame = scipy.minimum_filter(
|
||||||
|
mask_frame.astype(np.float32), size=3)
|
||||||
|
# mask_frame = scipy.median_filter(
|
||||||
|
# mask_frame.astype(np.float32), size=2)
|
||||||
|
mask_frames += (mask_frame,)
|
||||||
|
elif mask.ndim == 3:
|
||||||
|
for f in range(self.frame_count):
|
||||||
|
mask_frame = mask[:, :, :]
|
||||||
|
mask_frame = scipy.minimum_filter(
|
||||||
|
mask_frame.astype(np.float32), size=3)
|
||||||
|
# mask_frame = scipy.median_filter(
|
||||||
|
# mask_frame.astype(np.float32), size=2)
|
||||||
|
mask_frames += (mask_frame,)
|
||||||
|
else:
|
||||||
|
raise Exception("Mask shape order should be TXYZ or XYZ")
|
||||||
|
|
||||||
|
_mask = np.stack(mask_frames)
|
||||||
|
_mask = np.expand_dims(_mask, axis=-1)
|
||||||
|
self.data = _mask * value + (1-_mask) * self.data
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# TXYZ > TXYZC
|
||||||
|
if self.kind in ['label', 'scalar']:
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
self.data = data
|
||||||
393
bioxelnodes/bioxel/parse.py
Normal file
393
bioxelnodes/bioxel/parse.py
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
from .layer import Layer
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import SimpleITK as sitk
|
||||||
|
from pyometiff import OMETIFFReader
|
||||||
|
import mrcfile
|
||||||
|
import transforms3d
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
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',
|
||||||
|
'.lsm', '.LSM',
|
||||||
|
'.mnc', '.MNC',
|
||||||
|
'.mrc', '.rec',
|
||||||
|
'.mha', '.mhd',
|
||||||
|
'.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5',
|
||||||
|
'.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz',
|
||||||
|
'.nrrd', '.nhdr',
|
||||||
|
'.vtk',
|
||||||
|
'.gz']
|
||||||
|
|
||||||
|
OME_EXTS = ['.ome.tiff', '.ome.tif',
|
||||||
|
'.tif', '.TIF', '.tiff', '.TIFF']
|
||||||
|
|
||||||
|
MRC_EXTS = ['.mrc', '.mrc.gz', '.map', '.map.gz']
|
||||||
|
|
||||||
|
DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA']
|
||||||
|
|
||||||
|
SEQUENCE_EXTS = ['.bmp', '.BMP',
|
||||||
|
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||||
|
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||||
|
'.png', '.PNG']
|
||||||
|
|
||||||
|
|
||||||
|
def get_ext(filepath: Path) -> str:
|
||||||
|
if filepath.name.endswith(".nii.gz"):
|
||||||
|
return ".nii.gz"
|
||||||
|
elif filepath.name.endswith(".img.gz"):
|
||||||
|
return ".img.gz"
|
||||||
|
elif filepath.name.endswith(".gipl.gz"):
|
||||||
|
return ".gipl.gz"
|
||||||
|
elif filepath.name.endswith(".ome.tiff"):
|
||||||
|
return ".ome.tiff"
|
||||||
|
elif filepath.name.endswith(".ome.tif"):
|
||||||
|
return ".ome.tif"
|
||||||
|
elif filepath.name.endswith(".mrc.gz"):
|
||||||
|
return ".mrc.gz"
|
||||||
|
elif filepath.name.endswith(".map.gz"):
|
||||||
|
return ".map.gz"
|
||||||
|
else:
|
||||||
|
return filepath.suffix
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_name(filepath: Path):
|
||||||
|
ext = get_ext(filepath)
|
||||||
|
return filepath.name.removesuffix(ext).replace(" ", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_number(filepath: Path) -> str:
|
||||||
|
name = get_file_name(filepath)
|
||||||
|
digits = ""
|
||||||
|
|
||||||
|
# Iterate through the characters in reverse order
|
||||||
|
started = False
|
||||||
|
for char in name[::-1]:
|
||||||
|
if char.isdigit():
|
||||||
|
started = True
|
||||||
|
# If the character is a digit, add it to the digits string
|
||||||
|
digits += char
|
||||||
|
else:
|
||||||
|
if started:
|
||||||
|
# If a non-digit character is encountered, stop the loop
|
||||||
|
break
|
||||||
|
|
||||||
|
# Reverse the digits string to get the correct order
|
||||||
|
return digits[::-1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_sequence_name(filepath: Path) -> str:
|
||||||
|
name = get_file_name(filepath)
|
||||||
|
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):
|
||||||
|
file_dict = {}
|
||||||
|
for f in filepath.parent.iterdir():
|
||||||
|
if f.is_file() \
|
||||||
|
and get_ext(filepath) == get_ext(f) \
|
||||||
|
and get_sequence_name(filepath) == get_sequence_name(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):
|
||||||
|
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(filepath)]
|
||||||
|
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
|
||||||
|
def parse_volumetric_data(data_file: str, series_id="", progress_callback=None) -> Layer:
|
||||||
|
"""Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_file (str): file path
|
||||||
|
series_id (str, optional): DICOM series id. Defaults to "".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_type_: _description_
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_path = Path(data_file).resolve()
|
||||||
|
ext = get_ext(data_path)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0, "Reading the Data...")
|
||||||
|
|
||||||
|
is_sequence = False
|
||||||
|
if ext in SEQUENCE_EXTS:
|
||||||
|
sequence = collect_sequence(data_path)
|
||||||
|
if len(sequence) > 1:
|
||||||
|
is_sequence = True
|
||||||
|
|
||||||
|
data = None
|
||||||
|
name = "",
|
||||||
|
description = ""
|
||||||
|
affine = np.identity(4)
|
||||||
|
spacing = (1, 1, 1)
|
||||||
|
origin = (0, 0, 0)
|
||||||
|
direction = (1, 0, 0, 0, 1, 0, 0, 0, 1)
|
||||||
|
|
||||||
|
# Parsing with mrcfile
|
||||||
|
if data is None and ext in MRC_EXTS and not is_sequence:
|
||||||
|
print("Parsing with mrcfile...")
|
||||||
|
# TODO: much to do with mrc
|
||||||
|
with mrcfile.open(data_path, 'r') as mrc:
|
||||||
|
data = mrc.data
|
||||||
|
# mrc.print_header()
|
||||||
|
# print(data.shape)
|
||||||
|
# print(mrc.voxel_size)
|
||||||
|
|
||||||
|
if mrc.is_single_image():
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif mrc.is_image_stack():
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif mrc.is_volume():
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif mrc.is_volume_stack():
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
name = get_file_name(data_path)
|
||||||
|
spacing = (mrc.voxel_size.x,
|
||||||
|
mrc.voxel_size.y,
|
||||||
|
mrc.voxel_size.z)
|
||||||
|
|
||||||
|
# Parsing with OMETIFFReader
|
||||||
|
if data is None and ext in OME_EXTS and not is_sequence:
|
||||||
|
print("Parsing with OMETIFFReader...")
|
||||||
|
reader = OMETIFFReader(fpath=data_path)
|
||||||
|
ome_image, metadata, xml_metadata = reader.read()
|
||||||
|
|
||||||
|
# TODO: some old bio-format tiff the header is not the same.
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# print(ome_image.shape)
|
||||||
|
# for key in metadata:
|
||||||
|
# print(f"{key},{metadata[key]}")
|
||||||
|
ome_order = metadata['DimOrder BF Array']
|
||||||
|
if ome_image.ndim == 2:
|
||||||
|
ome_order = ome_order.replace("T", "")\
|
||||||
|
.replace("C", "").replace("Z", "")
|
||||||
|
bioxel_order = (ome_order.index('X'),
|
||||||
|
ome_order.index('Y'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif ome_image.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'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
elif ome_image.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'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
elif ome_image.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'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
|
||||||
|
try:
|
||||||
|
spacing = (metadata['PhysicalSizeX'],
|
||||||
|
metadata['PhysicalSizeY'],
|
||||||
|
metadata['PhysicalSizeZ'])
|
||||||
|
except:
|
||||||
|
...
|
||||||
|
|
||||||
|
name = get_file_name(data_path)
|
||||||
|
except:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Parsing with SimpleITK
|
||||||
|
if data is None:
|
||||||
|
print("Parsing with SimpleITK...")
|
||||||
|
if ext in DICOM_EXTS:
|
||||||
|
data_dirpath = data_path.parent
|
||||||
|
reader = sitk.ImageSeriesReader()
|
||||||
|
reader.MetaDataDictionaryArrayUpdateOn()
|
||||||
|
reader.LoadPrivateTagsOn()
|
||||||
|
series_files = reader.GetGDCMSeriesFileNames(
|
||||||
|
str(data_dirpath), series_id)
|
||||||
|
reader.SetFileNames(series_files)
|
||||||
|
|
||||||
|
itk_image = 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 data_dirpath.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 and is_sequence:
|
||||||
|
itk_image = sitk.ReadImage(sequence)
|
||||||
|
name = get_sequence_name(data_path)
|
||||||
|
else:
|
||||||
|
itk_image = sitk.ReadImage(data_path)
|
||||||
|
name = get_file_name(data_path)
|
||||||
|
|
||||||
|
# for key in itk_image.GetMetaDataKeys():
|
||||||
|
# print(f"{key},{itk_image.GetMetaData(key)}")
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||||
|
|
||||||
|
if itk_image.GetDimension() == 2:
|
||||||
|
|
||||||
|
data = sitk.GetArrayFromImage(itk_image)
|
||||||
|
|
||||||
|
if data.ndim == 3:
|
||||||
|
data = np.transpose(data, (1, 0, 2))
|
||||||
|
|
||||||
|
data = np.expand_dims(data, axis=-2) # expend Z
|
||||||
|
else:
|
||||||
|
data = np.transpose(data)
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
|
||||||
|
elif itk_image.GetDimension() == 3:
|
||||||
|
if ext not in SEQUENCE_EXTS:
|
||||||
|
itk_image = sitk.DICOMOrient(itk_image, 'RAS')
|
||||||
|
# After sitk.DICOMOrient(), origin and direction will also orient base on LPS
|
||||||
|
# so we need to convert them into RAS
|
||||||
|
# affine = axis_conversion(from_forward='-Z',
|
||||||
|
# from_up='-Y',
|
||||||
|
# to_forward='-Z',
|
||||||
|
# to_up='Y').to_4x4()
|
||||||
|
|
||||||
|
affine = np.array([[-1.0000, 0.0000, 0.0000, 0.0000],
|
||||||
|
[0.0000, -1.0000, 0.0000, 0.0000],
|
||||||
|
[0.0000, 0.0000, 1.0000, 0.0000],
|
||||||
|
[0.0000, 0.0000, 0.0000, 1.0000]])
|
||||||
|
|
||||||
|
spacing = tuple(itk_image.GetSpacing())
|
||||||
|
origin = tuple(itk_image.GetOrigin())
|
||||||
|
direction = tuple(itk_image.GetDirection())
|
||||||
|
|
||||||
|
data = sitk.GetArrayFromImage(itk_image)
|
||||||
|
# transpose ijk to kji
|
||||||
|
if data.ndim == 4:
|
||||||
|
data = np.transpose(data, (2, 1, 0, 3))
|
||||||
|
else:
|
||||||
|
data = np.transpose(data)
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
|
||||||
|
elif itk_image.GetDimension() == 4:
|
||||||
|
|
||||||
|
spacing = tuple(itk_image.GetSpacing()[:3])
|
||||||
|
origin = tuple(itk_image.GetOrigin()[:3])
|
||||||
|
# FIXME: not sure...
|
||||||
|
direction = np.array(itk_image.GetDirection())
|
||||||
|
direction = direction.reshape(3, 3) if itk_image.GetDimension() == 3 \
|
||||||
|
else direction.reshape(4, 4)
|
||||||
|
|
||||||
|
direction = direction[1:, 1:]
|
||||||
|
direction = tuple(direction.flatten())
|
||||||
|
|
||||||
|
data = sitk.GetArrayFromImage(itk_image)
|
||||||
|
|
||||||
|
if data.ndim == 5:
|
||||||
|
data = np.transpose(data, (0, 3, 2, 1, 4))
|
||||||
|
else:
|
||||||
|
data = np.transpose(data, (0, 3, 2, 1))
|
||||||
|
data = np.expand_dims(data, axis=-1)
|
||||||
|
|
||||||
|
if itk_image.GetDimension() > 5:
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
t = origin
|
||||||
|
r = np.array(direction).reshape((3, 3))
|
||||||
|
affine = np.dot(affine,
|
||||||
|
transforms3d.affines.compose(t, r, [1, 1, 1]))
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"spacing": spacing,
|
||||||
|
"affine": affine,
|
||||||
|
"xyz_shape": data.shape[1:4],
|
||||||
|
"frame_count": data.shape[0],
|
||||||
|
"channel_count": data.shape[-1],
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, meta
|
||||||
5
bioxelnodes/bioxel/scipy/__init__.py
Normal file
5
bioxelnodes/bioxel/scipy/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from ._interpolation import zoom
|
||||||
|
from ._filters import (gaussian_filter,
|
||||||
|
median_filter,
|
||||||
|
maximum_filter,
|
||||||
|
minimum_filter)
|
||||||
0
bioxelnodes/bioxelutils/__init__.py
Normal file
0
bioxelnodes/bioxelutils/__init__.py
Normal file
180
bioxelnodes/bioxelutils/container.py
Normal file
180
bioxelnodes/bioxelutils/container.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
from ..bioxel.container import Container
|
||||||
|
from bpy_extras.io_utils import axis_conversion
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
|
|
||||||
|
from .layer import Layer, get_container_layer_objs, layer_to_obj, obj_to_layer
|
||||||
|
from ..nodes import custom_nodes
|
||||||
|
from .node import get_nodes_by_type, move_node_to_node
|
||||||
|
|
||||||
|
|
||||||
|
NODE_TYPE = {
|
||||||
|
"label": "BioxelNodes_MaskByLabel",
|
||||||
|
"scalar": "BioxelNodes_MaskByThreshold"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calc_bbox_verts(origin: tuple, size: tuple):
|
||||||
|
bbox_origin = Vector(
|
||||||
|
(origin[0], origin[1], origin[2]))
|
||||||
|
bbox_size = Vector(
|
||||||
|
(size[0], size[1], size[2]))
|
||||||
|
bbox_verts = [
|
||||||
|
(
|
||||||
|
bbox_origin[0] + 0,
|
||||||
|
bbox_origin[1] + 0,
|
||||||
|
bbox_origin[2] + 0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
bbox_origin[0] + 0,
|
||||||
|
bbox_origin[1] + 0,
|
||||||
|
bbox_origin[2] + bbox_size[2]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
bbox_origin[0] + 0,
|
||||||
|
bbox_origin[1] + bbox_size[1],
|
||||||
|
bbox_origin[2] + 0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
bbox_origin[0] + 0,
|
||||||
|
bbox_origin[1] + bbox_size[1],
|
||||||
|
bbox_origin[2] + bbox_size[2]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
bbox_origin[0] + bbox_size[0],
|
||||||
|
bbox_origin[1] + 0,
|
||||||
|
bbox_origin[2] + 0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
bbox_origin[0] + bbox_size[0],
|
||||||
|
bbox_origin[1] + 0,
|
||||||
|
bbox_origin[2] + bbox_size[2],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
bbox_origin[0] + bbox_size[0],
|
||||||
|
bbox_origin[1] + bbox_size[1],
|
||||||
|
bbox_origin[2] + 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
bbox_origin[0] + bbox_size[0],
|
||||||
|
bbox_origin[1] + bbox_size[1],
|
||||||
|
bbox_origin[2] + bbox_size[2],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return bbox_verts
|
||||||
|
|
||||||
|
|
||||||
|
def get_container_objs_from_selection():
|
||||||
|
container_objs = []
|
||||||
|
for obj in bpy.context.selected_objects:
|
||||||
|
if get_container_obj(obj):
|
||||||
|
container_objs.append(obj)
|
||||||
|
|
||||||
|
return list(set(container_objs))
|
||||||
|
|
||||||
|
|
||||||
|
def get_container_obj(current_obj):
|
||||||
|
if current_obj:
|
||||||
|
if current_obj.get('bioxel_container'):
|
||||||
|
return current_obj
|
||||||
|
elif current_obj.get('bioxel_layer'):
|
||||||
|
parent = current_obj.parent
|
||||||
|
return parent if parent.get('bioxel_container') else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_layers(layers: list[Layer],
|
||||||
|
container_obj: bpy.types.Object,
|
||||||
|
cache_dir: str):
|
||||||
|
|
||||||
|
container_node_group = container_obj.modifiers[0].node_group
|
||||||
|
|
||||||
|
for i, layer in enumerate(layers):
|
||||||
|
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
|
||||||
|
mask_node = custom_nodes.add_node(container_node_group,
|
||||||
|
NODE_TYPE[layer.kind])
|
||||||
|
mask_node.label = layer_obj.name
|
||||||
|
mask_node.inputs[0].default_value = layer_obj
|
||||||
|
|
||||||
|
# Connect to output if no output linked
|
||||||
|
output_node = get_nodes_by_type(container_node_group,
|
||||||
|
'NodeGroupOutput')[0]
|
||||||
|
if len(output_node.inputs[0].links) == 0:
|
||||||
|
container_node_group.links.new(mask_node.outputs[0],
|
||||||
|
output_node.inputs[0])
|
||||||
|
move_node_to_node(mask_node, output_node, (-300, 0))
|
||||||
|
else:
|
||||||
|
move_node_to_node(mask_node, output_node, (0, -100 * (i+1)))
|
||||||
|
|
||||||
|
return container_obj
|
||||||
|
|
||||||
|
|
||||||
|
def obj_to_container(container_obj: bpy.types.Object):
|
||||||
|
layer_objs = get_container_layer_objs(container_obj)
|
||||||
|
layers = [obj_to_layer(obj) for obj in layer_objs]
|
||||||
|
container = Container(name=container_obj.name,
|
||||||
|
layers=layers)
|
||||||
|
return container
|
||||||
|
|
||||||
|
|
||||||
|
def container_to_obj(container: Container,
|
||||||
|
scene_scale: float,
|
||||||
|
cache_dir: str):
|
||||||
|
# Wrapper a Container
|
||||||
|
|
||||||
|
# Make transformation
|
||||||
|
# (S)uperior -Z -> Y
|
||||||
|
# (A)osterior Y -> Z
|
||||||
|
mat_ras2blender = axis_conversion(from_forward='-Z',
|
||||||
|
from_up='Y',
|
||||||
|
to_forward='Y',
|
||||||
|
to_up='Z').to_4x4()
|
||||||
|
|
||||||
|
mat_scene_scale = Matrix.Scale(scene_scale,
|
||||||
|
4)
|
||||||
|
|
||||||
|
bpy.ops.mesh.primitive_cube_add(enter_editmode=False,
|
||||||
|
align='WORLD',
|
||||||
|
location=(0, 0, 0),
|
||||||
|
scale=(1, 1, 1))
|
||||||
|
|
||||||
|
container_obj = bpy.context.active_object
|
||||||
|
|
||||||
|
bbox_verts = calc_bbox_verts((0, 0, 0), container.layers[0].shape)
|
||||||
|
for i, vert in enumerate(container_obj.data.vertices):
|
||||||
|
transform = Matrix(container.layers[0].affine)
|
||||||
|
vert.co = transform @ Vector(bbox_verts[i])
|
||||||
|
|
||||||
|
container_obj.matrix_world = mat_ras2blender @ mat_scene_scale
|
||||||
|
container_obj.name = container.name
|
||||||
|
container_obj.data.name = container.name
|
||||||
|
container_obj.show_in_front = True
|
||||||
|
container_obj['bioxel_container'] = True
|
||||||
|
|
||||||
|
bpy.ops.node.new_geometry_nodes_modifier()
|
||||||
|
container_node_group = container_obj.modifiers[0].node_group
|
||||||
|
input_node = get_nodes_by_type(container_node_group,
|
||||||
|
'NodeGroupInput')[0]
|
||||||
|
container_node_group.links.remove(
|
||||||
|
input_node.outputs[0].links[0])
|
||||||
|
|
||||||
|
for i, layer in enumerate(container.layers):
|
||||||
|
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
|
||||||
|
mask_node = custom_nodes.add_node(container_node_group,
|
||||||
|
NODE_TYPE[layer.kind])
|
||||||
|
mask_node.label = layer_obj.name
|
||||||
|
mask_node.inputs[0].default_value = layer_obj
|
||||||
|
|
||||||
|
# Connect to output if no output linked
|
||||||
|
output_node = get_nodes_by_type(container_node_group,
|
||||||
|
'NodeGroupOutput')[0]
|
||||||
|
if len(output_node.inputs[0].links) == 0:
|
||||||
|
container_node_group.links.new(mask_node.outputs[0],
|
||||||
|
output_node.inputs[0])
|
||||||
|
move_node_to_node(mask_node, output_node, (-300, 0))
|
||||||
|
else:
|
||||||
|
move_node_to_node(mask_node, output_node, (0, -100 * (i+1)))
|
||||||
|
|
||||||
|
return container_obj
|
||||||
232
bioxelnodes/bioxelutils/layer.py
Normal file
232
bioxelnodes/bioxelutils/layer.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import random
|
||||||
|
import re
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import pyopenvdb as vdb
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from ..nodes import custom_nodes
|
||||||
|
from ..bioxel.layer import Layer
|
||||||
|
from .node import get_nodes_by_type, move_node_between_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_obj(current_obj: bpy.types.Object):
|
||||||
|
if current_obj:
|
||||||
|
if current_obj.get('bioxel_layer') and current_obj.parent:
|
||||||
|
if current_obj.parent.get('bioxel_container'):
|
||||||
|
return current_obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_container_layer_objs(container_obj: bpy.types.Object):
|
||||||
|
layer_objs = []
|
||||||
|
for obj in bpy.context.scene.objects:
|
||||||
|
if obj.parent == container_obj and get_layer_obj(obj):
|
||||||
|
layer_objs.append(obj)
|
||||||
|
|
||||||
|
return layer_objs
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_layer_objs():
|
||||||
|
layer_objs = []
|
||||||
|
for obj in bpy.context.scene.objects:
|
||||||
|
if get_layer_obj(obj):
|
||||||
|
layer_objs.append(obj)
|
||||||
|
|
||||||
|
return layer_objs
|
||||||
|
|
||||||
|
|
||||||
|
def obj_to_layer(layer_obj: bpy.types.Object):
|
||||||
|
cache_filepath = Path(bpy.path.abspath(layer_obj.data.filepath)).resolve()
|
||||||
|
is_sequence = re.search(r'\.\d{4}\.',
|
||||||
|
cache_filepath.name) is not None
|
||||||
|
if is_sequence:
|
||||||
|
cache_path = cache_filepath.parent
|
||||||
|
data_frames = ()
|
||||||
|
for f in cache_path.iterdir():
|
||||||
|
if not f.is_file() or f.suffix != ".vdb":
|
||||||
|
continue
|
||||||
|
grids, base_metadata = vdb.readAll(str(f))
|
||||||
|
grid = grids[0]
|
||||||
|
metadata = grid.metadata
|
||||||
|
data_frame = np.ndarray(grid["data_shape"], np.float32)
|
||||||
|
grid.copyToArray(data_frame)
|
||||||
|
data_frames += (data_frame,)
|
||||||
|
data = np.stack(data_frames)
|
||||||
|
else:
|
||||||
|
grids, base_metadata = vdb.readAll(str(cache_filepath))
|
||||||
|
grid = grids[0]
|
||||||
|
metadata = grid.metadata
|
||||||
|
data = np.ndarray(grid["data_shape"], np.float32)
|
||||||
|
grid.copyToArray(data)
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
|
||||||
|
name = metadata["layer_name"]
|
||||||
|
kind = metadata["layer_kind"]
|
||||||
|
affine = metadata["layer_affine"]
|
||||||
|
dtype = metadata.get("data_dtype") or "float32"
|
||||||
|
offset = metadata.get("data_offset") or 0
|
||||||
|
data = data - np.full_like(data, offset)
|
||||||
|
data = data.astype(dtype)
|
||||||
|
|
||||||
|
if kind in ["scalar", "label"]:
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
layer = Layer(data=data,
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
affine=affine)
|
||||||
|
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
def layer_to_obj(layer: Layer,
|
||||||
|
container_obj: bpy.types.Object,
|
||||||
|
cache_dir: str):
|
||||||
|
|
||||||
|
data = layer.data
|
||||||
|
|
||||||
|
# TXYZC > TXYZ
|
||||||
|
if layer.kind in ['label', 'scalar']:
|
||||||
|
data = np.amax(data, -1)
|
||||||
|
|
||||||
|
offset = 0
|
||||||
|
if layer.kind in ['scalar']:
|
||||||
|
data = data.astype(np.float32)
|
||||||
|
orig_min = float(np.min(data))
|
||||||
|
if orig_min < 0:
|
||||||
|
offset = -orig_min
|
||||||
|
|
||||||
|
data = data + np.full_like(data, offset)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"layer_name": layer.name,
|
||||||
|
"layer_kind": layer.kind,
|
||||||
|
"layer_affine": layer.affine.tolist(),
|
||||||
|
"data_shape": layer.shape,
|
||||||
|
"data_dtype": layer.data.dtype.str,
|
||||||
|
"data_offset": offset
|
||||||
|
}
|
||||||
|
|
||||||
|
if layer.frame_count > 1:
|
||||||
|
print(f"Saving the Cache of {layer.name}...")
|
||||||
|
vdb_name = str(uuid4())
|
||||||
|
sequence_path = Path(cache_dir, vdb_name)
|
||||||
|
sequence_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
cache_filepaths = []
|
||||||
|
for f in range(layer.frame_count):
|
||||||
|
grid = vdb.FloatGrid()
|
||||||
|
grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32))
|
||||||
|
grid.transform = vdb.createLinearTransform(
|
||||||
|
layer.affine.transpose())
|
||||||
|
grid.metadata = metadata
|
||||||
|
grid.name = layer.kind
|
||||||
|
|
||||||
|
cache_filepath = Path(sequence_path,
|
||||||
|
f"{vdb_name}.{str(f+1).zfill(4)}.vdb")
|
||||||
|
vdb.write(str(cache_filepath), grids=[grid])
|
||||||
|
cache_filepaths.append(cache_filepath)
|
||||||
|
|
||||||
|
print(f"Loading the Cache of {layer.name}...")
|
||||||
|
files = [{"name": str(cache_filepath.name), "name": str(cache_filepath.name)}
|
||||||
|
for cache_filepath in cache_filepaths]
|
||||||
|
|
||||||
|
bpy.ops.object.volume_import(filepath=str(cache_filepaths[0]),
|
||||||
|
directory=str(cache_filepaths[0].parent),
|
||||||
|
files=files, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
|
|
||||||
|
else:
|
||||||
|
grid = vdb.FloatGrid()
|
||||||
|
grid.copyFromArray(data[0, :, :, :].copy().astype(np.float32))
|
||||||
|
grid.transform = vdb.createLinearTransform(
|
||||||
|
layer.affine.transpose())
|
||||||
|
grid.metadata = metadata
|
||||||
|
grid.name = layer.kind
|
||||||
|
|
||||||
|
print(f"Saving the Cache of {layer.name}...")
|
||||||
|
cache_filepath = Path(cache_dir, f"{uuid4()}.vdb")
|
||||||
|
vdb.write(str(cache_filepath), grids=[grid])
|
||||||
|
cache_filepaths = [cache_filepath]
|
||||||
|
|
||||||
|
print(f"Loading the Cache of {layer.name}...")
|
||||||
|
bpy.ops.object.volume_import(filepath=str(cache_filepaths[0]),
|
||||||
|
align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
|
|
||||||
|
layer_obj = bpy.context.active_object
|
||||||
|
layer_obj.data.sequence_mode = 'REPEAT'
|
||||||
|
|
||||||
|
# Set props to VDB object
|
||||||
|
layer_obj.name = f"{container_obj.name}_{layer.name}"
|
||||||
|
layer_obj.data.name = f"{container_obj.name}_{layer.name}"
|
||||||
|
|
||||||
|
layer_obj.lock_location[0] = True
|
||||||
|
layer_obj.lock_location[1] = True
|
||||||
|
layer_obj.lock_location[2] = True
|
||||||
|
layer_obj.lock_rotation[0] = True
|
||||||
|
layer_obj.lock_rotation[1] = True
|
||||||
|
layer_obj.lock_rotation[2] = True
|
||||||
|
layer_obj.lock_scale[0] = True
|
||||||
|
layer_obj.lock_scale[1] = True
|
||||||
|
layer_obj.lock_scale[2] = True
|
||||||
|
|
||||||
|
layer_obj.visible_camera = False
|
||||||
|
layer_obj.visible_diffuse = False
|
||||||
|
layer_obj.visible_glossy = False
|
||||||
|
layer_obj.visible_transmission = False
|
||||||
|
layer_obj.visible_volume_scatter = False
|
||||||
|
layer_obj.visible_shadow = False
|
||||||
|
|
||||||
|
layer_obj.hide_select = True
|
||||||
|
layer_obj.hide_render = True
|
||||||
|
layer_obj.hide_viewport = True
|
||||||
|
|
||||||
|
layer_obj['bioxel_layer'] = True
|
||||||
|
layer_obj.parent = container_obj
|
||||||
|
|
||||||
|
for collection in layer_obj.users_collection:
|
||||||
|
collection.objects.unlink(layer_obj)
|
||||||
|
|
||||||
|
for collection in container_obj.users_collection:
|
||||||
|
collection.objects.link(layer_obj)
|
||||||
|
|
||||||
|
print(f"Creating Node for {layer.name}...")
|
||||||
|
bpy.ops.node.new_geometry_nodes_modifier()
|
||||||
|
node_group = layer_obj.modifiers[0].node_group
|
||||||
|
layer_node = custom_nodes.add_node(node_group,
|
||||||
|
"BioxelNodes__Layer")
|
||||||
|
|
||||||
|
layer_node.inputs['name'].default_value = layer.name
|
||||||
|
layer_node.inputs['shape'].default_value = layer.shape
|
||||||
|
layer_node.inputs['kind'].default_value = layer.kind
|
||||||
|
|
||||||
|
for i in range(layer.affine.shape[1]):
|
||||||
|
for j in range(layer.affine.shape[0]):
|
||||||
|
affine_key = f"affine{i}{j}"
|
||||||
|
layer_node.inputs[affine_key].default_value = layer.affine[j, i]
|
||||||
|
|
||||||
|
layer_node.inputs['id'].default_value = random.randint(-200000000,
|
||||||
|
200000000)
|
||||||
|
layer_node.inputs['bioxel_size'].default_value = layer.bioxel_size[0]
|
||||||
|
layer_node.inputs['dtype'].default_value = layer.dtype.str
|
||||||
|
layer_node.inputs['dtype_num'].default_value = layer.dtype.num
|
||||||
|
layer_node.inputs['offset'].default_value = max(0, -layer.min)
|
||||||
|
layer_node.inputs['min'].default_value = layer.min
|
||||||
|
layer_node.inputs['max'].default_value = layer.max
|
||||||
|
|
||||||
|
input_node = get_nodes_by_type(node_group,
|
||||||
|
'NodeGroupInput')[0]
|
||||||
|
output_node = get_nodes_by_type(node_group,
|
||||||
|
'NodeGroupOutput')[0]
|
||||||
|
|
||||||
|
node_group.links.new(input_node.outputs[0],
|
||||||
|
layer_node.inputs[0])
|
||||||
|
node_group.links.new(layer_node.outputs[0],
|
||||||
|
output_node.inputs[0])
|
||||||
|
|
||||||
|
move_node_between_nodes(
|
||||||
|
layer_node, [input_node, output_node])
|
||||||
|
|
||||||
|
return layer_obj
|
||||||
26
bioxelnodes/bioxelutils/node.py
Normal file
26
bioxelnodes/bioxelutils/node.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
def move_node_to_node(node, target_node, offset=(0, 0)):
|
||||||
|
node.location.x = target_node.location.x + offset[0]
|
||||||
|
node.location.y = target_node.location.y + offset[1]
|
||||||
|
|
||||||
|
|
||||||
|
def move_node_between_nodes(node, target_nodes, offset=(0, 0)):
|
||||||
|
xs = []
|
||||||
|
ys = []
|
||||||
|
for target_node in target_nodes:
|
||||||
|
xs.append(target_node.location.x)
|
||||||
|
ys.append(target_node.location.y)
|
||||||
|
|
||||||
|
node.location.x = sum(xs) / len(xs) + offset[0]
|
||||||
|
node.location.y = sum(ys) / len(ys) + offset[1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_type(node):
|
||||||
|
node_type = type(node).__name__
|
||||||
|
if node_type == "GeometryNodeGroup":
|
||||||
|
node_type = node.node_tree.name
|
||||||
|
|
||||||
|
return node_type
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
38
bioxelnodes/blender_manifest.toml
Normal file
38
bioxelnodes/blender_manifest.toml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "bioxelnodes"
|
||||||
|
version = "0.3.3"
|
||||||
|
name = "Bioxel Nodes"
|
||||||
|
tagline = "For scientific volumetric data visualization in Blender"
|
||||||
|
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
||||||
|
type = "add-on"
|
||||||
|
website = "https://omoolab.github.io/BioxelNodes/latest"
|
||||||
|
|
||||||
|
tags = ["Geometry Nodes", "Render", "Import-Export"]
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
license = ["SPDX:MIT"]
|
||||||
|
copyright = ["2024 OmooLab"]
|
||||||
|
platforms = ["windows-x64", "linux-x64", "macos-arm64", "macos-x64"]
|
||||||
|
|
||||||
|
wheels = [
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl",
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-macosx_11_0_arm64.whl",
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-win_amd64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-macosx_10_9_arm64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-win_amd64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"./wheels/tifffile-2024.7.24-py3-none-any.whl",
|
||||||
|
"./wheels/pyometiff-1.0.0-py3-none-any.whl",
|
||||||
|
"./wheels/mrcfile-1.5.1-py2.py3-none-any.whl",
|
||||||
|
"./wheels/transforms3d-0.4.2-py3-none-any.whl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
files = "Import/export volume data from/to disk"
|
||||||
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import shutil
|
|
||||||
import bpy
|
import bpy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .nodes import AddCustomNode
|
from .nodes import AddCustomNode
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class AddCustomNode():
|
|||||||
self.get_node_tree(self.node_type, self.node_link)
|
self.get_node_tree(self.node_type, self.node_link)
|
||||||
node = node_group.nodes.new("GeometryNodeGroup")
|
node = node_group.nodes.new("GeometryNodeGroup")
|
||||||
self.assign_node_tree(node)
|
self.assign_node_tree(node)
|
||||||
|
node.show_options = False
|
||||||
|
|
||||||
return node
|
return node
|
||||||
|
|
||||||
@@ -121,5 +122,6 @@ class CUSTOMNODES_OT_Add_Custom_Node(bpy.types.Operator, AddCustomNode):
|
|||||||
node = bpy.context.active_node
|
node = bpy.context.active_node
|
||||||
|
|
||||||
self.assign_node_tree(node)
|
self.assign_node_tree(node)
|
||||||
|
node.show_options = False
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
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)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .preferences import ExternalPackagePreferences
|
|
||||||
from .package import PackageInstaller
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
"""
|
|
||||||
Handling installation of external python packages inside of Blender's bundled python.
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
from importlib.metadata import version as get_version, PackageNotFoundError
|
|
||||||
import bpy
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ADDON_NAME = __package__.split(".")[0]
|
|
||||||
|
|
||||||
PYPI_MIRROR = {
|
|
||||||
# the original.
|
|
||||||
'Default': '',
|
|
||||||
# two mirrors in China Mainland to help those poor victims under GFW.
|
|
||||||
'BFSU (Beijing)': 'https://mirrors.bfsu.edu.cn/pypi/web/simple',
|
|
||||||
'TUNA (Beijing)': 'https://pypi.tuna.tsinghua.edu.cn/simple',
|
|
||||||
# append more if necessary.
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class InstallationError(Exception):
|
|
||||||
"""
|
|
||||||
Exception raised when there is an error installing a package.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
package_name : str
|
|
||||||
The name of the package that failed to install.
|
|
||||||
error_message : str
|
|
||||||
The error message returned by pip.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, package_name, error_message):
|
|
||||||
self.package_name = package_name
|
|
||||||
self.error_message = error_message
|
|
||||||
super().__init__(f"Failed to install {package_name}: {error_message}")
|
|
||||||
|
|
||||||
|
|
||||||
class PackageInstaller():
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
requirements_dir: str,
|
|
||||||
log_dir: str = None,
|
|
||||||
pypi_mirror_provider='Default',
|
|
||||||
) -> None:
|
|
||||||
self.pypi_mirror_provider = pypi_mirror_provider
|
|
||||||
self.log_path: Path = Path(log_dir) if log_dir \
|
|
||||||
else Path.home() / '.externalpackage' / 'logs'
|
|
||||||
self.requirements_path: Path = Path(requirements_dir)
|
|
||||||
|
|
||||||
def start_logging(self, logfile_name: str = 'side-packages-install') -> logging.Logger:
|
|
||||||
"""
|
|
||||||
Configure and start logging to a file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
logfile_name : str, optional
|
|
||||||
The name of the log file. Defaults to 'side-packages-install'.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
logging.Logger
|
|
||||||
A Logger object that can be used to write log messages.
|
|
||||||
|
|
||||||
This function sets up a logging configuration with a specified log file name and logging level.
|
|
||||||
The log file will be created in the `ADDON_DIR/logs` directory. If the directory
|
|
||||||
does not exist, it will be created. The function returns a Logger object that can be used to
|
|
||||||
write log messages.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Create the logs directory if it doesn't exist
|
|
||||||
self.log_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Set up logging configuration
|
|
||||||
logfile_path = Path(self.log_path, f"{logfile_name}.log")
|
|
||||||
logging.basicConfig(filename=logfile_path,
|
|
||||||
level=logging.INFO, encoding='utf-8')
|
|
||||||
|
|
||||||
# Return logger object
|
|
||||||
return logging.getLogger()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pypi_mirror_url(self) -> str:
|
|
||||||
"""
|
|
||||||
Process a PyPI mirror provider and return the corresponding URL.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pypi_mirror_provider : str
|
|
||||||
The PyPI mirror provider to process.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
The URL of the PyPI mirror.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
ValueError
|
|
||||||
If the provided PyPI mirror provider is invalid.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.pypi_mirror_provider.startswith('https:'):
|
|
||||||
return self.pypi_mirror_provider
|
|
||||||
elif self.pypi_mirror_provider in PYPI_MIRROR.keys():
|
|
||||||
return PYPI_MIRROR[self.pypi_mirror_provider]
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid PyPI mirror provider: {self.pypi_mirror_provider}")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def packages(self) -> dict:
|
|
||||||
"""
|
|
||||||
Read a requirements file and extract package information into a dictionary.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
requirements : str, optional
|
|
||||||
The path to the requirements file. If not provided, the function looks for a `requirements.txt`
|
|
||||||
file in the same directory as the script.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict
|
|
||||||
A dictionary containing package information. Each element of the dictionary is a dictionary containing the package name, version, and description.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
Given the following requirements file:
|
|
||||||
```python
|
|
||||||
Flask==1.1.2 # A micro web framework for Python
|
|
||||||
pandas==1.2.3 # A fast, powerful, flexible, and easy-to-use data analysis and manipulation tool
|
|
||||||
numpy==1.20.1 # Fundamental package for scientific computing
|
|
||||||
```
|
|
||||||
The function would return the following dictionary:
|
|
||||||
```python
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"package": "Flask",
|
|
||||||
"version": "1.1.2",
|
|
||||||
"description": "A micro web framework for Python"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "pandas",
|
|
||||||
"version": "1.2.3",
|
|
||||||
"description": "A fast, powerful, flexible, and easy-to-use data analysis and manipulation tool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "numpy",
|
|
||||||
"version": "1.20.1",
|
|
||||||
"description": "Fundamental package for scientific computing"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
requirements_filepath = self.requirements_path / "requirements.txt"
|
|
||||||
if requirements_filepath.is_file():
|
|
||||||
with requirements_filepath.open('r') as f:
|
|
||||||
lines = f.read().splitlines()
|
|
||||||
packages = {}
|
|
||||||
for line in lines:
|
|
||||||
try:
|
|
||||||
package, description = line.split('#')
|
|
||||||
package_meta = package.split('==')
|
|
||||||
name = package_meta[0].strip()
|
|
||||||
packages[name] = {
|
|
||||||
"name": name,
|
|
||||||
"version": package_meta[1].strip(),
|
|
||||||
"description": description.strip()
|
|
||||||
}
|
|
||||||
except ValueError:
|
|
||||||
# Skip line if it doesn't have the expected format
|
|
||||||
pass
|
|
||||||
return packages
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError(requirements_filepath)
|
|
||||||
|
|
||||||
def is_installed(self, package_name: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if the specified package is installed and the version matches that specified
|
|
||||||
in the `requirements.txt` file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
package : str
|
|
||||||
The name of the package to check.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bool
|
|
||||||
True if the package is the current version, False otherwise.
|
|
||||||
|
|
||||||
"""
|
|
||||||
package = self.packages.get(package_name)
|
|
||||||
try:
|
|
||||||
available_version = get_version(package['name'])
|
|
||||||
return available_version == package['version']
|
|
||||||
except PackageNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def run_python(self, cmd_list: list = None, timeout: int = 600):
|
|
||||||
"""
|
|
||||||
Runs pip command using the specified command list and returns the command output.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
cmd_list : list, optional
|
|
||||||
List of pip commands to be executed. Defaults to None.
|
|
||||||
mirror_url : str, optional
|
|
||||||
URL of a package repository mirror to be used for the command. Defaults to ''.
|
|
||||||
timeout : int, optional
|
|
||||||
Time in seconds to wait for the command to complete. Defaults to 600.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
tuple
|
|
||||||
A tuple containing the command list, command return code, command standard output,
|
|
||||||
and command standard error.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
Install numpy using pip and print the command output
|
|
||||||
```python
|
|
||||||
cmd_list = ["-m", "pip", "install", "numpy"]
|
|
||||||
mirror_url = 'https://pypi.org/simple/'
|
|
||||||
cmd_output = run_python(cmd_list, timeout=300)
|
|
||||||
print(cmd_output)
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# path to python.exe
|
|
||||||
python_exe = str(Path(sys.executable).resolve())
|
|
||||||
|
|
||||||
# build the command list
|
|
||||||
cmd_list = [python_exe] + cmd_list
|
|
||||||
|
|
||||||
# add mirror to the command list if it's valid
|
|
||||||
if self.pypi_mirror_url and self.pypi_mirror_url.startswith('https'):
|
|
||||||
cmd_list += ['-i', self.pypi_mirror_url]
|
|
||||||
|
|
||||||
log = self.start_logging()
|
|
||||||
log.info(f"Running Pip: '{cmd_list}'")
|
|
||||||
|
|
||||||
# run the command and capture the output
|
|
||||||
result = subprocess.run(cmd_list, timeout=timeout,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if result.returncode != 0:
|
|
||||||
log.error('Command failed: %s', cmd_list)
|
|
||||||
log.error('stdout: %s', result.stdout.decode())
|
|
||||||
log.error('stderr: %s', result.stderr.decode(errors='ignore'))
|
|
||||||
else:
|
|
||||||
log.info('Command succeeded: %s', cmd_list)
|
|
||||||
log.info('stdout: %s', result.stdout.decode())
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
# return the command list, return code, stdout, and stderr as a tuple
|
|
||||||
return result
|
|
||||||
|
|
||||||
def install_package(self, package: str) -> list:
|
|
||||||
"""
|
|
||||||
Install a Python package and its dependencies using pip.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
package : str
|
|
||||||
The name of the package to install.
|
|
||||||
pypi_mirror_provider : str, optional
|
|
||||||
The name/url of the PyPI mirror provider to use. Default is 'Default'.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list
|
|
||||||
A list of tuples containing the command list, return code, stdout, and stderr
|
|
||||||
for each pip command run.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
ValueError
|
|
||||||
If the package name is not provided.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
To install the package 'requests' from the PyPI mirror 'MyMirror', use:
|
|
||||||
```
|
|
||||||
install_package('requests', 'MyMirror')
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not package:
|
|
||||||
raise ValueError("Package name must be provided.")
|
|
||||||
|
|
||||||
print(f"Installing {package}...")
|
|
||||||
print(f"Using PyPI mirror: {self.pypi_mirror_provider}")
|
|
||||||
|
|
||||||
self.run_python(["-m", "ensurepip"]),
|
|
||||||
self.run_python(["-m", "pip", "install", "--upgrade", "pip"])
|
|
||||||
result = self.run_python(["-m", "pip", "install", package, "--user"])
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def install_all_packages(self, pypi_mirror_provider: str = 'Default') -> list:
|
|
||||||
"""
|
|
||||||
Install all packages listed in the 'requirements.txt' file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pypi_mirror_provider : str, optional
|
|
||||||
The PyPI mirror to use for package installation. Defaults to 'Default',
|
|
||||||
which uses the official PyPI repository.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list
|
|
||||||
A list of tuples containing the installation results for each package.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InstallationError
|
|
||||||
If there is an error during package installation.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
To install all packages listed in the 'requirements.txt' file, run the following command:
|
|
||||||
```
|
|
||||||
install_all_packages(pypi_mirror_provider='https://pypi.org/simple/')
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
results = []
|
|
||||||
for package in self.packages.items():
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.install_package(
|
|
||||||
package=f"{package.get('name')}=={package.get('version')}"
|
|
||||||
)
|
|
||||||
results.append(result)
|
|
||||||
except InstallationError as e:
|
|
||||||
raise InstallationError(
|
|
||||||
f"Error installing package {package.get('name')}: {str(e)}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
class EXTERNALPACKAGE_OT_Install_Package(bpy.types.Operator):
|
|
||||||
bl_idname = 'externalpackage.install_package'
|
|
||||||
bl_label = 'Install Given Python Package'
|
|
||||||
bl_options = {'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
package: bpy.props.StringProperty(
|
|
||||||
name='Python Package',
|
|
||||||
description='Python Package to Install'
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
version: bpy.props.StringProperty(
|
|
||||||
name='Python Package',
|
|
||||||
description='Python Package to Install'
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
description: bpy.props.StringProperty(
|
|
||||||
name='Operator description',
|
|
||||||
default='Install specified python package.'
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def description(cls, context, properties):
|
|
||||||
return properties.description
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
preferences = context.preferences.addons[ADDON_NAME].preferences
|
|
||||||
installer = PackageInstaller(
|
|
||||||
pypi_mirror_provider=preferences.pypi_mirror_provider,
|
|
||||||
log_dir=preferences.log_dir,
|
|
||||||
requirements_dir=preferences.requirements_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
result = installer.install_package(f"{self.package}=={self.version}")
|
|
||||||
if result.returncode == 0 or installer.is_installed(self.package):
|
|
||||||
self.report(
|
|
||||||
{'INFO'},
|
|
||||||
f"Successfully installed {self.package} v{self.version}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.report(
|
|
||||||
{'ERROR'},
|
|
||||||
f"""Error installing package. Please check the log files
|
|
||||||
in {preferences.log_dir}."""
|
|
||||||
)
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
from .package import PackageInstaller, PYPI_MIRROR
|
|
||||||
|
|
||||||
# Defines the preferences panel for the addon, which shows the buttons for
|
|
||||||
# installing and reinstalling the required python packages defined in 'requirements.txt'
|
|
||||||
|
|
||||||
|
|
||||||
def get_pypi_mirrors(self, context, edit_text):
|
|
||||||
return PYPI_MIRROR.keys()
|
|
||||||
|
|
||||||
|
|
||||||
class RebootBlender(bpy.types.Operator):
|
|
||||||
bl_idname = "externalpackage.reboot_blender"
|
|
||||||
bl_label = "Reboot Blender"
|
|
||||||
bl_description = "Reboot Blender"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.invoke_confirm(self, event)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
blender_launcher = Path(
|
|
||||||
bpy.app.binary_path).parent / "blender-launcher.exe"
|
|
||||||
subprocess.run([str(blender_launcher), "-con", "--python-expr",
|
|
||||||
"import bpy; bpy.ops.wm.recover_last_session()"])
|
|
||||||
bpy.ops.wm.quit_blender()
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class ExternalPackagePreferences():
|
|
||||||
requirements_dir: bpy.props.StringProperty(
|
|
||||||
name="requirements.txt Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default=str(Path(__file__).parent)
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
log_dir: bpy.props.StringProperty(
|
|
||||||
name="Python Log Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default=str(Path(Path.home(), '.externalpackage', 'logs'))
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
pypi_mirror_provider: bpy.props.StringProperty(
|
|
||||||
name='pypi_mirror_provider',
|
|
||||||
description='PyPI Mirror Provider',
|
|
||||||
options={'TEXTEDIT_UPDATE', 'LIBRARY_EDITABLE'},
|
|
||||||
default='Default',
|
|
||||||
subtype='NONE',
|
|
||||||
search=get_pypi_mirrors,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(
|
|
||||||
text='**Install the required packages, then hit "Reboot Blender"**')
|
|
||||||
|
|
||||||
col_main = layout.column(heading='', align=False)
|
|
||||||
row_import = col_main.row()
|
|
||||||
row_import.prop(self, 'pypi_mirror_provider', text='PyPI Mirror')
|
|
||||||
|
|
||||||
installer = PackageInstaller(
|
|
||||||
pypi_mirror_provider=self.pypi_mirror_provider,
|
|
||||||
log_dir=self.log_dir,
|
|
||||||
requirements_dir=self.requirements_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
for package in installer.packages.values():
|
|
||||||
|
|
||||||
name = package.get('name')
|
|
||||||
version = package.get('version')
|
|
||||||
description = package.get('description')
|
|
||||||
|
|
||||||
if installer.is_installed(name):
|
|
||||||
row = layout.row()
|
|
||||||
row.label(text=f"{name} version {version} is installed.")
|
|
||||||
op = row.operator('externalpackage.install_package',
|
|
||||||
text=f'Reinstall {name}')
|
|
||||||
op.package = name
|
|
||||||
op.version = version
|
|
||||||
op.description = f'Reinstall {name}'
|
|
||||||
else:
|
|
||||||
row = layout.row(heading=f"Package: {name}")
|
|
||||||
col = row.column()
|
|
||||||
col.label(text=str(description))
|
|
||||||
col = row.column()
|
|
||||||
op = col.operator('externalpackage.install_package',
|
|
||||||
text=f'Install {name}')
|
|
||||||
op.package = name
|
|
||||||
op.version = version
|
|
||||||
op.description = f'Install required python package: {name}'
|
|
||||||
|
|
||||||
layout.operator(RebootBlender.bl_idname)
|
|
||||||
@@ -1,874 +0,0 @@
|
|||||||
import math
|
|
||||||
import bpy
|
|
||||||
import shutil
|
|
||||||
from bpy_extras.io_utils import axis_conversion
|
|
||||||
import pyopenvdb as vdb
|
|
||||||
import numpy as np
|
|
||||||
from pathlib import Path
|
|
||||||
from uuid import uuid4
|
|
||||||
import mathutils
|
|
||||||
import random
|
|
||||||
|
|
||||||
from . import skimage as ski
|
|
||||||
from .nodes import custom_nodes
|
|
||||||
from .props import BIOXELNODES_Series
|
|
||||||
from .utils import (calc_bbox_verts, get_all_layers, get_container_from_selection, get_layer, get_text_index_str,
|
|
||||||
get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, save_vdbs, select_object, show_message)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import SimpleITK as sitk
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
|
|
||||||
SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM',
|
|
||||||
'.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']
|
|
||||||
|
|
||||||
SEQUENCE_EXTS = ['.dcm', '.DCM', '.DICOM',
|
|
||||||
'.bmp', '.BMP',
|
|
||||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
|
||||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
|
||||||
'.png', '.PNG']
|
|
||||||
|
|
||||||
DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM']
|
|
||||||
|
|
||||||
FH_EXTS = ['', '.dcm', '.DCM', '.DICOM',
|
|
||||||
'.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_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple):
|
|
||||||
shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]),
|
|
||||||
int(orig_shape[1] / bioxel_size * orig_spacing[1]),
|
|
||||||
int(orig_shape[2] / bioxel_size * orig_spacing[2]))
|
|
||||||
|
|
||||||
return (shape[0] if shape[0] > 0 else 1,
|
|
||||||
shape[1] if shape[1] > 0 else 1,
|
|
||||||
shape[2] if shape[2] > 0 else 1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer_size(shape: tuple, bioxel_size: float, scale: float = 1.0):
|
|
||||||
size = (float(shape[0] * bioxel_size * scale),
|
|
||||||
float(shape[1] * bioxel_size * scale),
|
|
||||||
float(shape[2] * bioxel_size * scale))
|
|
||||||
|
|
||||||
return size
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
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())
|
|
||||||
files = [f for f in files if f.is_file()
|
|
||||||
and get_ext(file_path) == get_ext(f)
|
|
||||||
and get_sequence_name(file_path) == get_sequence_name(f)]
|
|
||||||
|
|
||||||
files.sort(key=get_sequence_index)
|
|
||||||
sequence = [str(f) for f in files]
|
|
||||||
return sequence
|
|
||||||
|
|
||||||
|
|
||||||
def parse_volume_data(filepath: str, series_id=""):
|
|
||||||
ext = get_ext(filepath)
|
|
||||||
|
|
||||||
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}"')
|
|
||||||
name = dir_path.name
|
|
||||||
|
|
||||||
elif ext in SEQUENCE_EXTS:
|
|
||||||
itk_volume = sitk.ReadImage(filepath)
|
|
||||||
if itk_volume.GetDimension() == 2:
|
|
||||||
sequence = collect_sequence(filepath)
|
|
||||||
itk_volume = sitk.ReadImage(sequence)
|
|
||||||
name = get_sequence_name(filepath)
|
|
||||||
else:
|
|
||||||
itk_volume = sitk.ReadImage(filepath)
|
|
||||||
name = Path(filepath).name.removesuffix(ext)
|
|
||||||
else:
|
|
||||||
itk_volume = sitk.ReadImage(filepath)
|
|
||||||
name = Path(filepath).name.removesuffix(ext)
|
|
||||||
|
|
||||||
# for key in itk_volume.GetMetaDataKeys():
|
|
||||||
# print(f"{key},{itk_volume.GetMetaData(key)}")
|
|
||||||
|
|
||||||
if itk_volume.GetDimension() == 3:
|
|
||||||
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
"name": name,
|
|
||||||
"shape": tuple(itk_volume.GetSize()),
|
|
||||||
"spacing": tuple(itk_volume.GetSpacing()),
|
|
||||||
"origin": tuple(itk_volume.GetOrigin()),
|
|
||||||
"direction": tuple(itk_volume.GetDirection()),
|
|
||||||
"is_oriented": True
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# FIXME: not sure...
|
|
||||||
print(itk_volume.GetDirection())
|
|
||||||
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())
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
"name": name,
|
|
||||||
"shape": tuple(itk_volume.GetSize()[:3]),
|
|
||||||
"spacing": tuple(itk_volume.GetSpacing()[:3]),
|
|
||||||
"origin": tuple(itk_volume.GetOrigin()[:3]),
|
|
||||||
"direction": direction,
|
|
||||||
"is_oriented": False
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return volume, meta
|
|
||||||
|
|
||||||
|
|
||||||
class ImportVolumeDataDialog(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.import_volume_data_dialog"
|
|
||||||
bl_label = "Volumetric Data as Bioxel Layer"
|
|
||||||
bl_description = "Import Volumetric Data as Bioxel Layer"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
filepath: bpy.props.StringProperty(
|
|
||||||
subtype="FILE_PATH"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
container_name: bpy.props.StringProperty(
|
|
||||||
name="Container Name"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
layer_name: bpy.props.StringProperty(
|
|
||||||
name="Layer Name",
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
series_id: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
container: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
read_as: bpy.props.EnumProperty(
|
|
||||||
name="Read as",
|
|
||||||
default="scalar",
|
|
||||||
items=[("scalar", "Scalar", ""),
|
|
||||||
("label", "Labels", "")]
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
bioxel_size: bpy.props.FloatProperty(
|
|
||||||
name="Bioxel Size (Larger size means small resolution)",
|
|
||||||
soft_min=0.1, soft_max=10.0,
|
|
||||||
min=0.1, max=1e2,
|
|
||||||
default=1,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
orig_spacing: bpy.props.FloatVectorProperty(
|
|
||||||
name="Original Spacing",
|
|
||||||
default=(1, 1, 1),
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
orig_shape: bpy.props.IntVectorProperty(
|
|
||||||
name="Original Shape",
|
|
||||||
default=(100, 100, 100)
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
scene_scale: bpy.props.FloatProperty(
|
|
||||||
name="Scene Scale (Bioxel Unit pre Blender Unit)",
|
|
||||||
soft_min=0.0001, soft_max=10.0,
|
|
||||||
min=1e-6, max=1e6,
|
|
||||||
default=0.01,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
is_time_sequence: bpy.props.BoolProperty(
|
|
||||||
name="Is Time Sequence",
|
|
||||||
default=False,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
split_channels: bpy.props.BoolProperty(
|
|
||||||
name="Split Channels",
|
|
||||||
default=False,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
is_first_import = len(get_all_layers()) == 0
|
|
||||||
volume, meta = parse_volume_data(self.filepath)
|
|
||||||
container_name = self.container_name or meta['name'] or "Container"
|
|
||||||
bioxel_size = self.bioxel_size
|
|
||||||
orig_spacing = self.orig_spacing
|
|
||||||
|
|
||||||
layer_spacing = (
|
|
||||||
meta['spacing'][0] / orig_spacing[0] * bioxel_size,
|
|
||||||
meta['spacing'][1] / orig_spacing[1] * bioxel_size,
|
|
||||||
meta['spacing'][2] / orig_spacing[2] * bioxel_size
|
|
||||||
)
|
|
||||||
|
|
||||||
dtype_index = volume.dtype.num
|
|
||||||
|
|
||||||
layer_shape = get_layer_shape(
|
|
||||||
bioxel_size, meta['shape'], orig_spacing)
|
|
||||||
|
|
||||||
# After sitk.DICOMOrient(), origin and direction will also orient base on LPS
|
|
||||||
# so we need to convert them into RAS
|
|
||||||
mat_lps2ras = axis_conversion(
|
|
||||||
from_forward='-Z',
|
|
||||||
from_up='-Y',
|
|
||||||
to_forward='-Z',
|
|
||||||
to_up='Y'
|
|
||||||
).to_4x4()
|
|
||||||
|
|
||||||
mat_location = mathutils.Matrix.Translation(
|
|
||||||
mathutils.Vector(meta['origin'])
|
|
||||||
)
|
|
||||||
|
|
||||||
mat_rotation = mathutils.Matrix(
|
|
||||||
np.array(meta['direction']).reshape((3, 3))
|
|
||||||
).to_4x4()
|
|
||||||
|
|
||||||
mat_scale = mathutils.Matrix.Scale(
|
|
||||||
bioxel_size, 4
|
|
||||||
)
|
|
||||||
|
|
||||||
transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \
|
|
||||||
if meta['is_oriented'] else mat_location @ mat_rotation @ mat_scale
|
|
||||||
|
|
||||||
# Wrapper a Container
|
|
||||||
if not self.container:
|
|
||||||
# Make transformation
|
|
||||||
scene_scale = float(self.scene_scale)
|
|
||||||
|
|
||||||
# (S)uperior -Z -> Y
|
|
||||||
# (A)osterior Y -> Z
|
|
||||||
mat_ras2blender = axis_conversion(
|
|
||||||
from_forward='-Z',
|
|
||||||
from_up='Y',
|
|
||||||
to_forward='Y',
|
|
||||||
to_up='Z'
|
|
||||||
).to_4x4()
|
|
||||||
|
|
||||||
mat_scene_scale = mathutils.Matrix.Scale(
|
|
||||||
scene_scale, 4
|
|
||||||
)
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
|
|
||||||
)
|
|
||||||
container = bpy.context.active_object
|
|
||||||
|
|
||||||
bbox_verts = calc_bbox_verts((0, 0, 0), layer_shape)
|
|
||||||
for index, vert in enumerate(container.data.vertices):
|
|
||||||
vert.co = transfrom @ mathutils.Vector(bbox_verts[index])
|
|
||||||
|
|
||||||
container.matrix_world = mat_ras2blender @ mat_scene_scale
|
|
||||||
container.name = container_name
|
|
||||||
container.data.name = container_name
|
|
||||||
|
|
||||||
container['bioxel_container'] = True
|
|
||||||
container['scene_scale'] = scene_scale
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
container_node_group = container.modifiers[0].node_group
|
|
||||||
input_node = get_nodes_by_type(container_node_group,
|
|
||||||
'NodeGroupInput')[0]
|
|
||||||
container_node_group.links.remove(input_node.outputs[0].links[0])
|
|
||||||
|
|
||||||
else:
|
|
||||||
container = bpy.data.objects[self.container]
|
|
||||||
container_node_group = container.modifiers[0].node_group
|
|
||||||
|
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
|
||||||
|
|
||||||
# TODO: change to transform when 4.2?
|
|
||||||
loc, rot, sca = transfrom.decompose()
|
|
||||||
layer_origin = tuple(loc)
|
|
||||||
layer_rotation = tuple(rot.to_euler())
|
|
||||||
|
|
||||||
def create_layer(volume, layer_name, layer_shape, layer_type="scalar"):
|
|
||||||
if volume.ndim == 4:
|
|
||||||
grids_sequence = []
|
|
||||||
for f in range(volume.shape[0]):
|
|
||||||
print(f"Resampling...")
|
|
||||||
frame = ski.resize(volume[f, :, :, :],
|
|
||||||
layer_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=volume.dtype.kind != "b")
|
|
||||||
|
|
||||||
grid = vdb.FloatGrid()
|
|
||||||
frame = frame.copy().astype(np.float32)
|
|
||||||
grid.copyFromArray(frame)
|
|
||||||
grid.transform = vdb.createLinearTransform(
|
|
||||||
transfrom.transposed())
|
|
||||||
grid.name = layer_type
|
|
||||||
grids_sequence.append([grid])
|
|
||||||
|
|
||||||
vdb_paths = save_vdbs(grids_sequence, context)
|
|
||||||
|
|
||||||
# Read VDB
|
|
||||||
print(f"Loading the cache to Blender scene...")
|
|
||||||
files = [{"name": str(vdb_path.name), "name": str(vdb_path.name)}
|
|
||||||
for vdb_path in vdb_paths]
|
|
||||||
|
|
||||||
bpy.ops.object.volume_import(filepath=str(vdb_paths[0]), directory=str(vdb_paths[0].parent),
|
|
||||||
files=files, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
|
|
||||||
else:
|
|
||||||
volume = ski.resize(volume,
|
|
||||||
layer_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=volume.dtype.kind != "b")
|
|
||||||
|
|
||||||
grid = vdb.FloatGrid()
|
|
||||||
volume = volume.copy().astype(np.float32)
|
|
||||||
grid.copyFromArray(volume)
|
|
||||||
grid.transform = vdb.createLinearTransform(
|
|
||||||
transfrom.transposed())
|
|
||||||
grid.name = layer_type
|
|
||||||
|
|
||||||
vdb_path = save_vdb([grid], context)
|
|
||||||
|
|
||||||
# Read VDB
|
|
||||||
print(f"Loading the cache to Blender scene...")
|
|
||||||
bpy.ops.object.volume_import(filepath=str(vdb_path),
|
|
||||||
align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
|
|
||||||
layer = bpy.context.active_object
|
|
||||||
layer.data.sequence_mode = 'REPEAT'
|
|
||||||
|
|
||||||
# Set props to VDB object
|
|
||||||
layer.name = layer_name
|
|
||||||
layer.data.name = layer_name
|
|
||||||
|
|
||||||
lock_transform(layer)
|
|
||||||
hide_in_ray(layer)
|
|
||||||
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['bioxel_layer'] = True
|
|
||||||
layer['bioxel_layer_type'] = layer_type
|
|
||||||
layer.parent = container
|
|
||||||
|
|
||||||
for collection in layer.users_collection:
|
|
||||||
collection.objects.unlink(layer)
|
|
||||||
|
|
||||||
for collection in container.users_collection:
|
|
||||||
collection.objects.link(layer)
|
|
||||||
|
|
||||||
print(f"Creating layer ...")
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
node_group = layer.modifiers[0].node_group
|
|
||||||
|
|
||||||
input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
|
|
||||||
to_layer_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes__ConvertToLayer")
|
|
||||||
|
|
||||||
node_group.links.new(input_node.outputs[0],
|
|
||||||
to_layer_node.inputs[0])
|
|
||||||
node_group.links.new(to_layer_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
|
|
||||||
# for compatibility to old vdb
|
|
||||||
# to_layer_node.inputs['Not Transfromed'].default_value = True
|
|
||||||
to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
|
||||||
200000000)
|
|
||||||
to_layer_node.inputs['Data Type'].default_value = dtype_index
|
|
||||||
to_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
|
||||||
to_layer_node.inputs['Shape'].default_value = layer_shape
|
|
||||||
to_layer_node.inputs['Origin'].default_value = layer_origin
|
|
||||||
to_layer_node.inputs['Rotation'].default_value = layer_rotation
|
|
||||||
|
|
||||||
move_node_between_nodes(to_layer_node, [input_node, output_node])
|
|
||||||
|
|
||||||
return layer
|
|
||||||
|
|
||||||
def create_mask_node(layer, node_type, node_label, offset):
|
|
||||||
mask_node = custom_nodes.add_node(container_node_group,
|
|
||||||
node_type)
|
|
||||||
mask_node.label = node_label
|
|
||||||
mask_node.inputs[0].default_value = layer
|
|
||||||
|
|
||||||
# Connect to output if no output linked
|
|
||||||
output_node = get_nodes_by_type(container_node_group,
|
|
||||||
'NodeGroupOutput')[0]
|
|
||||||
if len(output_node.inputs[0].links) == 0:
|
|
||||||
container_node_group.links.new(mask_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
|
||||||
else:
|
|
||||||
move_node_to_node(mask_node, output_node, offset)
|
|
||||||
|
|
||||||
# change shape as sequence or not
|
|
||||||
if self.is_time_sequence:
|
|
||||||
# 4->5 or 5->5
|
|
||||||
if volume.ndim == 4:
|
|
||||||
# channel as frame
|
|
||||||
volume = volume.transpose(3, 0, 1, 2)
|
|
||||||
volume = np.expand_dims(volume, axis=-1)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# 4->4 or 5->4
|
|
||||||
if volume.ndim == 5:
|
|
||||||
# select frame 0
|
|
||||||
volume = volume[0, :, :, :, :]
|
|
||||||
|
|
||||||
if self.read_as == "label":
|
|
||||||
layer_name = self.layer_name or "Label"
|
|
||||||
volume = np.amax(volume, -1)
|
|
||||||
volume = volume.astype(int)
|
|
||||||
orig_max = int(np.max(volume))
|
|
||||||
orig_min = int(np.min(volume))
|
|
||||||
|
|
||||||
for i in range(orig_max):
|
|
||||||
label = volume == np.full_like(volume, i+1)
|
|
||||||
layer_name_i = f"{layer_name}_{i+1}"
|
|
||||||
|
|
||||||
layer = create_layer(volume=label,
|
|
||||||
layer_name=f"{container_name}_{layer_name_i}",
|
|
||||||
layer_shape=layer_shape,
|
|
||||||
layer_type="label")
|
|
||||||
|
|
||||||
mask_node = create_mask_node(layer=layer,
|
|
||||||
node_type='BioxelNodes_MaskByLabel',
|
|
||||||
node_label=layer_name_i,
|
|
||||||
offset=(0, -100 * (i+1)))
|
|
||||||
|
|
||||||
else:
|
|
||||||
layer_name = self.layer_name or "Scalar"
|
|
||||||
# SHOULD NOT change any value!
|
|
||||||
volume = volume.astype(np.float32)
|
|
||||||
|
|
||||||
if self.split_channels:
|
|
||||||
for i in range(volume.shape[-1]):
|
|
||||||
scalar = volume[:, :, :, :,
|
|
||||||
0] if self.is_time_sequence else volume[:, :, :, i]
|
|
||||||
orig_max = float(np.max(scalar))
|
|
||||||
orig_min = float(np.min(scalar))
|
|
||||||
|
|
||||||
scalar_offset = 0
|
|
||||||
if orig_min < 0:
|
|
||||||
scalar_offset = -orig_min
|
|
||||||
scalar = scalar + np.full_like(scalar, scalar_offset)
|
|
||||||
|
|
||||||
layer_name_i = f"{layer_name}_{i+1}"
|
|
||||||
layer = create_layer(volume=scalar,
|
|
||||||
layer_name=f"{container_name}_{layer_name_i}",
|
|
||||||
layer_shape=layer_shape,
|
|
||||||
layer_type="scalar")
|
|
||||||
|
|
||||||
layer_node_group = layer.modifiers[0].node_group
|
|
||||||
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
|
||||||
to_layer_node.inputs['Scalar Max'].default_value = orig_max
|
|
||||||
to_layer_node.inputs['Scalar Min'].default_value = orig_min
|
|
||||||
|
|
||||||
mask_node = create_mask_node(layer=layer,
|
|
||||||
node_type='BioxelNodes_MaskByThreshold',
|
|
||||||
node_label=layer_name_i,
|
|
||||||
offset=(0, -100 * (i+1)))
|
|
||||||
|
|
||||||
else:
|
|
||||||
volume = np.amax(volume, -1)
|
|
||||||
orig_max = float(np.max(volume))
|
|
||||||
orig_min = float(np.min(volume))
|
|
||||||
|
|
||||||
scalar_offset = 0
|
|
||||||
if orig_min < 0:
|
|
||||||
scalar_offset = -orig_min
|
|
||||||
volume = volume + np.full_like(volume, scalar_offset)
|
|
||||||
|
|
||||||
layer = create_layer(volume=volume,
|
|
||||||
layer_name=f"{container_name}_{layer_name}",
|
|
||||||
layer_shape=layer_shape,
|
|
||||||
layer_type="scalar")
|
|
||||||
|
|
||||||
layer_node_group = layer.modifiers[0].node_group
|
|
||||||
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
|
||||||
to_layer_node.inputs['Scalar Max'].default_value = orig_max
|
|
||||||
to_layer_node.inputs['Scalar Min'].default_value = orig_min
|
|
||||||
|
|
||||||
mask_node = create_mask_node(layer=layer,
|
|
||||||
node_type='BioxelNodes_MaskByThreshold',
|
|
||||||
node_label=layer_name,
|
|
||||||
offset=(0, -100))
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
# Change render setting for better result
|
|
||||||
if preferences.do_change_render_setting and is_first_import:
|
|
||||||
bpy.context.scene.render.engine = 'CYCLES'
|
|
||||||
try:
|
|
||||||
bpy.context.scene.cycles.shading_system = True
|
|
||||||
bpy.context.scene.cycles.volume_bounces = 12
|
|
||||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
|
||||||
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
|
||||||
bpy.context.scene.cycles.volume_step_rate = 10
|
|
||||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
|
||||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
|
||||||
bpy.context.scene.eevee.volumetric_samples = 256
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.report({"INFO"}, "Successfully Imported")
|
|
||||||
|
|
||||||
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"Import as **{volume_dtype}** (Add to Container: {self.container})" \
|
|
||||||
if self.container != "" else f"Import as **{volume_dtype}** (Init a Container)"
|
|
||||||
context.window_manager.invoke_props_dialog(self,
|
|
||||||
width=500,
|
|
||||||
title=title)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layer_shape = get_layer_shape(self.bioxel_size,
|
|
||||||
self.orig_shape,
|
|
||||||
self.orig_spacing)
|
|
||||||
layer_size = get_layer_size(layer_shape,
|
|
||||||
self.bioxel_size,
|
|
||||||
self.scene_scale)
|
|
||||||
|
|
||||||
bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2]
|
|
||||||
layer_shape_text = f"Shape will be: {str(layer_shape)} {bioxel_count:,} "
|
|
||||||
if bioxel_count > 100000000:
|
|
||||||
layer_shape_text += "**TOO LARGE!**"
|
|
||||||
|
|
||||||
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
|
||||||
|
|
||||||
layout = self.layout
|
|
||||||
if self.container == "":
|
|
||||||
layout.prop(self, "container_name")
|
|
||||||
layout.prop(self, "layer_name")
|
|
||||||
|
|
||||||
panel = layout.box()
|
|
||||||
panel.prop(self, "bioxel_size")
|
|
||||||
row = panel.row()
|
|
||||||
row.prop(self, "orig_spacing")
|
|
||||||
panel.label(text=layer_shape_text)
|
|
||||||
|
|
||||||
panel = layout.box()
|
|
||||||
panel.prop(self, "is_time_sequence")
|
|
||||||
if self.read_as == "scalar":
|
|
||||||
panel.prop(self, "split_channels")
|
|
||||||
|
|
||||||
if self.container == "":
|
|
||||||
panel = layout.box()
|
|
||||||
panel.prop(self, "scene_scale")
|
|
||||||
panel.label(text=layer_size_text)
|
|
||||||
|
|
||||||
|
|
||||||
def get_series_ids(self, context):
|
|
||||||
items = []
|
|
||||||
for index, series_id in enumerate(self.series_ids):
|
|
||||||
items.append((
|
|
||||||
series_id.id,
|
|
||||||
series_id.label,
|
|
||||||
"",
|
|
||||||
index
|
|
||||||
))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
class ParseVolumeData(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.parse_volume_data"
|
|
||||||
bl_label = "Volumetric Data as Bioxel Layer"
|
|
||||||
bl_description = "Import Volumetric Data as Bioxel Layer"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
|
||||||
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
|
|
||||||
container: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
read_as: bpy.props.EnumProperty(
|
|
||||||
name="Read as",
|
|
||||||
default="scalar",
|
|
||||||
items=[("scalar", "Scalar", ""),
|
|
||||||
("label", "Labels", "")]
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
series_id: bpy.props.EnumProperty(
|
|
||||||
name="Select Series",
|
|
||||||
items=get_series_ids
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
series_ids: bpy.props.CollectionProperty(
|
|
||||||
type=BIOXELNODES_Series) # type: ignore
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
ext = get_ext(self.filepath)
|
|
||||||
if ext not in SUPPORT_EXTS:
|
|
||||||
self.report({"WARNING"}, "Not supported extension.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
print("Collecting Meta Data...")
|
|
||||||
volume, meta = parse_volume_data(self.filepath)
|
|
||||||
|
|
||||||
for key, value in meta.items():
|
|
||||||
print(f"{key}: {value}")
|
|
||||||
|
|
||||||
if self.read_as == "label":
|
|
||||||
not_int = volume.dtype.kind != "b" and volume.dtype.kind != "i" and volume.dtype.kind != "u"
|
|
||||||
too_large = np.max(volume) > 100
|
|
||||||
|
|
||||||
if not_int or too_large:
|
|
||||||
self.report(
|
|
||||||
{"WARNING"}, "This volume data does not looks like label, please check again.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS
|
|
||||||
|
|
||||||
orig_shape = meta['shape']
|
|
||||||
orig_spacing = meta['spacing']
|
|
||||||
min_size = min(orig_spacing[0], orig_spacing[1], orig_spacing[2])
|
|
||||||
bioxel_size = max(min_size, 1.0)
|
|
||||||
|
|
||||||
layer_size = get_layer_size(orig_shape,
|
|
||||||
bioxel_size)
|
|
||||||
log10 = math.floor(math.log10(max(*layer_size)))
|
|
||||||
scene_scale = math.pow(10, -log10)
|
|
||||||
|
|
||||||
if self.container:
|
|
||||||
container = bpy.data.objects[self.container]
|
|
||||||
container_name = container.name
|
|
||||||
else:
|
|
||||||
container_name = meta['name']
|
|
||||||
|
|
||||||
bpy.ops.bioxelnodes.import_volume_data_dialog(
|
|
||||||
'INVOKE_DEFAULT',
|
|
||||||
filepath=self.filepath,
|
|
||||||
container_name=container_name,
|
|
||||||
orig_shape=orig_shape,
|
|
||||||
orig_spacing=orig_spacing,
|
|
||||||
bioxel_size=bioxel_size,
|
|
||||||
series_id=self.series_id or "",
|
|
||||||
# do_orient=do_orient,
|
|
||||||
container=self.container,
|
|
||||||
read_as=self.read_as,
|
|
||||||
scene_scale=scene_scale
|
|
||||||
)
|
|
||||||
|
|
||||||
self.report({"INFO"}, "Successfully Readed.")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def modal(self, context, event):
|
|
||||||
self.execute(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
if not self.filepath and not self.directory:
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
show_message('Parsing volume data, it may take a while...',
|
|
||||||
'Please be patient...')
|
|
||||||
|
|
||||||
if get_ext(self.filepath) == '.dcm':
|
|
||||||
dir_path = Path(self.filepath).parent
|
|
||||||
reader = sitk.ImageSeriesReader()
|
|
||||||
reader.MetaDataDictionaryArrayUpdateOn()
|
|
||||||
reader.LoadPrivateTagsOn()
|
|
||||||
series_ids = reader.GetGDCMSeriesIDs(str(dir_path))
|
|
||||||
|
|
||||||
for _id in series_ids:
|
|
||||||
series_id = self.series_ids.add()
|
|
||||||
series_id.id = _id
|
|
||||||
series_id.label = _id
|
|
||||||
|
|
||||||
if len(series_ids) > 1:
|
|
||||||
context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
else:
|
|
||||||
self.series_id = series_ids[0]
|
|
||||||
|
|
||||||
context.window_manager.modal_handler_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.prop(self, "series_id")
|
|
||||||
layout.label(
|
|
||||||
text="Reading image data, it may take a while...")
|
|
||||||
layout.label(
|
|
||||||
text="Please be patient...")
|
|
||||||
|
|
||||||
|
|
||||||
class ImportVolumeData():
|
|
||||||
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):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) > 0:
|
|
||||||
bpy.ops.bioxelnodes.parse_volume_data(
|
|
||||||
'INVOKE_DEFAULT',
|
|
||||||
filepath=self.filepath,
|
|
||||||
directory=self.directory,
|
|
||||||
container=containers[0].name,
|
|
||||||
read_as=self.read_as
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
bpy.ops.bioxelnodes.parse_volume_data(
|
|
||||||
'INVOKE_DEFAULT',
|
|
||||||
filepath=self.filepath,
|
|
||||||
directory=self.directory,
|
|
||||||
read_as=self.read_as
|
|
||||||
)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.fileselect_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class ImportAsScalarLayer(bpy.types.Operator, ImportVolumeData):
|
|
||||||
bl_idname = "bioxelnodes.import_as_scalar_layer"
|
|
||||||
bl_label = "Import as Scalar"
|
|
||||||
bl_description = "Import Volumetric Data to Container as Scalar"
|
|
||||||
read_as = "scalar"
|
|
||||||
|
|
||||||
|
|
||||||
class ImportAsLabelLayer(bpy.types.Operator, ImportVolumeData):
|
|
||||||
bl_idname = "bioxelnodes.import_as_label_layer"
|
|
||||||
bl_label = "Import as Label"
|
|
||||||
bl_description = "Import Volumetric Data to Container as Label"
|
|
||||||
read_as = "label"
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
class BIOXELNODES_FH_ImportVolumeData(bpy.types.FileHandler):
|
|
||||||
bl_idname = "BIOXELNODES_FH_ImportVolumeData"
|
|
||||||
bl_label = "File handler for dicom import"
|
|
||||||
bl_import_operator = "bioxelnodes.parse_volume_data"
|
|
||||||
bl_file_extensions = ";".join(FH_EXTS)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll_drop(cls, context):
|
|
||||||
return (context.area and context.area.type == 'VIEW_3D')
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class ExportVolumeData(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.export_volume_data"
|
|
||||||
bl_label = "Export Layer as VDB"
|
|
||||||
bl_description = "Export Layer as VDB"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
filepath: bpy.props.StringProperty(
|
|
||||||
subtype="FILE_PATH"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
filename_ext = ".vdb"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
return True if layer else False
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.fileselect_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
filepath = f"{self.filepath.split('.')[0]}.vdb"
|
|
||||||
# "//"
|
|
||||||
source_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
|
|
||||||
output_path: Path = Path(filepath).resolve()
|
|
||||||
source_path: Path = Path(source_dir).resolve()
|
|
||||||
# print('output_path', output_path)
|
|
||||||
# print('source_path', source_path)
|
|
||||||
shutil.copy(source_path, output_path)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully exported to {output_path}")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# TODO: WIP
|
|
||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
|
|
||||||
|
|
||||||
def create_points_obj(points, name="points"):
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
|
|
||||||
)
|
|
||||||
obj = bpy.context.active_object
|
|
||||||
mesh = obj.data
|
|
||||||
|
|
||||||
bm = bmesh.new()
|
|
||||||
value_layer = bm.verts.layers.float.new('value')
|
|
||||||
|
|
||||||
for point in points:
|
|
||||||
pos = point['pos']
|
|
||||||
value = point['value']
|
|
||||||
vert = bm.verts.new(pos) # add a new vert
|
|
||||||
vert[value_layer] = value
|
|
||||||
|
|
||||||
# make the bmesh the object's mesh
|
|
||||||
bm.to_mesh(mesh)
|
|
||||||
bm.free() # always do this when finished
|
|
||||||
return obj
|
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from .utils import get_container_from_selection
|
from .bioxelutils.container import get_container_objs_from_selection
|
||||||
from .operators import (AddPieCutter, AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
|
from .operators.layer import (CombineLabels, SignScalar,
|
||||||
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange, PickBboxWire, PickMesh, PickVolume)
|
FillByLabel, FillByThreshold, FillByRange)
|
||||||
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer
|
from .operators.container import (SaveContainer, LoadContainer,
|
||||||
from .save import CleanAllCaches, ReLinkNodes, SaveLayers, SaveStagedData
|
AddPieCutter, AddPlaneCutter,
|
||||||
|
AddCylinderCutter, AddCubeCutter, AddSphereCutter,
|
||||||
|
PickBboxWire, PickMesh, PickVolume)
|
||||||
|
from .operators.io import (ExportVolumetricData,
|
||||||
|
ImportAsLabelLayer, ImportAsScalarLayer)
|
||||||
|
from .operators.misc import (CleanAllCaches,
|
||||||
|
ReLinkNodes, SaveCaches, SaveStagedData)
|
||||||
|
|
||||||
|
|
||||||
|
def container_is_selected():
|
||||||
|
container_objs = get_container_objs_from_selection()
|
||||||
|
return len(container_objs) > 0
|
||||||
|
|
||||||
|
|
||||||
class PickFromContainerMenu(bpy.types.Menu):
|
class PickFromContainerMenu(bpy.types.Menu):
|
||||||
@@ -24,7 +35,7 @@ class ModifyLayerMenu(bpy.types.Menu):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.operator(InvertScalar.bl_idname)
|
layout.operator(SignScalar.bl_idname)
|
||||||
layout.operator(FillByThreshold.bl_idname)
|
layout.operator(FillByThreshold.bl_idname)
|
||||||
layout.operator(FillByRange.bl_idname)
|
layout.operator(FillByRange.bl_idname)
|
||||||
layout.operator(FillByLabel.bl_idname)
|
layout.operator(FillByLabel.bl_idname)
|
||||||
@@ -60,8 +71,7 @@ class BioxelNodesView3DMenu(bpy.types.Menu):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
containers = get_container_from_selection()
|
is_selected = container_is_selected()
|
||||||
is_selected = len(containers) > 0
|
|
||||||
layout.operator(ImportAsScalarLayer.bl_idname,
|
layout.operator(ImportAsScalarLayer.bl_idname,
|
||||||
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
||||||
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
||||||
@@ -79,7 +89,7 @@ class BioxelNodesView3DMenu(bpy.types.Menu):
|
|||||||
layout.operator(PickVolume.bl_idname)
|
layout.operator(PickVolume.bl_idname)
|
||||||
layout.operator(PickBboxWire.bl_idname)
|
layout.operator(PickBboxWire.bl_idname)
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator(SaveLayers.bl_idname)
|
layout.operator(SaveCaches.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
||||||
@@ -88,8 +98,7 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
containers = get_container_from_selection()
|
is_selected = container_is_selected()
|
||||||
is_selected = len(containers) > 0
|
|
||||||
layout.operator(ImportAsScalarLayer.bl_idname,
|
layout.operator(ImportAsScalarLayer.bl_idname,
|
||||||
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
||||||
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
||||||
@@ -107,21 +116,18 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
|||||||
layout.operator(PickVolume.bl_idname)
|
layout.operator(PickVolume.bl_idname)
|
||||||
layout.operator(PickBboxWire.bl_idname)
|
layout.operator(PickBboxWire.bl_idname)
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator(SaveLayers.bl_idname)
|
layout.operator(SaveCaches.bl_idname)
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator(InvertScalar.bl_idname)
|
layout.operator(SignScalar.bl_idname)
|
||||||
layout.operator(FillByThreshold.bl_idname)
|
layout.operator(FillByThreshold.bl_idname)
|
||||||
layout.operator(FillByRange.bl_idname)
|
layout.operator(FillByRange.bl_idname)
|
||||||
layout.operator(FillByLabel.bl_idname)
|
layout.operator(FillByLabel.bl_idname)
|
||||||
layout.operator(CombineLabels.bl_idname)
|
layout.operator(CombineLabels.bl_idname)
|
||||||
layout.separator()
|
|
||||||
layout.operator(ExportVolumeData.bl_idname)
|
|
||||||
|
|
||||||
|
|
||||||
def TOPBAR_FILE_IMPORT(self, context):
|
def TOPBAR_FILE_IMPORT(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
containers = get_container_from_selection()
|
is_selected = container_is_selected()
|
||||||
is_selected = len(containers) > 0
|
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.menu(ImportLayerMenu.bl_idname, text="Volumetric Data as Bioxel (Add to)"
|
layout.menu(ImportLayerMenu.bl_idname, text="Volumetric Data as Bioxel (Add to)"
|
||||||
@@ -131,7 +137,7 @@ def TOPBAR_FILE_IMPORT(self, context):
|
|||||||
def TOPBAR_FILE_EXPORT(self, context):
|
def TOPBAR_FILE_EXPORT(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator(ExportVolumeData.bl_idname,
|
layout.operator(ExportVolumetricData.bl_idname,
|
||||||
text="Bioxel Layer (.vdb)")
|
text="Bioxel Layer (.vdb)")
|
||||||
|
|
||||||
|
|
||||||
@@ -153,12 +159,14 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
containers = get_container_from_selection()
|
is_selected = container_is_selected()
|
||||||
is_selected = len(containers) > 0
|
|
||||||
|
|
||||||
layout.menu(ImportLayerMenu.bl_idname, text=ImportLayerMenu.bl_label+" (Add to)"
|
layout.menu(ImportLayerMenu.bl_idname, text=ImportLayerMenu.bl_label+" (Add to)"
|
||||||
if is_selected else ImportLayerMenu.bl_label+" (Init)")
|
if is_selected else ImportLayerMenu.bl_label+" (Init)")
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
layout.operator(LoadContainer.bl_idname)
|
||||||
|
layout.operator(SaveContainer.bl_idname)
|
||||||
|
layout.separator()
|
||||||
layout.menu(AddCutterMenu.bl_idname)
|
layout.menu(AddCutterMenu.bl_idname)
|
||||||
layout.menu(PickFromContainerMenu.bl_idname)
|
layout.menu(PickFromContainerMenu.bl_idname)
|
||||||
layout.separator()
|
layout.separator()
|
||||||
@@ -175,7 +183,7 @@ def TOPBAR(self, context):
|
|||||||
|
|
||||||
def add():
|
def add():
|
||||||
bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
|
bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
|
||||||
bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
|
# bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
|
||||||
bpy.types.OUTLINER_MT_object.append(OUTLINER_OBJECT)
|
bpy.types.OUTLINER_MT_object.append(OUTLINER_OBJECT)
|
||||||
bpy.types.VIEW3D_MT_object_context_menu.append(VIEW3D_OBJECT)
|
bpy.types.VIEW3D_MT_object_context_menu.append(VIEW3D_OBJECT)
|
||||||
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
|
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
|
||||||
@@ -183,7 +191,7 @@ def add():
|
|||||||
|
|
||||||
def remove():
|
def remove():
|
||||||
bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT)
|
bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT)
|
||||||
bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT)
|
# bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT)
|
||||||
bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT)
|
bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT)
|
||||||
bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT)
|
bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT)
|
||||||
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
|
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
|
||||||
|
|||||||
@@ -1,62 +1,8 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .customnodes import CustomNodes
|
from .customnodes import CustomNodes
|
||||||
import bpy
|
|
||||||
|
|
||||||
# def set_object_to_node_factory(object_type:str):
|
NODE_FILE = "BioxelNodes_4.2"
|
||||||
# if object_type == "plane":
|
|
||||||
# create_object = """
|
|
||||||
# bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
# """
|
|
||||||
# callback_str = f"""
|
|
||||||
# import bpy
|
|
||||||
# from ..utils import get_container
|
|
||||||
# container = get_container(bpy.context.active_object)
|
|
||||||
# if container:
|
|
||||||
# {create_object}
|
|
||||||
# object = bpy.context.active_object
|
|
||||||
# node.inputs[0].default_value = object
|
|
||||||
# else:
|
|
||||||
# print('Cannot find any Bioxel Container.')
|
|
||||||
# """
|
|
||||||
# return callback_str
|
|
||||||
|
|
||||||
|
|
||||||
# def add_driver_to_node_factory(source_prop, target_prop):
|
|
||||||
# callback_str = f"""
|
|
||||||
# import bpy
|
|
||||||
# from .utils import add_direct_driver, get_bioxels_obj
|
|
||||||
# bioxels_obj = get_bioxels_obj(bpy.context.active_object)
|
|
||||||
# if bioxels_obj:
|
|
||||||
# container_obj = bioxels_obj.parent
|
|
||||||
# add_direct_driver(
|
|
||||||
# target=node,
|
|
||||||
# target_prop='{target_prop}',
|
|
||||||
# source=container_obj,
|
|
||||||
# source_prop='{source_prop}'
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# print('Cannot find any bioxels.')
|
|
||||||
# """
|
|
||||||
# return callback_str
|
|
||||||
|
|
||||||
|
|
||||||
# def set_prop_to_node_factory(source_prop, target_prop):
|
|
||||||
# callback_str = f"""
|
|
||||||
# import bpy
|
|
||||||
# from .utils import get_bioxels_obj
|
|
||||||
# bioxels_obj = get_bioxels_obj(bpy.context.active_object)
|
|
||||||
# if bioxels_obj:
|
|
||||||
# container_obj = bioxels_obj.parent
|
|
||||||
# node.inputs.get('{target_prop}').default_value = container_obj.get(
|
|
||||||
# '{source_prop}')
|
|
||||||
# else:
|
|
||||||
# print('Cannot find any bioxels.')
|
|
||||||
# """
|
|
||||||
# return callback_str
|
|
||||||
|
|
||||||
|
|
||||||
NODE_FILE = "BioxelNodes_4.1"
|
|
||||||
|
|
||||||
MENU_ITEMS = [
|
MENU_ITEMS = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,929 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
import random
|
|
||||||
import bpy
|
|
||||||
import pyopenvdb as vdb
|
|
||||||
import numpy as np
|
|
||||||
import bmesh
|
|
||||||
from . import skimage as ski
|
|
||||||
from . import scipy
|
|
||||||
from .nodes import custom_nodes
|
|
||||||
from .utils import (get_container, get_container_from_selection, get_container_layers,
|
|
||||||
get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, select_object)
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer_name(layer):
|
|
||||||
container = layer.parent
|
|
||||||
return layer.name.removeprefix(container.name).replace("_", "")
|
|
||||||
|
|
||||||
|
|
||||||
def get_grids(layer):
|
|
||||||
layer_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
grids, base_metadata = vdb.readAll(layer_dir)
|
|
||||||
return grids
|
|
||||||
|
|
||||||
|
|
||||||
def set_volume(grids, index, volume):
|
|
||||||
grids[index].clear()
|
|
||||||
grids[index].copyFromArray(volume.copy().astype(np.float32))
|
|
||||||
|
|
||||||
|
|
||||||
def get_volume(grids, index, shape):
|
|
||||||
volume = np.ndarray(shape, np.float32)
|
|
||||||
volume.fill(index)
|
|
||||||
grids[0].copyToArray(volume)
|
|
||||||
return volume
|
|
||||||
|
|
||||||
|
|
||||||
def get_shape(layer):
|
|
||||||
layer_node = layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
return [int(a)
|
|
||||||
for a in layer_node.inputs['Shape'].default_value]
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer_meta(layer, key: str):
|
|
||||||
layer_node = layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
return layer_node.inputs[key].default_value
|
|
||||||
|
|
||||||
|
|
||||||
def set_layer_meta(layer, key: str, value):
|
|
||||||
layer_node = layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
layer_node.inputs[key].default_value = value
|
|
||||||
|
|
||||||
|
|
||||||
def add_mask_node(container, layer, node_type: str, node_label: str):
|
|
||||||
modifier = container.modifiers[0]
|
|
||||||
container_node_group = modifier.node_group
|
|
||||||
|
|
||||||
mask_node = custom_nodes.add_node(container_node_group, node_type)
|
|
||||||
mask_node.label = node_label
|
|
||||||
mask_node.inputs[0].default_value = layer
|
|
||||||
|
|
||||||
# Connect to output if no output linked
|
|
||||||
output_node = get_nodes_by_type(container_node_group,
|
|
||||||
'NodeGroupOutput')[0]
|
|
||||||
|
|
||||||
if len(output_node.inputs[0].links) == 0:
|
|
||||||
container_node_group.links.new(mask_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
|
||||||
else:
|
|
||||||
move_node_to_node(mask_node, output_node, (0, -100))
|
|
||||||
|
|
||||||
return mask_node
|
|
||||||
|
|
||||||
|
|
||||||
def deep_copy_layer(vdb_path, base_layer, name):
|
|
||||||
# Read VDB
|
|
||||||
print(f"Loading the cache to Blender scene...")
|
|
||||||
bpy.ops.object.volume_import(
|
|
||||||
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
|
|
||||||
copyed_layer = bpy.context.active_object
|
|
||||||
|
|
||||||
# Set props to VDB object
|
|
||||||
copyed_layer.name = name
|
|
||||||
copyed_layer.data.name = name
|
|
||||||
|
|
||||||
lock_transform(copyed_layer)
|
|
||||||
hide_in_ray(copyed_layer)
|
|
||||||
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['bioxel_layer'] = True
|
|
||||||
copyed_layer['bioxel_layer_type'] = base_layer['bioxel_layer_type']
|
|
||||||
copyed_layer.parent = base_layer.parent
|
|
||||||
|
|
||||||
for collection in copyed_layer.users_collection:
|
|
||||||
collection.objects.unlink(copyed_layer)
|
|
||||||
|
|
||||||
for collection in base_layer.users_collection:
|
|
||||||
collection.objects.link(copyed_layer)
|
|
||||||
|
|
||||||
# add convert to layer node
|
|
||||||
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
|
|
||||||
not_transformed = base_layer_node.inputs['Not Transfromed'].default_value
|
|
||||||
dtype_index = base_layer_node.inputs['Data Type'].default_value
|
|
||||||
bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
|
|
||||||
layer_shape = base_layer_node.inputs['Shape'].default_value
|
|
||||||
layer_origin = base_layer_node.inputs['Origin'].default_value
|
|
||||||
layer_rotation = base_layer_node.inputs['Rotation'].default_value
|
|
||||||
scalar_offset = base_layer_node.inputs['Scalar Offset'].default_value
|
|
||||||
scalar_min = base_layer_node.inputs['Scalar Min'].default_value
|
|
||||||
scalar_max = base_layer_node.inputs['Scalar Max'].default_value
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
node_group = copyed_layer.modifiers[0].node_group
|
|
||||||
|
|
||||||
input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
|
|
||||||
copyed_layer_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes__ConvertToLayer")
|
|
||||||
|
|
||||||
node_group.links.new(input_node.outputs[0], copyed_layer_node.inputs[0])
|
|
||||||
node_group.links.new(copyed_layer_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
# for compatibility to old vdb
|
|
||||||
copyed_layer_node.inputs['Not Transfromed'].default_value = not_transformed
|
|
||||||
copyed_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
|
||||||
200000000)
|
|
||||||
copyed_layer_node.inputs['Data Type'].default_value = dtype_index
|
|
||||||
copyed_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
|
||||||
copyed_layer_node.inputs['Shape'].default_value = layer_shape
|
|
||||||
copyed_layer_node.inputs['Origin'].default_value = layer_origin
|
|
||||||
copyed_layer_node.inputs['Rotation'].default_value = layer_rotation
|
|
||||||
copyed_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
|
||||||
copyed_layer_node.inputs['Scalar Min'].default_value = scalar_min
|
|
||||||
copyed_layer_node.inputs['Scalar Max'].default_value = scalar_max
|
|
||||||
|
|
||||||
return copyed_layer
|
|
||||||
|
|
||||||
|
|
||||||
def get_scalar_layer_selection(self, context):
|
|
||||||
items = [("None", "None", "")]
|
|
||||||
container = get_container(bpy.context.active_object)
|
|
||||||
for layer in get_container_layers(container):
|
|
||||||
if layer.get("bioxel_layer_type") == "scalar":
|
|
||||||
items.append((
|
|
||||||
layer.name,
|
|
||||||
layer.name,
|
|
||||||
""
|
|
||||||
))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def get_label_layer_selection(self, context):
|
|
||||||
items = [("None", "None", "")]
|
|
||||||
container = get_container(bpy.context.active_object)
|
|
||||||
for layer in get_container_layers(container):
|
|
||||||
if layer.get("bioxel_layer_type") == "label":
|
|
||||||
items.append((
|
|
||||||
layer.name,
|
|
||||||
layer.name,
|
|
||||||
""
|
|
||||||
))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
# class JoinLayers(bpy.types.Operator):
|
|
||||||
# bl_idname = "bioxelnodes.join_layers"
|
|
||||||
# bl_label = "Join Bioxel Layers"
|
|
||||||
# bl_description = "Join Additional Bioxel Layers"
|
|
||||||
# bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
# base_layer: bpy.props.StringProperty(
|
|
||||||
# options={"HIDDEN"}
|
|
||||||
# ) # type: ignore
|
|
||||||
|
|
||||||
# scalar_layer: bpy.props.EnumProperty(
|
|
||||||
# name="Scalar Layer",
|
|
||||||
# items=get_scalar_layer_selection
|
|
||||||
# ) # type: ignore
|
|
||||||
|
|
||||||
# label_layer: bpy.props.EnumProperty(
|
|
||||||
# name="Label Layer",
|
|
||||||
# items=get_label_layer_selection
|
|
||||||
# ) # type: ignore
|
|
||||||
# # color_layer: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def poll(cls, context):
|
|
||||||
# layer = get_layer(bpy.context.active_object)
|
|
||||||
# return True if layer else False
|
|
||||||
|
|
||||||
# def execute(self, context):
|
|
||||||
# base_layer = bpy.data.objects[self.base_layer]
|
|
||||||
|
|
||||||
# if not base_layer:
|
|
||||||
# self.report({"WARNING"}, "Cannot find any bioxel layer as base.")
|
|
||||||
# return {'FINISHED'}
|
|
||||||
|
|
||||||
# base_layer_dir = bpy.path.abspath(base_layer.data.filepath)
|
|
||||||
# base_grids, base_metadata = vdb.readAll(base_layer_dir)
|
|
||||||
|
|
||||||
# layers = []
|
|
||||||
# if self.scalar_layer != "None":
|
|
||||||
# scalar_layer = bpy.data.objects[self.scalar_layer]
|
|
||||||
# layers.append(scalar_layer)
|
|
||||||
|
|
||||||
# if self.label_layer != "None":
|
|
||||||
# label_layer = bpy.data.objects[self.label_layer]
|
|
||||||
# layers.append(label_layer)
|
|
||||||
|
|
||||||
# # TODO: add color and vector
|
|
||||||
|
|
||||||
# if len(layers) == 0:
|
|
||||||
# self.report({"WARNING"}, "No additinal layers setted.")
|
|
||||||
# return {'FINISHED'}
|
|
||||||
|
|
||||||
# for layer in layers:
|
|
||||||
# layer_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
# grids, metadata = vdb.readAll(layer_dir)
|
|
||||||
# base_grids.extend(grids)
|
|
||||||
|
|
||||||
# vdb_path = save_vdb(base_grids, context)
|
|
||||||
|
|
||||||
# joined_layer = deep_copy_layer(vdb_path,
|
|
||||||
# base_layer,
|
|
||||||
# f"{base_layer.name}_Joined")
|
|
||||||
|
|
||||||
# return {'FINISHED'}
|
|
||||||
|
|
||||||
# def invoke(self, context, event):
|
|
||||||
# base_layer = get_layer(bpy.context.active_object)
|
|
||||||
# self.base_layer = base_layer.name
|
|
||||||
# context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
# return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class InvertScalar(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.invert_scalar"
|
|
||||||
bl_label = "Invert Scalar"
|
|
||||||
bl_description = "Invert the scalar value"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
inverted_layer_name = f"{get_layer_name(base_layer)}_Inverted"
|
|
||||||
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
|
|
||||||
base_volume = -(base_volume - scalar_offset)
|
|
||||||
|
|
||||||
base_min = float(np.min(base_volume))
|
|
||||||
base_max = float(np.max(base_volume))
|
|
||||||
|
|
||||||
scalar_offset = 0
|
|
||||||
if base_min < 0:
|
|
||||||
scalar_offset = -base_min
|
|
||||||
base_volume = base_volume + scalar_offset
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
inverted_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{inverted_layer_name}")
|
|
||||||
set_layer_meta(inverted_layer, 'Scalar Offset', scalar_offset)
|
|
||||||
set_layer_meta(inverted_layer, 'Scalar Min', base_min)
|
|
||||||
set_layer_meta(inverted_layer, 'Scalar Max', base_max)
|
|
||||||
|
|
||||||
add_mask_node(container,
|
|
||||||
inverted_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
inverted_layer_name)
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class FillByThreshold(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.fill_by_threshold"
|
|
||||||
bl_label = "Fill Value by Threshold"
|
|
||||||
bl_description = "Fill Value by Threshold"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
threshold: bpy.props.FloatProperty(
|
|
||||||
name="Threshold",
|
|
||||||
soft_min=0, soft_max=1024,
|
|
||||||
default=128,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
fill_value: bpy.props.FloatProperty(
|
|
||||||
name="Fill Value",
|
|
||||||
soft_min=0, soft_max=1024.0,
|
|
||||||
default=0,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
invert: bpy.props.BoolProperty(
|
|
||||||
name="Invert Area",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
filled_layer_name = f"{get_layer_name(base_layer)}_{self.threshold}-Filled"
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
mask = base_volume > (self.threshold + scalar_offset)
|
|
||||||
mask = scipy.median_filter(mask.astype(np.float32), size=2)
|
|
||||||
if self.invert:
|
|
||||||
base_volume = mask * base_volume + \
|
|
||||||
(1-mask) * (self.fill_value + scalar_offset)
|
|
||||||
else:
|
|
||||||
base_volume = (1-mask) * base_volume + \
|
|
||||||
mask * (self.fill_value + scalar_offset)
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
filled_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{filled_layer_name}")
|
|
||||||
mask_node = add_mask_node(container,
|
|
||||||
filled_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
filled_layer_name)
|
|
||||||
|
|
||||||
mask_node.inputs[1].default_value = self.threshold
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
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
|
|
||||||
context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class FillByRange(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.fill_by_range"
|
|
||||||
bl_label = "Fill Value by Range"
|
|
||||||
bl_description = "Fill Value by Range"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
from_min: bpy.props.FloatProperty(
|
|
||||||
name="From Min",
|
|
||||||
soft_min=0, soft_max=1024,
|
|
||||||
default=128,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
from_max: bpy.props.FloatProperty(
|
|
||||||
name="From Max",
|
|
||||||
soft_min=0, soft_max=1024,
|
|
||||||
default=256,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
fill_value: bpy.props.FloatProperty(
|
|
||||||
name="Fill Value",
|
|
||||||
soft_min=0, soft_max=1024.0,
|
|
||||||
default=0,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
invert: bpy.props.BoolProperty(
|
|
||||||
name="Invert Area",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
filled_layer_name = f"{get_layer_name(base_layer)}_{self.from_min}-{self.from_max}-Filled"
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
|
|
||||||
mask = (base_volume > self.from_min + scalar_offset) \
|
|
||||||
& (base_volume < self.from_max + scalar_offset)
|
|
||||||
mask = scipy.median_filter(mask.astype(np.float32), size=2)
|
|
||||||
|
|
||||||
if self.invert:
|
|
||||||
base_volume = mask * base_volume + \
|
|
||||||
(1-mask) * (self.fill_value + scalar_offset)
|
|
||||||
else:
|
|
||||||
base_volume = (1-mask) * base_volume + \
|
|
||||||
mask * (self.fill_value + scalar_offset)
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
filled_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{filled_layer_name}")
|
|
||||||
mask_node = add_mask_node(container,
|
|
||||||
filled_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
filled_layer_name)
|
|
||||||
|
|
||||||
mask_node.inputs[1].default_value = self.from_min
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
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
|
|
||||||
context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class FillByLabel(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.fill_by_label"
|
|
||||||
bl_label = "Fill Value by Label"
|
|
||||||
bl_description = "Fill Value by Label Area"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
label_layer: bpy.props.EnumProperty(
|
|
||||||
name="Label Layer",
|
|
||||||
items=get_label_layer_selection
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
fill_value: bpy.props.FloatProperty(
|
|
||||||
name="Fill Value",
|
|
||||||
soft_min=0, soft_max=1024.0,
|
|
||||||
default=0,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
invert: bpy.props.BoolProperty(
|
|
||||||
name="Invert Label",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
label_layer = bpy.data.objects[self.label_layer]
|
|
||||||
|
|
||||||
if not label_layer:
|
|
||||||
self.report({"WARNING"}, "Cannot find any label layer.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
filled_layer_name = f"{get_layer_name(base_layer)}_{get_layer_name(label_layer)}-Filled"
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
label_shape = get_shape(label_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
|
|
||||||
label_grids = get_grids(label_layer)
|
|
||||||
mask = get_volume(label_grids, 0, label_shape)
|
|
||||||
mask = ski.resize(mask,
|
|
||||||
base_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=True)
|
|
||||||
mask = scipy.median_filter(mask.astype(np.float32), size=2)
|
|
||||||
|
|
||||||
if self.invert:
|
|
||||||
base_volume = mask * base_volume + \
|
|
||||||
(1-mask) * (self.fill_value + scalar_offset)
|
|
||||||
else:
|
|
||||||
base_volume = (1-mask) * base_volume + \
|
|
||||||
mask * (self.fill_value + scalar_offset)
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
filled_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{filled_layer_name}")
|
|
||||||
add_mask_node(container,
|
|
||||||
filled_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
filled_layer_name)
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
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
|
|
||||||
context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class CombineLabels(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.combine_labels"
|
|
||||||
bl_label = "Combine Labels"
|
|
||||||
bl_description = "Combine all selected labels"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
labels = [obj for obj in context.selected_ids
|
|
||||||
if obj.get("bioxel_layer_type") == "label"]
|
|
||||||
return True if len(labels) > 1 else False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
labels = [obj for obj in context.selected_ids
|
|
||||||
if obj.get("bioxel_layer_type") == "label"]
|
|
||||||
base_layer = labels[0]
|
|
||||||
labels = labels[1:]
|
|
||||||
container = base_layer.parent
|
|
||||||
label_names = [get_layer_name(base_layer)]
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
base_volume = base_volume
|
|
||||||
|
|
||||||
for label in labels:
|
|
||||||
label_shape = get_shape(label)
|
|
||||||
label_grids = get_grids(label)
|
|
||||||
label_volume = get_volume(label_grids, 0, label_shape)
|
|
||||||
label_volume = ski.resize(label_volume,
|
|
||||||
base_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=True)
|
|
||||||
base_volume = np.maximum(base_volume, label_volume)
|
|
||||||
label_names.append(get_layer_name(label))
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
|
|
||||||
combined_layer_name = f"{'-'.join(label_names)}-Combined"
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
combined_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{combined_layer_name}")
|
|
||||||
add_mask_node(container,
|
|
||||||
combined_layer,
|
|
||||||
"BioxelNodes_MaskByLabel",
|
|
||||||
combined_layer_name)
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class ConvertToMesh(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.convert_to_mesh"
|
|
||||||
bl_label = "Convert To Mesh"
|
|
||||||
bl_description = "Convert Container To Mesh"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
mesh = bpy.context.active_object
|
|
||||||
|
|
||||||
mesh.name = f"Mesh_{container.name}"
|
|
||||||
|
|
||||||
# bpy.ops.object.constraint_add(type='COPY_TRANSFORMS')
|
|
||||||
# mesh.constraints[0].target = container
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = mesh.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
to_mesh_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickMesh")
|
|
||||||
|
|
||||||
to_mesh_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(to_mesh_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
# bpy.ops.constraint.apply(
|
|
||||||
# constraint=mesh.constraints[0].name, owner='OBJECT')
|
|
||||||
bpy.ops.object.modifier_apply(modifier=mesh.modifiers[0].name)
|
|
||||||
|
|
||||||
select_object(mesh)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully convert to mesh")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class PickMesh(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.pick_mesh"
|
|
||||||
bl_label = "Pick Mesh"
|
|
||||||
bl_description = "Pick Container Mesh"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
mesh = bpy.context.active_object
|
|
||||||
|
|
||||||
mesh.name = f"Mesh_{container.name}"
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = mesh.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
pick_mesh_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickMesh")
|
|
||||||
|
|
||||||
pick_mesh_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
select_object(mesh)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully picked mesh")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class PickVolume(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.pick_volume"
|
|
||||||
bl_label = "Pick Volume"
|
|
||||||
bl_description = "Pick Container Volume"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
volume = bpy.context.active_object
|
|
||||||
|
|
||||||
volume.name = f"Volume_{container.name}"
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = volume.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
pick_volume_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickVolume")
|
|
||||||
|
|
||||||
pick_volume_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(
|
|
||||||
pick_volume_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
select_object(volume)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully picked volume")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class PickBboxWire(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.pick_bbox_wire"
|
|
||||||
bl_label = "Pick Bbox Wire"
|
|
||||||
bl_description = "Pick Container Bbox Wire"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
bbox_wire = bpy.context.active_object
|
|
||||||
|
|
||||||
bbox_wire.name = f"Wire_{container.name}"
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = bbox_wire.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
pick_bbox_wire_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickBboxWire")
|
|
||||||
|
|
||||||
pick_bbox_wire_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(
|
|
||||||
pick_bbox_wire_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
select_object(bbox_wire)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully picked bbox wire")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AddCutter():
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
if self.object_type == "plane":
|
|
||||||
node_type = "BioxelNodes_PlaneObjectCutter"
|
|
||||||
bpy.ops.mesh.primitive_plane_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
elif self.object_type == "cylinder":
|
|
||||||
node_type = "BioxelNodes_CylinderObjectCutter"
|
|
||||||
bpy.ops.mesh.primitive_cylinder_add(
|
|
||||||
radius=1, depth=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
bpy.context.object.rotation_euler[0] = container.rotation_euler[0]
|
|
||||||
elif self.object_type == "cube":
|
|
||||||
node_type = "BioxelNodes_CubeObjectCutter"
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
elif self.object_type == "sphere":
|
|
||||||
node_type = "BioxelNodes_SphereObjectCutter"
|
|
||||||
bpy.ops.mesh.primitive_ico_sphere_add(
|
|
||||||
radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
elif self.object_type == "pie":
|
|
||||||
node_type = "BioxelNodes_PieObjectCutter"
|
|
||||||
# Create mesh
|
|
||||||
pie_mesh = bpy.data.meshes.new('Pie')
|
|
||||||
|
|
||||||
# Create object
|
|
||||||
pie = bpy.data.objects.new('Pie', pie_mesh)
|
|
||||||
|
|
||||||
# Link object to scene
|
|
||||||
bpy.context.scene.collection.objects.link(pie)
|
|
||||||
# Get a BMesh representation
|
|
||||||
bm = bmesh.new() # create an empty BMesh
|
|
||||||
bm.from_mesh(pie_mesh) # fill it in from a Mesh
|
|
||||||
|
|
||||||
# Hot to create vertices
|
|
||||||
v_0 = bm.verts.new((0.0, -1.0, 0.0))
|
|
||||||
v_1 = bm.verts.new((-1.0, -1.0, 1.0))
|
|
||||||
v_2 = bm.verts.new((0.0, 1.0, 0.0))
|
|
||||||
v_3 = bm.verts.new((-1.0, 1.0, 1.0))
|
|
||||||
v_4 = bm.verts.new((1.0, -1.0, 1.0))
|
|
||||||
v_5 = bm.verts.new((1.0, 1.0, 1.0))
|
|
||||||
|
|
||||||
# Initialize the index values of this sequence.
|
|
||||||
bm.verts.index_update()
|
|
||||||
|
|
||||||
# How to create a face
|
|
||||||
# it's not necessary to create the edges before, I made it only to show how create
|
|
||||||
# edges too
|
|
||||||
bm.faces.new((v_0, v_1, v_3, v_2))
|
|
||||||
bm.faces.new((v_0, v_2, v_5, v_4))
|
|
||||||
|
|
||||||
# Finish up, write the bmesh back to the mesh
|
|
||||||
bm.to_mesh(pie_mesh)
|
|
||||||
bpy.context.view_layer.objects.active = pie
|
|
||||||
|
|
||||||
cutter = bpy.context.active_object
|
|
||||||
cutter.visible_camera = False
|
|
||||||
cutter.visible_diffuse = False
|
|
||||||
cutter.visible_glossy = False
|
|
||||||
cutter.visible_transmission = False
|
|
||||||
cutter.visible_volume_scatter = False
|
|
||||||
cutter.visible_shadow = False
|
|
||||||
cutter.hide_render = True
|
|
||||||
cutter.display_type = 'WIRE'
|
|
||||||
|
|
||||||
modifier = container.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
cutter_node = custom_nodes.add_node(node_group, node_type)
|
|
||||||
cutter_node.inputs[0].default_value = cutter
|
|
||||||
|
|
||||||
cut_nodes = get_nodes_by_type(node_group,
|
|
||||||
'BioxelNodes_Cut')
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
if len(cut_nodes) == 0:
|
|
||||||
cut_node = custom_nodes.add_node(node_group, 'BioxelNodes_Cut')
|
|
||||||
if len(output_node.inputs[0].links) == 0:
|
|
||||||
node_group.links.new(cut_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
move_node_to_node(cut_node, output_node, (-300, 0))
|
|
||||||
else:
|
|
||||||
pre_output_node = output_node.inputs[0].links[0].from_node
|
|
||||||
node_group.links.new(pre_output_node.outputs[0],
|
|
||||||
cut_node.inputs[0])
|
|
||||||
node_group.links.new(cut_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
move_node_between_nodes(cut_node,
|
|
||||||
[pre_output_node, output_node])
|
|
||||||
|
|
||||||
node_group.links.new(cutter_node.outputs[0],
|
|
||||||
cut_node.inputs[1])
|
|
||||||
|
|
||||||
move_node_to_node(cutter_node, cut_node, (-300, -300))
|
|
||||||
select_object(cutter)
|
|
||||||
else:
|
|
||||||
move_node_to_node(cutter_node, output_node, (0, -100))
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AddPlaneCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_plane_cutter"
|
|
||||||
bl_label = "Add a Plane Cutter"
|
|
||||||
bl_description = "Add a Plane Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "plane"
|
|
||||||
|
|
||||||
|
|
||||||
class AddCylinderCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_cylinder_cutter"
|
|
||||||
bl_label = "Add a Cylinder Cutter"
|
|
||||||
bl_description = "Add a Cylinder Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "cylinder"
|
|
||||||
|
|
||||||
|
|
||||||
class AddCubeCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_cube_cutter"
|
|
||||||
bl_label = "Add a Cube Cutter"
|
|
||||||
bl_description = "Add a Cube Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "cube"
|
|
||||||
|
|
||||||
|
|
||||||
class AddSphereCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_sphere_cutter"
|
|
||||||
bl_label = "Add a Sphere Cutter"
|
|
||||||
bl_description = "Add a Sphere Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "sphere"
|
|
||||||
|
|
||||||
|
|
||||||
class AddPieCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_pie_cutter"
|
|
||||||
bl_label = "Add a Pie Cutter"
|
|
||||||
bl_description = "Add a Pie Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "pie"
|
|
||||||
0
bioxelnodes/operators/__init__.py
Normal file
0
bioxelnodes/operators/__init__.py
Normal file
290
bioxelnodes/operators/container.py
Normal file
290
bioxelnodes/operators/container.py
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
from ..nodes import custom_nodes
|
||||||
|
from ..bioxel.io import load_container, save_container
|
||||||
|
from ..bioxelutils.layer import get_all_layer_objs
|
||||||
|
from ..bioxelutils.container import (container_to_obj, obj_to_container,
|
||||||
|
get_container_objs_from_selection)
|
||||||
|
from ..bioxelutils.node import get_nodes_by_type, move_node_between_nodes, move_node_to_node
|
||||||
|
from .utils import change_render_setting, get_cache_dir, get_preferences, select_object
|
||||||
|
|
||||||
|
|
||||||
|
class SaveContainer(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.save_container"
|
||||||
|
bl_label = "Save Container (.bioxel) (BETA)"
|
||||||
|
bl_description = "Clean all caches saved in temp"
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(
|
||||||
|
subtype="FILE_PATH"
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
filename_ext = ".bioxel"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
container_objs = get_container_objs_from_selection()
|
||||||
|
return len(container_objs) > 0
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
container_objs = get_container_objs_from_selection()
|
||||||
|
|
||||||
|
if len(container_objs) == 0:
|
||||||
|
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
container = obj_to_container(container_objs[0])
|
||||||
|
save_path = f"{self.filepath.split('.')[0]}.bioxel"
|
||||||
|
save_container(container, save_path, overwrite=True)
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Successfully save to {save_path}")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
|
||||||
|
class LoadContainer(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.load_container"
|
||||||
|
bl_label = "Load Container (.bioxel) (BETA)"
|
||||||
|
bl_description = "Clean all caches saved in temp"
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(
|
||||||
|
subtype="FILE_PATH"
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
filename_ext = ".bioxel"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
load_path = self.filepath
|
||||||
|
container = load_container(self.filepath)
|
||||||
|
is_first_import = len(get_all_layer_objs()) == 0
|
||||||
|
|
||||||
|
container_obj = container_to_obj(container,
|
||||||
|
scene_scale=0.01,
|
||||||
|
cache_dir=get_cache_dir(context))
|
||||||
|
select_object(container_obj)
|
||||||
|
|
||||||
|
if is_first_import:
|
||||||
|
change_render_setting(context)
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Successfully load {load_path}")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
|
||||||
|
class PickObject():
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
container_objs = get_container_objs_from_selection()
|
||||||
|
return len(container_objs) > 0
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
container_objs = get_container_objs_from_selection()
|
||||||
|
|
||||||
|
if len(container_objs) == 0:
|
||||||
|
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
container_obj = container_objs[0]
|
||||||
|
|
||||||
|
bpy.ops.mesh.primitive_cube_add(
|
||||||
|
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
|
obj = bpy.context.active_object
|
||||||
|
|
||||||
|
obj.name = f"{self.object_type}_{container_obj.name}"
|
||||||
|
|
||||||
|
bpy.ops.node.new_geometry_nodes_modifier()
|
||||||
|
modifier = obj.modifiers[0]
|
||||||
|
node_group = modifier.node_group
|
||||||
|
|
||||||
|
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||||
|
pick_mesh_node = custom_nodes.add_node(node_group,
|
||||||
|
f"BioxelNodes_Pick{self.object_type}")
|
||||||
|
|
||||||
|
pick_mesh_node.inputs[0].default_value = container_obj
|
||||||
|
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0])
|
||||||
|
|
||||||
|
select_object(obj)
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Successfully picked")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class PickMesh(bpy.types.Operator, PickObject):
|
||||||
|
bl_idname = "bioxelnodes.pick_mesh"
|
||||||
|
bl_label = "Pick Mesh"
|
||||||
|
bl_description = "Pick Container Mesh"
|
||||||
|
object_type = "Mesh"
|
||||||
|
|
||||||
|
|
||||||
|
class PickVolume(bpy.types.Operator, PickObject):
|
||||||
|
bl_idname = "bioxelnodes.pick_volume"
|
||||||
|
bl_label = "Pick Volume"
|
||||||
|
bl_description = "Pick Container Volume"
|
||||||
|
object_type = "Volume"
|
||||||
|
|
||||||
|
|
||||||
|
class PickBboxWire(bpy.types.Operator, PickObject):
|
||||||
|
bl_idname = "bioxelnodes.pick_bbox_wire"
|
||||||
|
bl_label = "Pick Bbox Wire"
|
||||||
|
bl_description = "Pick Container Bbox Wire"
|
||||||
|
object_type = "BboxWire"
|
||||||
|
|
||||||
|
|
||||||
|
class AddCutter():
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
container_objs = get_container_objs_from_selection()
|
||||||
|
return len(container_objs) > 0
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
container_objs = get_container_objs_from_selection()
|
||||||
|
|
||||||
|
if len(container_objs) == 0:
|
||||||
|
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
container_obj = container_objs[0]
|
||||||
|
|
||||||
|
if self.object_type == "plane":
|
||||||
|
node_type = "BioxelNodes_PlaneObjectCutter"
|
||||||
|
bpy.ops.mesh.primitive_plane_add(
|
||||||
|
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
|
elif self.object_type == "cylinder":
|
||||||
|
node_type = "BioxelNodes_CylinderObjectCutter"
|
||||||
|
bpy.ops.mesh.primitive_cylinder_add(
|
||||||
|
radius=1, depth=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
|
bpy.context.object.rotation_euler[0] = container_obj.rotation_euler[0]
|
||||||
|
elif self.object_type == "cube":
|
||||||
|
node_type = "BioxelNodes_CubeObjectCutter"
|
||||||
|
bpy.ops.mesh.primitive_cube_add(
|
||||||
|
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
|
elif self.object_type == "sphere":
|
||||||
|
node_type = "BioxelNodes_SphereObjectCutter"
|
||||||
|
bpy.ops.mesh.primitive_ico_sphere_add(
|
||||||
|
radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||||
|
elif self.object_type == "pie":
|
||||||
|
node_type = "BioxelNodes_PieObjectCutter"
|
||||||
|
# Create mesh
|
||||||
|
pie_mesh = bpy.data.meshes.new('Pie')
|
||||||
|
|
||||||
|
# Create object
|
||||||
|
pie = bpy.data.objects.new('Pie', pie_mesh)
|
||||||
|
|
||||||
|
# Link object to scene
|
||||||
|
bpy.context.scene.collection.objects.link(pie)
|
||||||
|
# Get a BMesh representation
|
||||||
|
bm = bmesh.new() # create an empty BMesh
|
||||||
|
bm.from_mesh(pie_mesh) # fill it in from a Mesh
|
||||||
|
|
||||||
|
# Hot to create vertices
|
||||||
|
v_0 = bm.verts.new((0.0, -1.0, 0.0))
|
||||||
|
v_1 = bm.verts.new((-1.0, -1.0, 1.0))
|
||||||
|
v_2 = bm.verts.new((0.0, 1.0, 0.0))
|
||||||
|
v_3 = bm.verts.new((-1.0, 1.0, 1.0))
|
||||||
|
v_4 = bm.verts.new((1.0, -1.0, 1.0))
|
||||||
|
v_5 = bm.verts.new((1.0, 1.0, 1.0))
|
||||||
|
|
||||||
|
# Initialize the index values of this sequence.
|
||||||
|
bm.verts.index_update()
|
||||||
|
|
||||||
|
# How to create a face
|
||||||
|
# it's not necessary to create the edges before, I made it only to show how create
|
||||||
|
# edges too
|
||||||
|
bm.faces.new((v_0, v_1, v_3, v_2))
|
||||||
|
bm.faces.new((v_0, v_2, v_5, v_4))
|
||||||
|
|
||||||
|
# Finish up, write the bmesh back to the mesh
|
||||||
|
bm.to_mesh(pie_mesh)
|
||||||
|
bpy.context.view_layer.objects.active = pie
|
||||||
|
|
||||||
|
cutter_obj = bpy.context.active_object
|
||||||
|
cutter_obj.visible_camera = False
|
||||||
|
cutter_obj.visible_diffuse = False
|
||||||
|
cutter_obj.visible_glossy = False
|
||||||
|
cutter_obj.visible_transmission = False
|
||||||
|
cutter_obj.visible_volume_scatter = False
|
||||||
|
cutter_obj.visible_shadow = False
|
||||||
|
cutter_obj.hide_render = True
|
||||||
|
cutter_obj.display_type = 'WIRE'
|
||||||
|
|
||||||
|
modifier = container_obj.modifiers[0]
|
||||||
|
node_group = modifier.node_group
|
||||||
|
cutter_node = custom_nodes.add_node(node_group, node_type)
|
||||||
|
cutter_node.inputs[0].default_value = cutter_obj
|
||||||
|
|
||||||
|
cut_nodes = get_nodes_by_type(node_group,
|
||||||
|
'BioxelNodes_Cut')
|
||||||
|
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||||
|
if len(cut_nodes) == 0:
|
||||||
|
cut_node = custom_nodes.add_node(node_group, 'BioxelNodes_Cut')
|
||||||
|
if len(output_node.inputs[0].links) == 0:
|
||||||
|
node_group.links.new(cut_node.outputs[0],
|
||||||
|
output_node.inputs[0])
|
||||||
|
move_node_to_node(cut_node, output_node, (-300, 0))
|
||||||
|
else:
|
||||||
|
pre_output_node = output_node.inputs[0].links[0].from_node
|
||||||
|
node_group.links.new(pre_output_node.outputs[0],
|
||||||
|
cut_node.inputs[0])
|
||||||
|
node_group.links.new(cut_node.outputs[0],
|
||||||
|
output_node.inputs[0])
|
||||||
|
move_node_between_nodes(cut_node,
|
||||||
|
[pre_output_node, output_node])
|
||||||
|
|
||||||
|
node_group.links.new(cutter_node.outputs[0],
|
||||||
|
cut_node.inputs[1])
|
||||||
|
|
||||||
|
move_node_to_node(cutter_node, cut_node, (-300, -300))
|
||||||
|
select_object(cutter_obj)
|
||||||
|
else:
|
||||||
|
move_node_to_node(cutter_node, output_node, (0, -100))
|
||||||
|
select_object(container_obj)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class AddPlaneCutter(bpy.types.Operator, AddCutter):
|
||||||
|
bl_idname = "bioxelnodes.add_plane_cutter"
|
||||||
|
bl_label = "Add a Plane Cutter"
|
||||||
|
bl_description = "Add a Plane Cutter to Container"
|
||||||
|
object_type = "plane"
|
||||||
|
|
||||||
|
|
||||||
|
class AddCylinderCutter(bpy.types.Operator, AddCutter):
|
||||||
|
bl_idname = "bioxelnodes.add_cylinder_cutter"
|
||||||
|
bl_label = "Add a Cylinder Cutter"
|
||||||
|
bl_description = "Add a Cylinder Cutter to Container"
|
||||||
|
object_type = "cylinder"
|
||||||
|
|
||||||
|
|
||||||
|
class AddCubeCutter(bpy.types.Operator, AddCutter):
|
||||||
|
bl_idname = "bioxelnodes.add_cube_cutter"
|
||||||
|
bl_label = "Add a Cube Cutter"
|
||||||
|
bl_description = "Add a Cube Cutter to Container"
|
||||||
|
object_type = "cube"
|
||||||
|
|
||||||
|
|
||||||
|
class AddSphereCutter(bpy.types.Operator, AddCutter):
|
||||||
|
bl_idname = "bioxelnodes.add_sphere_cutter"
|
||||||
|
bl_label = "Add a Sphere Cutter"
|
||||||
|
bl_description = "Add a Sphere Cutter to Container"
|
||||||
|
object_type = "sphere"
|
||||||
|
|
||||||
|
|
||||||
|
class AddPieCutter(bpy.types.Operator, AddCutter):
|
||||||
|
bl_idname = "bioxelnodes.add_pie_cutter"
|
||||||
|
bl_label = "Add a Pie Cutter"
|
||||||
|
bl_description = "Add a Pie Cutter to Container"
|
||||||
|
object_type = "pie"
|
||||||
795
bioxelnodes/operators/io.py
Normal file
795
bioxelnodes/operators/io.py
Normal file
@@ -0,0 +1,795 @@
|
|||||||
|
import math
|
||||||
|
import bpy
|
||||||
|
import shutil
|
||||||
|
import threading
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
from ..exceptions import CancelledByUser
|
||||||
|
from ..props import BIOXELNODES_Series
|
||||||
|
from ..bioxel.layer import Layer
|
||||||
|
from ..bioxelutils.layer import (get_all_layer_objs,
|
||||||
|
get_layer_obj)
|
||||||
|
from ..bioxelutils.container import (Container,
|
||||||
|
add_layers,
|
||||||
|
container_to_obj,
|
||||||
|
get_container_objs_from_selection)
|
||||||
|
from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS,
|
||||||
|
get_ext, parse_volumetric_data)
|
||||||
|
from .utils import (change_render_setting, get_cache_dir, get_preferences,
|
||||||
|
progress_update, progress_bar, select_object)
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import SimpleITK as sitk
|
||||||
|
import transforms3d
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple):
|
||||||
|
shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]),
|
||||||
|
int(orig_shape[1] / bioxel_size * orig_spacing[1]),
|
||||||
|
int(orig_shape[2] / bioxel_size * orig_spacing[2]))
|
||||||
|
|
||||||
|
return (shape[0] if shape[0] > 0 else 1,
|
||||||
|
shape[1] if shape[1] > 0 else 1,
|
||||||
|
shape[2] if shape[2] > 0 else 1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_size(shape: tuple, bioxel_size: float, scale: float = 1.0):
|
||||||
|
size = (float(shape[0] * bioxel_size * scale),
|
||||||
|
float(shape[1] * bioxel_size * scale),
|
||||||
|
float(shape[2] * bioxel_size * scale))
|
||||||
|
|
||||||
|
return size
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
ImportVolumetricData
|
||||||
|
-> ParseVolumetricData -> ImportVolumetricDataDialog
|
||||||
|
FH_ImportVolumetricData
|
||||||
|
|
||||||
|
start import parse data execute import
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ImportVolumetricData():
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||||
|
|
||||||
|
read_as = "scalar"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
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'}
|
||||||
|
|
||||||
|
bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT',
|
||||||
|
filepath=self.filepath,
|
||||||
|
skip_read_as=True,
|
||||||
|
read_as=self.read_as)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
|
||||||
|
class ImportAsScalarLayer(bpy.types.Operator, ImportVolumetricData):
|
||||||
|
bl_idname = "bioxelnodes.import_as_scalar_layer"
|
||||||
|
bl_label = "Import as Scalar"
|
||||||
|
bl_description = "Import Volumetric Data to Container as Scalar"
|
||||||
|
read_as = "scalar"
|
||||||
|
|
||||||
|
|
||||||
|
class ImportAsLabelLayer(bpy.types.Operator, ImportVolumetricData):
|
||||||
|
bl_idname = "bioxelnodes.import_as_label_layer"
|
||||||
|
bl_label = "Import as Label"
|
||||||
|
bl_description = "Import Volumetric Data to Container as Label"
|
||||||
|
read_as = "label"
|
||||||
|
|
||||||
|
|
||||||
|
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(SUPPORT_EXTS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll_drop(cls, context):
|
||||||
|
return (context.area and context.area.type == 'VIEW_3D')
|
||||||
|
|
||||||
|
|
||||||
|
def get_series_ids(self, context):
|
||||||
|
items = []
|
||||||
|
for index, series_id in enumerate(self.series_ids):
|
||||||
|
items.append((
|
||||||
|
series_id.id,
|
||||||
|
series_id.label,
|
||||||
|
"",
|
||||||
|
index
|
||||||
|
))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
class ParseVolumetricData(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.parse_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
|
||||||
|
|
||||||
|
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",
|
||||||
|
items=[("scalar", "Scalar", ""),
|
||||||
|
("label", "Labels", "")]) # type: ignore
|
||||||
|
|
||||||
|
series_id: bpy.props.EnumProperty(name="Select Series",
|
||||||
|
items=get_series_ids) # type: ignore
|
||||||
|
|
||||||
|
series_ids: bpy.props.CollectionProperty(
|
||||||
|
type=BIOXELNODES_Series) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
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'}
|
||||||
|
|
||||||
|
print("Collecting Meta Data...")
|
||||||
|
|
||||||
|
def parse_volumetric_data_func(self, context, cancel):
|
||||||
|
def progress_callback(factor, text):
|
||||||
|
if cancel():
|
||||||
|
raise CancelledByUser
|
||||||
|
progress_update(context, factor, text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
series_id = self.series_id if self.series_id != "empty" else ""
|
||||||
|
data, meta = parse_volumetric_data(data_file=self.filepath,
|
||||||
|
series_id=series_id,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.meta = meta
|
||||||
|
|
||||||
|
# Init cancel flag
|
||||||
|
self.is_cancelled = False
|
||||||
|
self.has_error = None
|
||||||
|
|
||||||
|
# Create the thread
|
||||||
|
self.thread = threading.Thread(target=parse_volumetric_data_func,
|
||||||
|
args=(self, context, lambda: self.is_cancelled))
|
||||||
|
|
||||||
|
# Start the thread
|
||||||
|
self.thread.start()
|
||||||
|
# Add a timmer for modal
|
||||||
|
self._timer = context.window_manager.event_timer_add(time_step=0.1,
|
||||||
|
window=context.window)
|
||||||
|
# Append progress bar to status bar
|
||||||
|
bpy.types.STATUSBAR_HT_header.append(progress_bar)
|
||||||
|
|
||||||
|
# Start modal handler
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
# Check if user press 'ESC'
|
||||||
|
if event.type == 'ESC':
|
||||||
|
self.is_cancelled = True
|
||||||
|
progress_update(context, 0, "Canceling...")
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
# Check if is the timer time
|
||||||
|
if event.type != 'TIMER':
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
# Force update status bar
|
||||||
|
bpy.context.workspace.status_text_set_internal(None)
|
||||||
|
|
||||||
|
# Check if thread is still running
|
||||||
|
if self.thread.is_alive():
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
# Release the thread
|
||||||
|
self.thread.join()
|
||||||
|
# Remove the timer
|
||||||
|
context.window_manager.event_timer_remove(self._timer)
|
||||||
|
# Remove the progress bar from status bar
|
||||||
|
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
||||||
|
|
||||||
|
# Check if thread is cancelled by user
|
||||||
|
if self.is_cancelled:
|
||||||
|
self.report({"WARNING"}, "Canncelled by user.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Check if thread is cancelled by user
|
||||||
|
if self.has_error:
|
||||||
|
raise self.has_error
|
||||||
|
|
||||||
|
# Check if has return
|
||||||
|
if self.meta is None:
|
||||||
|
self.report({"ERROR"}, "Some thing went wrong.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# If not canncelled...
|
||||||
|
for key, value in self.meta.items():
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
|
||||||
|
orig_shape = self.meta['xyz_shape']
|
||||||
|
orig_spacing = self.meta['spacing']
|
||||||
|
min_size = min(orig_spacing[0],
|
||||||
|
orig_spacing[1], orig_spacing[2])
|
||||||
|
bioxel_size = max(min_size, 1.0)
|
||||||
|
|
||||||
|
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_obj_name:
|
||||||
|
container_obj = bpy.data.objects.get(self.container_obj_name)
|
||||||
|
container_name = container_obj.name
|
||||||
|
else:
|
||||||
|
container_name = self.meta['name']
|
||||||
|
|
||||||
|
series_id = self.series_id if self.series_id != "empty" else ""
|
||||||
|
bpy.ops.bioxelnodes.import_volumetric_data_dialog(
|
||||||
|
'INVOKE_DEFAULT',
|
||||||
|
filepath=self.filepath,
|
||||||
|
container_name=container_name,
|
||||||
|
layer_name=self.meta['description'],
|
||||||
|
orig_shape=orig_shape,
|
||||||
|
orig_spacing=orig_spacing,
|
||||||
|
bioxel_size=bioxel_size,
|
||||||
|
series_id=series_id,
|
||||||
|
frame_count=self.meta['frame_count'],
|
||||||
|
channel_count=self.meta['channel_count'],
|
||||||
|
container_obj_name=self.container_obj_name,
|
||||||
|
read_as=self.read_as,
|
||||||
|
scene_scale=scene_scale
|
||||||
|
)
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Successfully Readed.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
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
|
||||||
|
reader = sitk.ImageSeriesReader()
|
||||||
|
reader.MetaDataDictionaryArrayUpdateOn()
|
||||||
|
reader.LoadPrivateTagsOn()
|
||||||
|
|
||||||
|
series_ids = reader.GetGDCMSeriesIDs(str(data_dirpath))
|
||||||
|
series_items = {}
|
||||||
|
|
||||||
|
for series_id in series_ids:
|
||||||
|
series_files = reader.GetGDCMSeriesFileNames(
|
||||||
|
str(data_dirpath), series_id)
|
||||||
|
single = sitk.ImageFileReader()
|
||||||
|
single.SetFileName(series_files[0])
|
||||||
|
single.LoadPrivateTagsOn()
|
||||||
|
single.ReadImageInformation()
|
||||||
|
|
||||||
|
def get_meta(key):
|
||||||
|
try:
|
||||||
|
stirng = single.GetMetaData(key).removesuffix(" ")
|
||||||
|
if stirng in ["No study description",
|
||||||
|
"No series description",
|
||||||
|
""]:
|
||||||
|
return "Unknown"
|
||||||
|
else:
|
||||||
|
return stirng
|
||||||
|
except:
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
study_description = get_meta("0008|1030")
|
||||||
|
series_description = get_meta("0008|103e")
|
||||||
|
series_modality = get_meta("0008|0060")
|
||||||
|
size_x = get_meta("0028|0011")
|
||||||
|
size_y = get_meta("0028|0010")
|
||||||
|
count = get_meta("0020|0013")
|
||||||
|
count = count if count != "Unknown" else 1
|
||||||
|
|
||||||
|
# some series image count = 0 ????
|
||||||
|
if int(count) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# series_id cannot be "" in blender selection
|
||||||
|
if series_id == "":
|
||||||
|
series_id = "empty"
|
||||||
|
|
||||||
|
series_items[series_id] = "{:<20} {:>1}".format(f"{study_description}>{series_description}({series_modality})",
|
||||||
|
f"({size_x}x{size_y})x{count}")
|
||||||
|
|
||||||
|
for key, value in series_items.items():
|
||||||
|
series_item = self.series_ids.add()
|
||||||
|
series_item.id = key
|
||||||
|
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=title)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
elif len(series_items.keys()) == 1:
|
||||||
|
self.series_id = list(series_items.keys())[0]
|
||||||
|
else:
|
||||||
|
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='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 (1 frame)", "")]
|
||||||
|
orig_shape = tuple(self.orig_shape)
|
||||||
|
if self.frame_count > 1:
|
||||||
|
items.append(("0", f"Frame ({self.frame_count} frames)", ""))
|
||||||
|
elif self.frame_count == 1 and self.channel_count > 1:
|
||||||
|
items.append(("4", f"Channel ({self.channel_count} frames)", ""))
|
||||||
|
elif self.frame_count == 1 and self.channel_count == 1:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.import_volumetric_data_dialog"
|
||||||
|
bl_label = "Import Volumetric Data"
|
||||||
|
bl_description = "Import Volumetric Data as Layer"
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
layers = None
|
||||||
|
thread = None
|
||||||
|
_timer = None
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||||
|
|
||||||
|
container_name: bpy.props.StringProperty(
|
||||||
|
name="Container Name") # type: ignore
|
||||||
|
|
||||||
|
layer_name: bpy.props.StringProperty(name="Layer Name") # type: ignore
|
||||||
|
|
||||||
|
series_id: bpy.props.StringProperty() # type: ignore
|
||||||
|
|
||||||
|
container_obj_name: bpy.props.StringProperty() # type: ignore
|
||||||
|
|
||||||
|
frame_count: bpy.props.IntProperty() # type: ignore
|
||||||
|
|
||||||
|
channel_count: bpy.props.IntProperty() # type: ignore
|
||||||
|
|
||||||
|
read_as: bpy.props.EnumProperty(name="Read as",
|
||||||
|
default="scalar",
|
||||||
|
items=[("scalar", "Scalar", ""),
|
||||||
|
("label", "Labels", "")]) # type: ignore
|
||||||
|
|
||||||
|
bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)",
|
||||||
|
soft_min=0.1, soft_max=10.0,
|
||||||
|
min=0.1, max=1e2,
|
||||||
|
default=1) # type: ignore
|
||||||
|
|
||||||
|
orig_spacing: bpy.props.FloatVectorProperty(name="Original Spacing",
|
||||||
|
default=(1, 1, 1)) # type: ignore
|
||||||
|
|
||||||
|
orig_shape: bpy.props.IntVectorProperty(name="Original Shape",
|
||||||
|
default=(100, 100, 100)) # type: ignore
|
||||||
|
|
||||||
|
scene_scale: bpy.props.FloatProperty(name="Scene Scale (Bioxel Unit pre Blender Unit)",
|
||||||
|
soft_min=0.0001, soft_max=10.0,
|
||||||
|
min=1e-6, max=1e6,
|
||||||
|
default=0.01) # type: ignore
|
||||||
|
|
||||||
|
split_channels: bpy.props.BoolProperty(name="Split Channels",
|
||||||
|
default=False) # type: ignore
|
||||||
|
|
||||||
|
sequence_source: bpy.props.EnumProperty(name="Time Sequence From",
|
||||||
|
items=get_sequence_sources) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
def import_volumetric_data_func(self, context, cancel):
|
||||||
|
progress_update(context, 0.0, "Parsing Volumetirc Data...")
|
||||||
|
|
||||||
|
def progress_callback(factor, text):
|
||||||
|
if cancel():
|
||||||
|
raise CancelledByUser
|
||||||
|
progress_update(context, factor*0.2, text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data, meta = parse_volumetric_data(data_file=self.filepath,
|
||||||
|
series_id=self.series_id,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
shape = get_layer_shape(self.bioxel_size,
|
||||||
|
self.orig_shape,
|
||||||
|
self.orig_spacing)
|
||||||
|
|
||||||
|
mat_scale = transforms3d.zooms.zfdir2aff(self.bioxel_size)
|
||||||
|
affine = np.dot(meta['affine'], mat_scale)
|
||||||
|
kind = self.read_as
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
# change shape as sequence or not
|
||||||
|
if self.sequence_source == "-1":
|
||||||
|
data = data[0:1, :, :, :, :]
|
||||||
|
elif self.sequence_source == "0":
|
||||||
|
# frame as frame
|
||||||
|
pass
|
||||||
|
elif self.sequence_source == "1":
|
||||||
|
# X as frame
|
||||||
|
data = data.transpose(1, 0, 2, 3, 4)
|
||||||
|
shape = (1, shape[1], shape[2])
|
||||||
|
elif self.sequence_source == "2":
|
||||||
|
# Y as frame
|
||||||
|
data = data.transpose(2, 1, 0, 3, 4)
|
||||||
|
shape = (shape[0], 1, shape[2])
|
||||||
|
elif self.sequence_source == "3":
|
||||||
|
# Z as frame
|
||||||
|
data = data.transpose(3, 1, 2, 0, 4)
|
||||||
|
shape = (shape[0], shape[1], 1)
|
||||||
|
else:
|
||||||
|
# channel as frame
|
||||||
|
data = data.transpose(4, 1, 2, 3, 0)
|
||||||
|
|
||||||
|
def progress_callback_factory(layer_name, progress, progress_step):
|
||||||
|
def progress_callback(frame, total):
|
||||||
|
if cancel():
|
||||||
|
raise CancelledByUser
|
||||||
|
sub_progress_step = progress_step/total
|
||||||
|
sub_progress = progress + frame * sub_progress_step
|
||||||
|
progress_update(context, sub_progress,
|
||||||
|
f"Processing {layer_name} Frame {frame+1}...")
|
||||||
|
return progress_callback
|
||||||
|
|
||||||
|
layers = []
|
||||||
|
if kind == "label":
|
||||||
|
name = self.layer_name or "Label"
|
||||||
|
data = data.astype(int)
|
||||||
|
label_count = int(np.max(data))
|
||||||
|
progress_step = 0.7/label_count
|
||||||
|
|
||||||
|
for i in range(label_count):
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
name_i = f"{name}_{i+1}"
|
||||||
|
progress = 0.2+i*progress_step
|
||||||
|
progress_update(context, progress,
|
||||||
|
f"Processing {name_i}...")
|
||||||
|
|
||||||
|
progress_callback = progress_callback_factory(name_i,
|
||||||
|
progress,
|
||||||
|
progress_step)
|
||||||
|
try:
|
||||||
|
layer = Layer(data=data == np.full_like(data, i+1),
|
||||||
|
name=name_i,
|
||||||
|
kind=kind,
|
||||||
|
affine=affine)
|
||||||
|
|
||||||
|
layer.resize(shape=shape,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
|
layers.append(layer)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
elif kind == "scalar":
|
||||||
|
name = self.layer_name or "Scalar"
|
||||||
|
|
||||||
|
if self.split_channels:
|
||||||
|
progress_step = 0.7/self.channel_count
|
||||||
|
|
||||||
|
for i in range(self.channel_count):
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
name_i = f"{name}_{i+1}"
|
||||||
|
progress = 0.2 + i*progress_step
|
||||||
|
progress_update(context, progress,
|
||||||
|
f"Processing {name_i}...")
|
||||||
|
progress_callback = progress_callback_factory(name_i,
|
||||||
|
progress,
|
||||||
|
progress_step)
|
||||||
|
try:
|
||||||
|
layer = Layer(data=data[:, :, :, :, i:i+1],
|
||||||
|
name=name_i,
|
||||||
|
kind=kind,
|
||||||
|
affine=affine)
|
||||||
|
|
||||||
|
layer.resize(shape=shape,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
|
layers.append(layer)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
progress_update(context, 0.2,
|
||||||
|
f"Processing {name}...")
|
||||||
|
progress_callback = progress_callback_factory(name,
|
||||||
|
0.2,
|
||||||
|
0.7)
|
||||||
|
|
||||||
|
try:
|
||||||
|
layer = Layer(data=data,
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
affine=affine)
|
||||||
|
|
||||||
|
layer.resize(shape=shape,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
|
layers.append(layer)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.layers = layers
|
||||||
|
progress_update(context, 0.9, "Creating Layers...")
|
||||||
|
|
||||||
|
self.is_cancelled = False
|
||||||
|
self.has_error = None
|
||||||
|
|
||||||
|
self.thread = threading.Thread(target=import_volumetric_data_func,
|
||||||
|
args=(self, context, lambda: self.is_cancelled))
|
||||||
|
|
||||||
|
self.thread.start()
|
||||||
|
self._timer = context.window_manager.event_timer_add(time_step=0.1,
|
||||||
|
window=context.window)
|
||||||
|
bpy.types.STATUSBAR_HT_header.append(progress_bar)
|
||||||
|
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if event.type == 'ESC':
|
||||||
|
self.is_cancelled = True
|
||||||
|
progress_update(context, 0, "Canceling...")
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
if event.type != 'TIMER':
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
bpy.context.workspace.status_text_set_internal(None)
|
||||||
|
if self.thread.is_alive():
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
self.thread.join()
|
||||||
|
context.window_manager.event_timer_remove(self._timer)
|
||||||
|
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
||||||
|
|
||||||
|
if self.is_cancelled:
|
||||||
|
self.report({"WARNING"}, "Canncelled by user.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Check if thread is cancelled by user
|
||||||
|
if self.has_error:
|
||||||
|
raise self.has_error
|
||||||
|
|
||||||
|
# Check if has return
|
||||||
|
if self.layers is None:
|
||||||
|
self.report({"ERROR"}, "Some thing went wrong.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
is_first_import = len(get_all_layer_objs()) == 0
|
||||||
|
|
||||||
|
if self.container_obj_name:
|
||||||
|
container_obj = bpy.data.objects.get(self.container_obj_name)
|
||||||
|
if container_obj is None:
|
||||||
|
raise Exception("Could not find target container")
|
||||||
|
|
||||||
|
container_obj = add_layers(self.layers,
|
||||||
|
container_obj=container_obj,
|
||||||
|
cache_dir=get_cache_dir(context))
|
||||||
|
else:
|
||||||
|
container = Container(name=self.container_name,
|
||||||
|
layers=self.layers)
|
||||||
|
|
||||||
|
container_obj = container_to_obj(container,
|
||||||
|
scene_scale=self.scene_scale,
|
||||||
|
cache_dir=get_cache_dir(context))
|
||||||
|
|
||||||
|
select_object(container_obj)
|
||||||
|
|
||||||
|
# Change render setting for better result
|
||||||
|
if is_first_import:
|
||||||
|
change_render_setting(context)
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Successfully Imported")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
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)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layer_shape = get_layer_shape(self.bioxel_size,
|
||||||
|
self.orig_shape,
|
||||||
|
self.orig_spacing)
|
||||||
|
|
||||||
|
orig_shape = tuple(self.orig_shape)
|
||||||
|
|
||||||
|
# change shape as sequence or not
|
||||||
|
channel_count = self.channel_count
|
||||||
|
if self.sequence_source == "-1":
|
||||||
|
pass
|
||||||
|
elif self.sequence_source == "0":
|
||||||
|
# frame as frame
|
||||||
|
pass
|
||||||
|
elif self.sequence_source == "1":
|
||||||
|
layer_shape = (1, layer_shape[1], layer_shape[2])
|
||||||
|
elif self.sequence_source == "2":
|
||||||
|
layer_shape = (layer_shape[0], 1, layer_shape[2])
|
||||||
|
elif self.sequence_source == "3":
|
||||||
|
layer_shape = (layer_shape[0], layer_shape[1], 1)
|
||||||
|
else:
|
||||||
|
channel_count = 1
|
||||||
|
|
||||||
|
import_channel = channel_count if self.split_channels or channel_count == 1 else "combined"
|
||||||
|
|
||||||
|
bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2]
|
||||||
|
layer_shape_text = f"Shape from {str(orig_shape)} to {str(layer_shape)}"
|
||||||
|
if bioxel_count > 100000000:
|
||||||
|
layer_shape_text += "**TOO LARGE!**"
|
||||||
|
|
||||||
|
layout = self.layout
|
||||||
|
if self.container_obj_name == "":
|
||||||
|
layout.prop(self, "container_name")
|
||||||
|
layout.prop(self, "layer_name")
|
||||||
|
|
||||||
|
panel = layout.box()
|
||||||
|
panel.prop(self, "bioxel_size")
|
||||||
|
row = panel.row()
|
||||||
|
row.prop(self, "orig_spacing")
|
||||||
|
panel.label(text=layer_shape_text)
|
||||||
|
|
||||||
|
panel = layout.box()
|
||||||
|
panel.prop(self, "sequence_source")
|
||||||
|
|
||||||
|
if self.read_as == "scalar":
|
||||||
|
panel = layout.box()
|
||||||
|
panel.prop(self, "split_channels",
|
||||||
|
text=f"Split Channels (Get {channel_count} channels, import {import_channel} channels)")
|
||||||
|
|
||||||
|
if self.container_obj_name == "":
|
||||||
|
layer_size = get_layer_size(layer_shape,
|
||||||
|
self.bioxel_size,
|
||||||
|
self.scene_scale)
|
||||||
|
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
||||||
|
panel = layout.box()
|
||||||
|
panel.prop(self, "scene_scale")
|
||||||
|
panel.label(text=layer_size_text)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportVolumetricData(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.export_volumetric_data"
|
||||||
|
bl_label = "Export Layer as VDB"
|
||||||
|
bl_description = "Export Layer as VDB"
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(
|
||||||
|
subtype="FILE_PATH"
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
filename_ext = ".vdb"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
layer_obj = get_layer_obj(bpy.context.active_object)
|
||||||
|
return True if layer_obj else False
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
layer_obj = get_layer_obj(bpy.context.active_object)
|
||||||
|
|
||||||
|
filepath = f"{self.filepath.split('.')[0]}.vdb"
|
||||||
|
# "//"
|
||||||
|
source_dir = bpy.path.abspath(layer_obj.data.filepath)
|
||||||
|
|
||||||
|
output_path: Path = Path(filepath).resolve()
|
||||||
|
source_path: Path = Path(source_dir).resolve()
|
||||||
|
# print('output_path', output_path)
|
||||||
|
# print('source_path', source_path)
|
||||||
|
shutil.copy(source_path, output_path)
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Successfully exported to {output_path}")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
250
bioxelnodes/operators/layer.py
Normal file
250
bioxelnodes/operators/layer.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
from ..bioxel.layer import Layer
|
||||||
|
from ..bioxelutils.node import get_nodes_by_type
|
||||||
|
from ..bioxelutils.container import add_layers, get_container_obj
|
||||||
|
from ..bioxelutils.layer import obj_to_layer, get_container_layer_objs, get_layer_obj
|
||||||
|
from .utils import get_preferences, select_object
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_prop_value(layer_obj: bpy.types.Object, prop: str):
|
||||||
|
try:
|
||||||
|
node_group = layer_obj.modifiers[0].node_group
|
||||||
|
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||||
|
return layer_node.inputs[prop].default_value
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_label_layer_selection(self, context):
|
||||||
|
items = [("None", "None", "")]
|
||||||
|
container_obj = get_container_obj(bpy.context.active_object)
|
||||||
|
|
||||||
|
for layer_obj in get_container_layer_objs(container_obj):
|
||||||
|
if get_layer_prop_value(layer_obj, "kind") == "label":
|
||||||
|
items.append((layer_obj.name,
|
||||||
|
layer_obj.name,
|
||||||
|
""))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
class LayerOperator():
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
base_obj: bpy.types.Object = None
|
||||||
|
|
||||||
|
def add_layer(self, context, layer):
|
||||||
|
preferences = get_preferences(context)
|
||||||
|
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
||||||
|
container_obj = self.base_obj.parent
|
||||||
|
|
||||||
|
add_layers([layer],
|
||||||
|
container_obj,
|
||||||
|
cache_dir)
|
||||||
|
|
||||||
|
select_object(container_obj)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.base_obj = get_layer_obj(bpy.context.active_object)
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
|
||||||
|
class ScalarOperator(LayerOperator):
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
label_objs = [obj for obj in context.selected_ids
|
||||||
|
if get_layer_prop_value(obj, "kind") == "scalar"]
|
||||||
|
return True if len(label_objs) > 0 else False
|
||||||
|
|
||||||
|
|
||||||
|
class LabelOperator(LayerOperator):
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
label_objs = [obj for obj in context.selected_ids
|
||||||
|
if get_layer_prop_value(obj, "kind") == "label"]
|
||||||
|
return True if len(label_objs) > 0 else False
|
||||||
|
|
||||||
|
|
||||||
|
class SignScalar(bpy.types.Operator, ScalarOperator):
|
||||||
|
bl_idname = "bioxelnodes.sign_scalar"
|
||||||
|
bl_label = "Sign Scalar"
|
||||||
|
bl_description = "Sign the scalar value"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
base_layer = obj_to_layer(self.base_obj)
|
||||||
|
|
||||||
|
modified_layer = base_layer.copy()
|
||||||
|
modified_layer.data = -base_layer.data
|
||||||
|
modified_layer.name = f"{base_layer.name}_Signed"
|
||||||
|
|
||||||
|
self.add_layer(context, modified_layer)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class FillOperator(ScalarOperator):
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.base_obj = get_layer_obj(bpy.context.active_object)
|
||||||
|
scalar_min = get_layer_prop_value(self.base_obj, "min")
|
||||||
|
self.fill_value = min(scalar_min, 0)
|
||||||
|
context.window_manager.invoke_props_dialog(self, width=400)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
|
||||||
|
class FillByThreshold(bpy.types.Operator, FillOperator):
|
||||||
|
bl_idname = "bioxelnodes.fill_by_threshold"
|
||||||
|
bl_label = "Fill Value by Threshold"
|
||||||
|
bl_description = "Fill Value by Threshold"
|
||||||
|
|
||||||
|
threshold: bpy.props.FloatProperty(
|
||||||
|
name="Threshold",
|
||||||
|
soft_min=0, soft_max=1024,
|
||||||
|
default=128,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
fill_value: bpy.props.FloatProperty(
|
||||||
|
name="Fill Value",
|
||||||
|
soft_min=0, soft_max=1024.0,
|
||||||
|
default=0,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
invert: bpy.props.BoolProperty(
|
||||||
|
name="Invert Area",
|
||||||
|
default=True,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
base_layer = obj_to_layer(self.base_obj)
|
||||||
|
|
||||||
|
data = np.amax(base_layer.data, -1)
|
||||||
|
mask = data <= self.threshold \
|
||||||
|
if self.invert else data > self.threshold
|
||||||
|
|
||||||
|
modified_layer = base_layer.copy()
|
||||||
|
modified_layer.fill(self.fill_value, mask)
|
||||||
|
modified_layer.name = f"{base_layer.name}_{self.threshold}-Filled"
|
||||||
|
|
||||||
|
self.add_layer(context, modified_layer)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class FillByRange(bpy.types.Operator, FillOperator):
|
||||||
|
bl_idname = "bioxelnodes.fill_by_range"
|
||||||
|
bl_label = "Fill Value by Range"
|
||||||
|
bl_description = "Fill Value by Range"
|
||||||
|
|
||||||
|
from_min: bpy.props.FloatProperty(
|
||||||
|
name="From Min",
|
||||||
|
soft_min=0, soft_max=1024,
|
||||||
|
default=128,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
from_max: bpy.props.FloatProperty(
|
||||||
|
name="From Max",
|
||||||
|
soft_min=0, soft_max=1024,
|
||||||
|
default=256,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
fill_value: bpy.props.FloatProperty(
|
||||||
|
name="Fill Value",
|
||||||
|
soft_min=0, soft_max=1024.0,
|
||||||
|
default=0,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
invert: bpy.props.BoolProperty(
|
||||||
|
name="Invert Area",
|
||||||
|
default=True,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
base_layer = obj_to_layer(self.base_obj)
|
||||||
|
|
||||||
|
data = np.amax(base_layer.data, -1)
|
||||||
|
mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \
|
||||||
|
(data > self.from_min) & (data < self.from_max)
|
||||||
|
|
||||||
|
modified_layer = base_layer.copy()
|
||||||
|
modified_layer.fill(self.fill_value, mask)
|
||||||
|
modified_layer.name = f"{base_layer.name}_{self.from_min}-{self.from_max}-Filled"
|
||||||
|
|
||||||
|
self.add_layer(context, modified_layer)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class FillByLabel(bpy.types.Operator, FillOperator):
|
||||||
|
bl_idname = "bioxelnodes.fill_by_label"
|
||||||
|
bl_label = "Fill Value by Label"
|
||||||
|
bl_description = "Fill Value by Label Area"
|
||||||
|
|
||||||
|
label_obj_name: bpy.props.EnumProperty(
|
||||||
|
name="Label Layer",
|
||||||
|
items=get_label_layer_selection
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
fill_value: bpy.props.FloatProperty(
|
||||||
|
name="Fill Value",
|
||||||
|
soft_min=0, soft_max=1024.0,
|
||||||
|
default=0,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
invert: bpy.props.BoolProperty(
|
||||||
|
name="Invert Label",
|
||||||
|
default=True,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
label_obj = bpy.data.objects.get(self.label_obj_name)
|
||||||
|
if not label_obj:
|
||||||
|
self.report({"WARNING"}, "Cannot find any label layer.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
base_layer = obj_to_layer(self.base_obj)
|
||||||
|
|
||||||
|
label_layer = obj_to_layer(label_obj)
|
||||||
|
label_layer.resize(base_layer.shape)
|
||||||
|
mask = np.amax(label_layer.data, -1)
|
||||||
|
if self.invert:
|
||||||
|
mask = 1 - mask
|
||||||
|
|
||||||
|
modified_layer = base_layer.copy()
|
||||||
|
modified_layer.fill(self.fill_value, mask)
|
||||||
|
modified_layer.name = f"{base_layer.name}_{label_layer.name}-Filled"
|
||||||
|
|
||||||
|
self.add_layer(context, modified_layer)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class CombineLabels(bpy.types.Operator, LabelOperator):
|
||||||
|
bl_idname = "bioxelnodes.combine_labels"
|
||||||
|
bl_label = "Combine Labels"
|
||||||
|
bl_description = "Combine all selected labels"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
label_objs = [obj for obj in context.selected_ids
|
||||||
|
if get_layer_prop_value(obj, "kind") == "label"]
|
||||||
|
|
||||||
|
if len(label_objs) < 2:
|
||||||
|
self.report({"WARNING"}, "Not enough layers.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
base_obj = label_objs[0]
|
||||||
|
label_objs = label_objs[1:]
|
||||||
|
|
||||||
|
base_layer = obj_to_layer(base_obj)
|
||||||
|
modified_layer = base_layer.copy()
|
||||||
|
label_names = [base_layer.name]
|
||||||
|
|
||||||
|
for label_obj in label_objs:
|
||||||
|
label_layer = obj_to_layer(label_obj)
|
||||||
|
label_layer.resize(base_layer.shape)
|
||||||
|
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"
|
||||||
|
|
||||||
|
self.add_layer(context, modified_layer)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
@@ -2,8 +2,13 @@ import re
|
|||||||
import bpy
|
import bpy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
from .utils import copy_to_dir, get_all_layers, get_container, get_container_from_selection, get_container_layers
|
|
||||||
from .nodes import custom_nodes
|
|
||||||
|
from ..utils import copy_to_dir
|
||||||
|
from .utils import get_cache_dir
|
||||||
|
from ..nodes import custom_nodes
|
||||||
|
from ..bioxelutils.container import get_container_objs_from_selection
|
||||||
|
from ..bioxelutils.layer import get_all_layer_objs, get_container_layer_objs
|
||||||
|
|
||||||
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
||||||
|
|
||||||
@@ -27,13 +32,37 @@ class ReLinkNodes(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
def save_layer_cache(layer_obj, output_dir):
|
||||||
|
pattern = r'\.\d{4}\.'
|
||||||
|
|
||||||
|
# "//"
|
||||||
|
output_dir = bpy.path.abspath(output_dir)
|
||||||
|
source_dir = bpy.path.abspath(layer_obj.data.filepath)
|
||||||
|
|
||||||
|
source_path: Path = Path(source_dir).resolve()
|
||||||
|
is_sequence = re.search(pattern, source_path.name) is not None
|
||||||
|
name = layer_obj.name if is_sequence else f"{layer_obj.name}.vdb"
|
||||||
|
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
|
||||||
|
if is_sequence else Path(output_dir, name).resolve()
|
||||||
|
|
||||||
|
if output_path != source_path:
|
||||||
|
copy_to_dir(source_path.parent if is_sequence else source_path,
|
||||||
|
output_path.parent.parent if is_sequence else output_path.parent,
|
||||||
|
new_name=name)
|
||||||
|
|
||||||
|
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||||
|
|
||||||
|
layer_obj.data.filepath = bpy.path.relpath(
|
||||||
|
str(output_path), start=str(blend_path))
|
||||||
|
|
||||||
|
|
||||||
class SaveStagedData(bpy.types.Operator):
|
class SaveStagedData(bpy.types.Operator):
|
||||||
bl_idname = "bioxelnodes.save_staged_data"
|
bl_idname = "bioxelnodes.save_staged_data"
|
||||||
bl_label = "Save Staged Data"
|
bl_label = "Save Staged Data"
|
||||||
bl_description = "Save all staged data in this file for sharing"
|
bl_description = "Save all staged data in this file for sharing"
|
||||||
|
|
||||||
save_layer: bpy.props.BoolProperty(
|
save_cache: bpy.props.BoolProperty(
|
||||||
name="Save Layer VDB Cache",
|
name="Save Layer Caches",
|
||||||
default=True,
|
default=True,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
@@ -83,11 +112,11 @@ class SaveStagedData(bpy.types.Operator):
|
|||||||
|
|
||||||
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
||||||
|
|
||||||
if self.save_layer:
|
if self.save_cache:
|
||||||
fails = []
|
fails = []
|
||||||
for layer in get_all_layers():
|
for layer in get_all_layer_objs():
|
||||||
try:
|
try:
|
||||||
save_layer(layer, self.cache_dir)
|
save_layer_cache(layer, self.cache_dir)
|
||||||
except:
|
except:
|
||||||
fails.append(layer)
|
fails.append(layer)
|
||||||
|
|
||||||
@@ -111,42 +140,17 @@ class SaveStagedData(bpy.types.Operator):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
panel = layout.box()
|
panel = layout.box()
|
||||||
panel.prop(self, "save_layer")
|
panel.prop(self, "save_cache")
|
||||||
panel.prop(self, "cache_dir")
|
panel.prop(self, "cache_dir")
|
||||||
panel = layout.box()
|
panel = layout.box()
|
||||||
panel.prop(self, "save_lib")
|
panel.prop(self, "save_lib")
|
||||||
panel.prop(self, "lib_dir")
|
panel.prop(self, "lib_dir")
|
||||||
|
|
||||||
|
|
||||||
def save_layer(layer, output_dir):
|
class SaveCaches(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.save_caches"
|
||||||
pattern = r'\.\d{4}\.'
|
bl_label = "Save Caches"
|
||||||
|
bl_description = "Save Container's caches to directory."
|
||||||
# "//"
|
|
||||||
output_dir = bpy.path.abspath(output_dir)
|
|
||||||
source_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
|
|
||||||
source_path: Path = Path(source_dir).resolve()
|
|
||||||
is_sequence = re.search(pattern, source_path.name) is not None
|
|
||||||
name = layer.name if is_sequence else f"{layer.name}.vdb"
|
|
||||||
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
|
|
||||||
if is_sequence else Path(output_dir, name).resolve()
|
|
||||||
|
|
||||||
if output_path != source_path:
|
|
||||||
copy_to_dir(source_path.parent if is_sequence else source_path,
|
|
||||||
output_path.parent.parent if is_sequence else output_path.parent,
|
|
||||||
new_name=name)
|
|
||||||
|
|
||||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
|
||||||
|
|
||||||
layer.data.filepath = bpy.path.relpath(
|
|
||||||
str(output_path), start=str(blend_path))
|
|
||||||
|
|
||||||
|
|
||||||
class SaveLayers(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.save_layers"
|
|
||||||
bl_label = "Save Layers"
|
|
||||||
bl_description = "Save Container Layers to Directory."
|
|
||||||
|
|
||||||
cache_dir: bpy.props.StringProperty(
|
cache_dir: bpy.props.StringProperty(
|
||||||
name="Layer Directory",
|
name="Layer Directory",
|
||||||
@@ -156,23 +160,23 @@ class SaveLayers(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
containers = get_container_from_selection()
|
container_objs = get_container_objs_from_selection()
|
||||||
return len(containers) > 0
|
return len(container_objs) > 0
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
containers = get_container_from_selection()
|
container_objs = get_container_objs_from_selection()
|
||||||
|
|
||||||
if len(containers) == 0:
|
if len(container_objs) == 0:
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
fails = []
|
fails = []
|
||||||
for container in containers:
|
for container_obj in container_objs:
|
||||||
for layer in get_container_layers(container):
|
for layer_obj in get_container_layer_objs(container_obj):
|
||||||
try:
|
try:
|
||||||
save_layer(layer, self.cache_dir)
|
save_layer_cache(layer_obj, self.cache_dir)
|
||||||
except:
|
except:
|
||||||
fails.append(layer)
|
fails.append(layer_obj)
|
||||||
|
|
||||||
if len(fails) == 0:
|
if len(fails) == 0:
|
||||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||||
@@ -194,8 +198,7 @@ class CleanAllCaches(bpy.types.Operator):
|
|||||||
bl_description = "Clean all caches saved in temp"
|
bl_description = "Clean all caches saved in temp"
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
cache_dir = get_cache_dir(context)
|
||||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(cache_dir)
|
shutil.rmtree(cache_dir)
|
||||||
self.report({"INFO"}, f"Successfully cleaned caches.")
|
self.report({"INFO"}, f"Successfully cleaned caches.")
|
||||||
60
bioxelnodes/operators/utils.py
Normal file
60
bioxelnodes/operators/utils.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
from .. import __package__ as base_package
|
||||||
|
|
||||||
|
|
||||||
|
def change_render_setting(context):
|
||||||
|
preferences = get_preferences(context)
|
||||||
|
if preferences.do_change_render_setting:
|
||||||
|
bpy.context.scene.render.engine = 'CYCLES'
|
||||||
|
try:
|
||||||
|
bpy.context.scene.cycles.shading_system = True
|
||||||
|
bpy.context.scene.cycles.volume_bounces = 12
|
||||||
|
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||||
|
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
||||||
|
bpy.context.scene.cycles.volume_step_rate = 10
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||||
|
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||||
|
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||||
|
bpy.context.scene.eevee.volumetric_samples = 256
|
||||||
|
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def select_object(target_obj):
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
obj.select_set(False)
|
||||||
|
|
||||||
|
target_obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = target_obj
|
||||||
|
|
||||||
|
|
||||||
|
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 progress_update(context, factor, text=""):
|
||||||
|
context.window_manager.bioxelnodes_progress_factor = factor
|
||||||
|
context.window_manager.bioxelnodes_progress_text = text
|
||||||
|
|
||||||
|
|
||||||
|
def get_preferences(context):
|
||||||
|
return context.preferences.addons[base_package].preferences
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_dir(context):
|
||||||
|
preferences = get_preferences(context)
|
||||||
|
cache_path = Path(preferences.cache_dir, 'VDBs')
|
||||||
|
cache_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return str(cache_path)
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .externalpackage import ExternalPackagePreferences
|
|
||||||
|
|
||||||
|
class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||||
class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferences):
|
|
||||||
bl_idname = __package__
|
bl_idname = __package__
|
||||||
|
|
||||||
cache_dir: bpy.props.StringProperty(
|
cache_dir: bpy.props.StringProperty(
|
||||||
@@ -20,10 +18,6 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
# ExternalPackagePreferences Config
|
|
||||||
self.requirements_dir = str(Path(__file__).parent)
|
|
||||||
super().draw(context)
|
|
||||||
|
|
||||||
layout.label(text="Configuration")
|
layout.label(text="Configuration")
|
||||||
layout.prop(self, 'cache_dir')
|
layout.prop(self, 'cache_dir')
|
||||||
layout.prop(self, "do_change_render_setting")
|
layout.prop(self, "do_change_render_setting")
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
SimpleITK==2.3.1 # Insight Segmentation and Registration Toolkit.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from ._interpolation import zoom
|
|
||||||
from ._filters import gaussian_filter, median_filter
|
|
||||||
Binary file not shown.
@@ -1,19 +1,7 @@
|
|||||||
import bpy
|
|
||||||
import mathutils
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pyopenvdb as vdb
|
|
||||||
from uuid import uuid4
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
def select_object(target_obj):
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
obj.select_set(False)
|
|
||||||
|
|
||||||
target_obj.select_set(True)
|
|
||||||
bpy.context.view_layer.objects.active = target_obj
|
|
||||||
|
|
||||||
|
|
||||||
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||||
source = Path(source_path)
|
source = Path(source_path)
|
||||||
target = Path(dir_path)
|
target = Path(dir_path)
|
||||||
@@ -42,246 +30,3 @@ def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
|||||||
|
|
||||||
if not target_path.exists():
|
if not target_path.exists():
|
||||||
raise Exception
|
raise Exception
|
||||||
|
|
||||||
|
|
||||||
def move_node_to_node(node, target_node, offset=(0, 0)):
|
|
||||||
node.location.x = target_node.location.x + offset[0]
|
|
||||||
node.location.y = target_node.location.y + offset[1]
|
|
||||||
|
|
||||||
|
|
||||||
def move_node_between_nodes(node, target_nodes, offset=(0, 0)):
|
|
||||||
xs = []
|
|
||||||
ys = []
|
|
||||||
for target_node in target_nodes:
|
|
||||||
xs.append(target_node.location.x)
|
|
||||||
ys.append(target_node.location.y)
|
|
||||||
|
|
||||||
node.location.x = sum(xs) / len(xs) + offset[0]
|
|
||||||
node.location.y = sum(ys) / len(ys) + offset[1]
|
|
||||||
|
|
||||||
|
|
||||||
def get_node_type(node):
|
|
||||||
node_type = type(node).__name__
|
|
||||||
if node_type == "GeometryNodeGroup":
|
|
||||||
node_type = node.node_tree.name
|
|
||||||
|
|
||||||
return node_type
|
|
||||||
|
|
||||||
|
|
||||||
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 draw(self, context):
|
|
||||||
self.layout.label(text=message)
|
|
||||||
|
|
||||||
bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
|
|
||||||
|
|
||||||
|
|
||||||
def calc_bbox_verts(origin, size):
|
|
||||||
bbox_origin = mathutils.Vector(
|
|
||||||
(origin[0], origin[1], origin[2]))
|
|
||||||
bbox_size = mathutils.Vector(
|
|
||||||
(size[0], size[1], size[2]))
|
|
||||||
bbox_verts = [
|
|
||||||
(
|
|
||||||
bbox_origin[0] + 0,
|
|
||||||
bbox_origin[1] + 0,
|
|
||||||
bbox_origin[2] + 0
|
|
||||||
),
|
|
||||||
(
|
|
||||||
bbox_origin[0] + 0,
|
|
||||||
bbox_origin[1] + 0,
|
|
||||||
bbox_origin[2] + bbox_size[2]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
bbox_origin[0] + 0,
|
|
||||||
bbox_origin[1] + bbox_size[1],
|
|
||||||
bbox_origin[2] + 0
|
|
||||||
),
|
|
||||||
(
|
|
||||||
bbox_origin[0] + 0,
|
|
||||||
bbox_origin[1] + bbox_size[1],
|
|
||||||
bbox_origin[2] + bbox_size[2]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
bbox_origin[0] + bbox_size[0],
|
|
||||||
bbox_origin[1] + 0,
|
|
||||||
bbox_origin[2] + 0
|
|
||||||
),
|
|
||||||
(
|
|
||||||
bbox_origin[0] + bbox_size[0],
|
|
||||||
bbox_origin[1] + 0,
|
|
||||||
bbox_origin[2] + bbox_size[2],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
bbox_origin[0] + bbox_size[0],
|
|
||||||
bbox_origin[1] + bbox_size[1],
|
|
||||||
bbox_origin[2] + 0,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
bbox_origin[0] + bbox_size[0],
|
|
||||||
bbox_origin[1] + bbox_size[1],
|
|
||||||
bbox_origin[2] + bbox_size[2],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
return bbox_verts
|
|
||||||
|
|
||||||
|
|
||||||
def lock_transform(obj):
|
|
||||||
obj.lock_location[0] = True
|
|
||||||
obj.lock_location[1] = True
|
|
||||||
obj.lock_location[2] = True
|
|
||||||
obj.lock_rotation[0] = True
|
|
||||||
obj.lock_rotation[1] = True
|
|
||||||
obj.lock_rotation[2] = True
|
|
||||||
obj.lock_scale[0] = True
|
|
||||||
obj.lock_scale[1] = True
|
|
||||||
obj.lock_scale[2] = True
|
|
||||||
|
|
||||||
|
|
||||||
def hide_in_ray(obj):
|
|
||||||
obj.visible_camera = False
|
|
||||||
obj.visible_diffuse = False
|
|
||||||
obj.visible_glossy = False
|
|
||||||
obj.visible_transmission = False
|
|
||||||
obj.visible_volume_scatter = False
|
|
||||||
obj.visible_shadow = False
|
|
||||||
|
|
||||||
|
|
||||||
def save_vdb(grids, context):
|
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
|
||||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
|
||||||
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)})...")
|
|
||||||
vdb.write(str(vdb_path), grids=grids)
|
|
||||||
|
|
||||||
return vdb_path
|
|
||||||
|
|
||||||
|
|
||||||
def save_vdbs(grids_sequence, context):
|
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
|
||||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
vdb_name = str(uuid4())
|
|
||||||
vdb_dir_path = Path(cache_dir, vdb_name)
|
|
||||||
vdb_dir_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
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)})...")
|
|
||||||
vdb.write(str(vdb_path), grids=grids)
|
|
||||||
vdb_paths.append(vdb_path)
|
|
||||||
|
|
||||||
return vdb_paths
|
|
||||||
|
|
||||||
|
|
||||||
def get_container_from_selection():
|
|
||||||
containers = []
|
|
||||||
for obj in bpy.context.selected_objects:
|
|
||||||
if get_container(obj):
|
|
||||||
containers.append(obj)
|
|
||||||
# if bpy.context.active_object:
|
|
||||||
# if bpy.context.active_object.get('bioxel_layer'):
|
|
||||||
# if bpy.context.active_object.parent.get('bioxel_container'):
|
|
||||||
# containers.append(bpy.context.active_object.parent)
|
|
||||||
|
|
||||||
return list(set(containers))
|
|
||||||
|
|
||||||
|
|
||||||
def get_container(current_obj):
|
|
||||||
if current_obj:
|
|
||||||
if current_obj.get('bioxel_container'):
|
|
||||||
return current_obj
|
|
||||||
elif current_obj.get('bioxel_layer'):
|
|
||||||
parent = current_obj.parent
|
|
||||||
return parent if parent.get('bioxel_container') else None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer(current_obj):
|
|
||||||
if current_obj:
|
|
||||||
if current_obj.get('bioxel_layer') and current_obj.parent:
|
|
||||||
if current_obj.parent.get('bioxel_container'):
|
|
||||||
return current_obj
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_container_layers(container):
|
|
||||||
layers = []
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
if obj.parent == container and get_layer(obj):
|
|
||||||
layers.append(obj)
|
|
||||||
|
|
||||||
return layers
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_layers():
|
|
||||||
layers = []
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
if get_layer(obj):
|
|
||||||
layers.append(obj)
|
|
||||||
|
|
||||||
return layers
|
|
||||||
|
|
||||||
|
|
||||||
def get_text_index_str(text):
|
|
||||||
# Initialize an empty string to store the digits
|
|
||||||
digits = ""
|
|
||||||
|
|
||||||
# Iterate through the characters in reverse order
|
|
||||||
started = False
|
|
||||||
for char in text[::-1]:
|
|
||||||
if char.isdigit():
|
|
||||||
started = True
|
|
||||||
# If the character is a digit, add it to the digits string
|
|
||||||
digits += char
|
|
||||||
else:
|
|
||||||
if started:
|
|
||||||
# If a non-digit character is encountered, stop the loop
|
|
||||||
break
|
|
||||||
|
|
||||||
# Reverse the digits string to get the correct order
|
|
||||||
last_number = digits[::-1]
|
|
||||||
|
|
||||||
return last_number
|
|
||||||
|
|
||||||
|
|
||||||
def add_driver(target_prop, var_sources, expression):
|
|
||||||
|
|
||||||
driver = target_prop.driver_add("default_value")
|
|
||||||
is_vector = isinstance(driver, list)
|
|
||||||
drivers = driver if is_vector else [driver]
|
|
||||||
|
|
||||||
for i, driver in enumerate(drivers):
|
|
||||||
for j, var_source in enumerate(var_sources):
|
|
||||||
|
|
||||||
source = var_source['source']
|
|
||||||
prop = var_source['prop']
|
|
||||||
|
|
||||||
var = driver.driver.variables.new()
|
|
||||||
var.name = f"var{j}"
|
|
||||||
|
|
||||||
var.targets[0].id_type = source.id_type
|
|
||||||
var.targets[0].id = source
|
|
||||||
var.targets[0].data_path = f'["{prop}"][{i}]'\
|
|
||||||
if is_vector else f'["{prop}"]'
|
|
||||||
|
|
||||||
driver.driver.expression = expression
|
|
||||||
|
|
||||||
|
|
||||||
def add_direct_driver(target, target_prop, source, source_prop):
|
|
||||||
target_prop = target.inputs.get(target_prop)
|
|
||||||
drivers = [
|
|
||||||
{
|
|
||||||
"source": source,
|
|
||||||
"prop": source_prop
|
|
||||||
}
|
|
||||||
]
|
|
||||||
expression = "var0"
|
|
||||||
add_driver(target_prop, drivers, expression)
|
|
||||||
|
|||||||
95
build.py
Normal file
95
build.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import tomlkit
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Platform:
|
||||||
|
pypi_suffix: str
|
||||||
|
blender_tag: str
|
||||||
|
|
||||||
|
|
||||||
|
required_packages = ["SimpleITK==2.3.1",
|
||||||
|
"pyometiff==1.0.0",
|
||||||
|
"mrcfile==1.5.1",
|
||||||
|
"h5py==3.11.0",
|
||||||
|
"transforms3d==0.4.2"]
|
||||||
|
|
||||||
|
|
||||||
|
platforms = {"windows-x64": Platform(pypi_suffix="win_amd64",
|
||||||
|
blender_tag="windows-x64"),
|
||||||
|
"linux-x64": Platform(pypi_suffix="manylinux2014_x86_64",
|
||||||
|
blender_tag="linux-x64"),
|
||||||
|
"macos-arm64": Platform(pypi_suffix="macosx_12_0_arm64",
|
||||||
|
blender_tag="macos-arm64"),
|
||||||
|
"macos-x64": Platform(pypi_suffix="macosx_10_16_x86_64",
|
||||||
|
blender_tag="macos-x64")}
|
||||||
|
|
||||||
|
packages_to_remove = {
|
||||||
|
"imagecodecs",
|
||||||
|
"numpy"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_python(args: str):
|
||||||
|
python = Path(sys.executable).resolve()
|
||||||
|
subprocess.run([python] + args.split(" "))
|
||||||
|
|
||||||
|
|
||||||
|
def build_extension(platform: Platform, python_version="3.11") -> None:
|
||||||
|
wheel_dirpath = Path("./bioxelnodes/wheels")
|
||||||
|
toml_filepath = Path("bioxelnodes/blender_manifest.toml")
|
||||||
|
scipy_ndimage_dirpath = Path("./scipy_ndimage", platform.blender_tag)
|
||||||
|
|
||||||
|
# download required_packages
|
||||||
|
run_python(
|
||||||
|
f"-m pip download {' '.join(required_packages)} --dest {wheel_dirpath.as_posix()} --only-binary=:all: --python-version={python_version} --platform={platform.pypi_suffix}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for f in wheel_dirpath.glob('*.whl'):
|
||||||
|
if any([package in f.name for package in packages_to_remove]):
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
elif platform.blender_tag == "macos-arm64" and \
|
||||||
|
"lxml" in f.name and "universal2" in f.name:
|
||||||
|
f.rename(Path(f.parent,
|
||||||
|
f.name.replace("universal2", "arm64")))
|
||||||
|
|
||||||
|
for ndimage_filepath in scipy_ndimage_dirpath.iterdir():
|
||||||
|
to_filepath = Path("./bioxelnodes/bioxel/scipy", ndimage_filepath.name)
|
||||||
|
shutil.copy(ndimage_filepath, to_filepath)
|
||||||
|
|
||||||
|
# Load the TOML file
|
||||||
|
with toml_filepath.open("r") as file:
|
||||||
|
manifest = tomlkit.parse(file.read())
|
||||||
|
|
||||||
|
manifest["platforms"] = [platform.blender_tag]
|
||||||
|
manifest["wheels"] = [f"./wheels/{f.name}"
|
||||||
|
for f in wheel_dirpath.glob('*.whl')]
|
||||||
|
|
||||||
|
# build = tomlkit.table(True)
|
||||||
|
# generated = tomlkit.table()
|
||||||
|
# generated["platforms"] = [platform.blender_tag]
|
||||||
|
# generated["wheels"] = [f"./wheels/{f.name}"
|
||||||
|
# for f in wheel_dirpath.glob('*.whl')]
|
||||||
|
|
||||||
|
# build.append('generated', generated)
|
||||||
|
# manifest.append('build', build)
|
||||||
|
|
||||||
|
# Write the updated TOML file
|
||||||
|
with toml_filepath.open("w") as file:
|
||||||
|
text = tomlkit.dumps(manifest)
|
||||||
|
file.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
platform_name = sys.argv[1]
|
||||||
|
platform = platforms[platform_name]
|
||||||
|
build_extension(platform)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,23 +1,34 @@
|
|||||||
|
[中文文档](https://uj6xfhbzp0.feishu.cn/wiki/Qx3VwHuNPimeI8kr6nDcvl1DnHf?from=from_copylink)
|
||||||
|
|
||||||
# Bioxel Nodes
|
# Bioxel Nodes
|
||||||
|
|
||||||
|
[](https://blender.org/)
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
[](https://discord.gg/pYkNyq2TjE)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Fantastic rendering result, also support EEVEE NEXT
|
- Realistic rendering result, also support EEVEE NEXT.
|
||||||
- Support multiple formats
|
- Support multiple formats.
|
||||||
- Support 4D volumetric data
|
- Support 4D volumetric data.
|
||||||
- All kinds of cutters
|
- All kinds of cutters.
|
||||||
- Simple and powerful nodes
|
- Simple and powerful nodes.
|
||||||
- Based on blender natively, can work without addon.
|
- 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
|
## Support Multiple Formats
|
||||||
|
|
||||||
| Format | EXT | Test |
|
| Format | EXT | Test |
|
||||||
| ------ | ---------------------------------------- | ------- |
|
| ------ | ---------------------------------------- | ------- |
|
||||||
| DICOM | .dcm, .DICOM | ✅ pass |
|
| DICOM | .dcm, .DCM, .DICOM, .ima, .IMA | ✅ pass |
|
||||||
| BMP | .bmp, .BMP | ✅ pass |
|
| BMP | .bmp, .BMP | ✅ pass |
|
||||||
| JPEG | .jpg, .JPG, .jpeg, .JPEG | ✅ pass |
|
| JPEG | .jpg, .JPG, .jpeg, .JPEG | ✅ pass |
|
||||||
| PNG | .png, .PNG | ✅ pass |
|
| PNG | .png, .PNG | ✅ pass |
|
||||||
@@ -25,16 +36,18 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It
|
|||||||
| Nifti | .nia, .nii, .nii.gz, .hdr, .img, .img.gz | ✅ pass |
|
| Nifti | .nia, .nii, .nii.gz, .hdr, .img, .img.gz | ✅ pass |
|
||||||
| Nrrd | .nrrd, .nhdr | ✅ pass |
|
| Nrrd | .nrrd, .nhdr | ✅ pass |
|
||||||
| HDF5 | .hdf, .h4, .hdf4, .he2, .h5, .hdf5, .he5 | ✅ pass |
|
| HDF5 | .hdf, .h4, .hdf4, .he2, .h5, .hdf5, .he5 | ✅ pass |
|
||||||
|
| OME | .ome.tiff, .ome.tif | ✅ pass |
|
||||||
|
| MRC | .mrc, .mrc.gz, .map, .map.gz | ✅ pass |
|
||||||
|
|
||||||
## Support 4D volumetric data
|
## Support 4D volumetric data
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
🥰 4D volumetric data can also be imported into Blender.
|
🥰 4D volumetric data can also be imported into Blender.
|
||||||
|
|
||||||
## Support EEVEE NEXT
|
## Support EEVEE NEXT
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
👍 EEVEE NEXT is absolutely AWESOME! Bioxel Nodes is fully support EEVEE NEXT now! However, there are some limitations:
|
👍 EEVEE NEXT is absolutely AWESOME! Bioxel Nodes is fully support EEVEE NEXT now! However, there are some limitations:
|
||||||
|
|
||||||
@@ -46,17 +59,18 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It
|
|||||||
- Only works with Cycles CPU , Cycles GPU (OptiX), EEVEE
|
- Only works with Cycles CPU , Cycles GPU (OptiX), EEVEE
|
||||||
- Section surface cannot be generated when convert to mesh (will be supported soon)
|
- 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**
|
**v0.3.x is not compatible to v0.2.x, Updating this addon may break old files. Read the following carefully before upgradation**
|
||||||
|
|
||||||
Before updating this addon, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
Before upgradation, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
||||||
|
|
||||||
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
||||||
|
|
||||||
Also, unlike the newer versions, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
Also, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- Better multi-format import experience
|
- Better multi-format import experience
|
||||||
- One-click bake model with texture
|
- One-click bake model with texture
|
||||||
- AI Segmentation to Generate Labels
|
- AI Segmentation to Generate Labels
|
||||||
@@ -6,7 +6,7 @@ First thing first, you need to get your volumetric data ready in disk. Bioxel No
|
|||||||
|
|
||||||
| Format | EXT | Test |
|
| Format | EXT | Test |
|
||||||
| ------ | ---------------------------------------- | ------- |
|
| ------ | ---------------------------------------- | ------- |
|
||||||
| DICOM | .dcm, .DICOM | ✅ pass |
|
| DICOM | .dcm, .DCM, .DICOM, .ima, .IMA | ✅ pass |
|
||||||
| BMP | .bmp, .BMP | ✅ pass |
|
| BMP | .bmp, .BMP | ✅ pass |
|
||||||
| JPEG | .jpg, .JPG, .jpeg, .JPEG | ✅ pass |
|
| JPEG | .jpg, .JPG, .jpeg, .JPEG | ✅ pass |
|
||||||
| PNG | .png, .PNG | ✅ pass |
|
| PNG | .png, .PNG | ✅ pass |
|
||||||
@@ -21,6 +21,8 @@ First thing first, you need to get your volumetric data ready in disk. Bioxel No
|
|||||||
| LSM | .lsm, .LSM | yet |
|
| LSM | .lsm, .LSM | yet |
|
||||||
| MINC | .mnc, .MNC | yet |
|
| MINC | .mnc, .MNC | yet |
|
||||||
| MRC | .mrc, .rec | yet |
|
| MRC | .mrc, .rec | yet |
|
||||||
|
| OME | .ome.tiff, .ome.tif | ✅ pass |
|
||||||
|
| MRC | .mrc, .mrc.gz, .map, .map.gz | ✅ pass |
|
||||||
|
|
||||||
## Download Data from Internet
|
## Download Data from Internet
|
||||||
|
|
||||||
@@ -32,10 +34,12 @@ If you don't have any volumetric data, you can access open research data from li
|
|||||||
| ------------------------------------------------------------------------------------ | ------------------ |
|
| ------------------------------------------------------------------------------------ | ------------------ |
|
||||||
| [MorphoSource](https://www.morphosource.org/) | Open Research Data |
|
| [MorphoSource](https://www.morphosource.org/) | Open Research Data |
|
||||||
| [Dryad](https://datadryad.org) | Open Research Data |
|
| [Dryad](https://datadryad.org) | Open Research Data |
|
||||||
|
| [Cell Image Library](http://cellimagelibrary.org/home) | Cells |
|
||||||
| [OpenOrganelle](https://openorganelle.janelia.org/datasets) | Cells |
|
| [OpenOrganelle](https://openorganelle.janelia.org/datasets) | Cells |
|
||||||
|
| [Allen Cell Explorer](https://www.allencell.org/3d-cell-viewer.html) | Cells |
|
||||||
|
| [EMDB](https://www.ebi.ac.uk/emdb/) | Protein, Viruses |
|
||||||
| [Embodi3D](https://www.embodi3d.com/files/category/37-medical-scans/) | Medical Images |
|
| [Embodi3D](https://www.embodi3d.com/files/category/37-medical-scans/) | Medical Images |
|
||||||
| [Github](https://github.com/sfikas/medical-imaging-datasets) | Medical Images |
|
| [Github](https://github.com/sfikas/medical-imaging-datasets) | Medical Images |
|
||||||
| [NIHR](https://nhsx.github.io/open-source-imaging-data-sets/) | Medical Images |
|
| [NIHR](https://nhsx.github.io/open-source-imaging-data-sets/) | Medical Images |
|
||||||
| [Medical Segmentation Decathlon](http://medicaldecathlon.com/) | Medical Images |
|
| [Medical Segmentation Decathlon](http://medicaldecathlon.com/) | Medical Images |
|
||||||
| [Allen Cell Explorer](https://www.allencell.org/3d-cell-viewer.html) | Cells |
|
|
||||||
| [Visible Human Project](https://www.nlm.nih.gov/research/visible/visible_human.html) | Medical Images |
|
| [Visible Human Project](https://www.nlm.nih.gov/research/visible/visible_human.html) | Medical Images |
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import bpy
|
|
||||||
|
|
||||||
from . import auto_load
|
|
||||||
from . import menus
|
|
||||||
|
|
||||||
|
|
||||||
auto_load.init()
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
auto_load.register()
|
|
||||||
menus.add()
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
menus.remove()
|
|
||||||
auto_load.unregister()
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
schema_version = "1.0.0"
|
|
||||||
|
|
||||||
# Example of manifest file for a Blender extension
|
|
||||||
# Change the values according to your extension
|
|
||||||
id = "bioxelnodes"
|
|
||||||
version = "0.2.7"
|
|
||||||
name = "Bioxel Nodes"
|
|
||||||
tagline = "For scientific volumetric data visualization in Blender"
|
|
||||||
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
|
||||||
# Supported types: "add-on", "theme"
|
|
||||||
type = "add-on"
|
|
||||||
|
|
||||||
# Optional link to documentation, support, source files, etc
|
|
||||||
website = "https://omoolab.github.io/BioxelNodes/latest"
|
|
||||||
|
|
||||||
# Optional list defined by Blender and server, see:
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
|
||||||
tags = ["Geometry Nodes", "Render"]
|
|
||||||
|
|
||||||
blender_version_min = "4.2.0"
|
|
||||||
# Optional: maximum supported Blender version
|
|
||||||
# blender_version_max = "5.1.0"
|
|
||||||
|
|
||||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
|
||||||
license = [
|
|
||||||
"SPDX:MIT",
|
|
||||||
]
|
|
||||||
# Optional: required by some licenses.
|
|
||||||
copyright = [
|
|
||||||
"2024 OmooLab"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
|
||||||
platforms = ["windows-x64"]
|
|
||||||
# Other supported platforms: "windows-arm64", "macos-x64"
|
|
||||||
|
|
||||||
# Optional: bundle 3rd party Python modules.
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
|
|
||||||
wheels = [
|
|
||||||
"./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
## Optional: add-ons can list which resources they will require:
|
|
||||||
## * files (for access of any filesystem operations)
|
|
||||||
## * network (for internet access)
|
|
||||||
## * clipboard (to read and/or write the system clipboard)
|
|
||||||
## * camera (to capture photos and videos)
|
|
||||||
## * microphone (to capture audio)
|
|
||||||
##
|
|
||||||
## If using network, remember to also check `bpy.app.online_access`
|
|
||||||
## https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
|
|
||||||
##
|
|
||||||
## For each permission it is important to also specify the reason why it is required.
|
|
||||||
## Keep this a single short sentence without a period (.) at the end.
|
|
||||||
## For longer explanations use the documentation or detail page.
|
|
||||||
#
|
|
||||||
[permissions]
|
|
||||||
files = "Import/export volume data from/to disk"
|
|
||||||
|
|
||||||
# Optional: build settings.
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
|
||||||
# [build]
|
|
||||||
# paths_exclude_pattern = [
|
|
||||||
# "__pycache__/",
|
|
||||||
# "/.git/",
|
|
||||||
# "/*.zip",
|
|
||||||
# ]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
|
||||||
bl_idname = __package__
|
|
||||||
|
|
||||||
cache_dir: bpy.props.StringProperty(
|
|
||||||
name="Set Cache Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
do_change_render_setting: bpy.props.BoolProperty(
|
|
||||||
name="Change Render Setting",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
layout.label(text="Configuration")
|
|
||||||
layout.prop(self, 'cache_dir')
|
|
||||||
layout.prop(self, "do_change_render_setting")
|
|
||||||
@@ -13,7 +13,7 @@ nav:
|
|||||||
- Prepare Your Data: prepare_data.md
|
- Prepare Your Data: prepare_data.md
|
||||||
- Step by Step: step_by_step.md
|
- Step by Step: step_by_step.md
|
||||||
- Concepts & Pipeline: concept.md
|
- Concepts & Pipeline: concept.md
|
||||||
- Visualizating any Bio-structure:
|
- Visualizing any Bio-structure:
|
||||||
- Import & Preprocess: import.md
|
- Import & Preprocess: import.md
|
||||||
- Mask Methods: mask.md
|
- Mask Methods: mask.md
|
||||||
- Assgin a Shader: shader.md
|
- Assgin a Shader: shader.md
|
||||||
|
|||||||
1822
poetry.lock
generated
1822
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,29 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "bioxelnodes"
|
name = "bioxelnodes"
|
||||||
version = "0.2.7"
|
version = "0.3.3"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "~=3.10.0"
|
python = ">=3.11.0,<3.12.dev0"
|
||||||
bpy = "~=4.0"
|
simpleitk = "2.3.1"
|
||||||
simpleitk = "^2.3.1"
|
pyometiff = "1.0.0"
|
||||||
scikit-image = "^0.24.0"
|
mrcfile = "1.5.1"
|
||||||
|
h5py = "3.11.0"
|
||||||
|
transforms3d = "0.4.2"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ipykernel = "^6.25.2"
|
bpy = "^4.2.0"
|
||||||
pytest = "^7.4.2"
|
pytest = "^8.3.2"
|
||||||
pytest-dependency = "^0.5.1"
|
pytest-dependency = "^0.6.0"
|
||||||
mkdocs = "^1.5.3"
|
pytest-cov = "^5.0.0"
|
||||||
mkdocs-material = "^9.4.6"
|
mkdocs = "^1.6.0"
|
||||||
mkdocstrings = { extras = ["python"], version = "^0.23.0" }
|
mkdocs-material = "^9.5.30"
|
||||||
mkdocs-click = "^0.8.1"
|
autopep8 = "^2.3.1"
|
||||||
autopep8 = "^2.0.4"
|
mike = "^2.1.2"
|
||||||
pytest-cov = "^4.1.0"
|
tomlkit = "^0.13.0"
|
||||||
mike = "^2.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
SimpleITK==2.3.1 # Insight Segmentation and Registration Toolkit.
|
||||||
|
pyometiff==1.0.0 # .ome.tiff
|
||||||
|
mrcfile==1.5.1 # .map
|
||||||
|
h5py==3.11.0 # .h5
|
||||||
|
transforms3d==0.4.2 # transforms
|
||||||
Binary file not shown.
BIN
scipy_ndimage/macos-arm64/_nd_image.cpython-311-darwin.so
Normal file
BIN
scipy_ndimage/macos-arm64/_nd_image.cpython-311-darwin.so
Normal file
Binary file not shown.
BIN
scipy_ndimage/macos-x64/_nd_image.cpython-311-darwin.so
Normal file
BIN
scipy_ndimage/macos-x64/_nd_image.cpython-311-darwin.so
Normal file
Binary file not shown.
Reference in New Issue
Block a user