feat: Update .gitattributes and .gitignore for improved asset management and ignore patterns

chore: Add app.json and omoolab.css for Obsidian integration

refactor: Clean up AGENTS.md and remove outdated test files
This commit is contained in:
2026-05-25 09:15:47 +08:00
parent ced0898f84
commit b83dd0acd9
9 changed files with 477 additions and 831 deletions

132
.gitattributes vendored
View File

@@ -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

304
.gitignore vendored
View File

@@ -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
# 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/

22
.obsidian/app.json vendored Normal file
View File

@@ -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"
}

30
.obsidian/snippets/omoolab.css vendored Normal file
View File

@@ -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;
}

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

236
AGENTS.md
View File

@@ -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 <platform> # 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
- 避免嵌套结构
- 特殊的、非常规的需要写注释
- 变量、函数名、Docstring 用全英文
- 同一事物用统一表达
- 使用的依赖越少越好
- 选用简单直接的方式实现
- 代码、执行方式改变,调整已有的文档、计划,而非新建

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -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

View File

@@ -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)