diff --git a/.gitattributes b/.gitattributes index 80a1944..f471a1f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,131 @@ -*.blend filter=lfs diff=lfs merge=lfs -text +# Define macros (only works in top-level gitattributes files) +[attr]lfs filter=lfs diff=lfs merge=lfs -text + +# 3D models +*.collada lfs +*.dae lfs +*.dxf lfs +*.FBX lfs +*.fbx lfs +*.jas lfs +*.lws lfs +*.lxo lfs +*.obj lfs +*.ply lfs +*.skp lfs +*.stl lfs +*.stp lfs +*.usd lfs +*.usda lfs +*.usdc lfs +*.usdz lfs +*.glb lfs +*.gltf lfs + +# Audio +*.aif lfs +*.aiff lfs +*.it lfs +*.mod lfs +*.mp3 lfs +*.ogg lfs +*.s3m lfs +*.wav lfs +*.xm lfs + +# Video +*.asf lfs +*.avi lfs +*.flv lfs +*.mov lfs +*.mp4 lfs +*.mpeg lfs +*.mpg lfs +*.ogv lfs +*.wmv lfs +*.mkv lfs + +# Images +*.bmp lfs +*.exr lfs +*.gif lfs +*.hdr lfs +*.iff lfs +*.jpeg lfs +*.jpg lfs +*.pict lfs +*.png lfs +*.psd lfs +*.tga lfs +*.tif lfs +*.tiff lfs +*.webp lfs + +# Compressed Archive +*.7z lfs +*.bz2 lfs +*.gz lfs +*.rar lfs +*.tar lfs +*.zip lfs + +# Compiled Dynamic Library +*.dll lfs +*.pdb lfs +*.so lfs + +# Fonts +*.otf lfs +*.ttf lfs +*.woff lfs +*.woff2 lfs + +# Executable/Installer +*.apk lfs +*.exe lfs + +# Documents +*.pdf lfs +*.doc lfs +*.xls lfs +*.ppt lfs +*.docx lfs +*.xlsx lfs +*.pptx lfs + +# Adobe +*.psd lfs +*.ai lfs +*.aep lfs +*.prproj lfs +*.spp lfs + +# Zbrush +*.zbr lfs +*.zpr lfs +*.ztl lfs +*.c4d lfs + +# Autodesk +*.max lfs +*.ma lfs +*.mb lfs +*.3dm lfs +*.3ds lfs + +# Blender +*.blend lfs +*.blend1 lfs +*.c4d lfs + +# Imaging +*.dicom lfs +*.dcm lfs +*.nii lfs +*.ome lfs +*.map lfs +*.mrc lfs + +# Others +*.abr lfs +*.tpl lfs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 960f98e..9063c92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,51 @@ +# Tempary files +temp +sandbox.* +notes/ + +# Environments +.env +.envrc +.venv +.env.* +!.env.example +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +.secrets +.secrets.* +!.secrets.example +secrets/ +SECRETS/ +secrets.bak/ + +# Claude +.claudian + +# Obsidian + +.obsidian/* +!.obsidian/snippets +!.obsidian/app.json + +*.canvas + +# Keep odox for extract to openwiki +!.obsidian/plugins +.obsidian/plugins/* +!.obsidian/plugins/odox + +# Blender +*.blend1 +blender_assets.cats.txt~ +blendcache_* + # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -20,7 +65,6 @@ parts/ sdist/ var/ wheels/ -pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -28,8 +72,8 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -47,8 +91,10 @@ htmlcov/ nosetests.xml coverage.xml *.cover +*.py.cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -71,6 +117,7 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook @@ -81,30 +128,70 @@ profile_default/ ipython_config.py # pyenv -.python-version +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +# Pipfile.lock -# celery beat schedule file +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ # SageMath parsed files *.sage.py -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - # Spyder project settings .spyderproject .spyproject @@ -123,18 +210,183 @@ dmypy.json # Pyre type checker .pyre/ -pythonlib* +# pytype static type analyzer +.pytype/ -*.ipynb +# Cython debug symbols +cython_debug/ -.secrets -.vdb +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ -!scipy_ndimage/*/** +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ -# Blender -*.blend1 -blendcache_* -*.cats.txt~ +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ -tests/data \ No newline at end of file +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# pnpm +.pnpm-store + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ \ No newline at end of file diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 0000000..ac3bb1b --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1,22 @@ +{ + "showInlineTitle": false, + "useMarkdownLinks": true, + "pdfExportSettings": { + "pageSize": "Letter", + "landscape": false, + "margin": "0", + "downscalePercent": 100 + }, + "userIgnoreFilters": [ + "notes/assets/" + ], + "showUnsupportedFiles": false, + "attachmentFolderPath": "notes/assets", + "livePreview": true, + "showIndentGuide": false, + "alwaysUpdateLinks": true, + "openBehavior": "file:README", + "newFileLocation": "folder", + "newFileFolderPath": "notes", + "newLinkFormat": "relative" +} \ No newline at end of file diff --git a/.obsidian/snippets/omoolab.css b/.obsidian/snippets/omoolab.css new file mode 100644 index 0000000..92c8a0d --- /dev/null +++ b/.obsidian/snippets/omoolab.css @@ -0,0 +1,30 @@ +/* not show label in canvas */ +.canvas-node-label { + display: none; +} + +.canvas-node-container:hover + .canvas-node-label { + display: inherit; +} + +/* image caption */ +.image-embed { + flex-wrap: wrap; +} + +.image-embed[alt]:after { + content: attr(alt); + display: block; + padding-bottom: 12px; + line-height: 2.0; + font-style: italic; + color: var(--text-faint); + text-align: center; + width: 100%; +} + + +/* fix image in canvas */ +.image-embed { + padding: 0; +} \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/AGENTS.md b/AGENTS.md index 661944b..870aad8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,227 +1,19 @@ -# BioxelNodes - Agent Development Guide +## 总规范 -This file contains essential information for agentic coding agents working on the BioxelNodes codebase. +- 说明性内容(例如提交说明、文档、注释、mermaid),以中文为主,专有名词、约定俗称可用英文 -## Project Overview +## Markdown 规范 -BioxelNodes is a Blender addon for scientific volumetric data visualization. It integrates with Blender's Geometry Nodes and Cycles rendering engine to process and visualize medical/scientific data. +- 不使用`---`分割器 +- `# Heading` 一级标题仅用于开头 +- mermaid 节点名用英文字符 -**Language:** Python 3.11 -**Framework:** Blender Python API (bpy) -**Package Manager:** uv +## 代码规范 -## Development Commands - -### Environment Setup -```bash -# Install dependencies -uv sync -``` - -### Build Commands -```bash -# Build for specific platform -uv run build.py # platforms: windows-x64, linux-x64, macos-arm64, macos-x64 -``` - -### Unit Test -```bash -uv run pytest tests/ -``` - -### Code Quality -```bash -# Format code (autopep8) -autopep8 --in-place --recursive src/ -``` - -### Documentation -```bash -# Serve documentation locally -uv run mkdocs serve - -# Build documentation -uv run mike deploy --push --update-aliases 0.2.x latest -``` - -## Code Style Guidelines - -### Formatting -- **4-space indentation** -- **PEP 8** compliance enforced via autopep8 - -### Naming Conventions -- **snake_case** for functions, variables, and files -- **PascalCase** for classes (Blender convention) -- **UPPER_CASE** for constants -- **bioxel_** prefix for addon-specific properties - -### Import Organization -```python -# Standard library imports first -import pathlib -import uuid - -# Third-party imports -import bpy -import numpy as np -import SimpleITK as sitk - -# Relative imports for internal modules -from ..exceptions import CancelledByUser -from ..utils import get_layer_obj -``` - -### Type Hints -- Use sparingly, mainly for Blender property annotations -- Focus on function signatures and complex data structures -- Example: `def process_data(data: np.ndarray) -> np.ndarray:` - -## Architecture Patterns - -### Auto-Registration System -- Uses `auto_load.py` for automatic class registration -- Classes are discovered automatically via reflection -- Registration order resolved via topological sorting -- All Blender classes must inherit from appropriate base types - -### Module Structure -``` -src/bioxelnodes/ -├── __init__.py # Addon entry point, asset library setup -├── auto_load.py # Auto-registration system -├── constants.py # Constants and paths -├── utils.py # Utility functions -├── preferences.py # Addon preferences -├── props.py # Blender properties -├── panels.py # UI panels -├── menus.py # UI menus -├── node.py # Node utilities -├── layer.py # Layer management -├── operators/ # Blender operators -├── bioxel/ # Core bioxel functionality -└── assets/ # Blender assets and node libraries -``` - -### Blender Integration Patterns - -#### Property Groups -```python -class BioxelProperties(bpy.types.PropertyGroup): - bl_label = "Bioxel Properties" - - bioxel_custom_prop: bpy.props.StringProperty( - name="Custom Property", - default="", - description="Description for UI" - ) -``` - -#### Operators -```python -class BIOXEL_OT_custom_operator(bpy.types.Operator): - bl_idname = "bioxel.custom_operator" - bl_label = "Custom Operator" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - # Operator logic here - return {'FINISHED'} -``` - -#### Panels -```python -class BIOXEL_PT_custom_panel(bpy.types.Panel): - bl_label = "Custom Panel" - bl_idname = "BIOXEL_PT_custom_panel" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_category = "Bioxel" - - def draw(self, context): - layout = self.layout - # UI drawing code here -``` - -## Error Handling - -### Error Handling Patterns -- Use try-catch blocks for file operations -- Handle user cancellation gracefully -- Report progress for long operations using `bioxel_progress_factor` and `bioxel_progress_text` -- Log warnings with print statements for debugging - - -## Key Dependencies - -### Core Libraries -- **bpy** (Blender Python API) - Core integration -- **numpy** - Array processing -- **SimpleITK** - Medical image processing -- **h5py** - HDF5 file handling - -### Scientific Libraries -- **matplotlib** - Plotting and visualization -- **transforms3d** - 3D transformations -- **pyometiff** - OME-TIFF support -- **mrcfile** - MRC file format support - -## Development Workflow - -### Making Changes -1. Identify the appropriate module (operators, panels, bioxel, etc.) -2. Follow existing naming conventions and patterns -3. Use auto-registration - no manual registration needed -4. Test with Blender's Python console -5. Format code with autopep8 before committing - -### Adding New Features -1. Create appropriate classes in relevant modules -2. Use proper prefixes (BIOXEL_OT_, BIOXEL_PT_, etc.) -3. Add to auto_load system automatically discovers new classes -4. Update documentation if needed - -### File Operations -- Use `pathlib.Path` for path operations -- Check file existence before operations -- Handle permissions and missing files gracefully -- Use appropriate file formats (HDF5 for data, PNG for images) - -## Blender Integration Notes - -### Asset Library -- Automatically managed via `add_asset_library_if_missing()` -- Located at `NODE_LIB_DIRPATH` -- Uses "PACK" import method -- Name: "O Bioxel" - -### Progress Reporting -- Use `bioxel_progress_factor` (0.0 to 1.0) for progress -- Use `bioxel_progress_text` for status messages -- Update via WindowManager properties - -### Layer Management -- Layers stored as HDF5 files -- Container objects manage multiple layers -- Use `layer.py` for layer operations -- Support for various medical image formats - -## Common Pitfalls - -### Blender API -- Always check context validity -- Handle different Blender versions (check `bpy.app.version`) -- Use proper property types for UI -- Register classes in correct order (auto_load handles this) - -### Performance -- Use NumPy for array operations -- Avoid expensive operations in UI draw calls -- Cache computed values where appropriate -- Use background threads for long operations - -### File I/O -- Always close file handles -- Use context managers for file operations -- Handle large files in chunks -- Validate file formats before processing \ No newline at end of file +- 避免嵌套结构 +- 特殊的、非常规的需要写注释 +- 变量、函数名、Docstring 用全英文 +- 同一事物用统一表达 +- 使用的依赖越少越好 +- 选用简单直接的方式实现 +- 代码、执行方式改变,调整已有的文档、计划,而非新建 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eef4bd2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md \ No newline at end of file diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index db4d862..0000000 --- a/tests/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# BioxelNodes Unit Tests - -This directory contains unit tests for the bioxel module. - -## Test Data - -Test files should be placed in `tests/data/`: - -## Running Tests - -Run all tests: -```bash -uv run pytest -``` - -Run specific test file: -```bash -uv run pytest tests/test_bioxel.py -``` - -Run specific test class: -```bash -uv run pytest tests/test_bioxel.py::TestDataClass -``` - -Run with verbose output: -```bash -uv run pytest -v -``` - -Run with coverage: -```bash -uv run pytest --cov=bioxelnodes.bioxel --cov-report=html -``` - -## Test Categories - -- **TestDataClass**: Data class initialization and properties -- **TestDataToLayers**: Data.to_layers() method -- **TestUtilityFunctions**: Utility functions -- **TestLayerClass**: Layer class functionality -- **TestConstants**: Module constants -- **TestEdgeCases**: Edge cases and error handling diff --git a/tests/test_bioxel.py b/tests/test_bioxel.py deleted file mode 100644 index 43cbe6c..0000000 --- a/tests/test_bioxel.py +++ /dev/null @@ -1,539 +0,0 @@ -""" -Unit tests for bioxel module. - -Tests cover: -- Data class functionality -- Parsing NRRD and DICOM formats -- Layer creation and transformation -- Utility functions -""" - -from bioxelnodes.bioxel import ( - Data, - read, - read_meta, - calc_layer_shape, - calc_layer_size, - Layer, -) -import pytest -import numpy as np -from pathlib import Path - -import sys - -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - - -# Test data files -NRRD_FILE = Path(__file__).parent / "data/VHP_M.nrrd" -DICOM_FILE = Path(__file__).parent / "data/VHP_M_CT_Head/IM000001.dcm" -DICOM_DIR = Path(__file__).parent / "data/VHP_M_CT_Head/" - - -class TestDataClass: - """Test Data class initialization and properties""" - - def test_data_initialization(self): - """Test basic Data initialization with NRRD""" - data = Data(filepath=str(NRRD_FILE), series_id="") - - assert data.filepath is not None - assert data.series_id == "" - assert data._data is None - assert data._meta is None - - def test_read_meta_nrrd(self): - """Test reading only meta from NRRD file""" - data_obj = read_meta(str(NRRD_FILE)) - - assert data_obj._data is None - assert data_obj._meta is not None - assert "name" in data_obj.meta - assert "spacing" in data_obj.meta - assert "affine" in data_obj.meta - assert "xyz_shape" in data_obj.meta - assert "frame_count" in data_obj.meta - assert "channel_count" in data_obj.meta - assert "dtype" in data_obj.meta - - def test_read_meta_dicom(self): - """Test reading only meta from DICOM file""" - data_obj = read_meta(str(DICOM_FILE), series_id="") - - assert data_obj._data is None - assert data_obj._meta is not None - assert "name" in data_obj.meta - - def test_read_full_data_nrrd(self): - """Test reading full NRRD data""" - data_obj = read(str(NRRD_FILE)) - - assert data_obj._data is not None - assert data_obj._meta is not None - assert isinstance(data_obj.data, np.ndarray) - assert data_obj.data.ndim == 5 - - def test_read_full_data_dicom(self): - """Test reading full DICOM data""" - data_obj = read(str(DICOM_FILE), series_id="") - - assert data_obj._data is not None - assert data_obj._meta is not None - assert isinstance(data_obj.data, np.ndarray) - - def test_data_properties_nrrd(self): - """Test Data property accessors""" - data_obj = read(str(NRRD_FILE)) - - assert hasattr(data_obj, "name") - assert hasattr(data_obj, "description") - assert hasattr(data_obj, "shape") - assert hasattr(data_obj, "xyz_shape") - assert hasattr(data_obj, "spacing") - assert hasattr(data_obj, "affine") - assert hasattr(data_obj, "frame_count") - assert hasattr(data_obj, "channel_count") - assert hasattr(data_obj, "dtype") - - assert isinstance(data_obj.shape, tuple) - assert isinstance(data_obj.xyz_shape, tuple) - assert isinstance(data_obj.spacing, tuple) - assert isinstance(data_obj.affine, np.ndarray) - assert isinstance(data_obj.dtype, np.dtype) - - def test_data_lazy_loading(self): - """Test lazy loading behavior""" - data_obj = Data(filepath=str(NRRD_FILE), series_id="") - - assert data_obj.is_loaded() is False - - data_obj.load_meta() - assert data_obj._data is None - assert data_obj._meta is not None - - data_obj.load_data() - assert data_obj._data is not None - assert data_obj.is_loaded() is True - - def test_shape_property(self): - """Test shape property returns TXYZC format""" - data_obj = read(str(NRRD_FILE)) - - shape = data_obj.shape - assert len(shape) == 5 - assert shape[0] == data_obj.meta["frame_count"] - assert shape[1:4] == data_obj.meta["xyz_shape"] - assert shape[4] == data_obj.meta["channel_count"] - - def test_load_meta_method(self): - """Test load_meta() method exists and works""" - data_obj = Data(filepath=str(NRRD_FILE), series_id="") - - assert hasattr(data_obj, "load_meta") - - data_obj.load_meta() - assert data_obj._meta is not None - assert data_obj._data is None - - -class TestDataToLayers: - """Test Data.to_layers() method""" - - def test_to_layers_dicom_scalar(self): - """Test converting DICOM data to scalar layers""" - data_obj = read(str(DICOM_FILE), series_id="") - - layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0) - - assert len(layers) == 1 - assert layers[0].kind == "scalar" - assert isinstance(layers[0], Layer) - - def test_to_layers_nrrd_scalar(self): - """Test converting NRRD data to scalar layers""" - data_obj = read(str(NRRD_FILE)) - - layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0) - - assert len(layers) == 1 - assert layers[0].kind == "scalar" - assert isinstance(layers[0], Layer) - - def test_to_layers_with_remap(self): - """Test data remapping""" - data_obj = read(str(NRRD_FILE)) - - layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0, remap=True) - - assert len(layers) == 1 - assert layers[0].data.dtype == np.float32 - assert np.all(layers[0].data >= 0) - assert np.all(layers[0].data <= 1) - - def test_to_layers_with_bioxel_size(self): - """Test with different bioxel sizes""" - data_obj = read(str(NRRD_FILE)) - - layers_1x = data_obj.to_layers(kind="scalar", bioxel_size=1.0) - layers_2x = data_obj.to_layers(kind="scalar", bioxel_size=2.0) - - assert len(layers_1x) == 1 - assert len(layers_2x) == 1 - - # Larger bioxel size means smaller layer - shape_1x = layers_1x[0].shape - shape_2x = layers_2x[0].shape - assert shape_2x[0] <= shape_1x[0] - assert shape_2x[1] <= shape_1x[1] - assert shape_2x[2] <= shape_1x[2] - - def test_to_layers_with_smooth(self): - """Test with smoothing parameter""" - data_obj = read(str(NRRD_FILE)) - - layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0, smooth=1) - - assert len(layers) == 1 - assert isinstance(layers[0], Layer) - - def test_to_layers_frame_source_first_frame(self): - """Test frame_source = '-1' (first frame only)""" - data_obj = read(str(NRRD_FILE)) - - layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0, frame_source="-1") - - assert len(layers) == 1 - assert layers[0].frame_count == 1 - - -class TestUtilityFunctions: - """Test utility functions""" - - def test_calc_layer_shape(self): - """Test layer shape calculation""" - shape = calc_layer_shape( - bioxel_size=1.0, orig_shape=(100, 100, 100), orig_spacing=(1.0, 1.0, 1.0) - ) - - assert len(shape) == 3 - assert shape[0] == 100 - assert shape[1] == 100 - assert shape[2] == 100 - - def test_calc_layer_shape_small_bioxel(self): - """Test layer shape with small bioxel size""" - shape = calc_layer_shape( - bioxel_size=2.0, orig_shape=(100, 100, 100), orig_spacing=(1.0, 1.0, 1.0) - ) - - assert len(shape) == 3 - assert shape[0] == 50 - assert shape[1] == 50 - assert shape[2] == 50 - - def test_calc_layer_shape_minimum(self): - """Test layer shape doesn't go below 1""" - shape = calc_layer_shape( - bioxel_size=100.0, orig_shape=(100, 100, 100), orig_spacing=(1.0, 1.0, 1.0) - ) - - assert shape == (1, 1, 1) - - def test_calc_layer_size(self): - """Test layer size calculation""" - size = calc_layer_size(shape=(100, 100, 100), bioxel_size=1.0, scale=1.0) - - assert len(size) == 3 - assert size[0] == 100.0 - assert size[1] == 100.0 - assert size[2] == 100.0 - - def test_calc_layer_size_with_scale(self): - """Test layer size with scale factor""" - size = calc_layer_size(shape=(100, 100, 100), bioxel_size=1.0, scale=0.01) - - assert len(size) == 3 - assert size[0] == 1.0 - assert size[1] == 1.0 - assert size[2] == 1.0 - - -class TestLayerClass: - """Test Layer class functionality""" - - def test_layer_initialization(self): - """Test Layer initialization""" - data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32) - layer = Layer(data=data, name="test_layer", kind="scalar") - - assert layer.name == "test_layer" - assert layer.kind == "scalar" - assert np.array_equal(layer.data, data) - - def test_layer_properties(self): - """Test Layer property accessors""" - data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32) - layer = Layer( - data=data, name="test_layer", kind="scalar", affine=np.identity(4) - ) - - assert hasattr(layer, "bioxel_size") - assert hasattr(layer, "shape") - assert hasattr(layer, "dtype") - assert hasattr(layer, "origin") - assert hasattr(layer, "euler") - assert hasattr(layer, "frame_count") - assert hasattr(layer, "channel_count") - assert hasattr(layer, "min") - assert hasattr(layer, "max") - - def test_layer_shape_property(self): - """Test layer shape returns XYZ (not TXYZC)""" - data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32) - layer = Layer(data=data, name="test", kind="scalar") - - shape = layer.shape - assert len(shape) == 3 - assert shape[0] == 8 - assert shape[1] == 8 - assert shape[2] == 8 - - def test_layer_affine_property(self): - """Test affine transformation matrix""" - data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32) - affine = np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - ) - layer = Layer(data=data, name="test", kind="scalar", affine=affine) - - result_affine = layer.affine - assert result_affine.shape == (4, 4) - assert np.allclose(result_affine, affine) - - def test_layer_min_max(self): - """Test layer min/max properties""" - data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32) - layer = Layer(data=data, name="test", kind="scalar") - - assert isinstance(layer.min, float) - assert isinstance(layer.max, float) - assert layer.min <= layer.max - - def test_layer_copy(self): - """Test layer deep copy""" - data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32) - layer = Layer(data=data, name="test", kind="scalar") - layer_copy = layer.copy() - - assert layer_copy.name == layer.name - assert layer_copy.kind == layer.kind - assert np.array_equal(layer_copy.data, layer.data) - - assert layer_copy is not layer - layer_copy.data[0, 0, 0, 0, 0] = 999.0 - assert layer.data[0, 0, 0, 0, 0] != 999.0 - - def test_layer_invalid_data_shape(self): - """Test Layer raises error for invalid data shape""" - data = np.random.rand(8, 8, 8).astype(np.float32) - - with pytest.raises(Exception): - Layer(data=data, name="test", kind="scalar") - - def test_layer_invalid_affine_shape(self): - """Test Layer raises error for invalid affine shape""" - data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32) - affine = np.identity(3) - - with pytest.raises(Exception): - Layer(data=data, name="test", kind="scalar", affine=affine) - - -class TestConstants: - """Test module constants""" - - def test_support_exts_exists(self): - """Test SUPPORT_EXTS constant""" - from bioxelnodes.bioxel import SUPPORT_EXTS - - assert isinstance(SUPPORT_EXTS, list) - assert len(SUPPORT_EXTS) > 0 - assert ".h5" in SUPPORT_EXTS - assert ".nrrd" in SUPPORT_EXTS - assert ".nii.gz" in SUPPORT_EXTS - assert ".dcm" in SUPPORT_EXTS - - def test_dicom_exts_exists(self): - """Test DICOM_EXTS constant""" - from bioxelnodes.bioxel import DICOM_EXTS - - assert isinstance(DICOM_EXTS, list) - assert "" in DICOM_EXTS - assert ".dcm" in DICOM_EXTS - assert ".DCM" in DICOM_EXTS - - def test_ome_exts_exists(self): - """Test OME_EXTS constant""" - from bioxelnodes.bioxel import OME_EXTS - - assert isinstance(OME_EXTS, list) - assert ".tiff" in OME_EXTS - assert ".ome.tiff" in OME_EXTS - - def test_mrc_exts_exists(self): - """Test MRC_EXTS constant""" - from bioxelnodes.bioxel import MRC_EXTS - - assert isinstance(MRC_EXTS, list) - assert ".mrc" in MRC_EXTS - - -class TestEdgeCases: - """Test edge cases and error handling""" - - def test_nonexistent_file(self): - """Test handling of nonexistent file""" - with pytest.raises(FileNotFoundError): - data_obj = read("/nonexistent/file.nii.gz", "") - - def test_frame_count_property_multiframe(self): - """Test frame_count property for multi-frame data""" - data_obj = read(str(NRRD_FILE)) - - assert data_obj.frame_count >= 1 - assert data_obj.frame_count == data_obj.meta["frame_count"] - - def test_channel_count_property(self): - """Test channel_count property""" - data_obj = read(str(NRRD_FILE)) - - assert data_obj.channel_count >= 1 - assert data_obj.channel_count == data_obj.meta["channel_count"] - - def test_nrrd_format_support(self): - """Test NRRD format is in supported extensions""" - from bioxelnodes.bioxel import SUPPORT_EXTS - - assert ".nrrd" in SUPPORT_EXTS - - def test_dicom_format_support(self): - """Test DICOM format is in supported extensions""" - from bioxelnodes.bioxel import SUPPORT_EXTS - - assert ".dcm" in SUPPORT_EXTS - assert "" in SUPPORT_EXTS - - -class TestRealDataFiles: - """Test with real data files""" - - def test_nrrd_file_exists(self): - """Test NRRD test file exists""" - assert NRRD_FILE.exists() - assert NRRD_FILE.is_file() - - def test_dicom_file_exists(self): - """Test DICOM test file exists""" - assert DICOM_FILE.exists() - assert DICOM_FILE.is_file() - - def test_dicom_directory_exists(self): - """Test DICOM directory exists""" - assert DICOM_DIR.exists() - assert DICOM_DIR.is_dir() - - def test_read_nrrd_data(self): - """Test reading real NRRD data""" - data_obj = read(str(NRRD_FILE)) - - assert data_obj._data is not None - assert data_obj._meta is not None - assert isinstance(data_obj.data, np.ndarray) - assert data_obj.data.size > 0 - - def test_read_dicom_data(self): - """Test reading real DICOM data""" - data_obj = read(str(DICOM_FILE), series_id="") - - assert data_obj._data is not None - assert data_obj._meta is not None - assert isinstance(data_obj.data, np.ndarray) - assert data_obj.data.size > 0 - - def test_nrrd_meta_contains_required_fields(self): - """Test NRRD meta contains all required fields""" - data_obj = read(str(NRRD_FILE)) - meta = data_obj.meta - - required_fields = [ - "name", - "description", - "spacing", - "affine", - "xyz_shape", - "frame_count", - "channel_count", - "dtype", - ] - - for field in required_fields: - assert field in meta, f"Meta missing field: {field}" - - def test_dicom_meta_contains_required_fields(self): - """Test DICOM meta contains all required fields""" - data_obj = read(str(DICOM_FILE), series_id="") - meta = data_obj.meta - - required_fields = [ - "name", - "description", - "spacing", - "affine", - "xyz_shape", - "frame_count", - "channel_count", - "dtype", - ] - - for field in required_fields: - assert field in meta, f"Meta missing field: {field}" - - def test_data_to_layers_with_real_nrrd(self): - """Test to_layers with real NRRD data""" - data_obj = read(str(NRRD_FILE)) - - layers = data_obj.to_layers(kind="scalar", bioxel_size=3.0) - - assert len(layers) > 0 - assert all(isinstance(layer, Layer) for layer in layers) - - def test_data_to_layers_with_real_dicom(self): - """Test to_layers with real DICOM data""" - data_obj = read(str(DICOM_FILE), series_id="") - - layers = data_obj.to_layers(kind="scalar", bioxel_size=3.0) - - assert len(layers) > 0 - assert all(isinstance(layer, Layer) for layer in layers) - - def test_nrrd_spacing_values(self): - """Test NRRD spacing values are reasonable""" - data_obj = read(str(NRRD_FILE)) - spacing = data_obj.spacing - - assert len(spacing) == 3 - assert all(s > 0 for s in spacing) - - def test_dicom_spacing_values(self): - """Test DICOM spacing values are reasonable""" - data_obj = read(str(DICOM_FILE), series_id="") - spacing = data_obj.spacing - - assert len(spacing) == 3 - assert all(s > 0 for s in spacing)