Add unit tests for bioxel module and documentation
- Created README.md for test instructions and categories. - Implemented comprehensive unit tests for the Data, Layer, and utility functions in the bioxel module. - Added tests for reading NRRD and DICOM formats, data properties, and edge cases. - Included tests for real data files to ensure functionality with actual datasets.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
from .layer import Layer
|
||||
from .container import Container
|
||||
from .io import load_container, save_container
|
||||
from .data import (
|
||||
Data,
|
||||
read,
|
||||
read_meta,
|
||||
calc_layer_shape,
|
||||
calc_layer_size,
|
||||
get_ext,
|
||||
SUPPORT_EXTS,
|
||||
DICOM_EXTS,
|
||||
OME_EXTS,
|
||||
MRC_EXTS,
|
||||
SEQUENCE_EXTS
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Layer',
|
||||
'Container',
|
||||
'load_container',
|
||||
'save_container',
|
||||
'Data',
|
||||
'read',
|
||||
'read_meta',
|
||||
'calc_layer_shape',
|
||||
'calc_layer_size',
|
||||
'get_ext',
|
||||
'SUPPORT_EXTS',
|
||||
'DICOM_EXTS',
|
||||
'OME_EXTS',
|
||||
'MRC_EXTS',
|
||||
'SEQUENCE_EXTS'
|
||||
]
|
||||
|
||||
831
src/bioxelnodes/bioxel/data.py
Normal file
831
src/bioxelnodes/bioxel/data.py
Normal file
@@ -0,0 +1,831 @@
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, List
|
||||
import numpy as np
|
||||
import SimpleITK as sitk
|
||||
from pyometiff import OMETIFFReader
|
||||
import mrcfile
|
||||
import transforms3d
|
||||
|
||||
from .layer import Layer
|
||||
|
||||
# 类型定义
|
||||
ProgressCallback = Optional[Callable[[float, str], None]]
|
||||
|
||||
|
||||
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",
|
||||
".mrc",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Data:
|
||||
filepath: str
|
||||
series_id: str
|
||||
_data: Optional[np.ndarray] = field(default=None, repr=False)
|
||||
_meta: Optional[dict] = field(default=None, repr=False)
|
||||
|
||||
@property
|
||||
def data(self) -> np.ndarray:
|
||||
if self._data is None:
|
||||
self.load_data()
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def meta(self) -> dict:
|
||||
if self._meta is None:
|
||||
self.load_meta()
|
||||
return self._meta
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.meta["name"]
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.meta["description"]
|
||||
|
||||
@property
|
||||
def shape(self) -> tuple:
|
||||
return (
|
||||
self.meta["frame_count"],
|
||||
*self.meta["xyz_shape"],
|
||||
self.meta["channel_count"],
|
||||
)
|
||||
|
||||
@property
|
||||
def xyz_shape(self) -> tuple:
|
||||
return self.meta["xyz_shape"]
|
||||
|
||||
@property
|
||||
def spacing(self) -> tuple:
|
||||
return self.meta["spacing"]
|
||||
|
||||
@property
|
||||
def affine(self) -> np.ndarray:
|
||||
return self.meta["affine"]
|
||||
|
||||
@property
|
||||
def frame_count(self) -> int:
|
||||
return self.meta["frame_count"]
|
||||
|
||||
@property
|
||||
def channel_count(self) -> int:
|
||||
return self.meta["channel_count"]
|
||||
|
||||
@property
|
||||
def dtype(self) -> np.dtype:
|
||||
return self.meta.get("dtype", np.float32)
|
||||
|
||||
def load_meta(self, progress_callback: ProgressCallback = None):
|
||||
data, meta = self._parse_file(progress_callback)
|
||||
self._meta = meta
|
||||
del data
|
||||
|
||||
def load_data(self, progress_callback: ProgressCallback = None):
|
||||
if not self.is_loaded():
|
||||
data, meta = self._parse_file(progress_callback)
|
||||
self._data = data
|
||||
self._meta = meta
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self._data is not None
|
||||
|
||||
def to_layers(
|
||||
self,
|
||||
kind: str = "scalar",
|
||||
layer_name: str = "",
|
||||
bioxel_size: float = 1.0,
|
||||
smooth: int = 0,
|
||||
remap: bool = False,
|
||||
split_channel: bool = False,
|
||||
frame_source: str = "-1",
|
||||
progress_callback: ProgressCallback = None,
|
||||
) -> List[Layer]:
|
||||
from .layer import Layer
|
||||
|
||||
data = self.data.copy()
|
||||
|
||||
data, layer_shape = self._transform_shape(data, frame_source)
|
||||
|
||||
data = self._preprocess(data, kind, remap)
|
||||
|
||||
mat_scale = transforms3d.zooms.zfdir2aff(bioxel_size)
|
||||
affine = np.dot(self.meta["affine"], mat_scale)
|
||||
|
||||
base_name = layer_name or kind.capitalize()
|
||||
|
||||
if kind == "label":
|
||||
return self._create_label_layers(
|
||||
data, base_name, layer_shape, affine, smooth, progress_callback
|
||||
)
|
||||
elif kind == "color":
|
||||
return self._create_color_layers(
|
||||
data, base_name, layer_shape, affine, progress_callback
|
||||
)
|
||||
elif kind == "scalar":
|
||||
return self._create_scalar_layers(
|
||||
data,
|
||||
base_name,
|
||||
layer_shape,
|
||||
affine,
|
||||
smooth,
|
||||
split_channel,
|
||||
progress_callback,
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def _parse_file(self, progress_callback: ProgressCallback = None):
|
||||
data_path = Path(self.filepath).resolve()
|
||||
ext = self._get_ext(data_path)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(0.0, "Reading Data...")
|
||||
|
||||
is_sequence = False
|
||||
sequence = None
|
||||
if ext in SEQUENCE_EXTS:
|
||||
sequence = self._collect_sequence(data_path)
|
||||
if sequence and 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)
|
||||
|
||||
if data is None and ext in MRC_EXTS and not is_sequence:
|
||||
data, name, spacing = self._parse_mrc(data_path)
|
||||
|
||||
if data is None and ext in OME_EXTS and not is_sequence:
|
||||
data, name, spacing = self._parse_ome_tiff(data_path, progress_callback)
|
||||
|
||||
if data is None:
|
||||
data, name, description, spacing, origin, direction = self._parse_sitk(
|
||||
data_path, ext, is_sequence, sequence, progress_callback
|
||||
)
|
||||
|
||||
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],
|
||||
"dtype": data.dtype,
|
||||
}
|
||||
|
||||
return data, meta
|
||||
|
||||
def _parse_mrc(self, data_path: Path):
|
||||
print("Parsing with mrcfile...")
|
||||
with mrcfile.open(data_path, "r") as mrc:
|
||||
mrc_data = mrc.data
|
||||
|
||||
if mrc.is_single_image():
|
||||
data = np.expand_dims(np.asarray(mrc_data), axis=0)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
elif mrc.is_image_stack():
|
||||
data = np.expand_dims(np.asarray(mrc_data), axis=-1)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
elif mrc.is_volume():
|
||||
data = np.expand_dims(np.asarray(mrc_data), axis=0)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
elif mrc.is_volume_stack():
|
||||
data = np.expand_dims(np.asarray(mrc_data), axis=-1)
|
||||
|
||||
name = self._get_file_no_digits_name(data_path)
|
||||
spacing = (mrc.voxel_size.x, mrc.voxel_size.y, mrc.voxel_size.z)
|
||||
|
||||
return data, name, spacing
|
||||
|
||||
def _parse_ome_tiff(self, data_path: Path, progress_callback=None):
|
||||
print("Parsing with OMETIFFReader...")
|
||||
reader = OMETIFFReader(fpath=data_path)
|
||||
ome_image, metadata, xml_metadata = reader.read()
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||
|
||||
try:
|
||||
ome_order = metadata["DimOrder BF Array"]
|
||||
data = self._transpose_ome_image(ome_image, ome_order)
|
||||
|
||||
try:
|
||||
spacing = (
|
||||
metadata["PhysicalSizeX"],
|
||||
metadata["PhysicalSizeY"],
|
||||
metadata["PhysicalSizeZ"],
|
||||
)
|
||||
except:
|
||||
spacing = (1, 1, 1)
|
||||
|
||||
name = self._get_file_no_digits_name(data_path)
|
||||
except Exception as e:
|
||||
print(f"Error parsing OME-TIFF: {e}")
|
||||
data = np.zeros((1, 1, 1, 1, 1), dtype=np.float32)
|
||||
name = ""
|
||||
spacing = (1, 1, 1)
|
||||
|
||||
return data, name, spacing
|
||||
|
||||
def _parse_sitk(
|
||||
self,
|
||||
data_path: Path,
|
||||
ext: str,
|
||||
is_sequence: bool,
|
||||
sequence: Optional[list],
|
||||
progress_callback=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), self.series_id
|
||||
)
|
||||
reader.SetFileNames(series_files)
|
||||
|
||||
itk_image = reader.Execute()
|
||||
name, description = self._extract_dicom_meta(reader, data_dirpath)
|
||||
|
||||
elif ext in SEQUENCE_EXTS and is_sequence and sequence:
|
||||
sequence_list = sequence if sequence else []
|
||||
itk_image = sitk.ReadImage(sequence_list)
|
||||
name = self._get_file_no_digits_name(data_path)
|
||||
description = ""
|
||||
|
||||
else:
|
||||
itk_image = sitk.ReadImage(data_path)
|
||||
name = self._get_filename(data_path)
|
||||
description = ""
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||
|
||||
data = self._convert_sitk_to_numpy(itk_image, ext, is_sequence)
|
||||
|
||||
if itk_image.GetDimension() >= 3 and ext not in SEQUENCE_EXTS:
|
||||
spacing = tuple(itk_image.GetSpacing())
|
||||
origin = tuple(itk_image.GetOrigin())
|
||||
direction = tuple(itk_image.GetDirection())
|
||||
else:
|
||||
spacing = (1, 1, 1)
|
||||
origin = (0, 0, 0)
|
||||
direction = (1, 0, 0, 0, 1, 0, 0, 0, 1)
|
||||
|
||||
return data, name, description, spacing, origin, direction
|
||||
|
||||
def _transpose_ome_image(self, ome_image, ome_order: str):
|
||||
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)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
|
||||
elif ome_image.ndim == 3:
|
||||
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)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
|
||||
elif ome_image.ndim == 4:
|
||||
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)
|
||||
|
||||
elif ome_image.ndim == 5:
|
||||
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)
|
||||
|
||||
return data
|
||||
|
||||
def _convert_sitk_to_numpy(self, itk_image, ext: str, is_sequence: bool):
|
||||
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)
|
||||
else:
|
||||
data = np.transpose(data)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
|
||||
data = np.expand_dims(data, axis=0)
|
||||
|
||||
elif itk_image.GetDimension() == 3:
|
||||
if ext not in SEQUENCE_EXTS:
|
||||
itk_image = sitk.DICOMOrient(itk_image, "RAS")
|
||||
|
||||
data = sitk.GetArrayFromImage(itk_image)
|
||||
|
||||
if data.ndim == 4:
|
||||
data = np.transpose(data, (2, 1, 0, 3))
|
||||
else:
|
||||
data = np.transpose(data)
|
||||
data = np.expand_dims(data, axis=-1)
|
||||
|
||||
data = np.expand_dims(data, axis=0)
|
||||
|
||||
elif itk_image.GetDimension() == 4:
|
||||
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)
|
||||
|
||||
return data
|
||||
|
||||
def _extract_dicom_meta(self, reader, data_dirpath: Path):
|
||||
def get_meta(key):
|
||||
try:
|
||||
string = reader.GetMetaData(0, key).removesuffix(" ")
|
||||
string.encode("utf-8")
|
||||
if string in ["No study description", "No series description", ""]:
|
||||
return None
|
||||
else:
|
||||
return string
|
||||
except:
|
||||
return None
|
||||
|
||||
study_description = get_meta("0008|1030") or data_dirpath.name
|
||||
series_description = get_meta("0008|103e")
|
||||
series_modality = get_meta("0008|0060")
|
||||
|
||||
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 = study_description.replace(" ", "-")
|
||||
description = description.replace(" ", "-")
|
||||
|
||||
return name, description
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
suffix = filepath.suffix
|
||||
return "" if len(suffix) > 5 else suffix
|
||||
|
||||
@staticmethod
|
||||
def _get_filename(filepath: Path) -> str:
|
||||
ext = Data._get_ext(filepath)
|
||||
return filepath.name.removesuffix(ext)
|
||||
|
||||
@staticmethod
|
||||
def _get_file_no_digits_name(filepath: Path) -> str:
|
||||
prefix, digits, suffix = Data._get_filename_parts(filepath)
|
||||
prefix = Data._remove_end_str(prefix, "_")
|
||||
prefix = Data._remove_end_str(prefix, ".")
|
||||
prefix = Data._remove_end_str(prefix, "-")
|
||||
prefix = Data._remove_end_str(prefix, " ")
|
||||
return prefix + suffix
|
||||
|
||||
@staticmethod
|
||||
def _get_filename_parts(filepath: Path) -> tuple:
|
||||
def has_digits(s):
|
||||
return any(char.isdigit() for char in s)
|
||||
|
||||
name = Data._get_filename(filepath)
|
||||
parts = name.replace(".", " ").replace("_", " ").split(" ")
|
||||
skip_prefixs = ["CH", "ch", "channel"]
|
||||
number_part = None
|
||||
number_part_i = None
|
||||
|
||||
for i, part in enumerate(parts[::-1]):
|
||||
if has_digits(part):
|
||||
if not any([part.startswith(prefix) for prefix in skip_prefixs]):
|
||||
number_part = part
|
||||
number_part_i = len(parts) - i
|
||||
break
|
||||
|
||||
if number_part is None:
|
||||
return name, "", ""
|
||||
|
||||
prefix_parts = parts[: number_part_i - 1]
|
||||
prefix_parts_count = sum([len(part) + 1 for part in prefix_parts])
|
||||
|
||||
digits = ""
|
||||
suffix = ""
|
||||
|
||||
started = False
|
||||
for char in number_part[::-1]:
|
||||
if char.isdigit():
|
||||
started = True
|
||||
digits += char
|
||||
else:
|
||||
if started:
|
||||
break
|
||||
else:
|
||||
suffix += char
|
||||
|
||||
digits = digits[::-1]
|
||||
|
||||
prefix_parts_count += len(number_part) - len(digits) - len(suffix)
|
||||
|
||||
prefix = name[:prefix_parts_count]
|
||||
suffix = name[prefix_parts_count + len(digits) :]
|
||||
|
||||
return prefix, digits, suffix
|
||||
|
||||
@staticmethod
|
||||
def _get_file_index(filepath: Path) -> int:
|
||||
prefix, digits, suffix = Data._get_filename_parts(filepath)
|
||||
return int(digits) if digits != "" else 0
|
||||
|
||||
@staticmethod
|
||||
def _collect_sequence(filepath: Path):
|
||||
file_dict = {}
|
||||
for f in filepath.parent.iterdir():
|
||||
if (
|
||||
f.is_file()
|
||||
and Data._get_ext(filepath) == Data._get_ext(f)
|
||||
and Data._get_file_no_digits_name(filepath)
|
||||
== Data._get_file_no_digits_name(f)
|
||||
):
|
||||
index = Data._get_file_index(f)
|
||||
file_dict[index] = f
|
||||
|
||||
for key in list(file_dict.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
|
||||
|
||||
@staticmethod
|
||||
def _remove_end_str(string: str, end: str) -> str:
|
||||
while string.endswith(end) and len(string) > 0:
|
||||
string = string.removesuffix(end)
|
||||
return string
|
||||
|
||||
def _transform_shape(self, data, frame_source: str) -> tuple:
|
||||
orig_shape = self.xyz_shape
|
||||
|
||||
if frame_source == "-1":
|
||||
data = data[0:1, :, :, :, :]
|
||||
new_shape = orig_shape
|
||||
elif frame_source == "0":
|
||||
new_shape = orig_shape
|
||||
elif frame_source == "1":
|
||||
data = data.transpose(1, 0, 2, 3, 4)
|
||||
new_shape = (1, orig_shape[1], orig_shape[2])
|
||||
elif frame_source == "2":
|
||||
data = data.transpose(2, 1, 0, 3, 4)
|
||||
new_shape = (orig_shape[0], 1, orig_shape[2])
|
||||
elif frame_source == "3":
|
||||
data = data.transpose(3, 1, 2, 0, 4)
|
||||
new_shape = (orig_shape[0], orig_shape[1], 1)
|
||||
else:
|
||||
data = data.transpose(4, 1, 2, 3, 0)
|
||||
new_shape = orig_shape
|
||||
|
||||
return data, new_shape
|
||||
|
||||
def _preprocess(self, data, kind: str, remap: bool) -> np.ndarray:
|
||||
if kind == "color":
|
||||
if np.issubdtype(data.dtype, np.uint8):
|
||||
data = np.multiply(data, 1.0 / 256, dtype=np.float32)
|
||||
elif data.dtype.kind in ["u", "i"]:
|
||||
data = data.astype(np.float32)
|
||||
min_val, max_val = data.min(), data.max()
|
||||
if max_val != min_val:
|
||||
data = (data - min_val) / (max_val - min_val)
|
||||
else:
|
||||
data = np.zeros_like(data, dtype=np.float32)
|
||||
else:
|
||||
data = data.astype(np.float32)
|
||||
|
||||
if data.shape[4] == 1:
|
||||
data = np.repeat(data, repeats=3, axis=4)
|
||||
elif data.shape[4] == 2:
|
||||
zeros_shape = list(data.shape[:4]) + [1]
|
||||
zeros = np.zeros(zeros_shape, dtype=np.float32)
|
||||
data = np.concatenate((data, zeros), axis=-1)
|
||||
elif data.shape[4] > 3:
|
||||
data = data[:, :, :, :, :3]
|
||||
|
||||
elif kind == "scalar":
|
||||
if remap:
|
||||
data = data.astype(np.float32)
|
||||
min_val, max_val = data.min(), data.max()
|
||||
if max_val != min_val:
|
||||
data = (data - min_val) / (max_val - min_val)
|
||||
else:
|
||||
data = np.zeros_like(data, dtype=np.float32)
|
||||
|
||||
elif kind == "label":
|
||||
data = data.astype(int)
|
||||
|
||||
return data
|
||||
|
||||
def _create_progress_cb_factory(self, progress_callback):
|
||||
def factory(name, progress, progress_step):
|
||||
def callback(frame, total):
|
||||
if progress_callback:
|
||||
sub_progress = progress + frame * (progress_step / total)
|
||||
progress_callback(
|
||||
sub_progress, f"Processing {name} Frame {frame+1}..."
|
||||
)
|
||||
|
||||
return callback
|
||||
|
||||
return factory
|
||||
|
||||
def _create_label_layers(
|
||||
self,
|
||||
data,
|
||||
base_name,
|
||||
layer_shape,
|
||||
affine,
|
||||
smooth,
|
||||
progress_callback: ProgressCallback,
|
||||
):
|
||||
from .layer import Layer
|
||||
|
||||
layers = []
|
||||
label_count = int(np.max(data))
|
||||
if label_count == 0:
|
||||
return layers
|
||||
|
||||
progress_step = 0.7 / label_count
|
||||
cb_factory = self._create_progress_cb_factory(progress_callback)
|
||||
|
||||
for i in range(label_count):
|
||||
name_i = f"{base_name}_{i+1}"
|
||||
progress = 0.2 + i * progress_step
|
||||
if progress_callback:
|
||||
progress_callback(progress, f"Processing {name_i}...")
|
||||
|
||||
label_data = data == np.full_like(data, i + 1)
|
||||
progress_cb = cb_factory(name_i, progress, progress_step)
|
||||
|
||||
layer = Layer(data=label_data, name=name_i, kind="label")
|
||||
layer.resize(
|
||||
shape=layer_shape, smooth=smooth, progress_callback=progress_cb
|
||||
)
|
||||
layer.affine = affine
|
||||
layers.append(layer)
|
||||
|
||||
return layers
|
||||
|
||||
def _create_color_layers(
|
||||
self, data, base_name, layer_shape, affine, progress_callback: ProgressCallback
|
||||
):
|
||||
from .layer import Layer
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(0.2, f"Processing {base_name}...")
|
||||
|
||||
cb_factory = self._create_progress_cb_factory(progress_callback)
|
||||
progress_cb = cb_factory(base_name, 0.2, 0.7)
|
||||
|
||||
layer = Layer(data=data, name=base_name, kind="color")
|
||||
layer.resize(shape=layer_shape, progress_callback=progress_cb)
|
||||
layer.affine = affine
|
||||
|
||||
return [layer]
|
||||
|
||||
def _create_scalar_layers(
|
||||
self,
|
||||
data,
|
||||
base_name,
|
||||
layer_shape,
|
||||
affine,
|
||||
smooth,
|
||||
split_channel,
|
||||
progress_callback: ProgressCallback,
|
||||
):
|
||||
from .layer import Layer
|
||||
|
||||
layers = []
|
||||
cb_factory = self._create_progress_cb_factory(progress_callback)
|
||||
|
||||
if split_channel:
|
||||
channel_count = data.shape[-1]
|
||||
progress_step = 0.7 / channel_count
|
||||
|
||||
for i in range(channel_count):
|
||||
name_i = f"{base_name}_{i+1}"
|
||||
progress = 0.2 + i * progress_step
|
||||
if progress_callback:
|
||||
progress_callback(progress, f"Processing {name_i}...")
|
||||
|
||||
progress_cb = cb_factory(name_i, progress, progress_step)
|
||||
layer = Layer(
|
||||
data=data[:, :, :, :, i : i + 1], name=name_i, kind="scalar"
|
||||
)
|
||||
layer.resize(
|
||||
shape=layer_shape, smooth=smooth, progress_callback=progress_cb
|
||||
)
|
||||
layer.affine = affine
|
||||
layers.append(layer)
|
||||
else:
|
||||
if progress_callback:
|
||||
progress_callback(0.2, f"Processing {base_name}...")
|
||||
|
||||
progress_cb = cb_factory(base_name, 0.2, 0.7)
|
||||
layer = Layer(data=data, name=base_name, kind="scalar")
|
||||
layer.resize(
|
||||
shape=layer_shape, smooth=smooth, progress_callback=progress_cb
|
||||
)
|
||||
layer.affine = affine
|
||||
layers.append(layer)
|
||||
|
||||
return layers
|
||||
|
||||
|
||||
# 模块级函数签名
|
||||
def read(
|
||||
filepath: str, series_id: str = "", progress_callback: ProgressCallback = None
|
||||
) -> Data:
|
||||
filepath = Path(filepath).resolve()
|
||||
if not filepath.exists():
|
||||
raise FileNotFoundError(f"File not found: {filepath}")
|
||||
|
||||
data_obj = Data(filepath=filepath, series_id=series_id)
|
||||
data_obj.load_data(progress_callback)
|
||||
return data_obj
|
||||
|
||||
|
||||
def read_meta(filepath: str, series_id: str = "") -> Data:
|
||||
data_obj = Data(filepath=filepath, series_id=series_id)
|
||||
data_obj.load_meta()
|
||||
return data_obj
|
||||
|
||||
|
||||
def calc_layer_shape(
|
||||
bioxel_size: float, orig_shape: tuple, orig_spacing: tuple
|
||||
) -> 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 calc_layer_size(shape: tuple, bioxel_size: float, scale: float = 1.0) -> tuple:
|
||||
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: 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:
|
||||
suffix = filepath.suffix
|
||||
return "" if len(suffix) > 5 else suffix
|
||||
@@ -1,6 +1,12 @@
|
||||
import copy
|
||||
import numpy as np
|
||||
|
||||
# 条件导入 openvdb(只在 Blender 环境中存在)
|
||||
try:
|
||||
import openvdb as vdb
|
||||
except ImportError:
|
||||
vdb = None
|
||||
|
||||
from . import scipy
|
||||
from . import scipy as ndi
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import matplotlib.pyplot as plt
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import openvdb as vdb
|
||||
|
||||
try:
|
||||
import openvdb as vdb
|
||||
except ImportError:
|
||||
vdb = None
|
||||
|
||||
from .bioxel.layer import Layer
|
||||
from .utils import ndarray_to_png
|
||||
@@ -57,12 +61,10 @@ def cache_layer_data(layer: Layer, cache_path: str):
|
||||
grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32))
|
||||
else: # 颜色类型
|
||||
grid = vdb.Vec3SGrid()
|
||||
grid.copyFromArray(
|
||||
data[f, :, :, :, :].copy().astype(np.float32))
|
||||
grid.copyFromArray(data[f, :, :, :, :].copy().astype(np.float32))
|
||||
|
||||
# 仅设置transform,不存储metadata
|
||||
grid.transform = vdb.createLinearTransform(
|
||||
layer.affine.transpose())
|
||||
grid.transform = vdb.createLinearTransform(layer.affine.transpose())
|
||||
grid.name = layer.kind
|
||||
|
||||
# 保存序列帧VDB
|
||||
@@ -272,6 +274,5 @@ def save_layers_to_json(layers: List[Layer], cache_dir: str) -> List[int]:
|
||||
added_ids.append(cache_id)
|
||||
|
||||
set_layer_caches(existing_data)
|
||||
print(
|
||||
f"Successfully added {len(added_ids)} layers to the internal text datablock")
|
||||
print(f"Successfully added {len(added_ids)} layers to the internal text datablock")
|
||||
return added_ids
|
||||
|
||||
@@ -105,7 +105,7 @@ def add_bioxel_node(name: str):
|
||||
bpy.ops.object.modifier_add_node_group(
|
||||
asset_library_type="CUSTOM",
|
||||
asset_library_identifier="O Bioxel",
|
||||
relative_asset_identifier=f"Nodes.blend\\NodeTree\\{name}",
|
||||
relative_asset_identifier=f"BioxelNodes.blend\\NodeTree\\{name}",
|
||||
)
|
||||
bpy.ops.object.modifier_remove(modifier=name)
|
||||
try:
|
||||
|
||||
@@ -12,37 +12,20 @@ import transforms3d
|
||||
from ..props import BIOXEL_Series
|
||||
from ..utils import get_layer_obj, wrapped_label
|
||||
|
||||
from ..bioxel.layer import Layer
|
||||
from ..bioxel.parse import DICOM_EXTS, SUPPORT_EXTS, get_ext, parse_volumetric_data
|
||||
from ..bioxel import (
|
||||
DICOM_EXTS,
|
||||
SUPPORT_EXTS,
|
||||
read,
|
||||
read_meta,
|
||||
calc_layer_shape,
|
||||
calc_layer_size,
|
||||
get_ext,
|
||||
)
|
||||
|
||||
from ..utils import get_cache_dir, progress_update, progress_bar
|
||||
from ..layer import get_layer_caches, save_layers_to_json
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
"""
|
||||
ImportData -> ParseVolumetricData -> ImportDataDialog
|
||||
start import parse data execute import
|
||||
@@ -211,11 +194,7 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
|
||||
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,
|
||||
)
|
||||
data = read_meta(self.filepath, series_id=series_id)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
except Exception as e:
|
||||
@@ -225,8 +204,8 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
if cancel():
|
||||
return
|
||||
|
||||
self.meta = meta
|
||||
self.label_count = int(np.max(data))
|
||||
self.meta = data.meta
|
||||
self.label_count = int(np.max(data.data))
|
||||
self.dtype = data.dtype
|
||||
|
||||
# Init cancel flag
|
||||
@@ -334,8 +313,8 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
|
||||
bioxel_size = max(min(*orig_spacing), 1.0)
|
||||
|
||||
layer_shape = get_layer_shape(bioxel_size, orig_shape, orig_spacing)
|
||||
layer_size = get_layer_size(layer_shape, bioxel_size, 0.01)
|
||||
layer_shape = calc_layer_shape(bioxel_size, orig_shape, orig_spacing)
|
||||
layer_size = calc_layer_size(layer_shape, bioxel_size, 0.01)
|
||||
min_log10 = math.floor(math.log10(min(*layer_size)))
|
||||
max_log10 = math.floor(math.log10(max(*layer_size)))
|
||||
|
||||
@@ -547,33 +526,23 @@ class ImportDataDialog(bpy.types.Operator):
|
||||
|
||||
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 KeyboardInterrupt("Cancelled by user")
|
||||
progress_update(context, factor * 0.2, text)
|
||||
|
||||
def progress_callback_factory(layer_name, progress, progress_step):
|
||||
def progress_callback(frame, total):
|
||||
if cancel():
|
||||
raise KeyboardInterrupt("Cancelled by user")
|
||||
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}...",
|
||||
)
|
||||
print(f"Processing {layer_name} Frame {frame+1}...")
|
||||
|
||||
return progress_callback
|
||||
|
||||
try:
|
||||
data, meta = parse_volumetric_data(
|
||||
data_file=self.filepath,
|
||||
series_id=self.series_id,
|
||||
progress_callback=progress_callback,
|
||||
data = read(self.filepath, self.series_id, progress_callback)
|
||||
|
||||
self.layers = data.to_layers(
|
||||
kind=self.read_as.lower(),
|
||||
layer_name=self.layer_name,
|
||||
bioxel_size=self.bioxel_size,
|
||||
smooth=self.smooth,
|
||||
remap=self.remap,
|
||||
split_channel=self.split_channel,
|
||||
frame_source=self.frame_source,
|
||||
progress_callback=lambda f, t: progress_callback(0.2 + f * 0.7, t),
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -585,203 +554,6 @@ class ImportDataDialog(bpy.types.Operator):
|
||||
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.lower()
|
||||
|
||||
if cancel():
|
||||
return
|
||||
|
||||
# change shape as sequence or not
|
||||
if self.frame_source == "-1":
|
||||
data = data[0:1, :, :, :, :]
|
||||
elif self.frame_source == "0":
|
||||
# frame as frame
|
||||
pass
|
||||
elif self.frame_source == "1":
|
||||
# X as frame
|
||||
data = data.transpose(1, 0, 2, 3, 4)
|
||||
shape = (1, shape[1], shape[2])
|
||||
elif self.frame_source == "2":
|
||||
# Y as frame
|
||||
data = data.transpose(2, 1, 0, 3, 4)
|
||||
shape = (shape[0], 1, shape[2])
|
||||
elif self.frame_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)
|
||||
|
||||
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
|
||||
)
|
||||
label_data = data == np.full_like(data, i + 1)
|
||||
# label_data = label_data.astype(np.float32)
|
||||
try:
|
||||
layer = Layer(data=label_data, name=name_i, kind=kind)
|
||||
|
||||
layer.resize(
|
||||
shape=shape,
|
||||
smooth=self.smooth,
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
except Exception as e:
|
||||
self.has_error = e
|
||||
return
|
||||
|
||||
if kind == "color":
|
||||
if np.issubdtype(np.uint8, data.dtype):
|
||||
data = np.multiply(data, 1.0 / 256, dtype=np.float32)
|
||||
elif data.dtype.kind in ["u", "i"]:
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
|
||||
min_val = data.min()
|
||||
max_val = data.max()
|
||||
# Avoid division by zero if all values are the same
|
||||
if max_val != min_val:
|
||||
# Normalize the array to the range (0,1)
|
||||
data = (data - min_val) / (max_val - min_val)
|
||||
else:
|
||||
# If all values are the same, the normalized array will be all zeros
|
||||
data = np.zeros_like(data, dtype=np.float32)
|
||||
|
||||
else:
|
||||
data = data.astype(np.float32)
|
||||
|
||||
# Gamma Correct
|
||||
# data = data ** 2.2
|
||||
|
||||
name = self.layer_name or "Color"
|
||||
if data.shape[4] == 1:
|
||||
data = np.repeat(data, repeats=3, axis=4)
|
||||
elif data.shape[4] == 2:
|
||||
d_shape = list(data.shape)
|
||||
d_shape = d_shape[:4] + [1]
|
||||
zore = np.zeros(tuple(d_shape), dtype=np.float32)
|
||||
data = np.concatenate((data, zore), axis=-1)
|
||||
elif data.shape[4] > 3:
|
||||
data = data[:, :, :, :, :3]
|
||||
|
||||
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)
|
||||
|
||||
layer.resize(shape=shape, progress_callback=progress_callback)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
except Exception as e:
|
||||
self.has_error = e
|
||||
return
|
||||
|
||||
elif kind == "scalar":
|
||||
name = self.layer_name or "Scalar"
|
||||
|
||||
if self.remap:
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
|
||||
min_val = data.min()
|
||||
max_val = data.max()
|
||||
# Avoid division by zero if all values are the same
|
||||
if max_val != min_val:
|
||||
# Normalize the array to the range (0,1)
|
||||
data = (data - min_val) / (max_val - min_val)
|
||||
else:
|
||||
# If all values are the same, the normalized array will be all zeros
|
||||
data = np.zeros_like(data, dtype=np.float32)
|
||||
|
||||
if self.split_channel:
|
||||
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
|
||||
)
|
||||
|
||||
layer.resize(
|
||||
shape=shape, progress_callback=progress_callback
|
||||
)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except KeyboardInterrupt:
|
||||
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)
|
||||
|
||||
layer.resize(shape=shape, progress_callback=progress_callback)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except KeyboardInterrupt:
|
||||
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
|
||||
@@ -851,7 +623,7 @@ class ImportDataDialog(bpy.types.Operator):
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def draw(self, context):
|
||||
layer_shape = get_layer_shape(
|
||||
layer_shape = calc_layer_shape(
|
||||
self.bioxel_size, self.orig_shape, self.orig_spacing
|
||||
)
|
||||
|
||||
|
||||
@@ -16,13 +16,11 @@ class RenameLayer(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
cache_id: bpy.props.StringProperty(options={"HIDDEN"}) # type: ignore
|
||||
new_name: bpy.props.StringProperty(
|
||||
name="New name", default="") # type: ignore
|
||||
new_name: bpy.props.StringProperty(name="New name", default="") # type: ignore
|
||||
|
||||
def invoke(self, context, event):
|
||||
caches = get_layer_caches()
|
||||
entry = next((c for c in caches if str(
|
||||
c.get("id", "")) == self.cache_id), None)
|
||||
entry = next((c for c in caches if str(c.get("id", "")) == self.cache_id), None)
|
||||
if entry:
|
||||
self.new_name = str(entry.get("name", ""))
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
@@ -127,8 +125,7 @@ class AddLayerNode(bpy.types.Operator):
|
||||
layer_node.inputs["Frame Count"].default_value = entry["frame_count"]
|
||||
layer_node.inputs["Animation"].default_value = True
|
||||
else:
|
||||
hidden_sockets = hidden_sockets + \
|
||||
["Frame Count", "Frame Offset", "Cycle"]
|
||||
hidden_sockets = hidden_sockets + ["Frame Count", "Frame Offset", "Cycle"]
|
||||
|
||||
# 将特定属性隐藏到hidden面板
|
||||
for socket_name in hidden_sockets:
|
||||
@@ -177,8 +174,7 @@ class RelocatePath(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
caches = get_layer_caches()
|
||||
entry = next((c for c in caches if str(
|
||||
c.get("id", "")) == self.cache_id), None)
|
||||
entry = next((c for c in caches if str(c.get("id", "")) == self.cache_id), None)
|
||||
if not entry:
|
||||
self.report({"ERROR"}, "Layer not found")
|
||||
return {"CANCELLED"}
|
||||
@@ -228,8 +224,7 @@ class SaveLayer(bpy.types.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Default to Blender file directory if possible
|
||||
blend_dir = Path(
|
||||
bpy.data.filepath).parent if bpy.data.filepath else Path.cwd()
|
||||
blend_dir = Path(bpy.data.filepath).parent if bpy.data.filepath else Path.cwd()
|
||||
context.window_manager.fileselect_add(self)
|
||||
if not self.directory:
|
||||
self.directory = str(blend_dir)
|
||||
@@ -244,8 +239,7 @@ class SaveLayer(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
caches = get_layer_caches()
|
||||
entry = next((c for c in caches if str(
|
||||
c.get("id")) == self.cache_id), None)
|
||||
entry = next((c for c in caches if str(c.get("id")) == self.cache_id), None)
|
||||
cache_id = entry.get("id")
|
||||
|
||||
if not entry:
|
||||
|
||||
Reference in New Issue
Block a user